Skip to content

Example 17: Vehicle routing with time windows ​

Problem and data ​

This demo models a capacitated vehicle-routing problem with service time windows. The current source contains one origin node, 100 demand nodes, one end node, and 25 identical vehicles. The origin and end are both at (40,50) with time window [0,1236]. Each vehicle has capacity 200 and fixed-used cost 500. Every demand node has a positive integer demand, a source-provided time window, and service duration 90.

The source uses Euclidean geometry: Node.distance is the distance between the two Point2 positions, and both Node.cost and Node.time return that distance. Thus travel cost and travel time share the same distance unit; there is no independent cost or speed matrix.

Sets and parameters ​

Let N be all 102 nodes, O={o} the origin, E={e} the end, D=N∖(O∪E) the 100 demand nodes, and K the 25-vehicle set. Let

A={(i,j)∈N2:i∉E, j∉O, i≠j}.

be the allowed arc set implemented by the source. For demand node j, qj is its integer demand and hj=90 is its service duration; qo=qe=0 and the origin/end service duration is zero. Each node has source data (positioni,[ei,li]). For vehicle k, Qk=200 and Fk=500.

Decision variables ​

For (i,j)∈A and k∈K:

xijk∈{0,1}

indicates that vehicle k uses arc i→j. The source creates BinVariable3 over all node pairs and vehicles, fixes disallowed entries to false, and registers only the allowed entries.

For every i∈N,k∈K, sik∈R≥0 is the service-start time, implemented by URealVariable2. Every sik is bounded again by the node's time-window constraints.

Intermediate values ​

For each vehicle k and demand node d:

Origink=∑j:(o,j)∈Axojk,Destinationk=∑i:(i,e)∈Axiek,Indk=∑i:(i,d)∈Axidk,Outdk=∑j:(d,j)∈Axdjk.

For each demand node d:

Serviced=∑k∈K∑j:(d,j)∈Axdjk.

For each vehicle:

Capacityk=∑i∈N∑j∈Nqjxijk.

The source registers two objectives:

UsedCost=∑k∈KFkOrigink,TravelCost=∑k∈K∑(i,j)∈Adistanceijxijk.

Objective ​

Demo17 calls minimize twice, first for UsedCost and then for TravelCost. The source does not construct a weighted sum or document a scalar coefficient between them; the exact multi-objective handling is left to the current model/solver policy.

Constraints and domain ​

Vehicle use and route flow:

Origink≤1,Destinationk≤1(∀k∈K),Indk=Outdk(∀d∈D, k∈K),Serviced=1(∀d∈D).

The source implements the equality In=Out as both greater-than-or-equal and less-than-or-equal constraints. For each i,j∈N and k∈K, it adds the time implication with the source's big-M:

sik+hi+distanceij−M(1−xijk)≤sjk,M=1236.

For every node and vehicle:

ei≤sik≤li(∀i∈N, k∈K),

and for every vehicle:

Capacityk≤Qk=200.

Disallowed arcs are fixed to zero before these expressions are registered. The source still creates time constraints over all node pairs, exactly as shown; the fixed-zero entries make those implications inactive for disallowed arcs.

Implementation differences and notes ​

The source builds the model through initVariable, initSymbol, initObject, initConstraint, solve, and analyzeSolution. It uses current core symbols and solves with ScipLinearSolver configured with a 300-second time limit. The two objective registrations, the 1236 big-M value, the allowed-arc filtering, and the nonnegative real time variables are implementation facts. This core demo is not a generic VRPTW formulation with optional customers: every demand node is required exactly once.

Expected result ​

A successful solve returns routes and service times for all 100 demand nodes, respecting each source time window and each vehicle's capacity. The current build test verifies model construction only; it does not assert a route list, objective values, or a unique optimum. Running this instance can be expensive and is subject to the five-minute solver limit.

Minimal current Kotlin example ​

kotlin
import kotlin.time.Duration.Companion.seconds
import fuookami.ospf.kotlin.utils.concept.*
import fuookami.ospf.kotlin.multiarray.*
import fuookami.ospf.kotlin.math.*
import fuookami.ospf.kotlin.math.algebra.number.*
import fuookami.ospf.kotlin.math.algebra.value_range.*
import fuookami.ospf.kotlin.math.geometry.*
import fuookami.ospf.kotlin.math.geometry.point2
import fuookami.ospf.kotlin.math.symbol.operation.*
import fuookami.ospf.kotlin.math.symbol.polynomial.*
import fuookami.ospf.kotlin.core.model.intermediate.*
import fuookami.ospf.kotlin.core.model.mechanism.*
import fuookami.ospf.kotlin.core.solver.config.*
import fuookami.ospf.kotlin.core.solver.scip.*
import fuookami.ospf.kotlin.core.symbol.*
import fuookami.ospf.kotlin.core.variable.*
import fuookami.ospf.kotlin.example.solveLinearMetaModel

