diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/AbstractInsertionCalculator.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/AbstractInsertionCalculator.java index 317003a87..5f169caf1 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/AbstractInsertionCalculator.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/AbstractInsertionCalculator.java @@ -34,7 +34,6 @@ * Created by schroeder on 06/02/17. */ abstract class AbstractInsertionCalculator implements JobInsertionCostsCalculator { - InsertionData checkRouteConstraints(JobInsertionContext insertionContext, ConstraintManager constraintManager) { for (HardRouteConstraint hardRouteConstraint : constraintManager.getHardRouteConstraints()) { if (!hardRouteConstraint.fulfilled(insertionContext)) { diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ServiceInsertionCalculator.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ServiceInsertionCalculator.java index 0650bd0d6..8d2166902 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ServiceInsertionCalculator.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ServiceInsertionCalculator.java @@ -135,13 +135,15 @@ public InsertionData getInsertionData(final VehicleRoute currentRoute, final Job nextAct = end; tourEnd = true; } + + ActivityContext activityContext = new ActivityContext(); + activityContext.setInsertionIndex(actIndex); + insertionContext.setActivityContext(activityContext); boolean not_fulfilled_break = true; - for(TimeWindow timeWindow : service.getTimeWindows()) { + for(TimeWindow timeWindow : service.getTimeWindows(insertionContext)) { deliveryAct2Insert.setTheoreticalEarliestOperationStartTime(timeWindow.getStart()); deliveryAct2Insert.setTheoreticalLatestOperationStartTime(timeWindow.getEnd()); - ActivityContext activityContext = new ActivityContext(); - activityContext.setInsertionIndex(actIndex); - insertionContext.setActivityContext(activityContext); + ConstraintsStatus status = fulfilled(insertionContext, prevAct, deliveryAct2Insert, nextAct, prevActStartTime, failedActivityConstraints, constraintManager); if (status.equals(ConstraintsStatus.FULFILLED)) { double additionalICostsAtActLevel = softActivityConstraint.getCosts(insertionContext, prevAct, deliveryAct2Insert, nextAct, prevActStartTime); diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ShipmentInsertionCalculator.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ShipmentInsertionCalculator.java index a09264151..d32a8067a 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ShipmentInsertionCalculator.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/algorithm/recreate/ShipmentInsertionCalculator.java @@ -140,12 +140,13 @@ public InsertionData getInsertionData(final VehicleRoute currentRoute, final Job } boolean pickupInsertionNotFulfilledBreak = true; - for(TimeWindow pickupTimeWindow : shipment.getPickupTimeWindows()) { - pickupShipment.setTheoreticalEarliestOperationStartTime(pickupTimeWindow.getStart()); + ActivityContext activityContext = new ActivityContext(); + activityContext.setInsertionIndex(i); + insertionContext.setActivityContext(activityContext); + for(TimeWindow pickupTimeWindow : shipment.getPickupTimeWindows(insertionContext)) { pickupShipment.setTheoreticalLatestOperationStartTime(pickupTimeWindow.getEnd()); - ActivityContext activityContext = new ActivityContext(); - activityContext.setInsertionIndex(i); - insertionContext.setActivityContext(activityContext); + pickupShipment.setTheoreticalEarliestOperationStartTime(pickupTimeWindow.getStart()); + ConstraintsStatus pickupShipmentConstraintStatus = fulfilled(insertionContext, prevAct, pickupShipment, nextAct, prevActEndTime, failedActivityConstraints, constraintManager); if (pickupShipmentConstraintStatus.equals(ConstraintsStatus.NOT_FULFILLED)) { pickupInsertionNotFulfilledBreak = false; @@ -190,12 +191,13 @@ else if (pickupShipmentConstraintStatus.equals(ConstraintsStatus.FULFILLED)) { } boolean deliveryInsertionNotFulfilledBreak = true; - for (TimeWindow deliveryTimeWindow : shipment.getDeliveryTimeWindows()) { + ActivityContext activityContext_ = new ActivityContext(); + activityContext_.setInsertionIndex(j); + insertionContext.setActivityContext(activityContext_); + for (TimeWindow deliveryTimeWindow : shipment.getDeliveryTimeWindows(insertionContext)) { deliverShipment.setTheoreticalEarliestOperationStartTime(deliveryTimeWindow.getStart()); deliverShipment.setTheoreticalLatestOperationStartTime(deliveryTimeWindow.getEnd()); - ActivityContext activityContext_ = new ActivityContext(); - activityContext_.setInsertionIndex(j); - insertionContext.setActivityContext(activityContext_); + ConstraintsStatus deliverShipmentConstraintStatus = fulfilled(insertionContext, prevAct_deliveryLoop, deliverShipment, nextAct_deliveryLoop, prevActEndTime_deliveryLoop, failedActivityConstraints, constraintManager); if (deliverShipmentConstraintStatus.equals(ConstraintsStatus.FULFILLED)) { double additionalDeliveryICosts = softActivityConstraint.getCosts(insertionContext, prevAct_deliveryLoop, deliverShipment, nextAct_deliveryLoop, prevActEndTime_deliveryLoop); diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Service.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Service.java index 0b24881c1..0cc54c4ef 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Service.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Service.java @@ -21,9 +21,11 @@ import com.graphhopper.jsprit.core.problem.Capacity; import com.graphhopper.jsprit.core.problem.Location; import com.graphhopper.jsprit.core.problem.Skills; +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindow; import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindows; import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindowsImpl; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindowsOverlapImpl; import com.graphhopper.jsprit.core.util.Coordinate; import java.util.ArrayList; @@ -87,7 +89,7 @@ public static Builder newInstance(String id) { protected Location location; - protected TimeWindowsImpl timeWindows; + protected TimeWindows timeWindows; private boolean twAdded = false; @@ -101,7 +103,7 @@ public static Builder newInstance(String id) { Builder(String id){ this.id = id; timeWindows = new TimeWindowsImpl(); - timeWindows.add(TimeWindow.newInstance(0.0, Double.MAX_VALUE)); + timeWindows.add(TimeWindowsImpl.defaultTimeWindow); } /** @@ -184,6 +186,20 @@ public Builder setTimeWindow(TimeWindow tw){ return this; } + public Builder setTimeWindows(TimeWindows timeWindows){ + if (timeWindows == null) throw new IllegalArgumentException("The time windows must not be null."); + if (twAdded) { + // Report already added TW for ascending compatibility and API clarity + // (otherwise previous calls to addTimeWindow would be silently ignored) + for (TimeWindow tw : this.timeWindows.getTimeWindows()) { + timeWindows.add(tw); + } + } + this.timeWindows = timeWindows; + twAdded = true; + return this; + } + public Builder addTimeWindow(TimeWindow timeWindow) { if (timeWindow == null) throw new IllegalArgumentException("The time window must not be null."); if(!twAdded){ @@ -310,6 +326,10 @@ public Collection getTimeWindows(){ return timeWindows.getTimeWindows(); } + public Collection getTimeWindows(JobInsertionContext insertionContext){ + return timeWindows.getTimeWindows(insertionContext); + } + @Override public String getId() { return id; diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Shipment.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Shipment.java index be95482bd..87586ee0e 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Shipment.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/job/Shipment.java @@ -21,7 +21,9 @@ import com.graphhopper.jsprit.core.problem.Capacity; import com.graphhopper.jsprit.core.problem.Location; import com.graphhopper.jsprit.core.problem.Skills; +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindow; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindows; import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindowsImpl; import java.util.ArrayList; @@ -76,13 +78,13 @@ public static class Builder { private Location deliveryLocation_; - protected TimeWindowsImpl deliveryTimeWindows; + protected TimeWindows deliveryTimeWindows; private boolean deliveryTimeWindowAdded = false; private boolean pickupTimeWindowAdded = false; - private TimeWindowsImpl pickupTimeWindows; + private TimeWindows pickupTimeWindows; private int priority = 2; @@ -108,9 +110,9 @@ public static Builder newInstance(String id) { if (id == null) throw new IllegalArgumentException("id must not be null"); this.id = id; pickupTimeWindows = new TimeWindowsImpl(); - pickupTimeWindows.add(TimeWindow.newInstance(0.0, Double.MAX_VALUE)); + pickupTimeWindows.add(TimeWindowsImpl.defaultTimeWindow); deliveryTimeWindows = new TimeWindowsImpl(); - deliveryTimeWindows.add(TimeWindow.newInstance(0.0, Double.MAX_VALUE)); + deliveryTimeWindows.add(TimeWindowsImpl.defaultTimeWindow); } /** @@ -176,7 +178,19 @@ public Builder setPickupTimeWindow(TimeWindow timeWindow) { return this; } - + public Builder setPickupTimeWindows(TimeWindows timeWindows){ + if (timeWindows == null) throw new IllegalArgumentException("The time windows must not be null."); + if (pickupTimeWindowAdded) { + // Report already added TW for ascending compatibility and API clarity + // (otherwise previous calls to addXXXTimeWindow would be silently ignored) + for (TimeWindow tw : this.pickupTimeWindows.getTimeWindows()) { + timeWindows.add(tw); + } + } + this.pickupTimeWindows = timeWindows; + pickupTimeWindowAdded = true; + return this; + } /** * Sets delivery location. @@ -222,6 +236,20 @@ public Builder setDeliveryTimeWindow(TimeWindow timeWindow) { return this; } + public Builder setDeliveryTimeWindows(TimeWindows timeWindows){ + if (timeWindows == null) throw new IllegalArgumentException("The time windows must not be null."); + if (deliveryTimeWindowAdded) { + // Report already added TW for ascending compatibility and API clarity + // (otherwise previous calls to addXXXTimeWindow would be silently ignored) + for (TimeWindow tw : this.deliveryTimeWindows.getTimeWindows()) { + timeWindows.add(tw); + } + } + this.deliveryTimeWindows = timeWindows; + deliveryTimeWindowAdded = true; + return this; + } + /** * Adds capacity dimension. * @@ -367,9 +395,9 @@ public Builder setMaxTimeInVehicle(double maxTimeInVehicle){ private final Location deliveryLocation_; - private final TimeWindowsImpl deliveryTimeWindows; + private final TimeWindows deliveryTimeWindows; - private final TimeWindowsImpl pickupTimeWindows; + private final TimeWindows pickupTimeWindows; private final int priority; @@ -442,6 +470,10 @@ public Collection getDeliveryTimeWindows() { return deliveryTimeWindows.getTimeWindows(); } + public Collection getDeliveryTimeWindows(JobInsertionContext insertionContext) { + return deliveryTimeWindows.getTimeWindows(insertionContext); + } + /** * Returns the time-window of pickup. * @@ -455,6 +487,10 @@ public Collection getPickupTimeWindows() { return pickupTimeWindows.getTimeWindows(); } + public Collection getPickupTimeWindows(JobInsertionContext insertionContext) { + return pickupTimeWindows.getTimeWindows(insertionContext); + } + /** * Returns a string with the shipment's attributes. diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindow.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindow.java index 3fac5c439..97af0a093 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindow.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindow.java @@ -17,6 +17,8 @@ */ package com.graphhopper.jsprit.core.problem.solution.route.activity; +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; + /** * TimeWindow consists of a startTime and endTime. * @@ -116,5 +118,12 @@ public boolean equals(Object obj) { return true; } - + /** + * Returns true if this time window is applicable in the current context. By default always true, only sub classes of TimeWindow may return false. + * @param time + * @return + */ + public boolean isApplicable(JobInsertionContext insertionContext) { + return true; + } } diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowConditionalOnVehicleType.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowConditionalOnVehicleType.java new file mode 100644 index 000000000..2fb820f7d --- /dev/null +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowConditionalOnVehicleType.java @@ -0,0 +1,34 @@ +package com.graphhopper.jsprit.core.problem.solution.route.activity; + +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; + +/** + * A time window that is only applicable for a specific type of vehicle. + * When using this, beware of vehicle switching. + */ +public class TimeWindowConditionalOnVehicleType extends TimeWindow { + private final String vehicleTypeId; + + /** + * Returns new instance of TimeWindowConditionalOnVehicleType. + * + * @param start + * @param end + * @param vehicleTypeId + * @return TimeWindow + * @throw IllegalArgumentException either if start or end < 0.0 or end < start + */ + public static TimeWindow newInstance(double start, double end, String vehicleTypeId) { + return new TimeWindowConditionalOnVehicleType(start, end, vehicleTypeId); + } + + public TimeWindowConditionalOnVehicleType(double start, double end, String vehicleTypeId) { + super(start, end); + this.vehicleTypeId = vehicleTypeId; + } + + @Override + public boolean isApplicable(JobInsertionContext insertionContext) { + return insertionContext.getNewVehicle().getType().getTypeId().equals(vehicleTypeId); + } +} diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindows.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindows.java index 615100a92..6ee730268 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindows.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindows.java @@ -20,11 +20,18 @@ import java.util.Collection; +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; + /** * Created by schroeder on 20/05/15. */ public interface TimeWindows { + static TimeWindow defaultTimeWindow = TimeWindow.newInstance(0.0, Double.MAX_VALUE); + + public void add(TimeWindow timeWindow); public Collection getTimeWindows(); + public Collection getTimeWindows(JobInsertionContext insertionContext); + } diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImpl.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImpl.java index 84b8f37c9..7a781cdec 100644 --- a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImpl.java +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImpl.java @@ -21,6 +21,9 @@ import java.util.ArrayList; import java.util.Collection; import java.util.Collections; +import java.util.List; + +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; /** * Created by schroeder on 26/05/15. @@ -29,6 +32,7 @@ public class TimeWindowsImpl implements TimeWindows { private Collection timeWindows = new ArrayList(); + @Override public void add(TimeWindow timeWindow){ for(TimeWindow tw : timeWindows){ if(timeWindow.getStart() > tw.getStart() && timeWindow.getStart() < tw.getEnd()){ @@ -44,10 +48,25 @@ public void add(TimeWindow timeWindow){ timeWindows.add(timeWindow); } + @Override public Collection getTimeWindows() { return Collections.unmodifiableCollection(timeWindows); } + @Override + public Collection getTimeWindows(JobInsertionContext insertionContext) { + List timeWindows = new ArrayList(this.timeWindows.size()); + for(TimeWindow tw : this.timeWindows){ + if (tw.isApplicable(insertionContext)) { + timeWindows.add(tw); + } + } + if (timeWindows.isEmpty()) { + timeWindows.add(TimeWindows.defaultTimeWindow); + } + return Collections.unmodifiableCollection(timeWindows); + } + @Override public String toString() { StringBuffer sb = new StringBuffer(timeWindows.size() * 60); diff --git a/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsOverlapImpl.java b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsOverlapImpl.java new file mode 100644 index 000000000..32b3b64c2 --- /dev/null +++ b/jsprit-core/src/main/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsOverlapImpl.java @@ -0,0 +1,152 @@ +package com.graphhopper.jsprit.core.problem.solution.route.activity; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; +import java.util.List; + +import com.graphhopper.jsprit.core.problem.misc.JobInsertionContext; + +/** + * An alternative implementation of TimeWindows that allows for overlapping time windows as well as exclusions. + */ +public class TimeWindowsOverlapImpl implements TimeWindows { + + private List includedTimeWindows = new ArrayList(); + private List excludedTimeWindows = new ArrayList(); + + public static TimeWindowsOverlapImpl newInstance() { + return new TimeWindowsOverlapImpl(); + } + + public static TimeWindowsOverlapImpl newInstance(List includedTimeWindows) { + TimeWindowsOverlapImpl res = new TimeWindowsOverlapImpl(); + res.includedTimeWindows = new ArrayList<>(includedTimeWindows); + return res; + } + + @Override + public void add(TimeWindow timeWindow) { + if (timeWindow == null) { + throw new IllegalArgumentException("The time window must not be null."); + } + + includedTimeWindows.add(timeWindow); + + // Keep collection sorted by start time - needed by getTimeWindows() + Collections.sort(includedTimeWindows, (a, b) -> (int)(a.getStart() - b.getStart())); + } + + public TimeWindowsOverlapImpl addExcludedTimeWindow(TimeWindow timeWindow) { + if (timeWindow == null) { + throw new IllegalArgumentException("The time window must not be null."); + } + excludedTimeWindows.add(timeWindow); + + // Keep collection sorted by start time - needed by getTimeWindows() + Collections.sort(excludedTimeWindows, (a, b) -> (int)(a.getStart() - b.getStart())); + return this; + } + + public TimeWindowsOverlapImpl addIncludedTimeWindow(TimeWindow timeWindow) { + // Synonym for the sake of symmetry + add(timeWindow); + return this; + } + + @Override + public Collection getTimeWindows() { + return Collections.unmodifiableCollection(includedTimeWindows); + } + + @Override + public Collection getTimeWindows(JobInsertionContext insertionContext) { + // First: filter included TW that are applicable + List result = new ArrayList(includedTimeWindows.size()); + for(TimeWindow includedTw : includedTimeWindows) { + if (!includedTw.isApplicable(insertionContext)) { + continue; + } + result.add(includedTw); + } + if (result.isEmpty()) { + // No applicable TW means the job has no time insertion constraints. So just use the "infinite" TW. + result.add(defaultTimeWindow); + } + + // First easy case: no exclusions, no performance loss. + if (excludedTimeWindows.isEmpty()) { + return Collections.unmodifiableCollection(result); + } + + // Second easy case: no applicable exclusions. + List applicableExclusions = new ArrayList(excludedTimeWindows.size()); + for (TimeWindow excludedTw : excludedTimeWindows) { + if (excludedTw.isApplicable(insertionContext)) { + applicableExclusions.add(excludedTw); + } + } + if (applicableExclusions.isEmpty()) { + return Collections.unmodifiableCollection(result); + } + + // Then remove the exclusions + for(TimeWindow excludedTw : applicableExclusions) { + result = cutTimeWindowsIfNeeded(excludedTw, result); + } + // Note that the result can be empty! This is normal: we can exclude everything and make the job impossible to insert. + + // And we are done! + return Collections.unmodifiableCollection(result); + } + + protected List cutTimeWindowsIfNeeded(TimeWindow excludedTw, List currentResult) { + List newResult = new ArrayList(); + for (TimeWindow includedTw : currentResult) { + if (excludedTw.getStart() >= includedTw.getStart() && excludedTw.getEnd() <= includedTw.getEnd()) { + // Inclusion [ ] + // Exclusion [ ] + if (excludedTw.getStart() > includedTw.getStart()) { + newResult.add(TimeWindow.newInstance(includedTw.getStart(), excludedTw.getStart())); + } + if (excludedTw.getEnd() < includedTw.getEnd()) { + newResult.add(TimeWindow.newInstance(excludedTw.getEnd(), includedTw.getEnd())); + } + } + else if (excludedTw.getStart() < includedTw.getStart() && excludedTw.getEnd() < includedTw.getEnd() && excludedTw.getEnd() > includedTw.getStart()) { + // Inclusion [ ] + // Exclusion [ ] + newResult.add(TimeWindow.newInstance(excludedTw.getEnd(), includedTw.getEnd())); + } + else if (excludedTw.getStart() > includedTw.getStart() && excludedTw.getStart() < includedTw.getEnd() && excludedTw.getEnd() > includedTw.getEnd()) { + // Inclusion [ ] + // Exclusion [ ] + newResult.add(TimeWindow.newInstance(includedTw.getStart(), excludedTw.getStart())); + } + else if (excludedTw.getStart() < includedTw.getStart() && excludedTw.getEnd() > includedTw.getEnd()) { + // Inclusion [ ] + // Exclusion [ ] + // => nothing to add + } + else { + // The exclusion does not overlap with the inclusion - keep inclusion as-is. + newResult.add(includedTw); + } + } + return newResult; + } + + @Override + public String toString() { + StringBuffer sb = new StringBuffer(includedTimeWindows.size() * 60); + sb.append("Included:\n"); + for (TimeWindow tw : includedTimeWindows) { + sb.append("[timeWindow=").append(tw).append("]"); + } + sb.append("\nExcluded:\n"); + for (TimeWindow tw : excludedTimeWindows) { + sb.append("[timeWindow=").append(tw).append("]"); + } + return sb.toString(); + } +} diff --git a/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/IgnoreConditionalTimeWindowTest.java b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/IgnoreConditionalTimeWindowTest.java new file mode 100644 index 000000000..d51657f54 --- /dev/null +++ b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/algorithm/IgnoreConditionalTimeWindowTest.java @@ -0,0 +1,237 @@ +/* + * Licensed to GraphHopper GmbH under one or more contributor + * license agreements. See the NOTICE file distributed with this work for + * additional information regarding copyright ownership. + * + * GraphHopper GmbH licenses this file to you under the Apache License, + * Version 2.0 (the "License"); you may not use this file except in + * compliance with the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +package com.graphhopper.jsprit.core.algorithm; + +import org.junit.Assert; +import org.junit.Test; + +import com.graphhopper.jsprit.core.algorithm.box.Jsprit; +import com.graphhopper.jsprit.core.problem.Location; +import com.graphhopper.jsprit.core.problem.VehicleRoutingProblem; +import com.graphhopper.jsprit.core.problem.job.Service; +import com.graphhopper.jsprit.core.problem.solution.VehicleRoutingProblemSolution; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindow; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindowConditionalOnVehicleType; +import com.graphhopper.jsprit.core.problem.solution.route.activity.TimeWindowsOverlapImpl; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleImpl; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleType; +import com.graphhopper.jsprit.core.problem.vehicle.VehicleTypeImpl; +import com.graphhopper.jsprit.core.util.Solutions; + +/** + * Created by marcanpilami on 29/08/2023. This is a test of insertion mechanisms when using a conditional time window. + */ +public class IgnoreConditionalTimeWindowTest { + + @Test + public void ignoreConditionalTimeWindow(){ + VehicleTypeImpl.Builder vehicleTypeBuilder1 = VehicleTypeImpl.Builder.newInstance("vehicleType1"); + VehicleTypeImpl.Builder vehicleTypeBuilder2 = VehicleTypeImpl.Builder.newInstance("vehicleType2"); + + VehicleType vehicleType1 = vehicleTypeBuilder1.build(); + VehicleType vehicleType2 = vehicleTypeBuilder2.build(); + + VehicleImpl vehicle1,vehicle2; + { + VehicleImpl.Builder vehicleBuilder = VehicleImpl.Builder.newInstance("v1"); + vehicleBuilder.setStartLocation(Location.newInstance(0, 0)); + vehicleBuilder.setType(vehicleType1); + vehicleBuilder.setEarliestStart(10).setLatestArrival(50); + + vehicle1 = vehicleBuilder.build(); + + vehicleBuilder = VehicleImpl.Builder.newInstance("v2"); + vehicleBuilder.setStartLocation(Location.newInstance(0, 0)); + vehicleBuilder.setType(vehicleType2); + vehicleBuilder.setEarliestStart(10).setLatestArrival(50); + + vehicle2 = vehicleBuilder.build(); + } + + // First service is impossible for vehicle v1 (TW after vehicle return), but possible for all others which ignore the TW. + Service service1 = Service.Builder.newInstance("s1").setLocation(Location.newInstance(0, 0)) + .setServiceTime(1.).setTimeWindow(TimeWindowConditionalOnVehicleType.newInstance(100,100, "vehicleType1")).build(); + + VehicleRoutingProblem vrp1 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle1) + .addJob(service1) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra1 = Jsprit.createAlgorithm(vrp1); + vra1.setMaxIterations(50); + VehicleRoutingProblemSolution solution1 = Solutions.bestOf(vra1.searchSolutions()); + + Assert.assertEquals(1, solution1.getUnassignedJobs().size()); + + // Second service is possible for v1. + Service service2 = Service.Builder.newInstance("s2").setLocation(Location.newInstance(0, 0)) + .setServiceTime(1.).setTimeWindow(TimeWindowConditionalOnVehicleType.newInstance(11, 11, "vehicleType1")).build(); + + VehicleRoutingProblem vrp2 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle1) + .addJob(service1) + .addJob(service2) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra2 = Jsprit.createAlgorithm(vrp2); + vra2.setMaxIterations(50); + VehicleRoutingProblemSolution solution2 = Solutions.bestOf(vra2.searchSolutions()); + + Assert.assertEquals(1, solution2.getUnassignedJobs().size()); + Assert.assertEquals(1, solution2.getRoutes().size()); + Assert.assertEquals(1, solution2.getRoutes().iterator().next().getActivities().size()); + + // First service should be possible for v2. + VehicleRoutingProblem vrp3 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle2) + .addJob(service1) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra3 = Jsprit.createAlgorithm(vrp3); + vra3.setMaxIterations(50); + VehicleRoutingProblemSolution solution3 = Solutions.bestOf(vra3.searchSolutions()); + + Assert.assertEquals(0, solution3.getUnassignedJobs().size()); + } + + @Test + public void ignoreConditionalTimeWindowWithExclusion(){ + VehicleTypeImpl.Builder vehicleTypeBuilder1 = VehicleTypeImpl.Builder.newInstance("vehicleType1"); + VehicleTypeImpl.Builder vehicleTypeBuilder2 = VehicleTypeImpl.Builder.newInstance("vehicleType2"); + + VehicleType vehicleType1 = vehicleTypeBuilder1.build(); + VehicleType vehicleType2 = vehicleTypeBuilder2.build(); + + VehicleImpl.Builder vehicleBuilder = VehicleImpl.Builder.newInstance("v1"); + vehicleBuilder.setStartLocation(Location.newInstance(0, 0)); + vehicleBuilder.setType(vehicleType1); + vehicleBuilder.setEarliestStart(0).setLatestArrival(200); + + VehicleImpl vehicle1 = vehicleBuilder.build(); + + vehicleBuilder = VehicleImpl.Builder.newInstance("v2"); + vehicleBuilder.setStartLocation(Location.newInstance(0, 0)); + vehicleBuilder.setType(vehicleType2); + vehicleBuilder.setEarliestStart(0).setLatestArrival(200); + + VehicleImpl vehicle2 = vehicleBuilder.build(); + + + // First service is possible for vehicle v1 but only after a while due to exclusion. + TimeWindowsOverlapImpl tws = new TimeWindowsOverlapImpl(); + tws.addIncludedTimeWindow(TimeWindow.newInstance(0,100)); + tws.addExcludedTimeWindow(TimeWindowConditionalOnVehicleType.newInstance(0,40, "vehicleType1")); + Service service1 = Service.Builder.newInstance("s1").setLocation(Location.newInstance(0, 0)) + .setServiceTime(0).setTimeWindows(tws).build(); + + VehicleRoutingProblem vrp1 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle1) + .addJob(service1) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra1 = Jsprit.createAlgorithm(vrp1); + vra1.setMaxIterations(50); + VehicleRoutingProblemSolution solution1 = Solutions.bestOf(vra1.searchSolutions()); + + Assert.assertEquals(0, solution1.getUnassignedJobs().size()); + Assert.assertEquals(1, (int)solution1.getRoutes().iterator().next().getActivities().size()); + Assert.assertEquals(40, (int)solution1.getRoutes().iterator().next().getActivities().get(0).getEndTime()); + + // For a vehicle not respecting the condition, the exclusion should not apply. + VehicleRoutingProblem vrp2 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle2) + .addJob(service1) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra2 = Jsprit.createAlgorithm(vrp2); + vra2.setMaxIterations(50); + VehicleRoutingProblemSolution solution2 = Solutions.bestOf(vra2.searchSolutions()); + + Assert.assertEquals(0, solution2.getUnassignedJobs().size()); + Assert.assertEquals(0, (int)solution2.getRoutes().iterator().next().getActivities().get(0).getArrTime()); + + // Finaly, try the different kinds of exclusion + tws = new TimeWindowsOverlapImpl(); + tws.addIncludedTimeWindow(TimeWindow.newInstance(50,100)); + tws.addExcludedTimeWindow(TimeWindowConditionalOnVehicleType.newInstance(0,70, "vehicleType1")); + Service service3 = Service.Builder.newInstance("s1").setLocation(Location.newInstance(0, 0)) + .setServiceTime(0).setTimeWindows(tws).build(); + + VehicleRoutingProblem vrp3 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle1) + .addJob(service3) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra3 = Jsprit.createAlgorithm(vrp3); + vra3.setMaxIterations(50); + VehicleRoutingProblemSolution solution3 = Solutions.bestOf(vra3.searchSolutions()); + + Assert.assertEquals(0, solution3.getUnassignedJobs().size()); + Assert.assertEquals(1, (int)solution3.getRoutes().iterator().next().getActivities().size()); + Assert.assertEquals(70, (int)solution3.getRoutes().iterator().next().getActivities().get(0).getEndTime()); + + // + tws = new TimeWindowsOverlapImpl(); + tws.addIncludedTimeWindow(TimeWindow.newInstance(50,100)); + tws.addExcludedTimeWindow(TimeWindowConditionalOnVehicleType.newInstance(70,150, "vehicleType1")); + Service service4 = Service.Builder.newInstance("s1").setLocation(Location.newInstance(0, 0)) + .setServiceTime(0).setTimeWindows(tws).build(); + + VehicleRoutingProblem vrp4 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle1) + .addJob(service4) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra4 = Jsprit.createAlgorithm(vrp4); + vra4.setMaxIterations(50); + VehicleRoutingProblemSolution solution4 = Solutions.bestOf(vra4.searchSolutions()); + + Assert.assertEquals(0, solution4.getUnassignedJobs().size()); + Assert.assertEquals(1, (int)solution4.getRoutes().iterator().next().getActivities().size()); + Assert.assertEquals(50, (int)solution4.getRoutes().iterator().next().getActivities().get(0).getEndTime()); + + // + tws = new TimeWindowsOverlapImpl(); + tws.addIncludedTimeWindow(TimeWindow.newInstance(50,100)); + tws.addExcludedTimeWindow(TimeWindowConditionalOnVehicleType.newInstance(120,150, "vehicleType1")); + Service service5 = Service.Builder.newInstance("s1").setLocation(Location.newInstance(0, 0)) + .setServiceTime(0).setTimeWindows(tws).build(); + + VehicleRoutingProblem vrp5 = VehicleRoutingProblem.Builder.newInstance() + .addVehicle(vehicle1) + .addJob(service5) + .setFleetSize(VehicleRoutingProblem.FleetSize.FINITE) + .build(); + + VehicleRoutingAlgorithm vra5 = Jsprit.createAlgorithm(vrp5); + vra4.setMaxIterations(50); + VehicleRoutingProblemSolution solution5 = Solutions.bestOf(vra4.searchSolutions()); + + Assert.assertEquals(0, solution5.getUnassignedJobs().size()); + Assert.assertEquals(1, (int)solution5.getRoutes().iterator().next().getActivities().size()); + Assert.assertEquals(50, (int)solution5.getRoutes().iterator().next().getActivities().get(0).getEndTime()); + } +} diff --git a/jsprit-core/src/test/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImplTest.java b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImplTest.java index 0780458bf..8b3795871 100644 --- a/jsprit-core/src/test/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImplTest.java +++ b/jsprit-core/src/test/java/com/graphhopper/jsprit/core/problem/solution/route/activity/TimeWindowsImplTest.java @@ -18,6 +18,10 @@ package com.graphhopper.jsprit.core.problem.solution.route.activity; +import java.util.Collection; +import java.util.Iterator; + +import org.junit.Assert; import org.junit.Test; /** @@ -45,4 +49,50 @@ public void overlappingTW3_shouldThrowException(){ tws.add(TimeWindow.newInstance(50, 100)); tws.add(TimeWindow.newInstance(50, 100)); } + + @Test + public void slightlyOverlappingTw_shouldReturnExclusion() { + TimeWindowsOverlapImpl tws = new TimeWindowsOverlapImpl(); + tws.addExcludedTimeWindow(TimeWindow.newInstance(1695461400, 1695488400)); + tws.addIncludedTimeWindow(TimeWindow.newInstance(1695454200, 1695465300)); + + Collection res = tws.getTimeWindows(null); + + Assert.assertEquals(1, res.size()); + Assert.assertEquals(1695454200, res.iterator().next().getStart(), 1); + Assert.assertEquals(1695461400, res.iterator().next().getEnd(), 1); + } + + @Test + public void entirelyExcludedTW_shouldReturnEmptyList() { + TimeWindowsOverlapImpl tws = new TimeWindowsOverlapImpl(); + tws.addExcludedTimeWindow(TimeWindow.newInstance(10, 100)); + tws.addIncludedTimeWindow(TimeWindow.newInstance(20, 90)); + + Collection res = tws.getTimeWindows(null); + + Assert.assertEquals(0, res.size()); + } + + @Test + public void twoIncludedAndExcludedTW_shouldReturnTwoExclusions() { + TimeWindowsOverlapImpl tws = new TimeWindowsOverlapImpl(); + tws.addExcludedTimeWindow(TimeWindow.newInstance(10, 100)); + tws.addExcludedTimeWindow(TimeWindow.newInstance(120, 200)); + tws.addIncludedTimeWindow(TimeWindow.newInstance(0, 90)); + tws.addIncludedTimeWindow(TimeWindow.newInstance(110, 150)); + + Collection res = tws.getTimeWindows(null); + + Assert.assertEquals(2, res.size()); + Iterator iterator = res.iterator(); + TimeWindow first = iterator.next(); + TimeWindow second = iterator.next(); + + Assert.assertEquals(0, first.getStart(), 1); + Assert.assertEquals(110, second.getStart(), 1); + + Assert.assertEquals(10, first.getEnd(), 1); + Assert.assertEquals(120, second.getEnd(), 1); + } }