Skip to content

Commit dc71df8

Browse files
authored
feat: Adding VRP quickstart documentation (#623)
This pull request adds documentation for VRP quickstart using Quarkus. The structure of the document is based on existing quickstart documents.
1 parent 0098847 commit dc71df8

File tree

10 files changed

+2529
-0
lines changed

10 files changed

+2529
-0
lines changed
1.79 MB
Loading
141 KB
Loading
133 KB
Loading
171 KB
Loading

docs/src/modules/ROOT/pages/_attributes.adoc

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,3 +4,4 @@
44
:hello-world-java-quickstart-url: https://github.com/TimefoldAI/timefold-quickstarts/tree/stable/hello-world
55
:spring-boot-quickstart-url: https://github.com/TimefoldAI/timefold-quickstarts/tree/stable/technology/java-spring-boot
66
:quarkus-quickstart-url: https://github.com/TimefoldAI/timefold-quickstarts/tree/stable/use-cases/school-timetabling
7+
:vrp-quickstart-url: https://github.com/TimefoldAI/timefold-quickstarts/tree/stable/use-cases/vehicle-routing

docs/src/modules/ROOT/pages/quickstart/.quickstart.adoc

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,3 +11,5 @@ include::hello-world/hello-world-quickstart.adoc[leveloffset=+1]
1111
include::quarkus/quarkus-quickstart.adoc[leveloffset=+1]
1212

1313
include::spring-boot/spring-boot-quickstart.adoc[leveloffset=+1]
14+
15+
include::vrp-quarkus/vrp-quarkus-quickstart.adoc[leveloffset=+1]
Lines changed: 281 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,281 @@
1+
= Define the constraints and calculate the score
2+
:imagesdir: ../..
3+
4+
A _score_ represents the quality of a specific solution.
5+
The higher the better.
6+
Timefold Solver looks for the best solution, which is the solution with the highest score found in the available time.
7+
It might be the _optimal_ solution.
8+
9+
Because this use case has hard and soft constraints,
10+
use the `HardSoftScore` class to represent the score:
11+
12+
* Hard constraints must not be broken.
13+
For example: _The vehicle capacity must not be exceeded._
14+
* Soft constraints should not be broken.
15+
For example: _The sum total of travel time._
16+
17+
Hard constraints are weighted against other hard constraints.
18+
Soft constraints are weighted too, against other soft constraints.
19+
*Hard constraints always outweigh soft constraints*, regardless of their respective weights.
20+
21+
To calculate the score, you could implement an `EasyScoreCalculator` class:
22+
23+
[tabs]
24+
====
25+
Java::
26+
+
27+
--
28+
[source,java]
29+
----
30+
package org.acme.vehiclerouting.solver;
31+
32+
import java.util.List;
33+
34+
import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
35+
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator;
36+
37+
import org.acme.vehiclerouting.domain.Vehicle;
38+
import org.acme.vehiclerouting.domain.VehicleRoutePlan;
39+
import org.acme.vehiclerouting.domain.Visit;
40+
41+
public class VehicleRoutingEasyScoreCalculator implements EasyScoreCalculator<VehicleRoutePlan, HardSoftLongScore> {
42+
@Override
43+
public HardSoftLongScore calculateScore(VehicleRoutePlan vehicleRoutePlan) {
44+
45+
List<Vehicle> vehicleList = vehicleRoutePlan.getVehicles();
46+
47+
int hardScore = 0;
48+
int softScore = 0;
49+
for (Vehicle vehicle : vehicleList) {
50+
51+
// The demand exceeds the capacity
52+
if (vehicle.getVisits() != null && vehicle.getTotalDemand() > vehicle.getCapacity()) {
53+
hardScore -= vehicle.getTotalDemand() - vehicle.getCapacity();
54+
}
55+
56+
// Max end-time not met
57+
if (vehicle.getVisits() != null) {
58+
for (Visit visit: vehicle.getVisits()) {
59+
if (visit.isServiceFinishedAfterMaxEndTime()) {
60+
hardScore -= visit.getServiceFinishedDelayInMinutes();
61+
}
62+
}
63+
}
64+
65+
softScore -= (int) vehicle.getTotalDrivingTimeSeconds();
66+
}
67+
68+
return HardSoftLongScore.of(hardScore, softScore);
69+
}
70+
}
71+
----
72+
--
73+
74+
Kotlin::
75+
+
76+
--
77+
[source,kotlin]
78+
----
79+
package org.acme.vehiclerouting.solver;
80+
81+
import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore
82+
import ai.timefold.solver.core.api.score.calculator.EasyScoreCalculator
83+
84+
import org.acme.vehiclerouting.domain.Vehicle
85+
import org.acme.vehiclerouting.domain.VehicleRoutePlan
86+
87+
class VehicleRoutingEasyScoreCalculator :
88+
EasyScoreCalculator<VehicleRoutePlan, HardSoftLongScore> {
89+
override fun calculateScore(vehicleRoutePlan: VehicleRoutePlan): HardSoftLongScore {
90+
val vehicleList: List<Vehicle> = vehicleRoutePlan.vehicles!!
91+
92+
var hardScore = 0
93+
var softScore = 0
94+
for (vehicle in vehicleList) {
95+
// The demand exceeds the capacity
96+
97+
if (vehicle.visits != null && vehicle.totalDemand > vehicle.capacity) {
98+
hardScore -= (vehicle.totalDemand - vehicle.capacity).toInt()
99+
}
100+
101+
// Max end-time not met
102+
if (vehicle.visits != null) {
103+
for (visit in vehicle.visits!!) {
104+
if (visit.isServiceFinishedAfterMaxEndTime) {
105+
hardScore -= visit.serviceFinishedDelayInMinutes.toInt()
106+
}
107+
}
108+
}
109+
110+
softScore -= vehicle.totalDrivingTimeSeconds.toInt()
111+
}
112+
113+
return HardSoftLongScore.of(hardScore.toLong(), softScore.toLong())
114+
}
115+
}
116+
----
117+
--
118+
====
119+
120+
121+
Unfortunately **that does not scale well**, because it is non-incremental:
122+
every time a visit is scheduled to a different vehicle,
123+
all visits are re-evaluated to calculate the new score.
124+
125+
Instead, create a `VehicleRoutingConstraintProvider` class
126+
to perform incremental score calculation.
127+
It uses Timefold Solver's xref:constraints-and-score/score-calculation.adoc[Constraint Streams API]
128+
which is inspired by Java Streams and SQL:
129+
130+
[tabs]
131+
====
132+
Java::
133+
+
134+
--
135+
Create a `src/main/java/org/acme/vehiclerouting/solver/VehicleRoutingConstraintProvider.java` class:
136+
137+
[source,java]
138+
----
139+
package org.acme.vehiclerouting.solver;
140+
141+
import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore;
142+
import ai.timefold.solver.core.api.score.stream.Constraint;
143+
import ai.timefold.solver.core.api.score.stream.ConstraintFactory;
144+
import ai.timefold.solver.core.api.score.stream.ConstraintProvider;
145+
146+
import org.acme.vehiclerouting.domain.Visit;
147+
import org.acme.vehiclerouting.domain.Vehicle;
148+
import org.acme.vehiclerouting.solver.justifications.MinimizeTravelTimeJustification;
149+
import org.acme.vehiclerouting.solver.justifications.ServiceFinishedAfterMaxEndTimeJustification;
150+
import org.acme.vehiclerouting.solver.justifications.VehicleCapacityJustification;
151+
152+
public class VehicleRoutingConstraintProvider implements ConstraintProvider {
153+
154+
public static final String VEHICLE_CAPACITY = "vehicleCapacity";
155+
public static final String SERVICE_FINISHED_AFTER_MAX_END_TIME = "serviceFinishedAfterMaxEndTime";
156+
public static final String MINIMIZE_TRAVEL_TIME = "minimizeTravelTime";
157+
158+
@Override
159+
public Constraint[] defineConstraints(ConstraintFactory factory) {
160+
return new Constraint[] {
161+
vehicleCapacity(factory),
162+
serviceFinishedAfterMaxEndTime(factory),
163+
minimizeTravelTime(factory)
164+
};
165+
}
166+
167+
protected Constraint vehicleCapacity(ConstraintFactory factory) {
168+
return factory.forEach(Vehicle.class)
169+
.filter(vehicle -> vehicle.getTotalDemand() > vehicle.getCapacity())
170+
.penalizeLong(HardSoftLongScore.ONE_HARD,
171+
vehicle -> vehicle.getTotalDemand() - vehicle.getCapacity())
172+
.justifyWith((vehicle, score) -> new VehicleCapacityJustification(vehicle.getId(), vehicle.getTotalDemand(),
173+
vehicle.getCapacity()))
174+
.asConstraint(VEHICLE_CAPACITY);
175+
}
176+
177+
protected Constraint serviceFinishedAfterMaxEndTime(ConstraintFactory factory) {
178+
return factory.forEach(Visit.class)
179+
.filter(Visit::isServiceFinishedAfterMaxEndTime)
180+
.penalizeLong(HardSoftLongScore.ONE_HARD,
181+
Visit::getServiceFinishedDelayInMinutes)
182+
.justifyWith((visit, score) -> new ServiceFinishedAfterMaxEndTimeJustification(visit.getId(),
183+
visit.getServiceFinishedDelayInMinutes()))
184+
.asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME);
185+
}
186+
187+
protected Constraint minimizeTravelTime(ConstraintFactory factory) {
188+
return factory.forEach(Vehicle.class)
189+
.penalizeLong(HardSoftLongScore.ONE_SOFT,
190+
Vehicle::getTotalDrivingTimeSeconds)
191+
.justifyWith((vehicle, score) -> new MinimizeTravelTimeJustification(vehicle.getId(),
192+
vehicle.getTotalDrivingTimeSeconds()))
193+
.asConstraint(MINIMIZE_TRAVEL_TIME);
194+
}
195+
}
196+
197+
----
198+
--
199+
200+
Kotlin::
201+
+
202+
--
203+
Create a `src/main/kotlin/org/acme/vehiclerouting/solver/VehicleRoutingConstraintProvider.kt` class:
204+
205+
[source,kotlin]
206+
----
207+
package org.acme.vehiclerouting.solver
208+
209+
import ai.timefold.solver.core.api.score.buildin.hardsoftlong.HardSoftLongScore
210+
import ai.timefold.solver.core.api.score.stream.Constraint
211+
import ai.timefold.solver.core.api.score.stream.ConstraintFactory
212+
import ai.timefold.solver.core.api.score.stream.ConstraintProvider
213+
214+
import org.acme.vehiclerouting.domain.Visit
215+
import org.acme.vehiclerouting.domain.Vehicle
216+
import org.acme.vehiclerouting.solver.justifications.MinimizeTravelTimeJustification
217+
import org.acme.vehiclerouting.solver.justifications.ServiceFinishedAfterMaxEndTimeJustification
218+
import org.acme.vehiclerouting.solver.justifications.VehicleCapacityJustification
219+
220+
class VehicleRoutingConstraintProvider : ConstraintProvider {
221+
override fun defineConstraints(factory: ConstraintFactory): Array<Constraint> {
222+
return arrayOf(
223+
vehicleCapacity(factory),
224+
serviceFinishedAfterMaxEndTime(factory),
225+
minimizeTravelTime(factory)
226+
)
227+
}
228+
229+
protected fun vehicleCapacity(factory: ConstraintFactory): Constraint {
230+
return factory.forEach(Vehicle::class.java)
231+
.filter({ vehicle: Vehicle -> vehicle.totalDemand > vehicle.capacity })
232+
.penalizeLong(
233+
HardSoftLongScore.ONE_HARD
234+
) { vehicle: Vehicle -> vehicle.totalDemand - vehicle.capacity }
235+
.justifyWith({ vehicle: Vehicle, score: HardSoftLongScore? ->
236+
VehicleCapacityJustification(
237+
vehicle.id, vehicle.totalDemand.toInt(),
238+
vehicle.capacity
239+
)
240+
})
241+
.asConstraint(VEHICLE_CAPACITY)
242+
}
243+
244+
protected fun serviceFinishedAfterMaxEndTime(factory: ConstraintFactory): Constraint {
245+
return factory.forEach(Visit::class.java)
246+
.filter({ obj: Visit -> obj.isServiceFinishedAfterMaxEndTime })
247+
.penalizeLong(HardSoftLongScore.ONE_HARD,
248+
{ obj: Visit -> obj.serviceFinishedDelayInMinutes })
249+
.justifyWith({ visit: Visit, score: HardSoftLongScore? ->
250+
ServiceFinishedAfterMaxEndTimeJustification(
251+
visit.id,
252+
visit.serviceFinishedDelayInMinutes
253+
)
254+
})
255+
.asConstraint(SERVICE_FINISHED_AFTER_MAX_END_TIME)
256+
}
257+
258+
protected fun minimizeTravelTime(factory: ConstraintFactory): Constraint {
259+
return factory.forEach(Vehicle::class.java)
260+
.penalizeLong(HardSoftLongScore.ONE_SOFT,
261+
{ obj: Vehicle -> obj.totalDrivingTimeSeconds })
262+
.justifyWith({ vehicle: Vehicle, score: HardSoftLongScore? ->
263+
MinimizeTravelTimeJustification(
264+
vehicle.id,
265+
vehicle.totalDrivingTimeSeconds
266+
)
267+
})
268+
.asConstraint(MINIMIZE_TRAVEL_TIME)
269+
}
270+
271+
companion object {
272+
const val VEHICLE_CAPACITY: String = "vehicleCapacity"
273+
const val SERVICE_FINISHED_AFTER_MAX_END_TIME: String = "serviceFinishedAfterMaxEndTime"
274+
const val MINIMIZE_TRAVEL_TIME: String = "minimizeTravelTime"
275+
}
276+
}
277+
----
278+
--
279+
====
280+
281+
The `ConstraintProvider` scales much better than the `EasyScoreCalculator`: typically __O__(n) instead of __O__(n²).

0 commit comments

Comments
 (0)