LP/MILP-powered fuel stop optimization for long-haul trucking. Weight-dependent consumption models, multi-station planning, 5–15% cost savings. Powered by Ralph.
Fuel is 30–40% of a trucking company’s operating cost. Most drivers refuel at the nearest station or fill up everywhere. FuelWise formulates fuel purchasing as a mathematical optimization problem and solves it exactly—finding the minimum-cost strategy that respects tank capacity, safety margins, and weight-dependent consumption.
LP/MILP formulation with proven optimality. Not heuristics, not rules of thumb—provably optimal fuel purchasing via Ralph’s simplex and branch-and-bound solvers.
Trucks consume 24 L/100km empty, 36+ L/100km loaded. Built-in curves for EU standard (40t), US Class 8 (36t), and light trucks. Integrates cargo pickup/delivery events along the route.
LP relaxation for fast answers (<1ms). Full MILP for discrete stop decisions with minimum purchase constraints. Benders decomposition for 50+ station problems.
Pure C with no external dependencies. Compiles to WASM for browser demos. Embeds in fleet management systems, ELDs, or runs as a REST API.
FuelWise formulates the optimal refueling problem as a linear program. The MILP variant adds binary stop decisions and minimum purchase constraints.
Route: A ──────────────────────────────────────────── B
│ │ │ │
S1 S2 S3 S4
$1.45/L $1.52/L $1.38/L $1.49/L
Tank: ████████░░░░░░░ → ████░░░░░░░░░░░ → ████████████░░░
Start: 80L Low: skip S2 Fill cheap S3
LP Variables:
x[i] = liters purchased at station i (≥ 0)
y[i] = fuel level at station i (≤ capacity)
Objective: min Σ x[i] · price[i]
Constraints:
y[i] = y[i-1] + x[i-1] - consumed[i-1→i] (fuel balance)
y[i] ≥ min_fuel (safety margin)
y[i] + x[i] ≤ tank_capacity (no overflow)
y[end] ≥ min_end_fuel (reach dest)| Solver | 10 stations | 25 stations | 50 stations | Trade-off |
|---|---|---|---|---|
fw_solve_refuel_lp() | 1 ms | 1 ms | 1 ms | Fast; may split purchases fractionally |
fw_solve_refuel_milp() | 5 ms | 50 ms | 5 s | Exact discrete decisions; exponential worst case |
fw_solve_refuel_benders() | 10 ms | 30 ms | 300 ms | Scales to 100+; decomposes into master + sub |
The MILP variant adds binary variables z[i] ∈ {0,1} for stop decisions. Enables minimum purchase constraints (no 5L fill-ups), fixed stop costs (time penalty), and true all-or-nothing station selection. Ralph’s branch-and-bound with LP relaxation bounds solves these efficiently.
Empty trucks burn ~24 L/100km. Fully loaded (40t GVW), they burn ~36 L/100km. FuelWise integrates consumption over the route using weight profiles that track cargo pickup/delivery events.
| Curve | GVW | Range |
|---|---|---|
| EU Standard | 40t | 24–36.5 L/100km |
| US Class 8 | 36t | 28–42 L/100km |
| Light Truck | 12t | 12–20 L/100km |
/* Weight-dependent consumption */
FWConsumptionCurve *curve =
fw_curve_eu_standard();
FWWeightProfile *wp =
fw_weight_profile_create(15000);
fw_weight_profile_add_event(
wp, 100000, 10000); /* +10t at 100km */
fw_weight_profile_add_event(
wp, 300000, -5000); /* -5t at 300km */
double fuel = fw_calc_fuel_for_segment(
curve, wp, 0, 500000); /* 0→500km */FuelWise runs a three-stage pipeline: filter stations near the route, snap them to the polyline with distances, then solve the optimization problem.
Input: Output:
┌─────────────┐ ┌─────────────────────┐
│ Route (lat/lon polyline) │ Optimal stops: │
│ Fuel stations (price, location) │ S3: buy 120L │
│ Tank capacity, current fuel │ S7: buy 85L │
│ Consumption rate or curve │ Total cost: $142.50 │
│ Weight profile (optional) │ Savings: 12.3% │
└──────────────┬──────────────── └─────────────────────┘
│
┌───────────┼───────────┐
▼ ▼ ▼
┌──────┐ ┌────────┐ ┌────────┐
│Filter│ │ Snap │ │ Solve │
│ │→ │to Route│→ │LP/MILP │
│16km │ │project │ │ Ralph │
└──────┘ └────────┘ └────────┘
fw_filter_stations_two_step() → fw_solve_refuel_lp()
fw_solve_refuel_milp()
fw_solve_refuel_benders()For each candidate station, compute perpendicular distance to the route polyline. Stations within 16km (configurable) are projected onto the route with their distance-along-route recorded. Two-step filtering adds deduplication for overlapping station clusters.
FuelWise builds Ralph models directly: continuous variables for purchase amounts, binary variables for stop decisions, constraints for fuel balance and tank limits. Ralph returns primal values (purchases), dual values (shadow prices for business analysis), and Farkas rays (for Benders feasibility cuts).
Separation of concerns. FuelWise formulates the optimization problem. Ralph solves it. Neither knows the other’s internals. This lets Ralph improve (better cuts, faster branching) and FuelWise benefit automatically.
#include "fuelwise.h"
/* High-level: filter + snap + solve in one call */
FWOptimizeRequest req = {
.route = polyline,
.stations = all_stations,
.num_stations = 200,
.tank_capacity = 300.0, /* liters */
.current_fuel = 80.0, /* liters */
.consumption = 28.5, /* L/100km (or use curve) */
.min_fuel = 10.0, /* safety margin */
.max_detour_m = 16000, /* 16km filter radius */
.solver = FW_SOLVER_LP
};
FWOptimizeResponse resp;
FWStatus status = fw_optimize(&req, &resp);
if (status == FW_STATUS_OPTIMAL) {
printf("Total cost: $%.2f\n", resp.solution.total_cost);
printf("Stops: %d\n", resp.solution.num_stops);
for (int i = 0; i < resp.solution.num_stops; i++) {
printf(" Station %d: buy %.1f L @ $%.3f/L\n",
resp.solution.stops[i].station_id,
resp.solution.stops[i].liters,
resp.solution.stops[i].price);
}
}
fw_free_response(&resp);| Endpoint | Description |
|---|---|
POST /api/v1/optimize | Full pipeline: filter + solve with JSON input |
GET /api/v1/health | Health check |
GET /api/v1/stats | Solver stats, queue depth, rate limit info |
/* Solution to JSON */
char *json = fw_solution_to_json(&resp.solution);
printf("%s\n", json);
fw_free_json(json);
/* Full response (includes filtered stations) */
char *full = fw_response_to_json(&resp);
fw_free_json(full);
/* Status as string */
const char *s = fw_status_string(status); /* "OPTIMAL" */SI internally (meters, liters, L/100km). Imperial conversion at API boundaries. Metric and imperial inputs produce mathematically equivalent results—verified by round-trip tests.
Problem generator creates randomized scenarios with Gamma-distributed station gaps. Independent constraint validator checks feasibility without using solver internals. 123 validator tests across 5 scenario types ensure correctness.
| Scenario | Route | Stations | Tank | Weight Events |
|---|---|---|---|---|
| Short Urban | 200 km | ~20 | 100 L | 2 |
| Highway | 800 km | ~25 | 300 L | 4 |
| Long Haul | 2,000 km | ~50 | 500 L | 8 |
| Tight Margins | 500 km | ~8 | 200 L | 2 |
| Weight Heavy | 600 km | ~15 | 400 L | 6 |
Honest assessment. FuelWise solves the “where to buy fuel” problem optimally. It does not yet handle fuel card network filtering, volume discounts (non-convex pricing), DEF co-purchasing, or Hours-of-Service co-optimization. These are roadmap items. The core LP/MILP formulation is production-ready; the real-world constraint coverage is still growing.
fuelwise/ ├── include/ │ ├── fuelwise.h # Unified public API │ ├── fw_types.h # Data types (291 lines) │ ├── fw_api.h # Transport-agnostic handler │ ├── fw_refuel.h # LP/MILP/Benders solvers │ ├── fw_route.h # Station filtering │ ├── fw_consumption.h # Weight-dependent models │ └── fw_geo.h # Geospatial utilities ├── src/ │ ├── fw_refuel.c # Solver formulation (994 lines) │ ├── fw_api.c # REST handler (804 lines) │ ├── fw_route.c # Station snapping (463 lines) │ ├── fw_consumption.c # Consumption curves (338 lines) │ ├── fuelwise.c # Pipeline glue (359 lines) │ └── fw_geo.c # Haversine utils (22 lines) ├── api/src/main.c # Mongoose HTTP wrapper ├── wasm/ # WASM wrapper ├── bench/ # Benchmark infrastructure └── tests/test_fuelwise.c # 157 tests
# Build library + tests make fuelwise # Run tests make test-fuelwise # 157 tests (34 unit + 123 validator) # Build API server make fuelwise-api # Start server ./fuelwise/api/fuelwise-server # WASM build (requires Emscripten) make fuelwise-wasm
# Optimize refueling (POST JSON)
curl -X POST http://localhost:8080/api/v1/optimize \
-H "Content-Type: application/json" \
-d '{
"route": [[47.5,19.0],[47.4,18.5],[47.3,18.0]],
"stations": [
{"id":1,"lat":47.45,"lon":18.7,"price":1.45},
{"id":2,"lat":47.38,"lon":18.3,"price":1.38}
],
"tank_capacity": 300,
"current_fuel": 80,
"consumption": 28.5
}'
# Health check
curl http://localhost:8080/api/v1/healthGreedy (“always fill at cheapest visible station”) can’t see far enough ahead. It may fill up at $1.40/L when a $1.30/L station is 50km further. LP considers all stations simultaneously and finds the provably optimal strategy. For 25 stations, LP solves in under 1ms—there’s no reason to approximate.
LP allows fractional purchases (“buy 23.7L at station 3”). Use it for fast planning and routing decisions. MILP adds binary stop decisions: if you stop, buy at least a minimum amount. Use it when stop costs matter (time penalty for pulling off highway) or when minimum purchase rules apply. For >50 stations, use Benders decomposition.
Create a FWWeightProfile with initial weight, then add events: fw_weight_profile_add_event(wp, distance_m, delta_kg). Positive delta = cargo pickup, negative = delivery. FuelWise integrates consumption(weight(d)) over each segment using piecewise-linear integration from the shared library. Built-in curves map GVW to L/100km.
fw_validate_problem() checks feasibility before solving: tank can reach the first station, overall fuel is sufficient, etc. If the solver returns FW_STATUS_INFEASIBLE, the problem has no valid refueling plan—stations are too far apart, tank is too small, or fuel isn’t enough. The error message indicates which constraint failed.
Yes. fw_consumption_curve_create() builds a custom piecewise-linear curve: add (weight, L/100km) points with fw_consumption_curve_add_point(). The solver interpolates between points. Or use the built-in curves: fw_curve_eu_standard(), fw_curve_us_class8(), fw_curve_light_truck().
Consumer apps suggest the cheapest nearby station. FuelWise optimizes across the entire route: “buy 50L cheap now, coast past 3 expensive stations, fill up at the discount station 200km ahead.” It considers cumulative fuel state, weight changes, and tank capacity. This multi-station lookahead is the difference between a suggestion and an optimization.