Skip to content

Commit

Permalink
docs: Simplify Vehicle Routing Quickstart (#1229)
Browse files Browse the repository at this point in the history
- 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.
  • Loading branch information
TomCools authored Nov 28, 2024
1 parent 65ce797 commit 8c769f7
Show file tree
Hide file tree
Showing 4 changed files with 50 additions and 558 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -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.
Expand Down Expand Up @@ -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`,
Expand All @@ -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":[<LIST OF VISITS ASSIGNED TO CAR 1>],"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":[<LIST OF VISITS ASSIGNED TO CAR 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}
----
Notice that your application assigned all five visits to one of the two vehicles.
Expand Down Expand Up @@ -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<VehicleRoutingConstraintProvider, VehicleRoutePlan> constraintVerifier;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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<VehicleRoutePlan, HardSoftLongScore> {
@Override
public HardSoftLongScore calculateScore(VehicleRoutePlan vehicleRoutePlan) {
List<Vehicle> 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<VehicleRoutePlan, HardSoftLongScore> {
override fun calculateScore(vehicleRoutePlan: VehicleRoutePlan): HardSoftLongScore {
val vehicleList: List<Vehicle> = 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:
Expand All @@ -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 {
Expand All @@ -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);
}
Expand All @@ -179,17 +70,13 @@ 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);
}
protected Constraint minimizeTravelTime(ConstraintFactory factory) {
return factory.forEach(Vehicle.class)
.penalizeLong(HardSoftLongScore.ONE_SOFT,
Vehicle::getTotalDrivingTimeSeconds)
.justifyWith((vehicle, score) -> new MinimizeTravelTimeJustification(vehicle.getId(),
vehicle.getTotalDrivingTimeSeconds()))
.asConstraint(MINIMIZE_TRAVEL_TIME);
}
}
Expand All @@ -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<Constraint> {
Expand All @@ -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)
}
Expand All @@ -246,25 +124,13 @@ 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)
}
protected fun minimizeTravelTime(factory: ConstraintFactory): Constraint {
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)
}
Expand All @@ -277,5 +143,3 @@ class VehicleRoutingConstraintProvider : ConstraintProvider {
----
--
====

The `ConstraintProvider` scales much better than the `EasyScoreCalculator`: typically __O__(n) instead of __O__(n²).
Loading

0 comments on commit 8c769f7

Please sign in to comment.