An urban shared e-bike company, "E-Donkey Express," needs to create a nightly battery-swapping plan for its fleet, which is distributed across three zones using its two mobile maintenance vans. The company's objective is to minimize the total nightly cost, which consists of a fixed dispatch fee for each van used and a variable operational cost. This operational cost is uniquely challenging, as it is proportional to the square of the number of batteries replaced by a van in a given zone (Cost = Coefficient × N²), reflecting a diseconomy of scale due to on-site logistical complexities.
The plan must ensure that all e-bikes with low-power batteries are serviced, while also respecting the maximum number of new batteries each van can carry. The core decision is how to assign the battery-swapping workload—which zones each van should service, and how many batteries to replace in each—to meet all demands at the lowest possible total cost. All necessary operational data is provided below.

Table 1: Zonal Demand Data

| Zone | Total E-Bikes | Low Battery Percentage (%) |
|:----:|:--------------:|:--------------------------:|
|   A  |      200       |             60             |
|   B  |      150       |             80             |
|   C  |      180       |             50             |

Table 2: Maintenance Station Parameters

| Parameter                    | Van 1 | Van 2 |
|:-----------------------------|:-----:|:-----:|
| Dispatch Cost ($)            |  300  |  320  |
| Max Battery Capacity (units) |  150  |  180  |
| Operational Cost Coefficient | 0.08  | 0.07  |

Problem type: NLP (Small: 8 variables, 5 constraints, 14 nonzeros)

Mathematical model:
Set
1.  Station
    The set of maintenance stations, \( I = \{1, 2\} \) # 1: Van 1, 2: Van 2
2.  Zone
    The set of zones, \( J = \{1, 2, 3\} \) # 1: Zone A, 2: Zone B, 3: Zone C

Parameter
1.  DispatchCost
    [300, 320] # Fixed cost for using van 1 and 2
2.  VanCapacity
    [150, 180] # Max battery capacity for van 1 and 2
3.  OperationalCostCoefficient
    [0.08, 0.07] # Operational cost coefficient for van 1 and 2
4.  BatteryDemand
    [120, 120, 90] # Number of batteries needed in Zone A, B, and C

Decision variable
1.  BatteriesReplaced
    Integer variable, \( x_{ij} \), \( \forall i \in I, j \in J \), the number of batteries replaced by station \(i\) in zone \(j\).
2.  StationUsed
    Binary variable, \( y_i \), \( \forall i \in I \), 1 if station \(i\) is used, 0 otherwise.

Objective
1.  Minimize the total operational cost.
    min: \( \sum_{i \in I} (\text{DispatchCost}[i] \cdot y_i) + \sum_{i \in I} \sum_{j \in J} (\text{OperationalCostCoefficient}[i] \cdot x_{ij}^2) \)

Constraint
1.  Demand Fulfillment constraint.
    \( \sum_{i \in I} x_{ij} = \text{BatteryDemand}[j], \forall j \in J \)
2.  Van Capacity constraint.
    \( \sum_{j \in J} x_{ij} \le \text{VanCapacity}[i] \cdot y_i, \forall i \in I \)
3.  Non-negativity constraint.
    \( x_{ij} \ge 0, \forall i \in I, j \in J \)

Type
Mixed, Non-linear, Linear
NLP

Gurobi implementation:
import gurobipy as gp
from gurobipy import GRB

def solve_ebike_battery_swapping(
    stations=["Van1", "Van2"],
    zones=["ZoneA", "ZoneB", "ZoneC"],
    dispatch_cost={"Van1": 300, "Van2": 320},
    van_capacity={"Van1": 150, "Van2": 180},
    op_cost_coeff={"Van1": 0.08, "Van2": 0.07},
    battery_demand={"ZoneA": 120, "ZoneB": 120, "ZoneC": 90}
):
    """
    Solves the E-Donkey Express battery swapping optimization problem.
    """
    # Create a new model
    model = gp.Model("EbikeBatterySwapping")

    # --- Decision Variables ---
    # Number of batteries replaced by station i in zone j
    batteries_replaced = model.addVars(stations,
                                       zones,
                                       vtype=GRB.INTEGER,
                                       name="BatteriesReplaced",
                                       lb=0)

    # Whether station i is used (dispatched)
    station_used = model.addVars(stations, vtype=GRB.BINARY, name="StationUsed")

    # --- Objective Function: Minimize Total Nightly Cost ---
    total_dispatch_cost = gp.quicksum(dispatch_cost[i] * station_used[i]
                                      for i in stations)

    # The operational cost is a quadratic term (cost = k * x^2)
    # Gurobi can handle quadratic objectives directly.
    total_operational_cost = gp.quicksum(
        op_cost_coeff[i] * batteries_replaced[i, j] * batteries_replaced[i, j]
        for i in stations for j in zones)

    model.setObjective(total_dispatch_cost + total_operational_cost,
                       GRB.MINIMIZE)

    # --- Constraints ---

    # 1. Demand Fulfillment constraint: All low-power batteries in each zone must be replaced.
    for j in zones:
        model.addConstr(
            gp.quicksum(batteries_replaced[i, j] for i in stations) ==
            battery_demand[j],
            name=f"DemandFulfillment_{j}")

    # 2. Van Capacity constraint: The number of batteries handled by a van cannot exceed its capacity.
    #    This constraint also links the station_used variable to the number of batteries replaced.
    #    If a van is not used (y_i=0), it cannot replace any batteries.
    for i in stations:
        model.addConstr(
            gp.quicksum(batteries_replaced[i, j] for j in zones) <=
            van_capacity[i] * station_used[i],
            name=f"Capacity_{i}")

    # Optimize the model
    # This is a Mixed-Integer Quadratically Constrained Program (MIQP).
    # The objective function is convex, so Gurobi will find the global optimum.
    model.optimize()

    # --- Return Results ---
    if model.status == GRB.OPTIMAL:
        return {"status": "optimal", "obj": model.objVal}
    else:
        return {"status": f"{model.status}"}


# Run the solver function
if __name__ == '__main__':
    result = solve_ebike_battery_swapping()
    print(result)

Solution status: OPTIMAL
Optimal objective value: 1998.5