Skip to content

Repository files navigation

Courier Route Optimizer

Interactive demo of route optimization for courier deliveries. It scatters a batch of delivery points across Madrid, runs several TSP heuristics over them, and shows how much each strategy saves compared to visiting the points in the order they came in. Click a row in the comparison table to see that route on the map.

Live at isistomin.com/tsp.

This is a public rebuild of a routing problem I worked on for a courier logistics bot: given a day's delivery list, produce a sane visiting order fast enough to not matter. Exact TSP solvers are overkill at this scale — a greedy constructor plus 2-opt refinement lands within a few percent of optimal in under a millisecond for 15–25 stops.

Algorithms

All strategies produce a closed tour and are implemented as pure functions in src/tsp.ts. Distances are great-circle (haversine), in kilometres.

Naive — visit points in generation order. Not an algorithm, just the baseline every other strategy is measured against.

Nearest neighbour — start at the first point and repeatedly hop to the closest unvisited one. O(n²), trivially simple, and usually already 10–20% better than the baseline. Its weakness is the endgame: the last few unvisited points tend to be scattered leftovers, and the tour pays for them with long closing edges.

Greedy edge — instead of growing a single path, sort all point-to-point edges by length and keep taking the shortest one that is still legal: no point may get a third neighbour, and no cycle may close before it spans every point (tracked with a union-find). The kept edges form one Hamiltonian cycle. It avoids nearest neighbour's endgame problem because short edges are claimed globally, not from the perspective of a single walker.

Genetic algorithm — a population of candidate tours evolves for a fixed number of generations: tournament selection picks parents, order crossover (OX1) recombines them while keeping every stop exactly once, and an occasional segment inversion mutates the offspring. The best tour survives each generation unchanged, and the initial population is seeded with the nearest-neighbour tour, so the result never regresses below that baseline. Much more machinery than the greedy constructors for a similar outcome at this problem size — which is rather the point of showing it.

Ant colony — an elitist ant system. Each iteration a colony of ants builds tours edge by edge, picking the next stop with probability proportional to pheromone^α · (1/distance)^β. Pheromone then evaporates, every ant deposits an amount inversely proportional to its tour length, and the best tour found so far gets an extra deposit. Over iterations the pheromone trail concentrates on short edges and the colony converges. Stochastic like the GA, and the most expensive strategy in the table, but it explores the search space in a qualitatively different way: through a shared memory of good edges rather than recombination of whole tours.

2-opt — a refinement pass over an existing tour, not a constructor. While any two edges (a→b, c→d) can be replaced by (a→c, b→d) for a net saving, reverse the segment between them and repeat. This uncrosses the route until it reaches a local minimum (with an iteration cap as a safety net). Applied here on top of both constructors; the refined variants are consistently the shortest, typically 15–30% below the baseline.

Quick start

npm ci
npm run dev

Other scripts: npm test (vitest over the TSP core), npm run lint, npm run typecheck, npm run build (emits dist/).

Deployment

The build is plain static files rooted at /tsp/ (base in vite.config.ts), served by the nginx container of isistomin.com via a bind mount. The build itself also runs in Docker (see docker-compose.yml), so the server needs no Node installation.

On the server:

git clone https://github.com/isistomin/tsp.git /root/tsp
cd /root/tsp && docker compose run --rm build

In the site's docker-compose.yml, mount the build into the nginx container:

volumes:
  - /root/tsp/dist:/var/www/tsp:ro

In the HTTPS server block:

location = /tsp { return 301 /tsp/; }
location /tsp/ {
    alias /var/www/tsp/;
    index index.html;
}

Recreate the container so the new volume is picked up (reload is not enough): docker compose up -d --force-recreate nginx. Verify with curl -sI https://isistomin.com/tsp/ | head -3.

Updating the demo afterwards is just git pull && docker compose run --rm build in the repo — nginx serves the new files directly, no restarts.

About

No description, website, or topics provided.

Resources

Stars

0 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages