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 iterationsmax_runtime/1- Stop after N seconds (float, like PyVRP)no_improvement/1- Stop after N iterations without improvementfirst_feasible/0- Stop when a feasible solution is foundmultiple_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)
Summary
Functions
Creates a combined criterion that stops when ALL of the sub-criteria are met.
Alias for multiple_criteria/1 for convenience.
Creates a criterion that stops when a feasible solution is found.
Creates a combined criterion that stops when EITHER a feasible solution is found OR the other criterion is met.
Creates a criterion that stops after a maximum number of iterations.
Creates a criterion that stops after a maximum runtime in seconds.
Creates a combined criterion that stops when ANY of the sub-criteria are met.
Creates a criterion that stops after N iterations without improvement.
Checks if the stopping criterion has been met.
Converts a StoppingCriteria struct to a stop function matching PyVRP's interface.
Types
@type stop_fn() :: (non_neg_integer() -> boolean())
@type t() :: %ExVrp.StoppingCriteria{ state: map(), type: :max_iterations | :max_runtime | :no_improvement | :multiple_criteria | :first_feasible }
Functions
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).
Alias for multiple_criteria/1 for convenience.
@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()
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))
@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)
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
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)
])
@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
Checks if the stopping criterion has been met.
Returns {should_stop?, updated_criteria} where the updated criteria
tracks any state changes (like iteration counts).
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