# `ExVrp.Solver`
[🔗](https://github.com/sephianl/ex_vrp/blob/v0.4.2/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
]
```

# `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 seconds (default: unlimited). Matches PyVRP.
- `: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.

## 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])
    |> 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 time limit (seconds, like PyVRP)
    {:ok, result} = Solver.solve(model, max_runtime: 60.0)

---

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