# `ExVrp.Solution`
[🔗](https://github.com/sephianl/ex_vrp/blob/v0.4.2/lib/ex_vrp/solution.ex#L1)

Represents a solution to a VRP.

A solution consists of routes (one per vehicle used) and provides
functions to query costs, distances, feasibility, and detailed schedules.

## Accessing Solutions

Solutions are returned as `result.best` from the solver:

    {:ok, result} = ExVrp.solve(model)
    solution = result.best

    solution.routes       #=> [[1, 3], [2]]
    solution.distance     #=> 4521
    solution.is_feasible  #=> true
    solution.num_clients  #=> 3

## Querying Routes

Use `routes/1` to get `ExVrp.Route` structs with full query capabilities:

    routes = ExVrp.Solution.routes(solution)

    for route <- routes do
      IO.puts("Distance: #{ExVrp.Route.distance(route)}")
      IO.puts("Duration: #{ExVrp.Route.duration(route)}")
      IO.puts("Vehicle type: #{route.vehicle_type}")
    end

Or query route properties directly by index:

    ExVrp.Solution.route_distance(solution, 0)
    ExVrp.Solution.route_delivery(solution, 0)
    ExVrp.Solution.route_feasible?(solution, 0)

## Schedules

Get detailed timing for each visit in a route:

    schedule = ExVrp.Solution.route_schedule(solution, 0)

    for visit <- schedule do
      IO.puts("Location #{visit.location}: #{visit.start_service}-#{visit.end_service}")
    end

## Constraint Violations

Check for specific constraint violations:

    ExVrp.Solution.has_time_warp?(solution)
    ExVrp.Solution.has_excess_load?(solution)
    ExVrp.Solution.has_excess_distance?(solution)
    ExVrp.Solution.time_warp(solution)        #=> total time warp
    ExVrp.Solution.excess_load(solution)       #=> per-dimension excess

## Unassigned Clients

For prize-collecting problems, check which clients were not visited:

    ExVrp.Solution.unassigned(solution)  #=> [4, 7]

# `t`

```elixir
@type t() :: %ExVrp.Solution{
  distance: non_neg_integer(),
  duration: non_neg_integer(),
  is_complete: boolean(),
  is_feasible: boolean(),
  num_clients: non_neg_integer(),
  problem_data: reference() | nil,
  routes: [[non_neg_integer()]],
  solution_ref: reference() | nil,
  stats: map() | nil
}
```

# `complete?`

```elixir
@spec complete?(t()) :: boolean()
```

Checks if the solution is complete (visits all required clients).

# `cost`

```elixir
@spec cost(t()) :: non_neg_integer()
```

Returns the total cost of the solution.

When called with a cost evaluator, uses that evaluator.
When called without, uses the distance as the cost (default unit_distance_cost=1).

# `cost`

```elixir
@spec cost(t(), reference()) :: non_neg_integer() | :infinity
```

# `distance`

```elixir
@spec distance(t()) :: non_neg_integer()
```

Returns the total distance of the solution.

# `distance_cost`

```elixir
@spec distance_cost(t()) :: non_neg_integer()
```

Returns the total distance cost of the solution.

# `duration`

```elixir
@spec duration(t()) :: non_neg_integer()
```

Returns the total duration of the solution.

# `duration_cost`

```elixir
@spec duration_cost(t()) :: non_neg_integer()
```

Returns the total duration cost of the solution.

# `excess_distance`

```elixir
@spec excess_distance(t()) :: non_neg_integer()
```

Returns the total excess distance of the solution (sum across all routes).

# `excess_load`

```elixir
@spec excess_load(t()) :: [non_neg_integer()]
```

Returns the total excess load of the solution per dimension.

# `feasible?`

```elixir
@spec feasible?(t()) :: boolean()
```

Checks if the solution is feasible (satisfies all constraints).

# `fixed_vehicle_cost`

```elixir
@spec fixed_vehicle_cost(t()) :: non_neg_integer()
```

Returns the total fixed vehicle cost of the solution.

# `group_feasible?`

```elixir
@spec group_feasible?(t()) :: boolean()
```

Checks if the solution is group feasible (same-vehicle constraints satisfied).

Returns true if all clients in each same-vehicle group that are visited
are on the same route.

# `has_excess_distance?`

```elixir
@spec has_excess_distance?(t()) :: boolean()
```

Returns true if the solution has any excess distance.

# `has_excess_load?`

```elixir
@spec has_excess_load?(t()) :: boolean()
```

Returns true if the solution has any excess load.

# `has_time_warp?`

```elixir
@spec has_time_warp?(t()) :: boolean()
```

Returns true if the solution has any time warp.

# `num_clients`

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

Returns the number of assigned clients in the solution.

# `num_routes`

```elixir
@spec num_routes(t()) :: non_neg_integer()
```

Returns the number of routes in the solution.

# `overtime`

```elixir
@spec overtime(t()) :: non_neg_integer()
```

Returns the total overtime of the solution (sum across all routes).

# `penalised_cost`

```elixir
@spec penalised_cost(t(), reference()) :: non_neg_integer()
```

Returns the penalised cost of the solution given a cost evaluator.

# `reload_cost`

```elixir
@spec reload_cost(t()) :: non_neg_integer()
```

Returns the total reload cost of the solution.

# `route`

```elixir
@spec route(t(), non_neg_integer()) :: ExVrp.Route.t()
```

Returns the route at the given index as a Route struct.

The returned Route struct has its `solution_ref` and `route_idx` populated,
enabling methods like `Route.distance/1`, `Route.feasible?/1`, etc.

## Example

    {:ok, result} = Solver.solve(model)
    route = Solution.route(result.best, 0)
    Route.distance(route)

# `route_centroid`

```elixir
@spec route_centroid(t(), non_neg_integer()) :: {float(), float()}
```

Returns the centroid of a specific route as {x, y}.

# `route_delivery`

```elixir
@spec route_delivery(t(), non_neg_integer()) :: [non_neg_integer()]
```

Returns the delivery load of a specific route in the solution.

# `route_distance`

```elixir
@spec route_distance(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the distance of a specific route in the solution.

# `route_distance_cost`

```elixir
@spec route_distance_cost(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the distance cost of a specific route.

# `route_duration`

```elixir
@spec route_duration(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the duration of a specific route in the solution.

# `route_duration_cost`

```elixir
@spec route_duration_cost(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the duration cost of a specific route.

# `route_end_depot`

```elixir
@spec route_end_depot(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the end depot of a specific route.

# `route_end_time`

```elixir
@spec route_end_time(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the end time of a specific route.

# `route_excess_distance`

```elixir
@spec route_excess_distance(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the excess distance of a specific route.

# `route_excess_load`

```elixir
@spec route_excess_load(t(), non_neg_integer()) :: [non_neg_integer()]
```

Returns the excess load of a specific route (per dimension).

# `route_feasible?`

```elixir
@spec route_feasible?(t(), non_neg_integer()) :: boolean()
```

Checks if a specific route in the solution is feasible.

# `route_has_excess_distance?`

```elixir
@spec route_has_excess_distance?(t(), non_neg_integer()) :: boolean()
```

Checks if a specific route has excess distance.

# `route_has_excess_load?`

```elixir
@spec route_has_excess_load?(t(), non_neg_integer()) :: boolean()
```

Checks if a specific route has excess load.

# `route_has_time_warp?`

```elixir
@spec route_has_time_warp?(t(), non_neg_integer()) :: boolean()
```

Checks if a specific route has time warp.

# `route_num_trips`

```elixir
@spec route_num_trips(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the number of trips in a specific route.

# `route_overtime`

```elixir
@spec route_overtime(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the overtime of a specific route.

# `route_pickup`

```elixir
@spec route_pickup(t(), non_neg_integer()) :: [non_neg_integer()]
```

Returns the pickup load of a specific route in the solution.

# `route_prizes`

```elixir
@spec route_prizes(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the prizes collected on a specific route.

# `route_reload_cost`

```elixir
@spec route_reload_cost(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the reload cost of a specific route.

# `route_schedule`

```elixir
@spec route_schedule(t(), non_neg_integer()) :: [ExVrp.ScheduledVisit.t()]
```

Returns the schedule of a specific route.

The schedule contains detailed timing information for each visit, including
service start/end times, waiting times, and any time warp (late arrival).

## Example

    schedule = Solution.route_schedule(solution, 0)

    Enum.each(schedule, fn visit ->
      IO.puts("Location #{visit.location}: #{visit.start_service}-#{visit.end_service}")
    end)

# `route_service_duration`

```elixir
@spec route_service_duration(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the service duration of a specific route.

# `route_slack`

```elixir
@spec route_slack(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the slack time of a specific route.

# `route_start_depot`

```elixir
@spec route_start_depot(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the start depot of a specific route.

# `route_start_time`

```elixir
@spec route_start_time(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the start time of a specific route.

# `route_time_warp`

```elixir
@spec route_time_warp(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the time warp of a specific route.

# `route_travel_duration`

```elixir
@spec route_travel_duration(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the travel duration of a specific route.

# `route_vehicle_type`

```elixir
@spec route_vehicle_type(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the vehicle type of a specific route.

# `route_visits`

```elixir
@spec route_visits(t(), non_neg_integer()) :: [non_neg_integer()]
```

Returns the visits (client indices) of a specific route.

# `route_wait_duration`

```elixir
@spec route_wait_duration(t(), non_neg_integer()) :: non_neg_integer()
```

Returns the wait duration of a specific route.

# `routes`

```elixir
@spec routes(t()) :: [ExVrp.Route.t()]
```

Returns all routes as Route structs.

Each returned Route struct has its `solution_ref` and `route_idx` populated,
enabling methods like `Route.distance/1`, `Route.feasible?/1`, etc.

## Example

    {:ok, result} = Solver.solve(model)
    routes = Solution.routes(result.best)

    Enum.each(routes, fn route ->
      IO.puts("Route distance: #{Route.distance(route)}")
    end)

# `time_warp`

```elixir
@spec time_warp(t()) :: non_neg_integer()
```

Returns the total time warp of the solution (sum across all routes).

# `unassigned`

```elixir
@spec unassigned(t()) :: [non_neg_integer()]
```

Returns a list of unassigned client indices.

---

*Consult [api-reference.md](api-reference.md) for complete listing*
