Newer
Older
#* Functions for handling the Network Data Dictionary (NDD) of PowerModels.jl
#*------------------------------------------------------------------------------
#=
Builds a network data dictionary conform with PowerModels.jl from several files containing certain input data:
pgfile - .RAW or .m file that contains the detailed grid data (e.g. generator and load information etc.)
busfile - .csv file that contains geographic locations of all buses
branchfile - .csv file that contains transmission line lengths
nddfile - .jld2 file in which the final network data dictionary will be saved
=#
function build_network_data(;
pgfile::String, busfile::String, branchfile::String, nddfile::String
)
### Read grid data and calculate initial operation point
network_data = calc_init_op(pgfile, model=:ac)
add_locs!(network_data, busfile) # add geographic bus locations
add_tl_lengths!(network_data, branchfile) # add transmission line lengths
add_tl_voltages!(network_data) # add transmission line voltages
save(nddfile, "network_data", network_data) # save network data
return nothing
end
#*------------------------------------------------------------------------------
#=
Reads the geographic bus locations (longitude and latitude coordinates in degrees) from a .csv file and adds them to the network data dictionary (NDD). Returns the resulting NDD, in which each bus has a "bus_lat" and "bus_lon" entry. The columns containg the longitude and latitude coordinates in the .csv file should be called "SubLongitude" and "SubLatitude", respectively.
function add_locs!(network_data::Dict{String,<:Any}, csvfile::String)
pos = Dict{String,Any}(
"baseMVA" => 100.0,
"bus" => Dict{String,Any}(),
"per_unit" => true
)
BusData::DataFrame = CSV.File(csvfile) |> DataFrame!
N = size(BusData, 1) # number of buses
sizehint!(pos["bus"], N)
for bus in eachrow(BusData)
pos["bus"][string(bus[:Number])] = Dict(
"bus_lon" => bus[:SubLongitude],
"bus_lat" => bus[:SubLatitude]
)
end
update_data!(network_data, pos) # add positions to network_data
return network_data
end
#*------------------------------------------------------------------------------
#=
Reads transmission line lengths from a .csv file and adds them to the network data dictionary.
=#
function add_tl_lengths!(network_data::Dict{String,<:Any}, csvfile::String)
len = Dict{String,Any}(
"baseMVA" => 100.0,
"branch" => Dict{String,Any}(),
"per_unit" => true
)
BranchData::DataFrame = CSV.File(csvfile) |> DataFrame!
N = size(BranchData, 1) # number of branches
sizehint!(len["branch"], N)
for branch in eachrow(BranchData)
from, to = branch[:BusNumFrom], branch[:BusNumTo]
indices = findall(
b -> ((b["f_bus"], b["t_bus"]) == (from, to)
|| (b["f_bus"], b["t_bus"]) == (to, from)), network_data["branch"]
)
for index in indices
len["branch"][index] = Dict("length" => branch[:GICLineDistanceKm])
end
end
update_data!(network_data, len) # add positions to network_data
return network_data
end
#*------------------------------------------------------------------------------
Determines voltage levels of transmission lines and adds them to the network data dictionary.
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
function add_tl_voltages!(network_data::Dict{String,<:Any})
voltages = Dict{String,Any}(
"baseMVA" => 100.0,
"branch" => Dict{String,Any}(),
"per_unit" => true
)
L = length(network_data["branch"])
sizehint!(voltages["branch"], L)
for (i, branch) in network_data["branch"]
if branch["transformer"] == true
### Transformers have a transmission line voltage of 0 kV
voltages["branch"][i] = Dict(
"tl_voltage" => 0.
)
else
### Transmission lines get base voltage of from bus
from = branch["f_bus"]
voltages["branch"][i] = Dict(
"tl_voltage" => network_data["bus"][string(from)]["base_kv"]
)
end
end
update_data!(network_data, voltages)
return network_data
end
#*------------------------------------------------------------------------------
Returns the bus types of all buses in the NDD in form an ordered dictionary. The different bus types are "Generator", "Load and generator", "Load" and "Empty bus". The ordering is fixed so that the dictionary can be used to plot the buses in a specific order.
#? How to call "empty" buses? Load and generator buses in a substation are connected to these empty buses? Is there only one empty bus per substation?
function get_bustypes(network_data::Dict{String,<:Any})
### Active loads
load = unique(
[l["load_bus"] for l in collect(values(network_data["load"]))
if l["status"] == 1]
gen = unique(
[g["gen_bus"] for g in collect(values(network_data["gen"]))
if g["gen_status"] == 1]
load_and_gen = [i for i in load if i in gen]
### Slack bus
slack = [
b["index"] for b in collect(values(network_data["bus"]))
if b["bus_type"] == 3
]
### Remove buses with load and generation from pure loads and generators
filter!(i -> i ∉ load_and_gen, load)
filter!(i -> i ∉ load_and_gen, gen)
### Remove slack bus from other arrays
filter!(i -> i ∉ slack, load)
filter!(i -> i ∉ slack, gen)
filter!(i -> i ∉ slack, load_and_gen)
### Empty buses with neither load nor generation
empty = unique(
[b["index"] for b in collect(values(network_data["bus"]))
if b["index"] ∉ vcat(load, gen, load_and_gen, slack)]
)
### Check whether the right number of bus types was obtained
sum = (
length(load) + length(gen) + length(load_and_gen) + length(empty) + length(slack)
)
N::Int64 = length(network_data["bus"]) # number of buses
@assert sum == N "$sum bus types were obtained instead of $N"
return OrderedDict(
"Generator" => gen,
"Load and generator" => load_and_gen,
"Load" => load,
"Slack" => slack,
"Empty bus" => empty
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
#*------------------------------------------------------------------------------
#=
Returns the (string) indices of transmission lines in the network data dictionary that would be considered as underground lines depending on the used maximum length of underground lines and minimum MW-load served. If mode = :sum, the sum of MW-loads of both ends of a line has to exceed min_MW_load, and if mode = :single, one end alone has to exceed min_MW_load.
=#
function get_underground_tl(
network_data::Dict{String,<:Any},
max_length = 12.875, # in km
min_MW_load = 2.; # in per-unit (assumed 100 MW base)
mode = :sum # :sum or :single
)
MW_loads = get_MW_loads(network_data) # dictionary of MW loads
underground_tl = []
if mode == :single # one end alone has to exceed min_MW_load
filter!(load -> last(load) >= min_MW_load, MW_loads)
large_MW_loads = collect(keys(MW_loads))
println(large_MW_loads)
counter = 0
### Go through branches and identify underground transmission lines
for (i, branch) in network_data["branch"]
if branch["transformer"] == false && branch["length"] < max_length
from, to = branch["f_bus"], branch["t_bus"]
if from in large_MW_loads || to in large_MW_loads
push!(underground_tl, i)
counter += 1
end
end
end
println(counter)
elseif mode == :sum # both ends together have to exceed min_MW_load
### Go through branches and identify underground transmission lines
for (i, branch) in network_data["branch"]
if branch["transformer"] == false && branch["length"] < max_length
from, to = branch["f_bus"], branch["t_bus"]
sum_MW_load = 0.
if haskey(MW_loads, from)
sum_MW_load += MW_loads[from]
end
if haskey(MW_loads, to)
sum_MW_load += MW_loads[to]
end
if sum_MW_load >= min_MW_load
push!(underground_tl, i)
end
end
end
else
throw(ArgumentError("Unknown mode $mode."))
end
return underground_tl
end
#=
Returns a dictionary with load bus indices (Int64) as keys and their respective MW load as values.
=#
function get_MW_loads(network_data::Dict{String,<:Any})
load_dict = Dict{Int64,Float64}()
for (i, load) in network_data["load"]
sub_dict = Dict(load["load_bus"] => load["pd"])
merge!(+, load_dict, sub_dict)
end
return load_dict
end
#*------------------------------------------------------------------------------
#=
Returns an array of tuples containing branch indices of branches with a minimum loading of min_loading and their actual loading (considering the power flow type pf_type). Choosing min_loading = 1. yields overloaded branches.
=#
function get_branches(
network_data::Dict{String,<:Any};
min_loading = 0.0, # minimum loading of branches
pf_type = "MVA-loading" # or "MW-loading", "Mvar-loading"
)
branches = [
(i, br[pf_type]) for (i, br) in network_data["branch"]
if br["br_status"] == 1 && br[pf_type] >= min_loading
]
return branches
end
#*------------------------------------------------------------------------------
Deactivates branches that have been overloaded due to a power flow redistribution (flow/capacity > 1.). The branches are identified by their string indices in branch_ids. The status of these branches is set to 0 in network_data. Parallel branches are not automatically deactivated. Afterwards PowerModels function simplify_network! is run in order to remove dangling buses etc. that might occur due to the failures.
=#
function disable_branches!(
network_data::Dict{String,<:Any},
branch_ids::Array
)
### Go through branches to deactivate
for id in branch_ids
network_data["branch"][id]["br_status"] = 0 # deactivate branch
end
simplify_network!(network_data) # remove dangling buses etc.
return network_data
end
#=
Function that deactivates (overhead) transmission lines identified by their indices (string) in tl_ids that have been destroyed by a hurricane. For any given line index all parallel lines also become deactivated. The status of these lines is set to 0 in network_data. Afterwards PowerModels function simplify_network! is run in order to remove dangling buses etc. that might occur due to the failures.
=#
function destroy_tl!(
network_data::Dict{String,<:Any},
tl_ids::Array # string ids of destroyed transmission lines
)
### Go through transmission lines to destroy
for id in tl_ids
## Deactivate id and all parallel lines
from = network_data["branch"][id]["f_bus"]
to = network_data["branch"][id]["t_bus"]
## Find all parallel branches
ids = findall(
b -> (b["f_bus"], b["t_bus"]) == (from, to),
network_data["branch"]
)
for idx in ids
network_data["branch"][idx]["br_status"] = 0
end
end
simplify_network!(network_data) # remove dangling buses etc.
return network_data
end
#=
Deactivates all components (generator, loads, etc.) in a connected component.
=#
function _deactivate_cc!(
network_data::Dict{String,<:Any},
active_cc_gens::Array{Int64,1},
active_cc_loads::Array{Int64,1}
)
### Deactivate all active generators in connected component
for g in active_cc_gens
network_data["gen"]["$g"]["gen_status"] = 0
end
### Deactivate all active loads in connected component
for l in active_cc_loads
end
### Deactivate rest of connected component (branches, dangling buses etc.)
simplify_network!(network_data)
return nothing