Skip to content

Commit bbf1c8d

Browse files
authored
Native DC Power Flow, PTDF-OPF and Lazy Flow Limit Solver (#640)
* add native dc pf solver * add basic ptdf formulation * add implicit injection_factors computation * add lazy flow cut solver with incremental model building * add some correctness checks to calc_susceptance_matrix and calc_bus_injection * add tests * add some basic docs to opf_flow_cuts functions * add notes to changelog
1 parent 5c1cab2 commit bbf1c8d

16 files changed

+1246
-57
lines changed

CHANGELOG.md

+4-1
Original file line numberDiff line numberDiff line change
@@ -2,7 +2,10 @@ PowerModels.jl Change Log
22
=========================
33

44
### Staged
5-
- Remove `Inf` bounds from variables (#630)
5+
- Added native DC Power Flow solver and AdmittanceMatrix data structures
6+
- Added PTDF-based OPF problem specification
7+
- Added iterative flow limit cut OPF solvers (#619)
8+
- Removed `Inf` bounds from variables (#630)
69
- Removal of unused functions in `solution.jl`: `get_solution`, `add_generator_power_setpoint`, `add_storage_setpoint`, `add_branch_flow_setpoint`, `add_dcline_flow_setpoint` (breaking) (#637)
710

811
### v0.13.1

docs/src/utilities.md

+10
Original file line numberDiff line numberDiff line change
@@ -9,3 +9,13 @@ To improve the quality of the convex relaxations available in PowerModels and al
99
```@docs
1010
run_obbt_opf!
1111
```
12+
13+
14+
## Lazy Line Flow Limits
15+
16+
The following functions are meta-algorithms for solving OPF problems where line flow limit constraints are added iteratively to exploit the property that the majority of line flows constraints will be inactive in the optimal solution.
17+
18+
```@docs
19+
run_opf_flow_cuts
20+
run_ptdf_opf_flow_cuts
21+
```

src/PowerModels.jl

+3
Original file line numberDiff line numberDiff line change
@@ -44,10 +44,12 @@ include("core/types.jl")
4444
include("core/variable.jl")
4545
include("core/constraint_template.jl")
4646
include("core/constraint.jl")
47+
include("core/expression_template.jl")
4748
include("core/relaxation_scheme.jl")
4849
include("core/objective.jl")
4950
include("core/solution.jl")
5051
include("core/multiconductor.jl")
52+
include("core/admittance_matrix.jl")
5153

5254
include("io/json.jl")
5355

@@ -72,6 +74,7 @@ include("prob/tnep.jl")
7274
include("prob/test.jl")
7375

7476
include("util/obbt.jl")
77+
include("util/flow_limit_cuts.jl")
7578

7679

7780
# this must come last to support automated export

src/core/admittance_matrix.jl

+311
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,311 @@
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

Comments
 (0)