# `ExVrp.Solver`
[🔗](https://github.com/sephianl/ex_vrp/blob/v0.8.0/lib/ex_vrp/solver.ex#L1)

Main solver interface for VRP problems.

This module provides the `solve/2` function which is a direct port of PyVRP's
`solve()` function. It sets up the solver components and runs Iterated Local
Search with Late Acceptance Hill-Climbing.

# `solve_opts`

```elixir
@type solve_opts() :: [
  max_iterations: pos_integer(),
  max_runtime: pos_integer(),
  stop: ExVrp.StoppingCriteria.t(),
  seed: non_neg_integer(),
  num_starts: pos_integer() | :auto,
  penalty_params: ExVrp.PenaltyManager.Params.t(),
  ils_params: ExVrp.IteratedLocalSearch.Params.t(),
  on_progress: (map() -&gt; any()) | nil,
  initial_routes: [[non_neg_integer()]] | nil,
  log_label: String.t() | nil
]
```

# `solve`

```elixir
@spec solve(ExVrp.Model.t(), solve_opts()) ::
  {:ok, ExVrp.IteratedLocalSearch.Result.t()} | {:error, term()}
```

Solves a VRP model using Iterated Local Search.

This is a port of PyVRP's `solve()` function. It:
1. Creates the problem data from the model
2. Initializes the PenaltyManager for dynamic penalty adjustment
3. Creates an initial solution using local search on empty solution
4. Runs Iterated Local Search until stopping criterion is met

## Options

- `:max_iterations` - Maximum number of iterations (default: 10_000)
- `:max_runtime` - Maximum runtime in milliseconds (default: unlimited). Note that
  `StoppingCriteria.max_runtime/1` takes seconds instead, matching PyVRP's `MaxRuntime`.
- `:stop` - Custom StoppingCriteria (overrides max_iterations/max_runtime)
- `:seed` - Random seed for reproducibility (default: random)
- `:num_starts` - Number of parallel independent solver starts (default: `:auto`).
  Each start uses a different seed and runs its own ILS chain.
  The best result across all starts is returned.
  Use `:auto` to pick based on available cores (`div(schedulers_online, 2)`).
- `:penalty_params` - PenaltyManager.Params for penalty adjustment
- `:ils_params` - IteratedLocalSearch.Params for ILS behavior
- `:on_progress` - Optional callback function receiving progress maps during ILS iterations (time-gated at ~1s intervals). When `num_starts > 1`, progress maps include `:seed_idx` and `:seed` fields.
- `:log_label` - Optional string folded into this solve's log lines, e.g.
  `log_label: "relaxed_15"` yields `[exvrp relaxed_15 start 2] ILS completed in ...`.
  Start indices only distinguish chains *within* one `solve/2` call, so a host that
  runs several solves concurrently needs this to tell their log lines apart.
- `:initial_routes` - Optional warm-start. A list of routes where the position
  in the outer list maps to the vehicle type index. Each inner list is a
  sequence of client IDs visited by that vehicle type. Empty inner lists are
  skipped (vehicle type unused in the warm-start). When provided, the solver
  skips the empty-solution local-search step and uses these routes as the
  initial solution directly. Example: `[[1, 2, 3], [], [4, 5]]` warm-starts
  with vehicle type 0 visiting clients 1, 2, 3 and vehicle type 2 visiting 4, 5.

  Capacity-overloaded and time-window-violating starts are passed through to
  the solver — these are valid infeasible starting points that the solver can
  repair via penalties. Structurally invalid inputs (duplicate clients,
  out-of-range vehicle types or client IDs, too many routes for
  `num_available`) are logged as warnings and the solver falls back to a
  cold (empty) start rather than crashing.

## Returns

- `{:ok, result}` - Successfully found a solution. Result has:
  - `result.best` - Best Solution found
  - `result.cost()` - Cost of best solution (infinity if infeasible)
  - `result.feasible?()` - Whether solution is feasible
  - `result.num_iterations` - Total iterations
  - `result.runtime` - Runtime in milliseconds
- `{:error, reason}` - Failed to solve

## Example

    model = Model.new()
    |> Model.add_depot(x: 0, y: 0)
    |> Model.add_vehicle_type(num_available: 2, capacity: [100], time_windows: [{0, 28_800}])
    |> Model.add_client(x: 10, y: 0, delivery: [20])

    {:ok, result} = Solver.solve(model, max_iterations: 1000)
    IO.puts("Best distance: #{result.best.distance}")

    # With a 60 second time limit
    {:ok, result} = Solver.solve(model, max_runtime: 60_000)

---

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