Case Study 3: Discrete Facility Location and Network Planning

Battery logistics for the automotive industry in northern Germany and Benelux

Author

Supply Chain Management – Group Work

NoteOverview
Topic Network design with the warehouse location problem (WLP)
Link to the lecture Chapter 5 of the course reader: uncapacitated and capacitated WLP as a MILP, add/drop heuristic
Type Data-based case study: realistic plant and site coordinates, quantities and costs estimated realistically
Group size 3–6 people
Deliverable Presentation (30 min) + discussion (15 min), slide deck (PDF) and reproducible R/Quarto code

1 Learning objectives

After working through this case study, you will be able to

  • build a transport cost matrix from geodata,
  • formulate an uncapacitated (UFLP) and a capacitated WLP (CFLP) as a mixed-integer linear programme and solve them in R,
  • apply construction and improvement heuristics (add, drop) and evaluate their solution quality against the optimum,
  • plan the supply for a given network as a transportation problem,
  • design network decisions robustly under uncertain future scenarios.

2 Initial situation

NordCell Logistics GmbH (fictitious) is a contract logistics provider specialising in dangerous goods. It has won the contract to store, sequence and deliver just in sequence lithium-ion battery modules for ten vehicle plants in northern Germany, North Rhine-Westphalia, Hesse and Benelux. The modules come from two sources:

  • imports from Asia via the container port of Hamburg (60% of the volume),
  • the cell factory in Salzgitter (40% of the volume).

Lithium-ion batteries are class 9 dangerous goods (UN 3480/3481). The warehouses therefore require fire compartments, sprinkler and firewater retention systems, a quarantine zone for damaged modules and official permits, which considerably increases the fixed costs of a site. NordCell has identified eight potential warehouse locations in industrial and logistics parks and must decide

  1. which warehouses to open/convert (strategic decision, contract term 7–12 years),
  2. which plant is supplied from which warehouse (tactical planning, annually),
  3. how robust this network is to the uncertain ramp-up of electric mobility.

3 Data

The plant locations are based on real vehicle plants. The quantities and costs are fictitious, but realistic in order of magnitude (1 MWh ≈ 6–7 t of battery modules including packaging).

3.1 Plants (sinks) and annual demand

Code
werke <- data.frame(
  id    = 1:10,
  werk  = c("VW Wolfsburg", "VW Hannover", "VW Emden", "VW Osnabrück",
            "Mercedes-Benz Bremen", "Ford Köln", "Opel Rüsselsheim",
            "Volvo Cars Gent", "Volvo Trucks Gent", "VDL Nedcar Born"),
  lat   = c(52.432, 52.380, 53.365, 52.279, 53.075, 50.950, 49.993, 51.054, 51.040, 51.030),
  lon   = c(10.787,  9.746,  7.213,  8.047,  8.808,  6.900,  8.422,  3.717,  3.730,  5.867),
  d2027 = c(28000, 22000, 18500, 12000, 25000, 20000, 15000, 30000, 14000, 11000),
  d2030 = c(42000, 26000, 28000, 12000, 31000, 27000, 21000, 36000, 17000,  6000)
)
knitr::kable(werke, format.args = list(big.mark = ",", decimal.mark = "."),
             col.names = c("#", "Plant", "Latitude", "Longitude",
                           "Demand 2027 [MWh/yr]", "Scenario 2030 [MWh/yr]"))
# Plant Latitude Longitude Demand 2027 [MWh/yr] Scenario 2030 [MWh/yr]
1 VW Wolfsburg 52.432 10.787 28,000 42,000
2 VW Hannover 52.380 9.746 22,000 26,000
3 VW Emden 53.365 7.213 18,500 28,000
4 VW Osnabrück 52.279 8.047 12,000 12,000
5 Mercedes-Benz Bremen 53.075 8.808 25,000 31,000
6 Ford Köln 50.950 6.900 20,000 27,000
7 Opel Rüsselsheim 49.993 8.422 15,000 21,000
8 Volvo Cars Gent 51.054 3.717 30,000 36,000
9 Volvo Trucks Gent 51.040 3.730 14,000 17,000
10 VDL Nedcar Born 51.030 5.867 11,000 6,000

3.2 Sources

Code
quellen <- data.frame(
  quelle = c("Hafen Hamburg", "Zellfabrik Salzgitter"),
  lat    = c(53.540, 52.160),
  lon    = c( 9.970, 10.400),
  anteil = c(0.60, 0.40)
)
knitr::kable(quellen, col.names = c("Source", "Latitude", "Longitude", "Volume share"))
Source Latitude Longitude Volume share
Hafen Hamburg 53.54 9.97 0.6
Zellfabrik Salzgitter 52.16 10.40 0.4

3.3 Candidate locations

Code
lager <- data.frame(
  id   = 1:8,
  ort  = c("Gütersloh", "Düren", "Dorsten/Marl", "Peine", "Langenhagen",
           "Venlo (NL)", "Bremen GVZ", "Gent Hafen (BE)"),
  lat  = c(51.906, 50.804, 51.660, 52.320, 52.438, 51.370, 53.050, 51.100),
  lon  = c( 8.383,  6.493,  6.965, 10.230,  9.737,  6.170,  8.700,  3.780),
  fix  = c(2400, 2600, 2500, 2300, 2700, 2800, 2600, 3000),   # thousand €/year
  kap  = c(90, 110, 80, 70, 100, 120, 90, 80) * 1000           # MWh/year
)
knitr::kable(lager, format.args = list(big.mark = ",", decimal.mark = "."),
             col.names = c("#", "Location", "Latitude", "Longitude",
                           "Fixed costs [thousand €/yr]", "Capacity [MWh/yr]"))