val model = LinearMetaModel<Flt64>("demo17", converter = flt64Converter)
val x = BinVariable3("x", Shape3(nodes.size, nodes.size, vehicles.size))
for (from in nodes) for (to in nodes) for (vehicle in vehicles) {
    val xi = x[from, to, vehicle]
    if (from !is EndNode && to !is OriginNode && from != to) model.add(xi)
    else xi.range.eq(false)
}
val s = URealVariable2("s", Shape2(nodes.size, vehicles.size))
model.add(s)
val origin = LinearIntermediateSymbols1<Flt64>("origin", Shape1(vehicles.size)) { i, _ ->
    LinearExpressionSymbol(
        sum(nodes.filterIsInstance<OriginNode>().flatMap { node -> x[node, _a, vehicles[i]] }),
        name = "origin_$i"
    )
}
val destination = LinearIntermediateSymbols1<Flt64>("destination", Shape1(vehicles.size)) { i, _ ->
    LinearExpressionSymbol(
        sum(nodes.filterIsInstance<EndNode>().flatMap { node -> x[_a, node, vehicles[i]] }),
        name = "destination_$i"
    )
}
val service = LinearIntermediateSymbols1<Flt64>("service", Shape1(nodes.size)) { i, _ ->
    LinearExpressionSymbol(
        sum(nodes.filterIsNotInstance<OriginNode, Node>().flatMap { node -> x[nodes[i], node, _a] }),
        name = "service_$i"
    )
}
val capacity = LinearIntermediateSymbols1<Flt64>("capacity", Shape1(vehicles.size)) { i, _ ->
    LinearExpressionSymbol(
        sum(nodes.flatMap { from ->
            nodes.mapNotNull { to -> (to as? DemandNode)?.demand?.let { it * x[from, to, vehicles[i]] } }
        }),
        name = "capacity_$i"
    )
}
model.add(origin)
model.add(destination)
model.add(service)
model.add(capacity)
model.minimize(sum(vehicles.map { it.fixedUsedCost * origin[it] }), "used cost")
model.minimize(
    sum(nodes.flatMap { from -> nodes.map { to -> from.cost(to) * sum(x[from, to, _a]) } }),
    "trans cost"
)
for (vehicle in vehicles) model.addConstraint(origin[vehicle] leq 1)
for (node in nodes.filterIsInstance<DemandNode>()) {
    model.addConstraint(service[node] eq 1)
    for (vehicle in vehicles) {
        model.addConstraint(inFlow[node, vehicle] geq outFlow[node, vehicle])
        model.addConstraint(inFlow[node, vehicle] leq outFlow[node, vehicle])
    }
}
for (vehicle in vehicles) {
    model.addConstraint(destination[vehicle] leq 1)
    model.addConstraint(capacity[vehicle] leq vehicle.capacity)
}
val m = nodes.filterIsInstance<EndNode>().maxOf { it.timeWindow.upperBound.value.unwrap() }
for (from in nodes) for (to in nodes) for (vehicle in vehicles) {
    model.addConstraint(
        s[from, vehicle] +
            ((from as? DemandNode)?.serviceTime ?: UInt64.zero).toFlt64() +
            from.time(to) -
            m.toFlt64() * (1 - x[from, to, vehicle]) leq s[to, vehicle]
    )
}
for (node in nodes) for (vehicle in vehicles) {
    model.addConstraint(s[node, vehicle] geq node.timeWindow.lowerBound.value.unwrap())
    model.addConstraint(s[node, vehicle] leq node.timeWindow.upperBound.value.unwrap())
}

suspend fun solve() = solveLinearMetaModel(
    ScipLinearSolver(config = SolverConfig(time = 300.seconds)),
    model
)

Source and verification ​

Kotlin/Rust correspondence ​

The Rust file is an independent compact VRPTW sample: 4 customers, 3 vehicles (capacity 25, fixed cost 100), Big-M 500, and one combined cost objective. The current Kotlin implementation uses 100 customers/102 nodes, 25 vehicles (capacity 200, fixed cost 500), Big-M 1236, and separate used/travel objective registrations; do not share the data table or claim model-instance equivalence.

kotlin
// See the linked Kotlin implementation for the complete model.
`` 

```rust [Rust]
// See the linked Rust implementation for the equivalent model.
``