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
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
be the allowed arc set implemented by the source. For demand node
Decision variables
For
indicates that vehicle k uses arc
For every
Intermediate values
For each vehicle
For each demand node
For each vehicle:
The source registers two objectives:
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:
The source implements the equality
For every node and vehicle:
and for every vehicle:
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
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.
// See the linked Kotlin implementation for the complete model.
``
```rust [Rust]
// See the linked Rust implementation for the equivalent model.
``