# Location Latitude Longitude Fixed costs [thousand €/yr] Capacity [MWh/yr]
1 Gütersloh 51.906 8.383 2,400 90,000
2 Düren 50.804 6.493 2,600 110,000
3 Dorsten/Marl 51.660 6.965 2,500 80,000
4 Peine 52.320 10.230 2,300 70,000
5 Langenhagen 52.438 9.737 2,700 100,000
6 Venlo (NL) 51.370 6.170 2,800 120,000
7 Bremen GVZ 53.050 8.700 2,600 90,000
8 Gent Hafen (BE) 51.100 3.780 3,000 80,000

4 Tasks

4.1 Part A – Data preparation

A1. Determine transport cost rates for the battery systems (in €/MWh\(\cdot\)km). Which assumptions underlie them?

A2. Show sources, warehouse candidates and plants on a map and calculate the transport distances, the transport cost matrix \(c_{ij}\) (€/MWh) and the annual transport costs. For two routes, compare the haversine distance × 1.2 with the road distance from a route planner.

4.2 Part B – Uncapacitated WLP (demand 2027)

B1. Solve the problem with the add or drop heuristic (add_heuristic_wlp(), drop_heuristic_wlp()) and exactly with ompr + GLPK. How far is the heuristic from the optimum?

B2. Adapt the MILP and determine the optimal total costs for a fixed number of open warehouses (\(\sum_i y_i = k\), \(k = 1, \dots, 4\)). Which locations are opened in each case and what total costs result?

4.3 Part C – Capacitated WLP and transportation problem

C1. Extend the model to include the warehouse capacities (CFLP). How does the solution change?

C2. Fix the warehouse decision from C1 and solve the transshipment problem for 2027, i.e. determine the transport-cost-optimal delivery quantities from sources -> warehouses -> plants.

4.4 Part D – Scenarios, recommendation and reflection

D1. Scenario 2030. Solve the CFLP for the 2030 scenario. Is the 2027 network still feasible or optimal? Determine a network that performs well in both scenarios, e.g. by minimising expected costs (50% / 50%) or maximum regret (minimax regret).

D2. Sensitivity. Examine one further sensitivity: a CO₂ price of €150/t, loss of the Salzgitter cell factory, or closure of the VDL Nedcar Born plant.

D3. Recommendation. Formulate a network recommendation for NordCell covering costs, locations, risks and expansion options.

D4. Reflection. Answer the following questions in your presentation. They form the starting point for the subsequent discussion:

  1. The cost function is flat near the optimum. What does this mean for the significance of the optimum, and what role should qualitative criteria (cf. Case Study 2) play in the final selection?
  2. Which model assumption do you consider the most critical (straight-line distance × detour factor, linear transport costs, deterministic demand, estimated fixed costs), and how could it change your recommendation?
  3. When would you use a heuristic instead of an exact method in practice?
  4. Which factors not modelled could tip the decision, e.g. permit procedures for dangerous goods warehouses, the labour market, or cross-border regulation between Germany, the Netherlands and Belgium?
  5. How would you communicate the result to a management team without knowledge of optimisation so that they trust the model without adopting it blindly?

5 Appendix: R starter code

Code
source("../R/scm_functions.R")   # haversine(), add_heuristic_wlp(), drop_heuristic_wlp(), vogels_approximation()
library(ompr); library(ompr.roi); library(ROI.plugin.glpk); library(dplyr)

uf <- 1.2
# Inbound costs per MWh for each warehouse (weighted over the sources)
c_in <- sapply(seq_len(nrow(lager)), function(i)
  sum(quellen$anteil * 0.25 * uf *
        haversine(lager$lat[i], lager$lon[i], quellen$lat, quellen$lon)))

# Outbound costs per MWh (warehouse x plant)
c_out <- outer(seq_len(nrow(lager)), seq_len(nrow(werke)), Vectorize(function(i, j)
  0.45 * uf * haversine(lager$lat[i], lager$lon[i], werke$lat[j], werke$lon[j])))

C <- c_in + c_out                       # €/MWh, matrix 8 x 10
dimnames(C) <- list(lager$ort, werke$werk)

# Uncapacitated WLP (costs in thousand €)
d  <- werke$d2027
f  <- lager$fix
nI <- nrow(lager); nJ <- nrow(werke)

m <- MIPModel() |>
  add_variable(y[i], i = 1:nI, type = "binary") |>
  add_variable(x[i, j], i = 1:nI, j = 1:nJ, type = "continuous", lb = 0, ub = 1) |>
  set_objective(sum_over(f[i] * y[i], i = 1:nI) +
                sum_over(C[i, j] * d[j] / 1000 * x[i, j], i = 1:nI, j = 1:nJ), "min") |>
  add_constraint(sum_over(x[i, j], i = 1:nI) == 1, j = 1:nJ) |>
  add_constraint(x[i, j] <= y[i], i = 1:nI, j = 1:nJ)

res <- solve_model(m, with_ROI(solver = "glpk"))
objective_value(res)
get_solution(res, y[i]) |> filter(value > 0.5)

# CFLP: additionally  sum_j d[j] * x[i,j] <= kap[i] * y[i]