-
Notifications
You must be signed in to change notification settings - Fork 1
/
mp-dc-ops.jl
289 lines (233 loc) · 10.9 KB
/
mp-dc-ops.jl
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
#### Multiperiod DC Optimal Power Shutoff ####
# This file provides a basic implentation of the multiperiod DC Optimal Power Shutoff
# problem for experimentation on algorithm and problem variations.
# This file can be run by calling `include("mp-dc-ops.jl")` from the Julia REPL
# Developed by Noah Rhodes(@noahrhodes) and Line Roald (@lroald)
###############################################################################
# 0. Initialization
###############################################################################
using Pkg; Pkg.activate(".")
Pkg.instantiate()
Pkg.precompile()
# Load Julia Packages
#--------------------
using PowerModels
using PowerModelsWildfire
using Gurobi
using JuMP
using CSV
using DataFrames
# Load System Data
# ----------------
# load the data file
data = PowerModels.parse_file(joinpath(@__DIR__, "data", "case5_risk_sys1.m"))
load_data = CSV.read(joinpath(@__DIR__, "data", "case5_load.csv"),DataFrame)
risk_data = CSV.read(joinpath(@__DIR__, "data", "case5_risk.csv"),DataFrame)
# Modify risk weighting
# data["risk_weight"] = 0.2
# Add zeros to turn linear objective functions into quadratic ones
# so that additional parameter checks are not required
PowerModels.standardize_cost_terms!(data, order=2)
# Adds reasonable rate_a values to branches without them
PowerModels.calc_thermal_limits!(data)
# Create multiperiod data model
mp_data = PowerModels.replicate(data, size(load_data,1))
for row in eachrow(load_data)
for col in names(row[2:end])
col
nwid = string(row[1])
comp_type, comp_id = split(col,"_")
load = row[col]
mp_data["nw"][nwid][comp_type][comp_id]["pd"] = load
end
end
for row in eachrow(risk_data)
for col in names(row[2:end])
col
nwid = string(row[1])
comp_type, comp_id = split(col,"_")
risk = row[col]
mp_data["nw"][nwid][comp_type][comp_id]["power_risk"] = risk
end
end
# use build_ref to filter out inactive components
ref = PowerModels.build_ref(mp_data)
ref_add_on_off_va_bounds!(ref, mp_data)
ref = ref[:it][:pm]
# Note: ref contains all the relevant system parameters needed to build the OPS model
# When we introduce constraints and variable bounds below, we use the parameters in ref.
###############################################################################
# 1. Building the Optimal Power Shutoff Model
###############################################################################
# Initialize a JuMP Optimization Model
#-------------------------------------
# model = Model(GLPK.Optimizer)
model = Model(Gurobi.Optimizer)
# Add Optimization and State Variables
# ------------------------------------
# Add voltage angles va for each bus
@variable(model, va[t in keys(ref[:nw]),i in keys(ref[:nw][1][:bus])])
# note: [i in keys(ref[:nw][1][:bus])] adds one `va` variable for each bus in the network
# Add active power generation variable pg for each generator
@variable(model, pg[t in keys(ref[:nw]),i in keys(ref[:nw][1][:gen])])
# Add power flow variables p to represent the active power flow for each branch
@variable(model, p[t in keys(ref[:nw]), (l,i,j) in ref[:nw][1][:arcs_from]])
# Build JuMP expressions for the value of p[(l,i,j)] and p[(l,j,i)] on the branches
p_expr = Dict([((t,(l,i,j)), 1.0*p[t,(l,i,j)]) for (l,i,j) in ref[:nw][1][:arcs_from], t in keys(ref[:nw])])
p_expr = merge(p_expr, Dict([((t,(l,j,i)), -1.0*p[t,(l,i,j)]) for (l,i,j) in ref[:nw][1][:arcs_from], t in keys(ref[:nw])]))
# note: this is used to make the definition of nodal power balance simpler
# Add binary variables to represent that state of all components
@variable(model, z_bus[t in keys(ref[:nw]), i in keys(ref[:nw][1][:bus])], Bin)
@variable(model, z_gen[t in keys(ref[:nw]), i in keys(ref[:nw][1][:gen])], Bin)
@variable(model, z_branch[t in keys(ref[:nw]), (l,i,j) in ref[:nw][1][:arcs_from]], Bin)
# Add continous load shed variable
@variable(model, 0.0 <= x_load[t in keys(ref[:nw]), l in keys(ref[:nw][1][:load])] <= 1.0)
@variable(model, 0.0 <= x_shunt[t in keys(ref[:nw]), s in keys(ref[:nw][1][:shunt])] <= 1.0)
# Add Objective Function
# ----------------------
# Maximize power delivery while minimizing wildfire risk
if haskey(ref[:nw][1], :risk_weight)
alpha = ref[:nw][1][:risk_weight]
else
@warn "network data should specify risk_weight, using 0.5 as a default"
alpha = 0.5
end
for comp_type in [:gen, :load, :bus, :branch]
for (nwid,nw) in ref[:nw]
for (id,comp) in nw[comp_type]
if ~haskey(comp, "power_risk")
@warn "$(comp_type) $(id) does not have a power_risk value, using 0.1 as a default"
comp["power_risk"] = 0.1
end
end
end
end
JuMP.@objective(model, Max,
sum(
(1-alpha)*(
sum(x_load[t,i]*load["pd"] for (i,load) in nw[:load])
)
- alpha*(
sum(z_gen[t,i]*gen["power_risk"] for (i,gen) in nw[:gen])
+ sum(z_bus[t,i]*bus["power_risk"] for (i,bus) in nw[:bus])
+ sum(z_branch[t,(l,i,j)]*nw[:branch][l]["power_risk"] for (l,i,j) in nw[:arcs_from])
+ sum(x_load[t,i]*load["power_risk"] for (i,load) in nw[:load])
)
for (t,nw) in ref[:nw]
)
)
# Add Constraints
# ---------------
# constraints for each period of OPS
for (t,nw) in ref[:nw]
# Fix the voltage angle to zero at the reference bus
for (i,bus) in nw[:ref_buses]
@constraint(model, va[t,i] == 0)
end
# Nodal power balance constraints
for (i,bus) in nw[:bus]
# Build a list of the loads and shunt elements connected to the bus i
bus_loads = [(l,nw[:load][l]) for l in nw[:bus_loads][i]]
bus_shunts = [(s,nw[:shunt][s]) for s in nw[:bus_shunts][i]]
# Active power balance at node i
@constraint(model,
sum(p_expr[(t,a)] for a in nw[:bus_arcs][i]) == # sum of active power flow on lines from bus i +
sum(pg[t,g] for g in nw[:bus_gens][i]) - # sum of active power generation at bus i -
sum(x_load[t,l]*load["pd"] for (l,load) in bus_loads) - # sum of active load * load shed at bus i -
sum(x_shunt[t,s]*shunt["gs"] for (s,shunt) in bus_shunts)*1.0^2 # sum of active shunt element injections * shed at bus i
)
end
# Branch power flow physics and limit constraints
for (i,branch) in nw[:branch]
# Build the from variable id of the i-th branch, which is a tuple given by (branch id, from bus, to bus)
f_idx = (i, branch["f_bus"], branch["t_bus"])
p_fr = p[t,f_idx] # p_fr is a reference to the optimization variable p[f_idx]
z_br = z_branch[t,f_idx]
va_fr = va[t,branch["f_bus"]] # va_fr is a reference to the optimization variable va on the from side of the branch
va_to = va[t,branch["t_bus"]] # va_fr is a reference to the optimization variable va on the to side of the branch
# Compute the branch parameters and transformer ratios from the data
g, b = PowerModels.calc_branch_y(branch)
# Voltage angle difference limit
JuMP.@constraint(model, va_fr - va_to <= branch["angmax"]*z_br + nw[:off_angmax]*(1-z_br))
JuMP.@constraint(model, va_fr - va_to >= branch["angmin"]*z_br + nw[:off_angmin]*(1-z_br))
# DC Power Flow Constraint
if b <= 0
JuMP.@constraint(model, p_fr <= -b*(va_fr - va_to + nw[:off_angmax]*(1-z_br)) )
JuMP.@constraint(model, p_fr >= -b*(va_fr - va_to + nw[:off_angmin]*(1-z_br)) )
else # account for bound reversal when b is positive
JuMP.@constraint(model, p_fr >= -b*(va_fr - va_to + nw[:off_angmax]*(1-z_br)) )
JuMP.@constraint(model, p_fr <= -b*(va_fr - va_to + nw[:off_angmin]*(1-z_br)) )
end
# Thermal limit
JuMP.@constraint(model, p_fr <= branch["rate_a"]*z_br)
JuMP.@constraint(model, p_fr >= -branch["rate_a"]*z_br)
# Connectivity constraint
JuMP.@constraint(model, z_br <= z_bus[t,branch["f_bus"]])
JuMP.@constraint(model, z_br <= z_bus[t,branch["t_bus"]])
end
# Generator constraints
for (i,gen) in nw[:gen]
# Power limit
JuMP.@constraint(model, pg[t,i] <= gen["pmax"]*z_gen[t,i])
JuMP.@constraint(model, pg[t,i] >= gen["pmin"]*z_gen[t,i])
# Connectivity constraint
JuMP.@constraint(model, z_gen[t,i] <= z_bus[t,gen["gen_bus"]])
end
# Load constraints
for (i, load) in nw[:load]
# Connectivity constraints
JuMP.@constraint(model, x_load[t,i] <= z_bus[t,load["load_bus"]])
end
end
# Inter-period constraints
for t in keys(ref[:nw])
if t != 1
# if component is disabled, it must stay disabled for future periods
for (i,branch) in ref[:nw][t][:branch]
f_idx = (i, branch["f_bus"], branch["t_bus"])
JuMP.@constraint(model, z_branch[t-1,f_idx] >= z_branch[t,f_idx])
end
for (i,bus) in ref[:nw][t][:bus]
JuMP.@constraint(model, z_bus[t-1,i] >= z_bus[t,i])
end
for (i,gen) in ref[:nw][t][:gen]
JuMP.@constraint(model, z_gen[t-1,i] >= z_gen[t,i])
end
end
end
###############################################################################
# 3. Solve the Optimal Power Flow Model and Review the Results
###############################################################################
# Solve the optimization problem
optimize!(model)
# Check that the solver terminated without an error
println("The solver termination status is $(termination_status(model))")
# Check the value of the objective function
println("The objective value is $(objective_value(model))")
total_load = sum(sum(value(x_load[t,i])*load["pd"] for (i,load) in nw[:load]) for (t,nw) in ref[:nw])
println("The load served is $total_load ")
total_risk = sum(
sum(value(z_branch[t,(l,i,j)])*nw[:branch][l]["power_risk"] for (l,i,j) in nw[:arcs_from]) +
sum(value(z_gen[t,g])*gen["power_risk"] for (g,gen) in nw[:gen]) +
sum(value(z_bus[t,i])*bus["power_risk"] for (i,bus) in nw[:bus]) +
sum(value(x_load[t,i])*load["power_risk"] for (i,load) in nw[:load])
for (t,nw) in ref[:nw]
)
println("The system risk is $total_risk")
# Get inactive devices
for t in sort(collect(keys(ref[:nw])))
nw = ref[:nw][t]
for l in sort(collect(keys(nw[:branch])))
f_idx = (l, nw[:branch][l]["f_bus"], nw[:branch][l]["t_bus"])
value(z_branch[t,f_idx])==0 ? println("Branch $l is disabled in period $t.") : false
end
for g in sort(collect(keys(nw[:gen])))
value(z_gen[t,g])==0 ? println("Generator $g is disabled in period $t.") : false
end
for i in sort(collect(keys(nw[:bus])))
value(z_bus[t,i])==0 ? println("Bus $i is disabled in period $t.") : false
end
for i in sort(collect(keys(nw[:load])))
value(x_load[t,i])!=1 ? println("Load $i is serving $(value(x_load[t,i])*100)% of demand in period $t.") : false
end
end