|
| 1 | +############################################################################### |
| 2 | +# Data Structures and Functions for working with a network Admittance Matrix |
| 3 | +############################################################################### |
| 4 | + |
| 5 | +""" |
| 6 | +Stores metadata related to an Admittance Matirx |
| 7 | +
|
| 8 | +Designed to work with both complex (i.e. Y) and real-valued (e.g. b) valued |
| 9 | +admittance matrices. |
| 10 | +
|
| 11 | +Typically the matrix will be sparse, but supports dense matricies as well. |
| 12 | +""" |
| 13 | +struct AdmittanceMatrix{T} |
| 14 | + idx_to_bus::Vector{Int} |
| 15 | + bus_to_idx::Dict{Int,Int} |
| 16 | + ref_idx::Int |
| 17 | + matrix::SparseArrays.SparseMatrixCSC{T,Int} |
| 18 | +end |
| 19 | + |
| 20 | +Base.show(io::IO, x::AdmittanceMatrix{<:Number}) = print(io, "AdmittanceMatrix($(length(x.idx_to_bus)) buses, $(length(nonzeros(x.matrix))) entries)") |
| 21 | + |
| 22 | + |
| 23 | +"data should be a PowerModels network data model; only supports networks with exactly one refrence bus" |
| 24 | +function calc_susceptance_matrix(data::Dict{String,<:Any}) |
| 25 | + if length(data["dcline"]) > 0 |
| 26 | + Memento.error(_LOGGER, "calc_susceptance_matrix does not support data with dclines") |
| 27 | + end |
| 28 | + if length(data["switch"]) > 0 |
| 29 | + Memento.error(_LOGGER, "calc_susceptance_matrix does not support data with switches") |
| 30 | + end |
| 31 | + |
| 32 | + #TODO check single connected component |
| 33 | + |
| 34 | + # NOTE currently exactly one refrence bus is required |
| 35 | + ref_bus = reference_bus(data) |
| 36 | + |
| 37 | + buses = [x.second for x in data["bus"] if (x.second[pm_component_status["bus"]] != pm_component_status_inactive["bus"])] |
| 38 | + sort!(buses, by=x->x["index"]) |
| 39 | + |
| 40 | + idx_to_bus = [x["index"] for x in buses] |
| 41 | + bus_to_idx = Dict(x["index"] => i for (i,x) in enumerate(buses)) |
| 42 | + #println(idx_to_bus) |
| 43 | + #println(bus_to_idx) |
| 44 | + |
| 45 | + I = Int64[] |
| 46 | + J = Int64[] |
| 47 | + V = Float64[] |
| 48 | + |
| 49 | + for (i,branch) in data["branch"] |
| 50 | + if branch[pm_component_status["branch"]] != pm_component_status_inactive["branch"] |
| 51 | + f_bus = bus_to_idx[branch["f_bus"]] |
| 52 | + t_bus = bus_to_idx[branch["t_bus"]] |
| 53 | + b_val = -branch["br_x"]/(branch["br_x"]^2+branch["br_r"]^2) |
| 54 | + push!(I, f_bus); push!(J, t_bus); push!(V, b_val) |
| 55 | + push!(I, t_bus); push!(J, f_bus); push!(V, b_val) |
| 56 | + push!(I, f_bus); push!(J, f_bus); push!(V, -b_val) |
| 57 | + push!(I, t_bus); push!(J, t_bus); push!(V, -b_val) |
| 58 | + end |
| 59 | + end |
| 60 | + |
| 61 | + m = sparse(I,J,V) |
| 62 | + #println(m) |
| 63 | + |
| 64 | + return AdmittanceMatrix(idx_to_bus, bus_to_idx, bus_to_idx[ref_bus["index"]], m) |
| 65 | +end |
| 66 | + |
| 67 | + |
| 68 | +""" |
| 69 | +Stores metadata related to an inverse of an Matirx |
| 70 | +
|
| 71 | +Designed to work with the inverse of both complex (i.e. Y) and real-valued (e.g. b) valued |
| 72 | +admittance matrices. |
| 73 | +
|
| 74 | +Typically the matrix will be dense. |
| 75 | +""" |
| 76 | +struct AdmittanceMatrixInverse{T} |
| 77 | + idx_to_bus::Vector{Int} |
| 78 | + bus_to_idx::Dict{Int,Int} |
| 79 | + ref_idx::Int |
| 80 | + matrix::Matrix{T} |
| 81 | +end |
| 82 | + |
| 83 | +Base.show(io::IO, x::AdmittanceMatrixInverse{<:Number}) = print(io, "AdmittanceMatrixInverse($(length(x.idx_to_bus)) buses, $(length(x.matrix)) entries)") |
| 84 | + |
| 85 | + |
| 86 | +"note, data should be a PowerModels network data model; only supports networks with exactly one refrence bus" |
| 87 | +function calc_susceptance_matrix_inv(data::Dict{String,<:Any}) |
| 88 | + #TODO check single connected component |
| 89 | + |
| 90 | + sm = calc_susceptance_matrix(data) |
| 91 | + sm_inv = calc_admittance_matrix_inv(sm) |
| 92 | + |
| 93 | + return sm_inv |
| 94 | +end |
| 95 | + |
| 96 | +"calculates the inverse of the susceptance matrix" |
| 97 | +function calc_admittance_matrix_inv(am::AdmittanceMatrix) |
| 98 | + M = Matrix(am.matrix) |
| 99 | + |
| 100 | + num_buses = length(am.idx_to_bus) |
| 101 | + nonref_buses = Int64[i for i in 1:num_buses if i != am.ref_idx] |
| 102 | + am_inv = zeros(Float64, num_buses, num_buses) |
| 103 | + am_inv[nonref_buses, nonref_buses] = inv(M[nonref_buses, nonref_buses]) |
| 104 | + |
| 105 | + return AdmittanceMatrixInverse(am.idx_to_bus, am.bus_to_idx, am.ref_idx, am_inv) |
| 106 | +end |
| 107 | + |
| 108 | + |
| 109 | +"extracts a mapping from bus injections to voltage angles from the inverse of an admittance matrix." |
| 110 | +function injection_factors_va(am_inv::AdmittanceMatrixInverse{T}, bus_id::Int)::Dict{Int,T} where T |
| 111 | + bus_idx = am_inv.bus_to_idx[bus_id] |
| 112 | + |
| 113 | + injection_factors = Dict( |
| 114 | + am_inv.idx_to_bus[i] => am_inv.matrix[bus_idx,i] |
| 115 | + for i in 1:length(am_inv.idx_to_bus) if !isapprox(am_inv.matrix[bus_idx,i], 0.0) |
| 116 | + ) |
| 117 | + |
| 118 | + return injection_factors |
| 119 | +end |
| 120 | + |
| 121 | + |
| 122 | +"computes a mapping from bus injections to voltage angles implicitly by solving a system of linear equations." |
| 123 | +function injection_factors_va(am::AdmittanceMatrix{T}, bus_id::Int; ref_bus::Int=typemin(Int))::Dict{Int,T} where T |
| 124 | + # this row is all zeros, an empty Dict is also a reasonable option |
| 125 | + |
| 126 | + if ref_bus == typemin(Int) |
| 127 | + ref_bus = am.idx_to_bus[am.ref_idx] |
| 128 | + end |
| 129 | + |
| 130 | + if ref_bus == bus_id |
| 131 | + return Dict{Int,T}() |
| 132 | + end |
| 133 | + |
| 134 | + ref_idx = am.bus_to_idx[ref_bus] |
| 135 | + bus_idx = am.bus_to_idx[bus_id] |
| 136 | + |
| 137 | + # need to remap the indexes to omit the ref_bus id |
| 138 | + # a reverse lookup is also required |
| 139 | + idx2_to_idx1 = Int64[] |
| 140 | + for i in 1:length(am.idx_to_bus) |
| 141 | + if i != ref_idx |
| 142 | + push!(idx2_to_idx1, i) |
| 143 | + end |
| 144 | + end |
| 145 | + idx1_to_idx2 = Dict(v => i for (i,v) in enumerate(idx2_to_idx1)) |
| 146 | + |
| 147 | + # rebuild the sparse version of the AdmittanceMatrix without the reference bus |
| 148 | + I = Int64[] |
| 149 | + J = Int64[] |
| 150 | + V = Float64[] |
| 151 | + |
| 152 | + I_src, J_src, V_src = findnz(am.matrix) |
| 153 | + for k in 1:length(V_src) |
| 154 | + if I_src[k] != ref_idx && J_src[k] != ref_idx |
| 155 | + push!(I, idx1_to_idx2[I_src[k]]) |
| 156 | + push!(J, idx1_to_idx2[J_src[k]]) |
| 157 | + push!(V, V_src[k]) |
| 158 | + end |
| 159 | + end |
| 160 | + M = sparse(I,J,V) |
| 161 | + |
| 162 | + # a vector to select which bus injection factors to compute |
| 163 | + va_vect = zeros(Float64, length(idx2_to_idx1)) |
| 164 | + va_vect[idx1_to_idx2[bus_idx]] = 1.0 |
| 165 | + |
| 166 | + if_vect = M \ va_vect |
| 167 | + |
| 168 | + # map injection factors back to original bus ids |
| 169 | + injection_factors = Dict(am.idx_to_bus[idx2_to_idx1[i]] => v for (i,v) in enumerate(if_vect) if !isapprox(v, 0.0)) |
| 170 | + |
| 171 | + return injection_factors |
| 172 | +end |
| 173 | + |
| 174 | + |
| 175 | +""" |
| 176 | +computes the power injection of each bus in the network |
| 177 | +
|
| 178 | +data should be a PowerModels network data model |
| 179 | +""" |
| 180 | +function calc_bus_injection(data::Dict{String,<:Any}) |
| 181 | + if length(data["dcline"]) > 0 |
| 182 | + Memento.error(_LOGGER, "calc_bus_injection does not support data with dclines") |
| 183 | + end |
| 184 | + if length(data["switch"]) > 0 |
| 185 | + Memento.error(_LOGGER, "calc_bus_injection does not support data with switches") |
| 186 | + end |
| 187 | + |
| 188 | + bus_values = Dict(bus["index"] => Dict{String,Float64}() for (i,bus) in data["bus"]) |
| 189 | + |
| 190 | + for (i,bus) in data["bus"] |
| 191 | + bvals = bus_values[bus["index"]] |
| 192 | + bvals["vm"] = bus["vm"] |
| 193 | + |
| 194 | + bvals["pd"] = 0.0 |
| 195 | + bvals["qd"] = 0.0 |
| 196 | + |
| 197 | + bvals["gs"] = 0.0 |
| 198 | + bvals["bs"] = 0.0 |
| 199 | + |
| 200 | + bvals["ps"] = 0.0 |
| 201 | + bvals["qs"] = 0.0 |
| 202 | + |
| 203 | + bvals["pg"] = 0.0 |
| 204 | + bvals["qg"] = 0.0 |
| 205 | + end |
| 206 | + |
| 207 | + for (i,load) in data["load"] |
| 208 | + if load["status"] != 0 |
| 209 | + bvals = bus_values[load["load_bus"]] |
| 210 | + bvals["pd"] += load["pd"] |
| 211 | + bvals["qd"] += load["qd"] |
| 212 | + end |
| 213 | + end |
| 214 | + |
| 215 | + for (i,shunt) in data["shunt"] |
| 216 | + if shunt["status"] != 0 |
| 217 | + bvals = bus_values[shunt["shunt_bus"]] |
| 218 | + bvals["gs"] += shunt["gs"] |
| 219 | + bvals["bs"] += shunt["bs"] |
| 220 | + end |
| 221 | + end |
| 222 | + |
| 223 | + for (i,storage) in data["storage"] |
| 224 | + if storage["status"] != 0 |
| 225 | + bvals = bus_values[storage["storage_bus"]] |
| 226 | + bvals["ps"] += storage["ps"] |
| 227 | + bvals["qs"] += storage["qs"] |
| 228 | + end |
| 229 | + end |
| 230 | + |
| 231 | + for (i,gen) in data["gen"] |
| 232 | + if gen["gen_status"] != 0 |
| 233 | + bvals = bus_values[gen["gen_bus"]] |
| 234 | + bvals["pg"] += gen["pg"] |
| 235 | + bvals["qg"] += gen["qg"] |
| 236 | + end |
| 237 | + end |
| 238 | + |
| 239 | + p_deltas = Dict{Int,Float64}() |
| 240 | + q_deltas = Dict{Int,Float64}() |
| 241 | + for (i,bus) in data["bus"] |
| 242 | + if bus["bus_type"] != 4 |
| 243 | + bvals = bus_values[bus["index"]] |
| 244 | + p_delta = - bvals["pg"] + bvals["ps"] + bvals["pd"] + bvals["gs"]*(bvals["vm"]^2) |
| 245 | + q_delta = - bvals["qg"] + bvals["qs"] + bvals["qd"] - bvals["bs"]*(bvals["vm"]^2) |
| 246 | + else |
| 247 | + p_delta = NaN |
| 248 | + q_delta = NaN |
| 249 | + end |
| 250 | + |
| 251 | + p_deltas[bus["index"]] = p_delta |
| 252 | + q_deltas[bus["index"]] = q_delta |
| 253 | + end |
| 254 | + |
| 255 | + return (p_deltas, q_deltas) |
| 256 | +end |
| 257 | + |
| 258 | +calc_bus_injection_active(data::Dict{String,<:Any}) = calc_bus_injection(data)[1] |
| 259 | + |
| 260 | + |
| 261 | +""" |
| 262 | +computes a dc power flow based on the susceptance matrix of the network data |
| 263 | +""" |
| 264 | +function solve_dc_pf(data::Dict{String,<:Any}) |
| 265 | + #TODO check single connected component |
| 266 | + |
| 267 | + sm = calc_susceptance_matrix(data) |
| 268 | + bi = calc_bus_injection_active(data) |
| 269 | + |
| 270 | + bi_idx = [bi[bus_id] for bus_id in sm.idx_to_bus] |
| 271 | + theta_idx = solve_theta(sm, bi_idx) |
| 272 | + |
| 273 | + bus_assignment= Dict{String,Any}() |
| 274 | + for (i,bus) in data["bus"] |
| 275 | + va = NaN |
| 276 | + if haskey(sm.bus_to_idx, bus["index"]) |
| 277 | + va = theta_idx[sm.bus_to_idx[bus["index"]]] |
| 278 | + end |
| 279 | + bus_assignment[i] = Dict("va" => va) |
| 280 | + end |
| 281 | + |
| 282 | + return Dict("per_unit" => data["per_unit"], "bus" => bus_assignment) |
| 283 | +end |
| 284 | + |
| 285 | + |
| 286 | +""" |
| 287 | +solves a DC power flow, assumes a single slack power variable at the refrence bus |
| 288 | +""" |
| 289 | +function solve_theta(am::AdmittanceMatrix, bus_injection::Vector{Float64}) |
| 290 | + #println(am.matrix) |
| 291 | + #println(bus_injection) |
| 292 | + |
| 293 | + m = deepcopy(am.matrix) |
| 294 | + bi = deepcopy(bus_injection) |
| 295 | + |
| 296 | + for i in 1:length(am.idx_to_bus) |
| 297 | + if i == am.ref_idx |
| 298 | + # TODO improve scaling of this value |
| 299 | + m[i,i] = 1.0 |
| 300 | + else |
| 301 | + if !iszero(m[am.ref_idx,i]) |
| 302 | + m[am.ref_idx,i] = 0.0 |
| 303 | + end |
| 304 | + end |
| 305 | + end |
| 306 | + bi[am.ref_idx] = 0.0 |
| 307 | + |
| 308 | + theta = m \ -bi |
| 309 | + |
| 310 | + return theta |
| 311 | +end |
0 commit comments