High-level builder for constructing VRP problems.
The Model provides a fluent API for defining depots, vehicle types, clients, and routing constraints. A model must have at least one depot and one vehicle type before it can be solved.
Basic Example
model =
ExVrp.Model.new()
|> ExVrp.Model.add_depot([])
|> ExVrp.Model.add_vehicle_type(num_available: 2, capacity: [100], time_windows: [{0, 28_800}])
|> ExVrp.Model.add_client(delivery: [10])
|> ExVrp.Model.add_client(delivery: [20])
|> ExVrp.Model.add_client(delivery: [15])
|> ExVrp.Model.set_euclidean_matrices([{0, 0}, {1, 1}, {2, 2}, {3, 1}])
{:ok, result} = ExVrp.solve(model)Distance and Duration Matrices
Locations carry no coordinates: the matrices are the solver's only notion of
distance, and a model without one will not validate. Supply them directly
(one per vehicle profile), or derive them from coordinates with
set_euclidean_matrices/2:
# Matrix rows/columns: [depot, client1, client2, ...]
distances = [
[0, 100, 200],
[100, 0, 150],
[200, 150, 0]
]
model =
ExVrp.Model.new()
|> ExVrp.Model.add_depot([])
|> ExVrp.Model.add_client(delivery: [10])
|> ExVrp.Model.add_client(delivery: [20])
|> ExVrp.Model.add_vehicle_type(num_available: 2, capacity: [100], time_windows: [{0, 28_800}])
|> ExVrp.Model.set_distance_matrices([distances])
|> ExVrp.Model.set_duration_matrices([distances])Multi-Dimensional Capacity
Vehicles and clients can have multiple capacity dimensions (e.g. weight and volume):
model
|> ExVrp.Model.add_vehicle_type(num_available: 3, capacity: [1000, 50], time_windows: [{0, 28_800}])
|> ExVrp.Model.add_client(delivery: [200, 10])Client Groups
Client groups allow mutually exclusive alternatives — only one client from the group will be visited:
{model, group} = ExVrp.Model.add_client_group(model, required: false)
model =
model
|> ExVrp.Model.add_client(group: group, required: false, prize: 100)
|> ExVrp.Model.add_client(group: group, required: false, prize: 150)Same-Vehicle Groups
Force specific clients onto the same route:
[c1, c2] = model.clients
model = ExVrp.Model.add_same_vehicle_group(model, [c1, c2])Validation
Models are validated automatically before solving. You can also validate explicitly:
case ExVrp.Model.validate(model) do
:ok -> :ready
{:error, reasons} -> IO.inspect(reasons)
end
Summary
Functions
Adds a client to the model.
Adds a new client group to the model.
Adds a depot to the model.
Adds a same-vehicle constraint group to the model.
Adds a vehicle type to the model.
Creates a new empty model.
Returns the number of clients in the model.
Returns the number of depots in the model.
Returns the total number of locations (depots + clients) in the model.
Returns the number of vehicle types in the model.
Returns the total number of vehicles in the model.
Sets custom distance matrices.
Sets custom duration matrices.
Sets both matrices to the rounded Euclidean distances between coordinates,
taking durations to equal distances.
Sets per-profile forbidden locations.
Sets per-profile, per-location penalties.
Converts the model to ProblemData for the solver.
Validates the model and returns any errors.
Types
@type t() :: %ExVrp.Model{ client_groups: [ExVrp.ClientGroup.t()], clients: [ExVrp.Client.t()], depots: [ExVrp.Depot.t()], distance_matrices: [[[non_neg_integer()]]], duration_matrices: [[[non_neg_integer()]]], forbidden: [[non_neg_integer()]], penalties: [[non_neg_integer()]], same_vehicle_groups: [ExVrp.SameVehicleGroup.t()], vehicle_types: [ExVrp.VehicleType.t()] }
Functions
Adds a client to the model.
See ExVrp.Client.new/1 for available options.
Options
:group- Group index fromadd_client_group/2(optional)- See
ExVrp.Client.new/1for other options
Example
model
|> ExVrp.Model.add_client(delivery: [10])
# With group assignment
{model, group} = Model.add_client_group(model, required: false)
model = Model.add_client(model, group: group)Raises
ArgumentErrorif group index is invalidArgumentErrorif required client is added to mutually exclusive group
@spec add_client_group( t(), keyword() ) :: {t(), non_neg_integer()}
Adds a new client group to the model.
Returns {model, group_index} where group_index can be passed to
add_client/2 to dynamically add clients to the group.
Options
:required- Whether at least one client must be visited (default:true):mutually_exclusive- Whether only one client can be visited (default:not required):name- Group name for identification (default:"")
Example
{model, group} = Model.add_client_group(model, required: false)
model = Model.add_client(model, x: 1, y: 1, group: group)
model = Model.add_client(model, x: 2, y: 2, group: group)
Adds a depot to the model.
See ExVrp.Depot.new/1 for available options.
Note: When adding a depot after clients have been added, all client group indices are recalculated to account for the new depot shifting client indices.
Example
model
|> ExVrp.Model.add_depot([])
@spec add_same_vehicle_group(t(), [ExVrp.Client.t()] | [non_neg_integer()], keyword()) :: t()
Adds a same-vehicle constraint group to the model.
All clients in this group that are visited must be served by the same vehicle. It is allowed to visit only a subset of the group (or none at all), but any visited clients must share a route.
Parameters
model- The model to add the group toclients- The clients that must be served by the same vehicle, given either asClientstructs or as zero-based client indices
Prefer indices. Structs are resolved by structural equality, so two clients that happen to carry identical data cannot be told apart: the group binds whichever equal clients are found first, which may not be the ones the caller meant. A caller that already knows its indices should pass them and avoid the ambiguity entirely.
Options
:name- Free-form name for the group (default:"")
Returns
The updated model with the new same-vehicle group added.
Example
model =
Model.new()
|> Model.add_depot([])
|> Model.add_client(delivery: [10])
|> Model.add_client(delivery: [20])
|> Model.add_vehicle_type(num_available: 2, capacity: [100], time_windows: [{0, 28_800}])
model = Model.add_same_vehicle_group(model, [0, 1], name: "group1")Raises
ArgumentErrorif a client given as a struct is not in the model
Adds a vehicle type to the model.
See ExVrp.VehicleType.new/1 for available options.
Example
model
|> ExVrp.Model.add_vehicle_type(num_available: 3, capacity: [100], time_windows: [{0, 28_800}])
@spec new() :: t()
Creates a new empty model.
@spec num_clients(t()) :: non_neg_integer()
Returns the number of clients in the model.
@spec num_depots(t()) :: non_neg_integer()
Returns the number of depots in the model.
@spec num_locations(t()) :: non_neg_integer()
Returns the total number of locations (depots + clients) in the model.
@spec num_vehicle_types(t()) :: non_neg_integer()
Returns the number of vehicle types in the model.
@spec num_vehicles(t()) :: non_neg_integer()
Returns the total number of vehicles in the model.
@spec set_distance_matrices(t(), [[[non_neg_integer()]]]) :: t()
Sets custom distance matrices.
A model must carry at least one distance matrix before it can be solved;
validate/1 rejects a model without one.
Example
model
|> ExVrp.Model.set_distance_matrices([matrix1, matrix2])
@spec set_duration_matrices(t(), [[[non_neg_integer()]]]) :: t()
Sets custom duration matrices.
If not provided, distances are used as durations.
Example
model
|> ExVrp.Model.set_duration_matrices([matrix1, matrix2])
Sets both matrices to the rounded Euclidean distances between coordinates,
taking durations to equal distances.
Coordinates are {x, y} tuples in location order — depots first, then
clients — and are used only to derive the matrices. Locations themselves do
not carry coordinates; the matrices are the solver's only notion of distance.
Example
model
|> ExVrp.Model.set_euclidean_matrices([{0, 0}, {1, 1}, {2, 0}])
@spec set_forbidden(t(), [[non_neg_integer()]]) :: t()
Sets per-profile forbidden locations.
One list per routing profile, each holding the location indices that
vehicles on that profile must never visit. Local search prunes these
rather than costing them, so a forbidden location is a hard exclusion —
use set_penalties/2 when the intent is merely expensive.
Works whatever the profile count, single-profile models included. Indices must name clients: a vehicle starts and ends at its depot regardless, so forbidding a depot is rejected rather than quietly doing nothing.
ExVrp.Solution.num_forbidden_visits/1 reports violations, and is zero on
any solution the solver produced.
Defaults to nothing forbidden when left unset.
Example
model
|> ExVrp.Model.set_forbidden([[1, 2]])
@spec set_penalties(t(), [[non_neg_integer()]]) :: t()
Sets per-profile, per-location penalties.
One list per routing profile, each holding one cost per location in the same order the matrices use: depots first, then clients. A penalty is charged once for each visited location, so it is a per-client cost rather than a per-leg one.
Depot entries must be zero — use Depot's reload_cost to price a reload.
A nonzero depot penalty is rejected when the model is solved.
Defaults to no penalties when left unset.
Example
model
|> ExVrp.Model.set_penalties([[0, 500, 0]])
Converts the model to ProblemData for the solver.
This is called internally by ExVrp.solve/2.
Validates the model and returns any errors.
Returns :ok if valid, {:error, reasons} otherwise.