ExVrp.Model (ExVrp v0.10.0)

Copy Markdown View Source

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

t()

@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

add_client(model, opts)

@spec add_client(
  t(),
  keyword()
) :: t()

Adds a client to the model.

See ExVrp.Client.new/1 for available options.

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

add_client_group(model, opts \\ [])

@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)

add_depot(model, opts)

@spec add_depot(
  t(),
  keyword()
) :: t()

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([])

add_same_vehicle_group(model, clients, opts \\ [])

@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 to
  • clients - The clients that must be served by the same vehicle, given either as Client structs 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

add_vehicle_type(model, opts)

@spec add_vehicle_type(
  t(),
  keyword()
) :: t()

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}])

new()

@spec new() :: t()

Creates a new empty model.

num_clients(model)

@spec num_clients(t()) :: non_neg_integer()

Returns the number of clients in the model.

num_depots(model)

@spec num_depots(t()) :: non_neg_integer()

Returns the number of depots in the model.

num_locations(model)

@spec num_locations(t()) :: non_neg_integer()

Returns the total number of locations (depots + clients) in the model.

num_vehicle_types(model)

@spec num_vehicle_types(t()) :: non_neg_integer()

Returns the number of vehicle types in the model.

num_vehicles(model)

@spec num_vehicles(t()) :: non_neg_integer()

Returns the total number of vehicles in the model.

set_distance_matrices(model, matrices)

@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])

set_duration_matrices(model, matrices)

@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])

set_euclidean_matrices(model, coordinates)

@spec set_euclidean_matrices(t(), [{number(), number()}]) :: t()

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}])

set_forbidden(model, forbidden)

@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]])

set_penalties(model, penalties)

@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]])

to_problem_data(model)

@spec to_problem_data(t()) :: {:ok, reference()} | {:error, term()}

Converts the model to ProblemData for the solver.

This is called internally by ExVrp.solve/2.

validate(model)

@spec validate(t()) :: :ok | {:error, [String.t()]}

Validates the model and returns any errors.

Returns :ok if valid, {:error, reasons} otherwise.