In [1]:
using Logjam.DataTools, DataFrames, CSV
DC = DataFrame(CSV.File("PopcoData.csv"))
Out[1]:
42×5 DataFrame
17 rows omitted
| Row | LAT | LON | DEMAND | PROD_COST | DIST_COST |
|---|---|---|---|---|---|
| Float64 | Float64 | Int64 | Int64 | Int64 | |
| 1 | 44.06 | -103.187 | 53489 | 18648080 | 11884204 |
| 2 | 45.7731 | -108.524 | 10920 | 5866471 | 1198643 |
| 3 | 45.9744 | -112.513 | 46607 | 15259325 | 11326980 |
| 4 | 45.6685 | -110.976 | 51626 | 17078413 | 13293327 |
| 5 | 46.905 | -114.048 | 41968 | 12580903 | 4487319 |
| 6 | 39.7747 | -104.973 | 526208 | 73128863 | 32131128 |
| 7 | 38.8827 | -104.816 | 63570 | 15695984 | 2297982 |
| 8 | 38.2341 | -104.613 | 41145 | 14232816 | 9397847 |
| 9 | 39.0685 | -108.588 | 56379 | 21346490 | 15721042 |
| 10 | 44.0189 | -107.958 | 91699 | 19820610 | 16644210 |
| 11 | 42.8556 | -106.36 | 6581 | 4107122 | 1019698 |
| 12 | 41.6606 | -109.215 | 60138 | 18124447 | 10351078 |
| 13 | 46.4217 | -117.002 | 56789 | 20196662 | 14598824 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 31 | 37.6122 | -122.083 | 329160 | 39258818 | 16453611 |
| 32 | 37.625 | -120.992 | 131664 | 37882871 | 12624983 |
| 33 | 38.4637 | -122.743 | 11846 | 7765734 | 762986 |
| 34 | 38.4832 | -121.399 | 225518 | 38320205 | 8576069 |
| 35 | 39.141 | -121.612 | 57883 | 15344574 | 13271637 |
| 36 | 44.9671 | -122.997 | 57603 | 20328700 | 1666417 |
| 37 | 45.0766 | -122.798 | 40593 | 13696966 | 4957374 |
| 38 | 44.0199 | -123.03 | 112759 | 23310251 | 9885323 |
| 39 | 42.369 | -122.883 | 82690 | 26411938 | 6740091 |
| 40 | 44.0743 | -121.301 | 49374 | 19314593 | 1391628 |
| 41 | 47.5827 | -122.299 | 202966 | 37023043 | 11416920 |
| 42 | 47.233 | -122.473 | 90519 | 26123924 | 4134356 |
In [2]:
rename!(DC, :DEMAND => :fDC, :PROD_COST => :TPC, :DIST_COST => :TDC)
first(DC, 5)
Out[2]:
5×5 DataFrame
| Row | LAT | LON | fDC | TPC | TDC |
|---|---|---|---|---|---|
| Float64 | Float64 | Int64 | Int64 | Int64 | |
| 1 | 44.06 | -103.187 | 53489 | 18648080 | 11884204 |
| 2 | 45.7731 | -108.524 | 10920 | 5866471 | 1198643 |
| 3 | 45.9744 | -112.513 | 46607 | 15259325 | 11326980 |
| 4 | 45.6685 | -110.976 | 51626 | 17078413 | 13293327 |
| 5 | 46.905 | -114.048 | 41968 | 12580903 | 4487319 |
In [3]:
using GeoMakie, Logjam.MapTools
fig, ax = makemap(DC.LON, DC.LAT)
scatter!(ax, DC.LON, DC.LAT; marker='.', markersize=36, color=:red)
fig
Out[3]:
In [4]:
using CairoMakie
dcf() = display(current_figure())
scatter(DC.fDC, DC.TPC)
dcf();
Step 1: Estimate fixed cost¶
In [5]:
using Optim
ŷ(p, x) = p[1] .+ p[2]*x
loss(p, x, y) = sum((y .- ŷ(p, x)).^2)
k, cp = optimize(p -> loss(p, DC.fDC, DC.TPC), [0., 1.]).minimizer
Out[5]:
2-element Vector{Float64}:
1.0603652532813296e7
126.15062860391504
In [6]:
lines!([0, maximum(DC.fDC)], [k, k + cp*maximum(DC.fDC)])
dcf();
Step 2: Allocate customer demand to DCs¶
In [7]:
# Get aggregate demand points
z3 = uszcta3()
z3.IDX .= 0 # Add index of allocated DC (0 if not allocated)
first(z3, 5)
Out[7]:
5×8 DataFrame
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | |
| 1 | 10 | 42.2125 | -72.5732 | 472839 | 1276.13 | 38.492 | true | 0 |
| 2 | 11 | 42.1064 | -72.5504 | 171760 | 40.946 | 1.716 | true | 0 |
| 3 | 12 | 42.4562 | -73.2286 | 128186 | 902.054 | 16.827 | true | 0 |
| 4 | 13 | 42.589 | -72.4882 | 82872 | 786.428 | 27.535 | true | 0 |
| 5 | 14 | 42.5801 | -71.7824 | 224307 | 474.268 | 14.954 | true | 0 |
In [8]:
function dgc(xy₁, xy₂; unit=:mi)
length(xy₁) == length(xy₂) == 2 || error("Inputs must have length 2.")
unit in [:mi, :km] || error("Unit must be :mi or :km")
Δx, Δy = xy₂[1] - xy₁[1], xy₂[2] - xy₁[2]
a = sind(Δy / 2)^2 + cosd(xy₁[2]) * cosd(xy₂[2]) * sind(Δx / 2)^2
2 * asin(min(sqrt(a), 1.0)) * (unit == :mi ? 3958.75 : 6371.00)
end
Dgc(X₁, X₂) = [dgc(i, j) for i in eachrow(X₁), j in eachrow(X₂)]
Daa(X, Xa, a) = [max(dgc(i, j), (2/3)*sqrt(k)/pi)
for i in eachrow(X), (j, k) in zip(eachrow(Xa), a)]
Out[8]:
Daa (generic function with 1 method)
In [9]:
# Allocate customer demand to DCs
for r in eachrow(z3)
d = Dgc([r.LON r.LAT], hcat(DC.LON, DC.LAT)) * 1.2 # Only need for est road distance
idx = argmin(d)[2]
if d[idx] <= 200 # Max service distance
r.IDX = idx
end
end
filter!(r -> r.IDX != 0, z3) # Remove unallocated demand points
first(z3, 5)
Out[9]:
5×8 DataFrame
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | |
| 1 | 575 | 43.9175 | -100.456 | 52132 | 14664.4 | 307.285 | true | 1 |
| 2 | 576 | 45.3991 | -101.115 | 20310 | 11570.9 | 271.706 | true | 1 |
| 3 | 577 | 44.1001 | -103.292 | 206436 | 17529.3 | 58.493 | true | 1 |
| 4 | 590 | 45.7035 | -108.941 | 87410 | 25896.4 | 105.174 | true | 2 |
| 5 | 591 | 45.7593 | -108.511 | 143646 | 813.669 | 3.984 | true | 2 |
Step 3: Allocate DC demand to customers based on population¶
In [10]:
gdf = groupby(z3, :IDX)
Out[10]:
GroupedDataFrame with 42 groups based on key: IDX
First Group (4 rows): IDX = 1
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | |
| 1 | 575 | 43.9175 | -100.456 | 52132 | 14664.4 | 307.285 | true | 1 |
| 2 | 576 | 45.3991 | -101.115 | 20310 | 11570.9 | 271.706 | true | 1 |
| 3 | 577 | 44.1001 | -103.292 | 206436 | 17529.3 | 58.493 | true | 1 |
| 4 | 693 | 42.1465 | -103.298 | 67226 | 12281.2 | 90.06 | true | 1 |
⋮
Last Group (4 rows): IDX = 42
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX |
|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | |
| 1 | 983 | 47.4039 | -122.556 | 818134 | 5243.77 | 282.374 | true | 42 |
| 2 | 984 | 47.2011 | -122.475 | 450094 | 160.548 | 12.763 | true | 42 |
| 3 | 985 | 46.9764 | -123.023 | 534122 | 5358.61 | 355.761 | true | 42 |
| 4 | 989 | 46.6216 | -120.449 | 301472 | 5330.97 | 55.173 | true | 42 |
In [11]:
length(gdf) == nrow(DC) # Test for DCs without demand
Out[11]:
true
In [12]:
z3 = transform(gdf, :POP => sum => :DCPOP)
first(z3, 5)
Out[12]:
5×9 DataFrame
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX | DCPOP |
|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | Int64 | |
| 1 | 575 | 43.9175 | -100.456 | 52132 | 14664.4 | 307.285 | true | 1 | 346104 |
| 2 | 576 | 45.3991 | -101.115 | 20310 | 11570.9 | 271.706 | true | 1 | 346104 |
| 3 | 577 | 44.1001 | -103.292 | 206436 | 17529.3 | 58.493 | true | 1 | 346104 |
| 4 | 590 | 45.7035 | -108.941 | 87410 | 25896.4 | 105.174 | true | 2 | 265435 |
| 5 | 591 | 45.7593 | -108.511 | 143646 | 813.669 | 3.984 | true | 2 | 265435 |
In [13]:
DC.IDX .= 1:nrow(DC)
first(DC, 5)
Out[13]:
5×6 DataFrame
| Row | LAT | LON | fDC | TPC | TDC | IDX |
|---|---|---|---|---|---|---|
| Float64 | Float64 | Int64 | Int64 | Int64 | Int64 | |
| 1 | 44.06 | -103.187 | 53489 | 18648080 | 11884204 | 1 |
| 2 | 45.7731 | -108.524 | 10920 | 5866471 | 1198643 | 2 |
| 3 | 45.9744 | -112.513 | 46607 | 15259325 | 11326980 | 3 |
| 4 | 45.6685 | -110.976 | 51626 | 17078413 | 13293327 | 4 |
| 5 | 46.905 | -114.048 | 41968 | 12580903 | 4487319 | 5 |
In [14]:
z3 = leftjoin(z3, DC[!, [:IDX, :fDC]], on=:IDX)
Out[14]:
162×10 DataFrame
137 rows omitted
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX | DCPOP | fDC |
|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | Int64 | Int64? | |
| 1 | 575 | 43.9175 | -100.456 | 52132 | 14664.4 | 307.285 | true | 1 | 346104 | 53489 |
| 2 | 576 | 45.3991 | -101.115 | 20310 | 11570.9 | 271.706 | true | 1 | 346104 | 53489 |
| 3 | 577 | 44.1001 | -103.292 | 206436 | 17529.3 | 58.493 | true | 1 | 346104 | 53489 |
| 4 | 590 | 45.7035 | -108.941 | 87410 | 25896.4 | 105.174 | true | 2 | 265435 | 10920 |
| 5 | 591 | 45.7593 | -108.511 | 143646 | 813.669 | 3.984 | true | 2 | 265435 | 10920 |
| 6 | 593 | 46.4994 | -105.476 | 34379 | 22237.8 | 219.858 | true | 2 | 265435 | 10920 |
| 7 | 594 | 47.6418 | -111.379 | 133271 | 21677.6 | 175.043 | true | 3 | 219183 | 46607 |
| 8 | 596 | 46.5932 | -111.989 | 85912 | 6238.3 | 78.791 | true | 3 | 219183 | 46607 |
| 9 | 597 | 45.7681 | -111.65 | 192749 | 14989.6 | 85.468 | true | 4 | 434940 | 51626 |
| 10 | 598 | 46.8957 | -114.128 | 209069 | 12823.1 | 148.578 | true | 5 | 337860 | 41968 |
| 11 | 599 | 48.2871 | -114.475 | 128791 | 8066.36 | 252.885 | true | 5 | 337860 | 41968 |
| 12 | 693 | 42.1465 | -103.298 | 67226 | 12281.2 | 90.06 | true | 1 | 346104 | 53489 |
| 13 | 798 | 31.2768 | -105.703 | 73294 | 9658.6 | 9.861 | true | 21 | 1282663 | 37586 |
| ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ | ⋮ |
| 151 | 982 | 48.2761 | -122.275 | 984003 | 5441.9 | 369.439 | true | 41 | 3834335 | 202966 |
| 152 | 983 | 47.4039 | -122.556 | 818134 | 5243.77 | 282.374 | true | 42 | 2103822 | 90519 |
| 153 | 984 | 47.2011 | -122.475 | 450094 | 160.548 | 12.763 | true | 42 | 2103822 | 90519 |
| 154 | 985 | 46.9764 | -123.023 | 534122 | 5358.61 | 355.761 | true | 42 | 2103822 | 90519 |
| 155 | 986 | 45.793 | -122.582 | 665335 | 3831.0 | 195.88 | true | 37 | 2817543 | 40593 |
| 156 | 988 | 47.5845 | -119.889 | 241952 | 10324.0 | 186.32 | true | 41 | 3834335 | 202966 |
| 157 | 989 | 46.6216 | -120.449 | 301472 | 5330.97 | 55.173 | true | 42 | 2103822 | 90519 |
| 158 | 990 | 47.6799 | -117.373 | 168691 | 2223.12 | 26.286 | true | 13 | 1617237 | 56789 |
| 159 | 991 | 47.6773 | -117.709 | 121014 | 11778.0 | 232.138 | true | 13 | 1617237 | 56789 |
| 160 | 992 | 47.6847 | -117.374 | 385114 | 336.764 | 1.33 | true | 13 | 1617237 | 56789 |
| 161 | 993 | 46.2929 | -119.044 | 404636 | 7537.07 | 155.382 | true | 13 | 1617237 | 56789 |
| 162 | 994 | 46.3593 | -117.295 | 22324 | 664.871 | 4.416 | true | 13 | 1617237 | 56789 |
In [15]:
z3.f = map(r -> r.fDC * r.POP / r.DCPOP, eachrow(z3))
first(z3, 5)
Out[15]:
5×11 DataFrame
| Row | ZCTA3 | LAT | LON | POP | ALAND | AWATER | ISCUS | IDX | DCPOP | fDC | f |
|---|---|---|---|---|---|---|---|---|---|---|---|
| Int64 | Float64 | Float64 | Int64 | Float64 | Float64 | Bool | Int64 | Int64 | Int64? | Float64 | |
| 1 | 575 | 43.9175 | -100.456 | 52132 | 14664.4 | 307.285 | true | 1 | 346104 | 53489 | 8056.79 |
| 2 | 576 | 45.3991 | -101.115 | 20310 | 11570.9 | 271.706 | true | 1 | 346104 | 53489 | 3138.83 |
| 3 | 577 | 44.1001 | -103.292 | 206436 | 17529.3 | 58.493 | true | 1 | 346104 | 53489 | 31903.9 |
| 4 | 590 | 45.7035 | -108.941 | 87410 | 25896.4 | 105.174 | true | 2 | 265435 | 10920 | 3596.05 |
| 5 | 591 | 45.7593 | -108.511 | 143646 | 813.669 | 3.984 | true | 2 | 265435 | 10920 | 5909.6 |
Step 4: Nominal transport rate¶
In [16]:
dDC2r(r) = Daa([DC[r.IDX,:LON] DC[r.IDX, :LAT]],[r.LON r.LAT], r.ALAND)[1]
rnom = sum(DC.TDC) / sum(map(r -> r.f * dDC2r(r), eachrow(z3)))
Out[16]:
2.8625376133920297
Step 5: Cost matrix for UFL¶
In [17]:
# Include demand + DC locations for NF sites
NF = hcat(vcat(z3.LON, DC.LON), vcat(z3.LAT, DC.LAT))
D = Daa(NF, hcat(z3.LON, z3.LAT), z3.ALAND)
C = rnom*z3.f'.*D
Out[17]:
204×162 Matrix{Float64}:
5.92658e5 9.64745e5 1.292e7 4.46354e6 … 3.73774e7 1.87629e6
2.47632e6 2.05098e5 1.27387e7 3.90231e6 3.51108e7 1.74961e6
3.26274e6 1.25328e6 2.56588e6 3.06537e6 3.17408e7 1.56637e6
1.00004e7 3.40615e6 2.71957e7 3.51524e5 1.97778e7 9.04456e5
9.55826e6 3.22017e6 2.55531e7 3.51524e5 2.05911e7 9.49078e5
6.9755e6 2.00204e6 1.79764e7 1.80016e6 … 2.62702e7 1.262e6
1.34957e7 4.59604e6 4.19432e7 1.82073e6 1.51729e7 6.56167e5
1.36062e7 4.74373e6 4.16259e7 1.62926e6 1.36833e7 5.67583e5
1.2975e7 4.57954e6 3.87712e7 1.34531e6 1.44956e7 6.13991e5
1.5991e7 5.66665e6 5.10211e7 2.68552e6 9.64018e6 3.47211e5
1.695e7 5.94058e6 5.54607e7 3.25191e6 … 1.03528e7 4.20822e5
4.34985e6 2.24362e6 1.23272e7 3.8397e6 3.37139e7 1.68301e6
2.1192e7 9.04313e6 8.17951e7 1.04155e7 5.11748e7 2.71901e6
⋮ ⋱ ⋮
2.7867e7 1.08369e7 9.82877e7 9.02859e6 2.52029e7 1.46342e6
2.66438e7 1.03851e7 9.35689e7 8.5855e6 2.46943e7 1.41917e6
2.80007e7 1.08332e7 9.85572e7 8.90098e6 2.32962e7 1.37301e6
2.64742e7 1.0266e7 9.26283e7 8.32624e6 … 2.2482e7 1.30789e6
2.62754e7 1.01495e7 9.16694e7 8.11076e6 2.07827e7 1.22181e6
2.56158e7 9.54883e6 8.85837e7 7.03751e6 8.61394e6 6.54261e5
2.53801e7 9.45183e6 8.76597e7 6.92664e6 8.12283e6 6.27847e5
2.58083e7 9.67618e6 8.92877e7 7.19273e6 1.01561e7 7.23769e5
2.61116e7 9.88787e6 9.05093e7 7.50102e6 … 1.34562e7 875048.0
2.38326e7 8.91583e6 8.1461e7 6.3279e6 7.67121e6 562839.0
2.48895e7 9.12562e6 8.61517e7 6.649e6 7.22077e6 5.6225e5
2.50296e7 9.19869e6 8.66208e7 6.7083e6 7.10896e6 5.65906e5
Step 6: Solve UFL¶
In [18]:
include("ufl_heuristics.jl")
y, TC = ufl(fill(k, size(C, 1)), C)
Add: 7.645948563275278e8 Xchg: 7.35417248086411e8 Add: 7.35417248086411e8 Drop: 7.351590430439293e8 Xchg: 7.34021255286948e8 Add: 7.34021255286948e8 Drop: 7.34021255286948e8
Out[18]:
([181, 17, 143, 82, 150, 163, 101, 24, 198, 128 … 108, 14, 20, 146, 54, 158, 36, 133, 8, 44], 7.34021255286948e8)
In [19]:
TCorig = k*nrow(DC) + sum(DC.TDC)
println("% reduction is TC = ", 100*(TCorig - TC)/TCorig, "\n")
DataFrame(_=["No. of NFs", "TDC","TC"],
Orig=[nrow(DC), sum(DC.TDC), TCorig],
New=[length(y), TC - k*length(y), TC])
% reduction is TC = 17.96078981797282
Out[19]:
3×3 DataFrame
| Row | _ | Orig | New |
|---|---|---|---|
| String | Float64 | Float64 | |
| 1 | No. of NFs | 42.0 | 27.0 |
| 2 | TDC | 4.49367e8 | 4.47723e8 |
| 3 | TC | 8.9472e8 | 7.34021e8 |
In [20]:
scatter!(ax, NF[y, 1], NF[y, 2]; marker=:circle, markersize=9, color=:transparent,
strokecolor=:green, strokewidth=1)
fig
Out[20]:
Save C matrix as CSV and re-print k¶
In [21]:
CSV.write("PopcoCmatrix.csv", DataFrame(C, :auto))
println("k = ", k)
k = 1.0603652532813296e7