FuelWise Refueling Optimizer

LP/MILP-powered fuel stop optimization for long-haul trucking. Weight-dependent consumption models, multi-station planning, 5–15% cost savings. Powered by Ralph.

157 tests ~3K LoC 5–15% savings WASM-ready Zero deps

Why FuelWise

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.

Mathematical Precision

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.

Weight-Aware Consumption

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.

Three Solver Tiers

LP relaxation for fast answers (<1ms). Full MILP for discrete stop decisions with minimum purchase constraints. Benders decomposition for 50+ station problems.

Runs Anywhere

Pure C with no external dependencies. Compiles to WASM for browser demos. Embeds in fleet management systems, ELDs, or runs as a REST API.

Optimization Model

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)
<1ms LP Solve (25 stations)
~50ms MILP Solve (25 stations)
5–15% Typical Savings
3 Solver Tiers

Solver Comparison

Solver10 stations25 stations50 stationsTrade-off
fw_solve_refuel_lp()1 ms1 ms1 msFast; may split purchases fractionally
fw_solve_refuel_milp()5 ms50 ms5 sExact discrete decisions; exponential worst case
fw_solve_refuel_benders()10 ms30 ms300 msScales to 100+; decomposes into master + sub

MILP Extensions

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.

Weight-Dependent Consumption

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.

CurveGVWRange
EU Standard40t24–36.5 L/100km
US Class 836t28–42 L/100km
Light Truck12t12–20 L/100km
C
/* 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 */

Pipeline

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()

Station Filtering

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.

Ralph Integration

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.

API

C
#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);

REST API

EndpointDescription
POST /api/v1/optimizeFull pipeline: filter + solve with JSON input
GET /api/v1/healthHealth check
GET /api/v1/statsSolver stats, queue depth, rate limit info

Serialization

C
/* 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" */

Architecture

Unit System

SI internally (meters, liters, L/100km). Imperial conversion at API boundaries. Metric and imperial inputs produce mathematically equivalent results—verified by round-trip tests.

Benchmark Infrastructure

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.

ScenarioRouteStationsTankWeight Events
Short Urban200 km~20100 L2
Highway800 km~25300 L4
Long Haul2,000 km~50500 L8
Tight Margins500 km~8200 L2
Weight Heavy600 km~15400 L6

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.

Structure
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

Quick Start

Build
# 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
Query
# 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/health

Developer FAQ

Why LP instead of a greedy algorithm?

Greedy (“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.

When should I use MILP vs LP?

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.

How does the weight profile work?

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.

What happens if no feasible solution exists?

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.

Can I use custom consumption models?

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().

How is this different from Google/Waze fuel suggestions?

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.