From 8c769f7ac5c9df0d9aff5016bf7bdd5e71d6fda3 Mon Sep 17 00:00:00 2001 From: Tom Cools Date: Thu, 28 Nov 2024 07:39:47 +0100 Subject: [PATCH] docs: Simplify Vehicle Routing Quickstart (#1229) - Still uses the old @ShadowVariable, changed that to the new @CascadingUpdateShadowVariable and removed quite a bit of code. (Listener class) - The mention of EasyScoreCalculator is weird and inefficient anyway, just left it out. - Constraints used justifications. I consider them extra and wouldn't add them in a quickstart. - Removed a bunch of Getter/Setter methods and left a comment, it's too much noise. People who want to move quick will just check out the quickstart from the repo. --- .../quarkus-vehicle-routing-quickstart.adoc | 7 +- .../vehicle-routing-constraints.adoc | 138 +------ .../vehicle-routing-model.adoc | 343 ++---------------- .../vehicle-routing-solution.adoc | 120 +----- 4 files changed, 50 insertions(+), 558 deletions(-) diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc index 3150c43353..d0654897f9 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/quarkus-vehicle-routing-quickstart.adoc @@ -21,7 +21,7 @@ You will build a REST application that optimizes a Vehicle Route Problem (VRP): image::quickstart/vehicle-routing/vehicleRouteScreenshot.png[] -Your service will assign `Vist` instances to `Vehicle` instances automatically +Your service will assign `Visit` instances to `Vehicle` instances automatically by using AI to adhere to hard and soft scheduling _constraints_, such as the following examples: * The demand for a vehicle cannot exceed its capacity. @@ -486,7 +486,7 @@ The following example uses the Linux command `curl` to send a POST request: [source,shell] ---- -$ curl -i -X POST http://localhost:8080/route-plans/solve -H "Content-Type:application/json" -d '{"name":"demo","southWestCorner":[39.7656099067391,-76.83782328143754],"northEastCorner":[40.77636644354855,-74.9300739430771],"startDateTime":"2024-02-10T07:30:00","endDateTime":"2024-02-11T00:00:00","vehicles":[{"id":"1","capacity":15,"homeLocation":[40.605994321126936,-75.68106859680056],"departureTime":"2024-02-10T07:30:00","visits":[],"totalDrivingTimeSeconds":0,"totalDemand":0,"arrivalTime":"2024-02-10T07:30:00"},{"id":"2","capacity":25,"homeLocation":[40.32196770776356,-75.69785667307953],"departureTime":"2024-02-10T07:30:00","visits":[],"totalDrivingTimeSeconds":0,"totalDemand":0,"arrivalTime":"2024-02-10T07:30:00"}],"visits":[{"id":"1","name":"Dan Green","location":[40.76104493121754,-75.16056341466826],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000,"vehicle":null,"previousVisit":null,"nextVisit":null,"arrivalTime":null,"startServiceTime":null,"departureTime":null,"drivingTimeSecondsFromPreviousStandstill":null},{"id":"2","name":"Ivy King","location":[40.13754381024318,-75.492526629236],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000,"vehicle":null,"previousVisit":null,"nextVisit":null,"arrivalTime":null,"startServiceTime":null,"departureTime":null,"drivingTimeSecondsFromPreviousStandstill":null},{"id":"3","name":"Flo Li","location":[39.87122455090297,-75.64520072015769],"demand":2,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":600.000000000,"vehicle":null,"previousVisit":null,"nextVisit":null,"arrivalTime":null,"startServiceTime":null,"departureTime":null,"drivingTimeSecondsFromPreviousStandstill":null},{"id":"4","name":"Flo Cole","location":[40.46124744193433,-75.18250987609025],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":2400.000000000,"vehicle":null,"previousVisit":null,"nextVisit":null,"arrivalTime":null,"startServiceTime":null,"departureTime":null,"drivingTimeSecondsFromPreviousStandstill":null},{"id":"5","name":"Carl Green","location":[40.61352381171549,-75.83301278355529],"demand":1,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":1800.000000000,"vehicle":null,"previousVisit":null,"nextVisit":null,"arrivalTime":null,"startServiceTime":null,"departureTime":null,"drivingTimeSecondsFromPreviousStandstill":null}],"totalDrivingTimeSeconds":0}' +$ curl -i -X POST http://localhost:8080/route-plans/solve -H "Content-Type:application/json" -d '{"name":"demo","vehicles":[{"id":"1","capacity":15,"homeLocation":[40.605994321126936,-75.68106859680056],"departureTime":"2024-02-10T07:30:00"},{"id":"2","capacity":25,"homeLocation":[40.32196770776356,-75.69785667307953],"departureTime":"2024-02-10T07:30:00"}],"visits":[{"id":"1","name":"Dan Green","location":[40.76104493121754,-75.16056341466826],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200},{"id":"2","name":"Ivy King","location":[40.13754381024318,-75.492526629236],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000},{"id":"3","name":"Flo Li","location":[39.87122455090297,-75.64520072015769],"demand":2,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":600.000000000},{"id":"4","name":"Flo Cole","location":[40.46124744193433,-75.18250987609025],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":2400.000000000},{"id":"5","name":"Carl Green","location":[40.61352381171549,-75.83301278355529],"demand":1,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":1800.000000000}]}' ---- After about five seconds, according to the termination spent time defined in your `application.properties`, @@ -498,7 +498,7 @@ HTTP/1.1 200 Content-Type: application/json ... -{"name":"demo","southWestCorner":[39.7656099067391,-76.83782328143754],"northEastCorner":[40.77636644354855,-74.9300739430771],"startDateTime":"2024-02-10T07:30:00","endDateTime":"2024-02-11T00:00:00","vehicles":[{"id":"1","capacity":15,"homeLocation":[40.605994321126936,-75.68106859680056],"departureTime":"2024-02-10T07:30:00","visits":["5","1","4"],"arrivalTime":"2024-02-10T15:34:11","totalDemand":3,"totalDrivingTimeSeconds":10826},{"id":"2","capacity":25,"homeLocation":[40.32196770776356,-75.69785667307953],"departureTime":"2024-02-10T07:30:00","visits":["3","2"],"arrivalTime":"2024-02-10T13:52:18","totalDemand":3,"totalDrivingTimeSeconds":7890}],"visits":[{"id":"1","name":"Dan Green","location":[40.76104493121754,-75.16056341466826],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000,"vehicle":"1","previousVisit":"5","nextVisit":"4","arrivalTime":"2024-02-10T09:40:50","startServiceTime":"2024-02-10T13:00:00","departureTime":"2024-02-10T13:20:00","drivingTimeSecondsFromPreviousStandstill":4250},{"id":"2","name":"Ivy King","location":[40.13754381024318,-75.492526629236],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000,"vehicle":"2","previousVisit":"3","nextVisit":null,"arrivalTime":"2024-02-10T09:19:12","startServiceTime":"2024-02-10T13:00:00","departureTime":"2024-02-10T13:20:00","drivingTimeSecondsFromPreviousStandstill":2329},{"id":"3","name":"Flo Li","location":[39.87122455090297,-75.64520072015769],"demand":2,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":600.000000000,"vehicle":"2","previousVisit":null,"nextVisit":"2","arrivalTime":"2024-02-10T08:30:23","startServiceTime":"2024-02-10T08:30:23","departureTime":"2024-02-10T08:40:23","drivingTimeSecondsFromPreviousStandstill":3623},{"id":"4","name":"Flo Cole","location":[40.46124744193433,-75.18250987609025],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":2400.000000000,"vehicle":"1","previousVisit":"1","nextVisit":null,"arrivalTime":"2024-02-10T14:00:04","startServiceTime":"2024-02-10T14:00:04","departureTime":"2024-02-10T14:40:04","drivingTimeSecondsFromPreviousStandstill":2404},{"id":"5","name":"Carl Green","location":[40.61352381171549,-75.83301278355529],"demand":1,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":1800.000000000,"vehicle":"1","previousVisit":null,"nextVisit":"1","arrivalTime":"2024-02-10T07:45:25","startServiceTime":"2024-02-10T08:00:00","departureTime":"2024-02-10T08:30:00","drivingTimeSecondsFromPreviousStandstill":925}],"score":"0hard/-18716soft","totalDrivingTimeSeconds":18716} +{"name":"demo","vehicles":[{"id":"1","capacity":15,"homeLocation":[40.605994321126936,-75.68106859680056],"departureTime":"2024-02-10T07:30:00","visits":[],"arrivalTime":"2024-02-10T15:34:11","totalDemand":3,"totalDrivingTimeSeconds":10826},{"id":"2","capacity":25,"homeLocation":[40.32196770776356,-75.69785667307953],"departureTime":"2024-02-10T07:30:00","visits":[],"arrivalTime":"2024-02-10T13:52:18","totalDemand":3,"totalDrivingTimeSeconds":7890}],"visits":[{"id":"1","name":"Dan Green","location":[40.76104493121754,-75.16056341466826],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000,"vehicle":"1","previousVisit":"5","nextVisit":"4","arrivalTime":"2024-02-10T09:40:50","startServiceTime":"2024-02-10T13:00:00","departureTime":"2024-02-10T13:20:00","drivingTimeSecondsFromPreviousStandstill":4250},{"id":"2","name":"Ivy King","location":[40.13754381024318,-75.492526629236],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":1200.000000000,"vehicle":"2","previousVisit":"3","nextVisit":null,"arrivalTime":"2024-02-10T09:19:12","startServiceTime":"2024-02-10T13:00:00","departureTime":"2024-02-10T13:20:00","drivingTimeSecondsFromPreviousStandstill":2329},{"id":"3","name":"Flo Li","location":[39.87122455090297,-75.64520072015769],"demand":2,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":600.000000000,"vehicle":"2","previousVisit":null,"nextVisit":"2","arrivalTime":"2024-02-10T08:30:23","startServiceTime":"2024-02-10T08:30:23","departureTime":"2024-02-10T08:40:23","drivingTimeSecondsFromPreviousStandstill":3623},{"id":"4","name":"Flo Cole","location":[40.46124744193433,-75.18250987609025],"demand":1,"minStartTime":"2024-02-10T13:00:00","maxEndTime":"2024-02-10T18:00:00","serviceDuration":2400.000000000,"vehicle":"1","previousVisit":"1","nextVisit":null,"arrivalTime":"2024-02-10T14:00:04","startServiceTime":"2024-02-10T14:00:04","departureTime":"2024-02-10T14:40:04","drivingTimeSecondsFromPreviousStandstill":2404},{"id":"5","name":"Carl Green","location":[40.61352381171549,-75.83301278355529],"demand":1,"minStartTime":"2024-02-10T08:00:00","maxEndTime":"2024-02-10T12:00:00","serviceDuration":1800.000000000,"vehicle":"1","previousVisit":null,"nextVisit":"1","arrivalTime":"2024-02-10T07:45:25","startServiceTime":"2024-02-10T08:00:00","departureTime":"2024-02-10T08:30:00","drivingTimeSecondsFromPreviousStandstill":925}],"score":"0hard/-18716soft","totalDrivingTimeSeconds":18716} ---- Notice that your application assigned all five visits to one of the two vehicles. @@ -588,6 +588,7 @@ class VehicleRoutingConstraintProviderTest { private static final Location LOCATION_3 = new Location(49.1767533245638, 16.50422914190477); private static final LocalDate TOMORROW = LocalDate.now().plusDays(1); + @Inject ConstraintVerifier constraintVerifier; diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-constraints.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-constraints.adoc index db25cb22ee..dbefeaed1c 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-constraints.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-constraints.adoc @@ -18,111 +18,7 @@ Hard constraints are weighted against other hard constraints. Soft constraints are weighted too, against other soft constraints. *Hard constraints always outweigh soft constraints*, regardless of their respective weights. -To calculate the score, you could implement an `EasyScoreCalculator` class: - -[tabs] -==== -Java:: -+ --- -[source,java] ----- -package org.acme.vehiclerouting.solver; - -import java.util.List; - -import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore; -import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator; - -import org.acme.vehiclerouting.domain.Vehicle; -import org.acme.vehiclerouting.domain.VehicleRoutePlan; -import org.acme.vehiclerouting.domain.Visit; - -public class VehicleRoutingEasyScoreCalculator implements EasyScoreCalculator { - @Override - public HardSoftLongScore calculateScore(VehicleRoutePlan vehicleRoutePlan) { - - List vehicleList = vehicleRoutePlan.getVehicles(); - - int hardScore = 0; - int softScore = 0; - for (Vehicle vehicle : vehicleList) { - - // The demand exceeds the capacity - if (vehicle.getVisits() != null && vehicle.getTotalDemand() > vehicle.getCapacity()) { - hardScore -= vehicle.getTotalDemand() - vehicle.getCapacity(); - } - - // Max end-time not met - if (vehicle.getVisits() != null) { - for (Visit visit: vehicle.getVisits()) { - if (visit.isServiceFinishedAfterMaxEndTime()) { - hardScore -= visit.getServiceFinishedDelayInMinutes(); - } - } - } - - softScore -= (int) vehicle.getTotalDrivingTimeSeconds(); - } - - return HardSoftLongScore.of(hardScore, softScore); - } -} ----- --- - -Kotlin:: -+ --- -[source,kotlin] ----- -package org.acme.vehiclerouting.solver; - -import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore -import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator - -import org.acme.vehiclerouting.domain.Vehicle -import org.acme.vehiclerouting.domain.VehicleRoutePlan - -class VehicleRoutingEasyScoreCalculator : - EasyScoreCalculator { - override fun calculateScore(vehicleRoutePlan: VehicleRoutePlan): HardSoftLongScore { - val vehicleList: List = vehicleRoutePlan.vehicles!! - - var hardScore = 0 - var softScore = 0 - for (vehicle in vehicleList) { - // The demand exceeds the capacity - - if (vehicle.visits != null && vehicle.totalDemand > vehicle.capacity) { - hardScore -= (vehicle.totalDemand - vehicle.capacity).toInt() - } - - // Max end-time not met - if (vehicle.visits != null) { - for (visit in vehicle.visits!!) { - if (visit.isServiceFinishedAfterMaxEndTime) { - hardScore -= visit.serviceFinishedDelayInMinutes.toInt() - } - } - } - - softScore -= vehicle.totalDrivingTimeSeconds.toInt() - } - - return HardSoftLongScore.of(hardScore.toLong(), softScore.toLong()) - } -} ----- --- -==== - - -Unfortunately **that does not scale well**, because it is non-incremental: -every time a visit is scheduled to a different vehicle, -all visits are re-evaluated to calculate the new score. - -Instead, create a `VehicleRoutingConstraintProvider` class +To calculate the score, create a `VehicleRoutingConstraintProvider` class to perform incremental score calculation. It uses Timefold Solver's xref:constraints-and-score/score-calculation.adoc[Constraint Streams API] which is inspired by Java Streams and SQL: @@ -145,9 +41,6 @@ import ai.timefold.solver.core.api.score.stream.ConstraintProvider; import org.acme.vehiclerouting.domain.Visit; import org.acme.vehiclerouting.domain.Vehicle; -import org.acme.vehiclerouting.solver.justifications.MinimizeTravelTimeJustification; -import org.acme.vehiclerouting.solver.justifications.ServiceFinishedAfterMaxEndTimeJustification; -import org.acme.vehiclerouting.solver.justifications.VehicleCapacityJustification; public class VehicleRoutingConstraintProvider implements ConstraintProvider { @@ -169,8 +62,6 @@ public class VehicleRoutingConstraintProvider implements ConstraintProvider { .filter(vehicle -> vehicle.getTotalDemand() > vehicle.getCapacity()) .penalizeLong(HardSoftLongScore.ONE_HARD, vehicle -> vehicle.getTotalDemand() - vehicle.getCapacity()) - .justifyWith((vehicle, score) -> new VehicleCapacityJustification(vehicle.getId(), vehicle.getTotalDemand(), - vehicle.getCapacity())) .asConstraint(VEHICLE_CAPACITY); } @@ -179,8 +70,6 @@ public class VehicleRoutingConstraintProvider implements ConstraintProvider { .filter(Visit::isServiceFinishedAfterMaxEndTime) .penalizeLong(HardSoftLongScore.ONE_HARD, Visit::getServiceFinishedDelayInMinutes) - .justifyWith((visit, score) -> new ServiceFinishedAfterMaxEndTimeJustification(visit.getId(), - visit.getServiceFinishedDelayInMinutes())) .asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME); } @@ -188,8 +77,6 @@ public class VehicleRoutingConstraintProvider implements ConstraintProvider { return factory.forEach(Vehicle.class) .penalizeLong(HardSoftLongScore.ONE_SOFT, Vehicle::getTotalDrivingTimeSeconds) - .justifyWith((vehicle, score) -> new MinimizeTravelTimeJustification(vehicle.getId(), - vehicle.getTotalDrivingTimeSeconds())) .asConstraint(MINIMIZE_TRAVEL_TIME); } } @@ -213,9 +100,6 @@ import ai.timefold.solver.core.api.score.stream.ConstraintProvider import org.acme.vehiclerouting.domain.Visit import org.acme.vehiclerouting.domain.Vehicle -import org.acme.vehiclerouting.solver.justifications.MinimizeTravelTimeJustification -import org.acme.vehiclerouting.solver.justifications.ServiceFinishedAfterMaxEndTimeJustification -import org.acme.vehiclerouting.solver.justifications.VehicleCapacityJustification class VehicleRoutingConstraintProvider : ConstraintProvider { override fun defineConstraints(factory: ConstraintFactory): Array { @@ -232,12 +116,6 @@ class VehicleRoutingConstraintProvider : ConstraintProvider { .penalizeLong( HardSoftLongScore.ONE_HARD ) { vehicle: Vehicle -> vehicle.totalDemand - vehicle.capacity } - .justifyWith({ vehicle: Vehicle, score: HardSoftLongScore? -> - VehicleCapacityJustification( - vehicle.id, vehicle.totalDemand.toInt(), - vehicle.capacity - ) - }) .asConstraint(VEHICLE_CAPACITY) } @@ -246,12 +124,6 @@ class VehicleRoutingConstraintProvider : ConstraintProvider { .filter({ obj: Visit -> obj.isServiceFinishedAfterMaxEndTime }) .penalizeLong(HardSoftLongScore.ONE_HARD, { obj: Visit -> obj.serviceFinishedDelayInMinutes }) - .justifyWith({ visit: Visit, score: HardSoftLongScore? -> - ServiceFinishedAfterMaxEndTimeJustification( - visit.id, - visit.serviceFinishedDelayInMinutes - ) - }) .asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME) } @@ -259,12 +131,6 @@ class VehicleRoutingConstraintProvider : ConstraintProvider { return factory.forEach(Vehicle::class.java) .penalizeLong(HardSoftLongScore.ONE_SOFT, { obj: Vehicle -> obj.totalDrivingTimeSeconds }) - .justifyWith({ vehicle: Vehicle, score: HardSoftLongScore? -> - MinimizeTravelTimeJustification( - vehicle.id, - vehicle.totalDrivingTimeSeconds - ) - }) .asConstraint(MINIMIZE_TRAVEL_TIME) } @@ -277,5 +143,3 @@ class VehicleRoutingConstraintProvider : ConstraintProvider { ---- -- ==== - -The `ConstraintProvider` scales much better than the `EasyScoreCalculator`: typically __O__(n) instead of __O__(n²). \ No newline at end of file diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc index bbc24f9d46..b82e01dba8 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-model.adoc @@ -10,6 +10,8 @@ image::quickstart/vehicle-routing/vehicleRoutingClassDiagramPure.png[] == Location The `Location` class is used to represent the destination for deliveries or the home location for vehicles. +The `drivingTimeSeconds` map contains the time required to drive from `this` location to any other location. +This field will be initialized later. [tabs] ==== @@ -146,41 +148,7 @@ public class Vehicle { this.visits = new ArrayList<>(); } - public String getId() { - return id; - } - - public void setId(String id) { - this.id = id; - } - - public int getCapacity() { - return capacity; - } - - public void setCapacity(int capacity) { - this.capacity = capacity; - } - - public Location getHomeLocation() { - return homeLocation; - } - - public void setHomeLocation(Location homeLocation) { - this.homeLocation = homeLocation; - } - - public LocalDateTime getDepartureTime() { - return departureTime; - } - - public List getVisits() { - return visits; - } - - public void setVisits(List visits) { - this.visits = visits; - } + // Getters and Setters excluded public int getTotalDemand() { int totalDemand = 0; @@ -306,12 +274,15 @@ The `Visit` class represents a delivery that needs to be made by vehicles. A visit includes a destination location, a delivery time window represented by `[minStartTime, maxEndTime]`, a demand that needs to be fulfilled by the vehicle, and a service duration time. +The `Visit` class has an `@PlanningEntity` annotation +but no genuine variables and is called a https://timefold.ai/docs/timefold-solver/latest/using-timefold-solver/modeling-planning-problems#shadowVariable[shadow entity]. + [tabs] ==== Java:: + -- -Create the `src/main/java/org/acme/vehiclerouting/domain/Visit.java` class: +Create or adjust the `src/main/java/org/acme/vehiclerouting/domain/Visit.java` class: [source,java] ---- @@ -341,12 +312,13 @@ public class Visit { private LocalDateTime maxEndTime; private Duration serviceDuration; + @InverseRelationShadowVariable(sourceVariableName = "visits") private Vehicle vehicle; + @PreviousElementShadowVariable(sourceVariableName = "visits") private Visit previousVisit; - private Visit nextVisit; - + @CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime") private LocalDateTime arrivalTime; public Visit() { @@ -363,81 +335,15 @@ public class Visit { this.serviceDuration = serviceDuration; } - public String getId() { - return id; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public Location getLocation() { - return location; - } - - public void setLocation(Location location) { - this.location = location; - } - - public int getDemand() { - return demand; - } - - public void setDemand(int demand) { - this.demand = demand; - } - - public LocalDateTime getMinStartTime() { - return minStartTime; - } - - public LocalDateTime getMaxEndTime() { - return maxEndTime; - } - - public Duration getServiceDuration() { - return serviceDuration; - } - - @InverseRelationShadowVariable(sourceVariableName = "visits") - public Vehicle getVehicle() { - return vehicle; - } - - public void setVehicle(Vehicle vehicle) { - this.vehicle = vehicle; - } - - @PreviousElementShadowVariable(sourceVariableName = "visits") - public Visit getPreviousVisit() { - return previousVisit; - } - - public void setPreviousVisit(Visit previousVisit) { - this.previousVisit = previousVisit; - } - - @NextElementShadowVariable(sourceVariableName = "visits") - public Visit getNextVisit() { - return nextVisit; - } + // Getters and Setters excluded - public void setNextVisit(Visit nextVisit) { - this.nextVisit = nextVisit; - } - - @ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "vehicle") - @ShadowVariable(variableListenerClass = ArrivalTimeUpdatingVariableListener.class, sourceVariableName = "previousVisit") - public LocalDateTime getArrivalTime() { - return arrivalTime; - } - - public void setArrivalTime(LocalDateTime arrivalTime) { - this.arrivalTime = arrivalTime; + private void updateArrivalTime() { + if (previousVisit == null && vehicle == null) { + arrivalTime = null; + return; + } + LocalDateTime departureTime = previousVisit == null ? vehicle.getDepartureTime() : previousVisit.getDepartureTime(); + arrivalTime = departureTime != null ? departureTime.plusSeconds(getDrivingTimeSecondsFromPreviousStandstill()) : null; } public LocalDateTime getDepartureTime() { @@ -517,22 +423,13 @@ class Visit { lateinit var maxEndTime: LocalDateTime lateinit var serviceDuration: Duration + @InverseRelationShadowVariable(sourceVariableName = "visits") private var vehicle: Vehicle? = null - @get:PreviousElementShadowVariable(sourceVariableName = "visits") + @PreviousElementShadowVariable(sourceVariableName = "visits") var previousVisit: Visit? = null - @get:NextElementShadowVariable(sourceVariableName = "visits") - var nextVisit: Visit? = null - - @get:ShadowVariable( - variableListenerClass = ArrivalTimeUpdatingVariableListener::class, - sourceVariableName = "previousVisit" - ) - @get:ShadowVariable( - variableListenerClass = ArrivalTimeUpdatingVariableListener::class, - sourceVariableName = "vehicle" - ) + @CascadingUpdateShadowVariable(targetMethodName = "updateArrivalTime") var arrivalTime: LocalDateTime? = null constructor() @@ -550,13 +447,13 @@ class Visit { this.serviceDuration = serviceDuration } - @InverseRelationShadowVariable(sourceVariableName = "visits") - fun getVehicle(): Vehicle? { - return vehicle - } - - fun setVehicle(vehicle: Vehicle?) { - this.vehicle = vehicle + private fun updateArrivalTime() { + if (previousVisit == null && vehicle == null) { + arrivalTime = null + return + } + val departureTime = previousVisit?.departureTime ?: vehicle?.departureTime + arrivalTime = departureTime?.plusSeconds(getDrivingTimeSecondsFromPreviousStandstill()) } val departureTime: LocalDateTime? @@ -608,193 +505,27 @@ class Visit { -- ==== -Some methods are annotated with `@InverseRelationShadowVariable`, `@PreviousElementShadowVariable`, -`@NextElementShadowVariable`, and `@ShadowVariable`. +Some methods are annotated with `@InverseRelationShadowVariable`, `@PreviousElementShadowVariable` and `@CascadingUpdateShadowVariable`. They are called https://timefold.ai/docs/timefold-solver/latest/using-timefold-solver/modeling-planning-problems#shadowVariable[shadow variables], and because Timefold Solver changes them, `Visit` is a https://timefold.ai/docs/timefold-solver/latest/using-timefold-solver/modeling-planning-problems#planningEntity[_planning entity_]: image::quickstart/vehicle-routing/vehicleRoutingCompleteClassDiagramAnnotated.png[] -The method `getVehicle()` has an `@InverseRelationShadowVariable` annotation, +The field `vehicle` has an `@InverseRelationShadowVariable` annotation, creating a bi-directional relationship with the `Vehicle`. The function returns a reference to the `Vehicle` where the visit is scheduled. Let's say the visit `Ann` was scheduled to the vehicle `V1` during the solving process. The method returns a reference of `V1`. -The methods `getPreviousVisit()` and `getNextVisit()` are annotated with `@PreviousElementShadowVariable` and -`@NextElementShadowVariable`, respectively. -The method returns a reference of the previous and next visit of the current visit instance. +The field `previousVisit` is annotated with `@PreviousElementShadowVariable`. +The solver will update this field with a reference of the visit preceding the current visit instance. Assuming that vehicle `V1` is assigned the visits of `Ann`, `Beth`, and `Carl`, -the `getNextVisit()` method returns `Carl`, -and the `getPreviousVisit()` method returns `Ann` for the visit of `Beth`. - -The method `getArrivalTime()` has two `@ShadowVariable` annotations, -one per each variable: `vehicle` and `previousVisit`. -The solver triggers `ArrivalTimeUpdatingVariableListener` to update `arrivalTime` field every time the fields `vehicle` -or `previousVisit` get updated. - -The `Visit` class has an `@PlanningEntity` annotation -but no genuine variables and is called https://timefold.ai/docs/timefold-solver/latest/using-timefold-solver/modeling-planning-problems#shadowVariable[shadow entity]. - -[tabs] -==== -Java:: -+ --- -Create the `src/main/java/org/acme/vehiclerouting/solver/ArrivalTimeUpdatingVariableListener.java` class: - -[source,java] ----- -package org.acme.vehiclerouting.solver; - -import java.time.LocalDateTime; -import java.util.Objects; - -import ai.timefold.solver.core.api.domain.variable.VariableListener; -import ai.timefold.solver.core.api.score.director.ScoreDirector; - -import org.acme.vehiclerouting.domain.Visit; -import org.acme.vehiclerouting.domain.VehicleRoutePlan; - -public class ArrivalTimeUpdatingVariableListener implements VariableListener { - - private static final String ARRIVAL_TIME_FIELD = "arrivalTime"; - - @Override - public void beforeVariableChanged(ScoreDirector scoreDirector, Visit visit) { - - } - - @Override - public void afterVariableChanged(ScoreDirector scoreDirector, Visit visit) { - if (visit.getVehicle() == null) { - if (visit.getArrivalTime() != null) { - scoreDirector.beforeVariableChanged(visit, ARRIVAL_TIME_FIELD); - visit.setArrivalTime(null); - scoreDirector.afterVariableChanged(visit, ARRIVAL_TIME_FIELD); - } - return; - } - - Visit previousVisit = visit.getPreviousVisit(); - LocalDateTime departureTime = - previousVisit == null ? visit.getVehicle().getDepartureTime() : previousVisit.getDepartureTime(); - - Visit nextVisit = visit; - LocalDateTime arrivalTime = calculateArrivalTime(nextVisit, departureTime); - while (nextVisit != null && !Objects.equals(nextVisit.getArrivalTime(), arrivalTime)) { - scoreDirector.beforeVariableChanged(nextVisit, ARRIVAL_TIME_FIELD); - nextVisit.setArrivalTime(arrivalTime); - scoreDirector.afterVariableChanged(nextVisit, ARRIVAL_TIME_FIELD); - departureTime = nextVisit.getDepartureTime(); - nextVisit = nextVisit.getNextVisit(); - arrivalTime = calculateArrivalTime(nextVisit, departureTime); - } - } - - @Override - public void beforeEntityAdded(ScoreDirector scoreDirector, Visit visit) { - - } - - @Override - public void afterEntityAdded(ScoreDirector scoreDirector, Visit visit) { - - } - - @Override - public void beforeEntityRemoved(ScoreDirector scoreDirector, Visit visit) { - - } - - @Override - public void afterEntityRemoved(ScoreDirector scoreDirector, Visit visit) { - - } - - private LocalDateTime calculateArrivalTime(Visit visit, LocalDateTime previousDepartureTime) { - if (visit == null || previousDepartureTime == null) { - return null; - } - return previousDepartureTime.plusSeconds(visit.getDrivingTimeSecondsFromPreviousStandstill()); - } -} ----- --- +the `previousVisit` field will be filled with `Ann` for the visit of `Beth`. -Kotlin:: -+ --- -Create the `src/main/kotlin/org/acme/vehiclerouting/solver/ArrivalTimeUpdatingVariableListener.kt` class: - -[source,kotlin] ----- -package org.acme.vehiclerouting.solver +NOTE: `@NextElementShadowVariable` also exists, which can be used to get a reference to the successor element. -import java.time.LocalDateTime - -import ai.timefold.solver.core.api.domain.variable.VariableListener -import ai.timefold.solver.core.api.score.director.ScoreDirector - -import org.acme.vehiclerouting.domain.Visit -import org.acme.vehiclerouting.domain.VehicleRoutePlan - -class ArrivalTimeUpdatingVariableListener : VariableListener { - - override fun beforeVariableChanged(scoreDirector: ScoreDirector, visit: Visit) { - } - - override fun afterVariableChanged(scoreDirector: ScoreDirector, visit: Visit) { - if (visit.getVehicle() == null) { - if (visit.arrivalTime != null) { - scoreDirector.beforeVariableChanged(visit, ARRIVAL_TIME_FIELD) - visit.arrivalTime = null - scoreDirector.afterVariableChanged(visit, ARRIVAL_TIME_FIELD) - } - return - } +The `arrivalTime` field has a `@CascadingUpdateShadowVariable` annotation. +This annotation indicates which method should be triggered to update this field whenever this entity is moved, in this case the `updateArrivalTime()` method. +This change is automatically propagated to the subsequent visits and stops when the `arrivalTime` value hasn't changed or when it's reached the end of the chain of visit objects. - val previousVisit: Visit? = visit.previousVisit - var departureTime: LocalDateTime? = - if (previousVisit == null) visit.getVehicle()!!.departureTime else previousVisit.departureTime - - var nextVisit: Visit? = visit - var arrivalTime = calculateArrivalTime(nextVisit, departureTime) - while (nextVisit != null && nextVisit.arrivalTime != arrivalTime) { - scoreDirector.beforeVariableChanged(nextVisit, ARRIVAL_TIME_FIELD) - nextVisit.arrivalTime = arrivalTime - scoreDirector.afterVariableChanged(nextVisit, ARRIVAL_TIME_FIELD) - departureTime = nextVisit.departureTime - nextVisit = nextVisit.nextVisit - arrivalTime = calculateArrivalTime(nextVisit, departureTime) - } - } - - override fun beforeEntityAdded(scoreDirector: ScoreDirector?, visit: Visit?) { - } - - override fun afterEntityAdded(scoreDirector: ScoreDirector?, visit: Visit?) { - } - - override fun beforeEntityRemoved(scoreDirector: ScoreDirector?, visit: Visit?) { - } - - override fun afterEntityRemoved(scoreDirector: ScoreDirector?, visit: Visit?) { - } - - private fun calculateArrivalTime(visit: Visit?, previousDepartureTime: LocalDateTime?): LocalDateTime? { - if (visit == null || previousDepartureTime == null) { - return null - } - return previousDepartureTime.plusSeconds(visit.drivingTimeSecondsFromPreviousStandstill) - } - - - companion object { - private const val ARRIVAL_TIME_FIELD = "arrivalTime" - } -} ----- --- -==== diff --git a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc index c01655f814..74ee74235e 100644 --- a/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc +++ b/docs/src/modules/ROOT/pages/quickstart/quarkus-vehicle-routing/vehicle-routing-solution.adoc @@ -41,15 +41,6 @@ import org.acme.vehiclerouting.domain.geo.HaversineDrivingTimeCalculator; @PlanningSolution public class VehicleRoutePlan { - private String name; - - private Location southWestCorner; - private Location northEastCorner; - - private LocalDateTime startDateTime; - - private LocalDateTime endDateTime; - @PlanningEntityCollectionProperty private List vehicles; @@ -60,33 +51,19 @@ public class VehicleRoutePlan { @PlanningScore private HardSoftLongScore score; - private SolverStatus solverStatus; - - private String scoreExplanation; + // Fields and constructors used for visualization excluded public VehicleRoutePlan() { } - public VehicleRoutePlan(String name, HardSoftLongScore score, SolverStatus solverStatus) { - this.name = name; - this.score = score; - this.solverStatus = solverStatus; - } - public VehicleRoutePlan(String name, - Location southWestCorner, - Location northEastCorner, - LocalDateTime startDateTime, - LocalDateTime endDateTime, List vehicles, List visits) { this.name = name; - this.southWestCorner = southWestCorner; - this.northEastCorner = northEastCorner; - this.startDateTime = startDateTime; - this.endDateTime = endDateTime; this.vehicles = vehicles; this.visits = visits; + + // Enhance locations with a pre-calculated driving time map List locations = Stream.concat( vehicles.stream().map(Vehicle::getHomeLocation), visits.stream().map(Visit::getLocation)).toList(); @@ -95,61 +72,7 @@ public class VehicleRoutePlan { drivingTimeCalculator.initDrivingTimeMaps(locations); } - public String getName() { - return name; - } - - public Location getSouthWestCorner() { - return southWestCorner; - } - - public Location getNorthEastCorner() { - return northEastCorner; - } - - public LocalDateTime getStartDateTime() { - return startDateTime; - } - - public LocalDateTime getEndDateTime() { - return endDateTime; - } - - public List getVehicles() { - return vehicles; - } - - public List getVisits() { - return visits; - } - - public HardSoftLongScore getScore() { - return score; - } - - public void setScore(HardSoftLongScore score) { - this.score = score; - } - - public long getTotalDrivingTimeSeconds() { - return vehicles == null ? 0 : vehicles.stream().mapToLong(Vehicle::getTotalDrivingTimeSeconds).sum(); - } - - public SolverStatus getSolverStatus() { - return solverStatus; - } - - public void setSolverStatus(SolverStatus solverStatus) { - this.solverStatus = solverStatus; - } - - public String getScoreExplanation() { - return scoreExplanation; - } - - public void setScoreExplanation(String scoreExplanation) { - this.scoreExplanation = scoreExplanation; - } + // Getters and Setters excluded } ---- -- @@ -179,14 +102,6 @@ import org.acme.vehiclerouting.domain.geo.HaversineDrivingTimeCalculator @PlanningSolution class VehicleRoutePlan { lateinit var name: String - var southWestCorner: Location? = null - private set - var northEastCorner: Location? = null - private set - var startDateTime: LocalDateTime? = null - private set - var endDateTime: LocalDateTime? = null - private set @PlanningEntityCollectionProperty var vehicles: List? = null @@ -200,34 +115,20 @@ class VehicleRoutePlan { @PlanningScore var score: HardSoftLongScore? = null - var solverStatus: SolverStatus? = null - - var scoreExplanation: String? = null + // Fields and constructors used for visualization excluded constructor() - constructor(name: String, score: HardSoftLongScore?, solverStatus: SolverStatus?) { - this.name = name - this.score = score - this.solverStatus = solverStatus - } - constructor( name: String, - southWestCorner: Location?, - northEastCorner: Location?, - startDateTime: LocalDateTime?, - endDateTime: LocalDateTime?, vehicles: List, visits: List ) { this.name = name - this.southWestCorner = southWestCorner - this.northEastCorner = northEastCorner - this.startDateTime = startDateTime - this.endDateTime = endDateTime this.vehicles = vehicles this.visits = visits + + // Enhance locations with a pre-calculated driving time map val locations = Stream.concat( vehicles.stream().map({ obj: Vehicle -> obj.homeLocation }), visits.stream().map({ obj: Visit -> obj.location }) @@ -236,10 +137,6 @@ class VehicleRoutePlan { val drivingTimeCalculator: DrivingTimeCalculator = HaversineDrivingTimeCalculator.INSTANCE drivingTimeCalculator.initDrivingTimeMaps(locations) } - - val totalDrivingTimeSeconds: Long - get() = if (vehicles == null) 0 else vehicles!!.stream() - .mapToLong({ obj: Vehicle -> obj.totalDrivingTimeSeconds }).sum() } ---- -- @@ -280,8 +177,7 @@ by matching the type of the planning list variable with the type returned by the == Distance calculation -The distance calculation method applies the Haversine approach, -which measures distances in meters. +A matrix of distances between each location is typically calculated before starting the solver. First create a contract for driving time calculation: [tabs]