# ExVrp
Elixir bindings for [PyVRP](https://github.com/PyVRP/PyVRP), a state-of-the-art
Vehicle Routing Problem (VRP) solver.
This is a **direct port of PyVRP's Python API** to Elixir, using the same C++ core
via NIFs. The API is designed to be a drop-in replacement where possible.
## Features
- Native Elixir API matching PyVRP's Python interface
- High-performance C++ solver via NIFs (using Fine library)
- Iterated Local Search with Late Acceptance Hill-Climbing
- Dynamic penalty adjustment via PenaltyManager
- Support for multiple VRP variants:
- Capacitated VRP (CVRP)
- VRP with Time Windows (VRPTW)
- VRP with Pickups and Deliveries
- Multi-depot VRP
- Heterogeneous fleet VRP
## Installation
Add `ex_vrp` to your dependencies in `mix.exs`:
```elixir
def deps do
[
{:ex_vrp, "~> 0.1.0"}
]
end
```
## Quick Start
```elixir
alias ExVrp.{Model, Solver, StoppingCriteria}
alias ExVrp.IteratedLocalSearch.Result
# Define a vehicle routing problem
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: 10, delivery: [20])
|> Model.add_client(x: 20, y: 0, delivery: [30])
|> Model.add_client(x: 0, y: 20, delivery: [25])
# Solve with max iterations
{:ok, result} = Solver.solve(model, max_iterations: 1000, seed: 42)
# Or with time limit (seconds, like PyVRP)
{:ok, result} = Solver.solve(model, max_runtime: 60.0)
# Or with custom stopping criteria
stop = StoppingCriteria.multiple_criteria([
StoppingCriteria.max_iterations(10_000),
StoppingCriteria.max_runtime(30.0),
StoppingCriteria.no_improvement(1000)
])
{:ok, result} = Solver.solve(model, stop: stop)
# Inspect results
IO.puts(Result.summary(result))
IO.puts("Feasible: #{Result.is_feasible(result)}")
IO.puts("Cost: #{Result.cost(result)}")
IO.puts("Routes: #{inspect(result.best.routes)}")
IO.puts("Distance: #{result.best.distance}")
```
## API Reference
### Solver
```elixir
# Main solve function - matches PyVRP's solve()
Solver.solve(model, opts)
# Options:
# max_iterations: 10_000 - Max iterations (default)
# max_runtime: nil - Max runtime in seconds (like PyVRP)
# stop: nil - Custom StoppingCriteria
# seed: nil - Random seed for reproducibility
# penalty_params: nil - PenaltyManager.Params
# ils_params: nil - IteratedLocalSearch.Params
```
### Result
```elixir
# Result struct matches PyVRP's Result class
result.best # Best Solution found
result.num_iterations # Total iterations performed
result.runtime # Runtime in milliseconds
result.stats # Statistics map
# Methods
Result.cost(result) # Cost (or :infinity if infeasible)
Result.is_feasible(result) # Boolean feasibility check
Result.summary(result) # Human-readable summary string
```
### Stopping Criteria
All criteria match PyVRP's `pyvrp.stop` module:
```elixir
# Stop after N iterations
StoppingCriteria.max_iterations(1000)
# Stop after N seconds (float, like PyVRP's MaxRuntime)
StoppingCriteria.max_runtime(60.0)
# Stop after N iterations without improvement
StoppingCriteria.no_improvement(500)
# Stop when feasible solution found
StoppingCriteria.first_feasible()
# Combine criteria (OR logic) - matches PyVRP's MultipleCriteria
StoppingCriteria.multiple_criteria([
StoppingCriteria.max_iterations(10_000),
StoppingCriteria.max_runtime(300.0)
])
```
### Model Builder
```elixir
model = Model.new()
|> Model.add_depot(x: 0, y: 0)
|> Model.add_vehicle_type(
num_available: 5,
capacity: [100], # Multi-dimensional capacity
tw_early: 0, # Time window start
tw_late: 28800, # Time window end (8 hours)
max_duration: 28800,
unit_distance_cost: 1,
unit_duration_cost: 0
)
|> Model.add_client(
x: 10, y: 20,
delivery: [25], # Delivery demand
pickup: [0], # Pickup demand
service_duration: 300, # Service time
tw_early: 0,
tw_late: 14400
)
```
## Architecture
```
┌─────────────────────────────────────────────────────────────┐
│ Elixir Application │
├─────────────────────────────────────────────────────────────┤
│ ExVrp.Solver - Main solve() interface │
│ ExVrp.IteratedLocalSearch - ILS with Late Acceptance HC │
│ ExVrp.PenaltyManager - Dynamic penalty adjustment │
│ ExVrp.StoppingCriteria - Stopping conditions │
│ ExVrp.Model - Problem builder │
│ ExVrp.Solution - Solution representation │
├─────────────────────────────────────────────────────────────┤
│ ExVrp.Native - NIF bindings (via Fine) │
├─────────────────────────────────────────────────────────────┤
│ c_src/ex_vrp_nif.cpp - C++ NIF implementation │
│ c_src/pyvrp/ - PyVRP C++ core │
└─────────────────────────────────────────────────────────────┘
```
### PyVRP API Mapping
| PyVRP (Python) | ExVrp (Elixir) |
|----------------|----------------|
| `model.solve(stop=..., seed=...)` | `Solver.solve(model, stop: ..., seed: ...)` |
| `result.cost()` | `Result.cost(result)` |
| `result.is_feasible()` | `Result.is_feasible(result)` |
| `result.best` | `result.best` |
| `MaxIterations(n)` | `StoppingCriteria.max_iterations(n)` |
| `MaxRuntime(secs)` | `StoppingCriteria.max_runtime(secs)` |
| `NoImprovement(n)` | `StoppingCriteria.no_improvement(n)` |
| `MultipleCriteria([...])` | `StoppingCriteria.multiple_criteria([...])` |
| `PenaltyManager.init_from(data)` | `PenaltyManager.init_from(data)` |
## Implementation Status
### Core Features ✅
- [x] Model builder (Client, Depot, VehicleType)
- [x] C++ NIF bindings via Fine
- [x] ProblemData creation
- [x] Solution extraction (routes, distance, duration, feasibility)
- [x] LocalSearch NIF (with all operators)
- [x] CostEvaluator NIF
- [x] IteratedLocalSearch (Late Acceptance Hill-Climbing)
- [x] PenaltyManager (dynamic penalty adjustment)
- [x] All stopping criteria (MaxIterations, MaxRuntime, NoImprovement, MultipleCriteria, FirstFeasible)
### Test Coverage
- 100 tests covering PyVRP API compatibility
- Tests ported from PyVRP's pytest suite
- Includes `test/pyvrp_api_test.exs` with explicit PyVRP behavior verification
## Development
### Prerequisites
- Elixir 1.15+
- C++20 compiler (gcc 11+ or clang 14+)
- Make
- Nix (optional, for reproducible builds)
### Setup
```bash
cd ex_vrp
mix deps.get
mix compile
mix test --include nif_required
```
### Running Tests
```bash
# Run all tests including NIF-dependent ones
mix test --include nif_required
# Run only pure Elixir tests
mix test
```
## License
MIT License - see LICENSE file.
## Acknowledgments
- [PyVRP](https://github.com/PyVRP/PyVRP) - The underlying solver
- [Fine](https://github.com/elixir-nx/fine) - Ergonomic C++ NIF bindings