Route optimization for Indian logistics — delivery fleets with resource-constrained pickup and delivery.
FleetPilot — a two-stage metaheuristic (ALNS + BRKGA) route-optimization solver for resource-constrained pickup and delivery in Indian logistics fleets.
FleetPilot solves problems where goods must be delivered, processed, and then picked up, potentially by different vehicles across multiple trips. It uses a two-stage metaheuristic — Adaptive Large Neighborhood Search followed by Biased Random-Key Genetic Algorithm — to find high-quality routes fast.
Built for Indian logistics: Supports time windows, multi-depot operations, traffic-aware routing, inter-vehicle transfers at hub nodes, multi-objective optimization (cost, distance, CO₂), and island-model parallel BRKGA via worker_threads.
This implementation surpasses the baseline algorithms described in arXiv:2602.23685v2 with several novel enhancements:
| Improvement | Description |
|---|---|
| Adaptive Removal Sizing | ALNS removal fraction auto-adjusts 10% → 45% based on stagnation ratio |
| Multi-Restart ALNS | Up to 3 restarts with temperature reset and weight zeroing on stagnation |
| Clone Avoidance | ALNS only clones solution on new best, avoiding regressions |
| Elite Diversity Preservation | Mild mutation on elite BRKGA copies proportional to stagnation |
| Adaptive Mutation Rate | Up to +5% extra mutants injected when population stagnates |
| Immigrant Injection | 20% of population replaced with fresh random individuals before breaking stagnation |
| Hall-of-Fame Tracking | Best-ever solution tracked separately from population elite |
| Decoder O(1) Capacity Checks | Incremental RouteLoad tracking replaces O(n) route simulation |
| Island-Model Parallelization | Multi-population BRKGA with elite migration via worker_threads |
- ALNS — Adaptive Large Neighborhood Search — 6 destroy + 4 repair operators, adaptive weight selection
- BRKGA — Biased Random-Key Genetic Algorithm — 4n chromosome, elite / mutant / crossover evolution
- Warm-start — ALNS solution seeds 15% of BRKGA population for faster convergence
- Time windows — Earliest / latest delivery and pickup constraints
- Multi-depot — Vehicles can start / end at different depots
- Traffic-aware — Time-dependent travel speeds via traffic model
- Inter-vehicle transfers — Exchange resources at hub nodes
- Multi-objective — Pareto optimization for makespan, distance, cost, CO₂
- Analytics — Vehicle utilization, wait times, load profiles, route comparison
- GIS export — GeoJSON, KML, CSV output for QGIS, Google Earth, Excel
- Serialization — Save / load solutions as JSON
- Parallel solving — Run ALNS and BRKGA concurrently via worker threads
- Progress callback — Real-time progress with iteration and best makespan
npm install fleetpilotgit clone https://github.com/sachncs/fleetpilot.git
cd fleetpilot
npm install# Install globally to use the fleetpilot binary
npm install -g fleetpilot
fleetpilot --problem problem.json --output solution.json
fleetpilot --problem problem.json --progress| Option | Default | Description |
|---|---|---|
--problem <file> |
required | Path to problem JSON file |
--output <file> |
stdout | Write solution JSON |
--alns-iterations <n> |
500 |
ALNS iterations |
--population-size <n> |
30000 |
BRKGA population size |
--max-generations <n> |
20000 |
BRKGA max generations |
--max-time <ms> |
0 (unlimited) |
Max solver time |
--target-makespan <n> |
0 (disabled) |
Early stopping target |
--parallel |
off | Run ALNS + BRKGA in parallel |
--no-warm-start |
on | Disable ALNS warm-start |
--progress |
off | Print progress to stderr |
import { FleetPilotSolver, Problem, LocationNode, Customer, Vehicle } from 'fleetpilot';
const nodes = {
0: new LocationNode(0, 0, 0, 'Depot'),
1: new LocationNode(1, 10, 0, 'Customer A - Drop'),
2: new LocationNode(2, 20, 0, 'Customer A - Pick'),
};
const customers = [new Customer(1, 1, 2, 50)]; // id, del-node, pk-node, processing-minutes
const vehicles = [new Vehicle(1, 5)]; // id, capacity
const problem = new Problem(nodes, customers, vehicles, 0);
const solver = new FleetPilotSolver(problem);
const solution = await solver.solve({ maxTimeMs: 30000 });
console.log(`Best makespan: ${solution.makespan.toFixed(2)} min`);
console.log(`Feasible: ${solution.isFeasible()}`);
console.log(`Distance: ${solution.totalDistance.toFixed(2)} km`);interface SolveOptions {
alnsIterations?: number; // Default: 500
populationSize?: number; // Default: 100 (practical) / 30000 (paper)
maxGenerations?: number; // Default: 100 (practical) / 20000 (paper)
initialTemp?: number; // Default: 100
coolingRate?: number; // Default: 0.9998
parallel?: boolean; // Default: false
warmStart?: boolean; // Default: true
maxTimeMs?: number; // Default: 0 (unlimited)
targetMakespan?: number; // Default: 0 (disabled)
seed?: number; // Default: 1 (deterministic by default)
signal?: AbortSignal; // Throws AbortError when triggered
islands?: number; // Default: 1 (single-island)
migrationInterval?: number; // Default: 50 generations between migrations
migrantFraction?: number; // Default: 0.05
logger?: Logger;
onProgress?: (p: SolverProgress) => void;
}No environment variables are required for core usage. Defaults are tuned for paper-quality results. FLEETPILOT_WORKER_PATH overrides the worker bundle path used by parallel: true (handy for tests).
| Setting | Default | Description |
|---|---|---|
alnsIterations |
500 |
ALNS iteration cap |
populationSize |
30000 |
BRKGA population size |
maxGenerations |
20000 |
BRKGA generation cap |
initialTemp |
100 |
Simulated-annealing start temperature |
coolingRate |
0.9998 |
Geometric cooling factor |
parallel |
false |
Run ALNS + BRKGA concurrently |
warmStart |
true |
Seed 15% of BRKGA from ALNS solution |
maxTimeMs |
0 |
Wall-clock cap (0 = unlimited) |
targetMakespan |
0 |
Early-stop on reaching target |
seed |
1 |
Deterministic mulberry32 seed |
islands |
1 |
BRKGA island count (multi-island ≥ 2) |
migrationInterval |
50 |
Generations between elite migrations |
migrantFraction |
0.05 |
Fraction of each island that emigrates |
| Symbol | Type | Description |
|---|---|---|
FleetPilotSolver |
class | Orchestrator (ALNS → warm-start → BRKGA) |
Problem |
class | Standard problem definition |
TrafficAwareProblem |
class | Problem with time-dependent travel times |
MultiDepotProblem |
class | Multi-depot variant |
Customer / CustomerWithTimeWindows |
class | Customer with optional delivery / pickup windows |
Vehicle / VehicleWithCapabilities |
class | Vehicle with capacity and directional capabilities |
LocationNode |
class | Node with coordinates |
RouteAnalytics |
class | Post-solution summary metrics |
SolutionComparator |
class | Pareto-front comparison |
GISExporter |
class | .toGeoJson() / .toKml() / .toCsv() |
TrafficModel |
class | Time-dependent segment factors |
Error → ValidationError | InfeasibleSolutionError | AlgorithmConvergenceError | AbortError |
classes | Typed error hierarchy |
SolutionWithTransfers / ProblemWithTransfers / TransferHub / TransferManager |
classes | Inter-vehicle resource transfers at hub nodes |
TransferAwareInsertionOperators / TransferAwareRemovalOperators |
objects | ALNS operators that respect transfer constraints |
MultiDepotProblem (extends Problem) + Depot |
classes | Vehicles start/end at different depots |
import { CustomerWithTimeWindows } from 'fleetpilot';
// Deliver between 9 AM and 1 PM (360–480 min), pick up between 11 AM and 10 PM (420–600 min)
const customer = new CustomerWithTimeWindows(
1,
1,
2, // id, delivery node, pickup node
30, // 30-min processing
360,
480, // earliest / latest delivery
420,
600, // earliest / latest pickup
);import { TrafficAwareProblem, TrafficModel } from 'fleetpilot';
const traffic = new TrafficModel();
traffic.setSegment({
fromId: depotNodeId,
toId: customerNodeId,
baseTravelTime: 30,
currentTravelTime: 30,
congestionLevel: 'low',
});
traffic.setTimeFactors(depotNodeId, customerNodeId, [
{ startTime: 8, factor: 1.5 }, // 8am rush hour
{ startTime: 9, factor: 2.0 },
{ startTime: 17, factor: 1.8 },
{ startTime: 18, factor: 1.6 },
]);
const problem = new TrafficAwareProblem(nodes, customers, vehicles, 0, traffic);import { RouteAnalytics, GISExporter } from 'fleetpilot';
const analytics = new RouteAnalytics(solution, problem);
console.log(analytics.getSummary());
// { makespan, totalDistance, totalCost, totalCo2, avgUtilization, ... }
const exporter = new GISExporter(solution, problem);
const geojson = exporter.toGeoJson(); // QGIS / Mapbox
const kml = exporter.toKml(); // Google Earth
const csv = exporter.toCsv(); // Excelconst solution = await solver.solve({
onProgress: (p) => {
console.log(
`[${p.stage}] ${p.iteration}/${p.maxIterations} — best: ${p.bestMakespan.toFixed(1)}min`,
);
},
});{
"nodes": [
{ "id": 0, "x": 28.61, "y": 77.23, "name": "Delhi Depot" },
{ "id": 1, "x": 28.54, "y": 77.2, "name": "Customer 1 Drop" },
{ "id": 2, "x": 28.56, "y": 77.25, "name": "Customer 1 Pick" }
],
"customers": [{ "id": 1, "deliveryNodeId": 1, "pickupNodeId": 2, "processingTime": 30 }],
"vehicles": [{ "id": 1, "capacity": 100, "costPerKm": 12, "co2PerKm": 0.15 }],
"depotNodeId": 0
}For time windows, add these fields to customers: earliestDeliveryTime, latestDeliveryTime, earliestPickupTime, latestPickupTime.
The solver is organized into four layers:
| Layer | Directory | Responsibility |
|---|---|---|
| Core | src/core/ |
Problem definition, solution model, schedule engine |
| Algorithms | src/algorithms/ |
ALNS metaheuristic, BRKGA evolutionary algorithm, chromosome decoder |
| Analytics | src/analytics/ |
Post-solution analysis, Pareto front computation |
| Export | src/export/ |
GIS serialization (GeoJSON, KML, CSV) |
The FleetPilotSolver orchestrator runs a two-stage metaheuristic: ALNS first, then BRKGA warm-started from the ALNS solution. In parallel mode both run concurrently via worker_threads and the best result is returned.
Problem ──► ALNS (adaptive destroy/repair) ──► warm-start ──► BRKGA (evolutionary) ──► Best Solution
│
(chromosome encoding
of ALNS solution,
15% of initial pop)
- Strategy pattern — ALNS destroy / repair operators are interchangeable functions selected via weighted roulette, enabling easy addition of new operators.
- Adaptive weighting — Operator weights update every segment via reinforcement learning:
w[i] = (1-λ)·w[i] + λ·(score[i]/usage[i])with λ = 0.1. - Simulated annealing acceptance — Worsening solutions accepted with probability
exp((cost_current - cost_new) / temperature); temperature decays geometrically. - Biased crossover (BRKGA) — Each child inherits each gene from the elite parent with probability 0.7.
- Message-passing IPC — Worker threads communicate via a typed request-response protocol supporting
evolve,inject, andfinishcommands. - Logger DI — A minimal
Loggerinterface allows silent library use or custom logging without coupling to a framework. - Error hierarchy —
Error→ValidationError|InfeasibleSolutionError|AlgorithmConvergenceError.
The decoder tracks three values per route: RouteLoad { currentLoad, minDelta, maxDelta }. A new operation is feasible iff initialLoadNeeded ≤ capacity and peakLoad ≤ capacity, where initialLoadNeeded = -newMin and peakLoad = initialLoadNeeded + newMax. This avoids an O(n) scan of the entire route on every insertion.
removalFraction = 0.1 + stagnationRatio × 0.35, growing from 10% to 45% as stagnation increases.
When iterationsSinceImprovement ≥ maxStagnation (5% of max iterations, min 25), ALNS restarts: temperature resets to initialTemp × 0.5^restartsUsed, cooling rate becomes baseCooling × (1 + restartsUsed × 0.02), all operator weights zero, up to 3 restarts allowed.
The 4n-gene chromosome is decoded in three passes: (1) deliveries scheduled in priority order, assigned via σ genes; (2) delivery visit times computed (needed for pickup feasibility); (3) pickups scheduled respecting resource-ready-time constraints.
Chromosome = { priorities (π), assignments (σ), dependencies (α), transfers (β) }
Each gene is a float in [0, 1). Total size = 4 × numCustomers.
priorities[i] = (routeIdx × 100 + position) / (numRoutes × 100), assignments[i] = routeIdx / numVehicles — seeds 15% of the initial BRKGA population.
Cross-route resource dependencies require iterative schedule computation. The loop visits all routes repeatedly until node times converge (max 1000 iterations), propagating resource-ready-time updates between vehicles.
- Elite diversity preservation: mild mutation on elite copies (
rate = min(0.05, stagnationRatio × 0.1)). - Adaptive mutation rate: extra mutants =
⌊stagnationRatio × populationSize × 0.05⌋. - Immigrant injection: 20% of population replaced with fresh random individuals before breaking stagnation.
Each customer c has a delivery node D_c, a pickup node P_c, and a processing time p_c. The resource constraint is:
arrivalTime(P_c) ≥ arrivalTime(D_c) + p_c
This creates temporal dependencies: one vehicle may deliver, another may pick up, but pickup cannot start until processing completes.
Euclidean distance between nodes: distance(i, j) = √((x_i - x_j)² + (y_i - y_j)²). Travel time: distance / speed, optionally modified by the traffic model.
Every segmentSize iterations (default 50): w[i] = (1 - λ) · w[i] + λ · (score[i] / usage[i]). Score per operator: new global best +33, better than current +9, accepted via SA +13, rejected 0. Selection probability: P(i) = w[i] / Σⱼ w[j].
P(accept worse) = exp((cost_current - cost_new) / temperature); temp ← temp × coolingRate each iteration.
Each gene of a child: child[i] = elite[i] with probability 0.7, else nonElite[i].
- Sort population by fitness ascending
- Copy top
e × popSizeelites (with mild mutation) - Replace bottom
m × popSizewith random mutants - Fill remainder via biased crossover between random elite + random non-elite
regret_k(c) = cost_k(c) - cost_1(c). The customer with the largest regret (most "urgent") is inserted first. If fewer than k routes can accommodate a customer, a fallback cost is used.
relatedness(c₁, c₂) = ||D_c₁ - D_c₂||₂ + |time(D_c₁) - time(D_c₂)|. Lower relatedness = more similar = removed together.
A solution is Pareto-optimal if no other solution dominates it. Dominance: other dominates current iff all objectives ≤ current and strictly better in at least one of {makespan, totalDistance, totalCost, totalCO₂}.
Time-dependent travel speed: if departureTime ≥ latest-matching factor.startTime, travelTime = baseTravelTime × factor.multiplier; else travelTime = segment.currentTravelTime. Congestion level from newTravelTime / baseTravelTime (low < 1.2, medium < 1.5, high < 2.0, severe otherwise).
transferDuration = amount × hub.transferTimePerUnit. Each hub enforces a concurrency limit. Vehicles have directional transfer permissions.
- Quick test:
alnsIterations: 100, populationSize: 1000, maxGenerations: 500 - Production:
alnsIterations: 500, populationSize: 30000, maxGenerations: 20000(paper defaults) - Time-constrained: Set
maxTimeMsto stop early with a feasible solution - Multi-core: Enable
parallel: true(ALNS + BRKGA concurrently). For island-model BRKGA, construct aBRKGAinstance directly withBRKGAOptionsand pass it viawarmStartSolution. - Stagnation resistance: The ALNS multi-restart and BRKGA adaptive-mutation / immigrant-injection mechanisms automatically handle most convergence issues
npm install
npm run build # rollup ESM + CJS bundles
npm run dev # rollup --watch
npm test # 350+ tests
npm run test:coverage # c8 text + lcov + html
npm run lint # eslint src tests
npm run lint:fix
npm run typecheck # tsc --noEmit
npm run docs # typedoc HTML
npm run docs:md # typedoc-plugin-markdown
npm run clean # rm -rf dist docs/api docs/mdnpm test # mocha (350+ tests)
npm run test:watch
npm run test:coverage # c8 with text / lcov / html reports and 85/85/70/85 thresholdsnpm run build # rollup ESM + CJS + .d.ts → dist/
npm run prepublishOnly # build + test gate (run automatically before publish)Artifacts:
dist/index.mjs,dist/index.cjs,dist/index.d.ts— librarydist/cli.mjs—fleetpilotbinary
npm run lint && npm run typecheck && npm test && npm run build
# Bump version in package.json (e.g. 0.1.2 → 0.1.3)
git tag v0.1.X && git push origin v0.1.X
# .github/workflows/publish.yml publishes to npm via trusted publishingsrc/
├── core/ # Problem & solution definitions
│ ├── problem.ts # - Routing problem
│ ├── solution.ts # - Solution routing
│ ├── multi-depot-problem.ts # - Multi-depot
│ ├── traffic-aware-problem.ts # - Traffic model
│ ├── transfer-hub.ts # - Inter-vehicle transfer hub
│ ├── transfer-manager.ts # - Inter-vehicle transfer scheduling
│ ├── resource-transfer-types.ts # - ResourceTransfer interface
│ ├── vehicle-with-capabilities.ts # - VehicleWithCapabilities + FleetManager
│ ├── solution-with-transfers.ts # - Solution/problem + transfers
│ └── validate-problem-base.ts # - Shared input validation
├── algorithms/
│ ├── alns/ # ALNS metaheuristic
│ │ ├── alns.ts
│ │ ├── operators.ts
│ │ └── transfer-aware-operators.ts
│ └── brkga/ # BRKGA evolutionary algorithm
│ ├── brkga.ts
│ ├── decoder.ts
│ └── island-messenger.ts # - Worker communication
├── analytics/ # Solution analysis
│ ├── route-analytics.ts
│ ├── solution-comparator.ts
│ └── *.ts # - Extracted result interfaces
├── export/ # GIS export (GeoJSON, KML, CSV)
│ ├── gis-exporter.ts
│ ├── geo-json.ts / geo-json-feature.ts
│ └── kml-placemark.ts
├── errors/ # Typed error classes (one per file)
│ ├── error.ts
│ ├── validation-error.ts
│ ├── infeasible-solution-error.ts
│ ├── algorithm-convergence-error.ts
│ └── abort-error.ts
├── utils/rng.ts # mulberry32 + RandomSource
├── logger.ts # Logger interface
├── env.ts # isNode / isBrowser
├── cli.ts # CLI entry point
├── index.ts # Public API exports
├── worker.ts # Node worker entry
├── worker-browser.ts # Browser worker entry
├── worker-core.ts # Shared worker task runner
├── worker-data.ts # Problem serialize / deserialize
├── worker-validation.ts # WorkerData validation
├── worker-path.ts # Env-aware worker bundle path
└── worker-spawn.ts # Node / browser worker spawn
| Category | Technology |
|---|---|
| Language | TypeScript 7.0+ |
| Module system | ES Modules with CommonJS + .d.ts support |
| Runtime | Node.js ≥ 20 |
| Build | Rollup + @rollup/plugin-typescript + rollup-plugin-dts |
| Test framework | Mocha + Chai |
| Coverage | c8 |
| Lint | ESLint + @typescript-eslint + eslint-plugin-import |
| Type check | tsc --noEmit |
| Format | Prettier 3.9 |
| Docs | TypeDoc + typedoc-plugin-markdown |
| CLI | dist/cli.mjs (fleetpilot bin) |
| Parallelism | worker_threads (Node) / Web Worker (browser via dist/worker.browser.js) |
- v2.0.0 — Current: Next.js web console, SQLite-backed jobs/keys, multi-depot solver, island-mode BRKGA, deterministic seeded RNG, Docker image, regression test suite.
- Next minor — Hardening: rate-limited geocoding, WebSocket auth, key self-protection, abort-signal coverage in the parallel and island paths, SBOM pipeline on npm 11.
- Future — Exploring (under research, no committed schedule): traffic-aware transfer hubs, lower-cost transfer models, additional benchmark families.
frontend/ (@fleetpilot/web) is a Next.js 16 + shadcn/ui frontend that lets users drop
a depot and customer stops on a real map, configure vehicles and time windows,
and step through the solved routes on a replayable timeline.
npm install
npm run build
# Development (custom server with WebSocket progress streaming)
npm run dev:frontend # http://localhost:3000
# Production
npm run start -w @fleetpilot/web
# Docker
docker compose up --build # mounts ./data for SQLite persistenceOn first launch an interactive setup wizard runs (skipped automatically in non-interactive environments such as Docker) and prints a generated API key used by the REST API.
Bug reports and PRs welcome on GitHub. Open an issue before sending large changes so we can align on direction. See CONTRIBUTING.md for the local setup, testing, and review workflow, and CODE_OF_CONDUCT.md for our community standards. Need help using the library? See SUPPORT.md.
Report vulnerabilities to sachncs@gmail.com. Please don't open a public issue for security-sensitive reports.
ISC © 2026 Sachin
- Saseendran, H., Sodhi, M., & Prasad, R. (2026). Vehicle Routing Problem with Resource-Constrained Pickup and Delivery. arXiv:2602.23685 · HTML