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

Stopping criteria for controlling when the solver terminates.

This is a port of PyVRP's stopping criteria. In PyVRP, criteria are called
with `stop(best_cost)` and return a boolean. We provide both the struct-based
API and a `to_stop_fn/1` that creates a closure matching PyVRP's interface.

## Available Criteria

- `max_iterations/1` - Stop after N iterations
- `max_runtime/1` - Stop after N seconds (float, like PyVRP)
- `no_improvement/1` - Stop after N iterations without improvement
- `first_feasible/0` - Stop when a feasible solution is found
- `multiple_criteria/1` - Combine criteria (stops when ANY is met)

## Example

    # Stop after 1000 iterations OR 60 seconds
    stop = StoppingCriteria.multiple_criteria([
      StoppingCriteria.max_iterations(1000),
      StoppingCriteria.max_runtime(60.0)
    ])

    {:ok, result} = Solver.solve(model, stop: stop)

# `stop_fn`

```elixir
@type stop_fn() :: (non_neg_integer() -&gt; boolean())
```

# `t`

```elixir
@type t() :: %ExVrp.StoppingCriteria{
  state: map(),
  type:
    :max_iterations
    | :max_runtime
    | :no_improvement
    | :multiple_criteria
    | :first_feasible
}
```

# `all`

```elixir
@spec all([t()]) :: t()
```

Creates a combined criterion that stops when ALL of the sub-criteria are met.

Note: PyVRP's `MultipleCriteria` uses OR logic (any). This is an extension
that uses AND logic (all must be met).

# `any`

```elixir
@spec any([t()]) :: t()
```

Alias for `multiple_criteria/1` for convenience.

# `first_feasible`

```elixir
@spec first_feasible() :: t()
```

Creates a criterion that stops when a feasible solution is found.

This matches PyVRP's `FirstFeasible` class.

## Example

    StoppingCriteria.first_feasible()

# `first_feasible_or`

```elixir
@spec first_feasible_or(t()) :: t()
```

Creates a combined criterion that stops when EITHER a feasible solution
is found OR the other criterion is met.

This is useful for fleet minimisation where we want to stop early if we
find a feasible solution, but also respect an overall stopping criterion.

## Example

    # Stop when feasible or after 1000 iterations, whichever comes first
    stop = StoppingCriteria.first_feasible_or(StoppingCriteria.max_iterations(1000))

# `max_iterations`

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

Creates a criterion that stops after a maximum number of iterations.

Raises `ArgumentError` if max_iterations is negative.

## Example

    StoppingCriteria.max_iterations(1000)

# `max_runtime`

```elixir
@spec max_runtime(number()) :: t()
```

Creates a criterion that stops after a maximum runtime in seconds.

This matches PyVRP's `MaxRuntime` which takes seconds as a float.

Raises `ArgumentError` if max_runtime is negative.

## Example

    StoppingCriteria.max_runtime(60.0)  # 60 seconds

# `multiple_criteria`

```elixir
@spec multiple_criteria([t()]) :: t()
```

Creates a combined criterion that stops when ANY of the sub-criteria are met.

This matches PyVRP's `MultipleCriteria` class.

Raises `ArgumentError` if the criteria list is empty.

## Example

    StoppingCriteria.multiple_criteria([
      StoppingCriteria.max_iterations(1000),
      StoppingCriteria.max_runtime(60.0)
    ])

# `no_improvement`

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

Creates a criterion that stops after N iterations without improvement.

The counter resets whenever an improving solution is found, matching
PyVRP's `NoImprovement` behavior.

Raises `ArgumentError` if max_iterations is negative.

## Example

    StoppingCriteria.no_improvement(100)  # Stop after 100 iterations without improvement

# `should_stop?`

```elixir
@spec should_stop?(t(), map()) :: {boolean(), t()}
```

Checks if the stopping criterion has been met.

Returns `{should_stop?, updated_criteria}` where the updated criteria
tracks any state changes (like iteration counts).

# `to_stop_fn`

```elixir
@spec to_stop_fn(t()) :: stop_fn()
```

Converts a StoppingCriteria struct to a stop function matching PyVRP's interface.

The returned function takes `best_cost` and returns `true` to stop.
Uses an Agent to maintain state across calls.

## Example

    criteria = StoppingCriteria.max_iterations(100)
    stop_fn = StoppingCriteria.to_stop_fn(criteria)
    stop_fn.(1000)  # => false (first call)
    # ... after 100 calls ...
    stop_fn.(1000)  # => true

---

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