diff --git a/Project.toml b/Project.toml index b61464a0..708b8ecb 100644 --- a/Project.toml +++ b/Project.toml @@ -9,6 +9,7 @@ DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" Dates = "ade2ca70-3891-5945-98fb-dc099432e06a" InfrastructureSystems = "2cd47ed4-ca9b-11e9-27f2-ab636a7671f1" JSON3 = "0f8b85d8-7281-11e9-16c2-39a750bddbf1" +KLU = "ef3ab10e-7fda-4108-b977-705223b18434" LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" NLsolve = "2774e3e8-f4cf-5e23-947b-6d7e65073b56" @@ -22,6 +23,7 @@ DataStructures = "0.18" Dates = "1" InfrastructureSystems = "2" JSON3 = "1" +KLU = "^0.6" LinearAlgebra = "1" Logging = "1" NLsolve = "4" diff --git a/src/PowerFlowData.jl b/src/PowerFlowData.jl index f064a02c..c22ca912 100644 --- a/src/PowerFlowData.jl +++ b/src/PowerFlowData.jl @@ -157,7 +157,7 @@ NOTE: use it for AC power flow computations. WARNING: functions for the evaluation of the multi-period AC PF still to be implemented. """ function PowerFlowData( - ::ACPowerFlow, + ::ACPowerFlow{<:ACPowerFlowSolverType}, sys::PSY.System; time_steps::Int = 1, timestep_names::Vector{String} = String[], @@ -440,7 +440,11 @@ Create an appropriate `PowerFlowContainer` for the given `PowerFlowEvaluationMod """ function make_power_flow_container end -make_power_flow_container(pfem::ACPowerFlow, sys::PSY.System; kwargs...) = +make_power_flow_container( + pfem::ACPowerFlow{<:ACPowerFlowSolverType}, + sys::PSY.System; + kwargs..., +) = PowerFlowData(pfem, sys; kwargs...) make_power_flow_container(pfem::DCPowerFlow, sys::PSY.System; kwargs...) = diff --git a/src/PowerFlows.jl b/src/PowerFlows.jl index 7d88f80e..00907a8b 100644 --- a/src/PowerFlows.jl +++ b/src/PowerFlows.jl @@ -4,7 +4,10 @@ export solve_powerflow export solve_ac_powerflow! export PowerFlowData export DCPowerFlow +export NLSolveACPowerFlow +export KLUACPowerFlow export ACPowerFlow +export ACPowerFlowSolverType export PTDFDCPowerFlow export vPTDFDCPowerFlow export PSSEExportPowerFlow @@ -20,10 +23,11 @@ import PowerSystems import PowerSystems: System import LinearAlgebra import NLsolve +import KLU import SparseArrays import InfrastructureSystems import PowerNetworkMatrices -import SparseArrays: SparseMatrixCSC +import SparseArrays: SparseMatrixCSC, sparse import JSON3 import DataStructures: OrderedDict import Dates @@ -40,6 +44,6 @@ include("psse_export.jl") include("solve_dc_powerflow.jl") include("ac_power_flow.jl") include("ac_power_flow_jacobian.jl") -include("nlsolve_ac_powerflow.jl") +include("newton_ac_powerflow.jl") include("post_processing.jl") end diff --git a/src/definitions.jl b/src/definitions.jl index d8ecad1a..36e53b60 100644 --- a/src/definitions.jl +++ b/src/definitions.jl @@ -10,4 +10,7 @@ const DEFAULT_MAX_REDISTRIBUTION_ITERATIONS = 10 const ISAPPROX_ZERO_TOLERANCE = 1e-6 +const DEFAULT_NR_MAX_ITER::Int64 = 30 # default maxIter for the NR power flow +const DEFAULT_NR_TOL::Float64 = 1e-9 # default tolerance for the NR power flow + const AC_PF_KW = [] diff --git a/src/newton_ac_powerflow.jl b/src/newton_ac_powerflow.jl new file mode 100644 index 00000000..014384b3 --- /dev/null +++ b/src/newton_ac_powerflow.jl @@ -0,0 +1,336 @@ +""" +Solves a the power flow into the system and writes the solution into the relevant structs. +Updates generators active and reactive power setpoints and branches active and reactive +power flows (calculated in the From - To direction) (see +[`flow_val`](@ref)) + +Supports passing NLsolve kwargs in the args. By default shows the solver trace. + +Arguments available for `nlsolve`: + + - `get_connectivity::Bool`: Checks if the network is connected. Default true + - `method` : See NLSolve.jl documentation for available solvers + - `xtol`: norm difference in `x` between two successive iterates under which + convergence is declared. Default: `0.0`. + - `ftol`: infinite norm of residuals under which convergence is declared. + Default: `1e-8`. + - `iterations`: maximum number of iterations. Default: `1_000`. + - `store_trace`: should a trace of the optimization algorithm's state be + stored? Default: `false`. + - `show_trace`: should a trace of the optimization algorithm's state be shown + on `STDOUT`? Default: `false`. + - `extended_trace`: should additifonal algorithm internals be added to the state + trace? Default: `false`. + +## Examples + +```julia +solve_ac_powerflow!(sys) +# Passing NLsolve arguments +solve_ac_powerflow!(sys, method=:newton) +``` +""" +function solve_ac_powerflow!( + pf::ACPowerFlow{<:ACPowerFlowSolverType}, + system::PSY.System; + kwargs..., +) + #Save per-unit flag + settings_unit_cache = deepcopy(system.units_settings.unit_system) + #Work in System per unit + PSY.set_units_base_system!(system, "SYSTEM_BASE") + check_reactive_power_limits = get(kwargs, :check_reactive_power_limits, false) + data = PowerFlowData( + pf, + system; + check_connectivity = get(kwargs, :check_connectivity, true), + ) + max_iterations = DEFAULT_MAX_REDISTRIBUTION_ITERATIONS + converged, x = _solve_powerflow!(pf, data, check_reactive_power_limits; kwargs...) + if converged + write_powerflow_solution!(system, x, max_iterations) + @info("PowerFlow solve converged, the results have been stored in the system") + #Restore original per unit base + PSY.set_units_base_system!(system, settings_unit_cache) + return converged + end + @error("The powerflow solver returned convergence = $(converged)") + PSY.set_units_base_system!(system, settings_unit_cache) + return converged +end + +""" +Similar to solve_powerflow!(sys) but does not update the system struct with results. +Returns the results in a dictionary of dataframes. + +## Examples + +```julia +res = solve_powerflow(sys) +# Passing NLsolve arguments +res = solve_powerflow(sys, method=:newton) +``` +""" +function solve_powerflow( + pf::ACPowerFlow{<:ACPowerFlowSolverType}, + system::PSY.System; + kwargs..., +) + #Save per-unit flag + settings_unit_cache = deepcopy(system.units_settings.unit_system) + #Work in System per unit + PSY.set_units_base_system!(system, "SYSTEM_BASE") + data = PowerFlowData( + pf, + system; + check_connectivity = get(kwargs, :check_connectivity, true), + ) + + converged, x = _solve_powerflow!(pf, data, pf.check_reactive_power_limits; kwargs...) + + if converged + @info("PowerFlow solve converged, the results are exported in DataFrames") + df_results = write_results(pf, system, data, x) + #Restore original per unit base + PSY.set_units_base_system!(system, settings_unit_cache) + return df_results + end + @error("The powerflow solver returned convergence = $(converged)") + PSY.set_units_base_system!(system, settings_unit_cache) + return converged +end + +function _check_q_limit_bounds!(data::ACPowerFlowData, zero::Vector{Float64}) + bus_names = data.power_network_matrix.axes[1] + within_limits = true + for (ix, b) in enumerate(data.bus_type) + if b == PSY.ACBusTypes.PV + Q_gen = zero[2 * ix - 1] + else + continue + end + + if Q_gen <= data.bus_reactivepower_bounds[ix][1] + @info "Bus $(bus_names[ix]) changed to PSY.ACBusTypes.PQ" + within_limits = false + data.bus_type[ix] = PSY.ACBusTypes.PQ + data.bus_reactivepower_injection[ix] = data.bus_reactivepower_bounds[ix][1] + elseif Q_gen >= data.bus_reactivepower_bounds[ix][2] + @info "Bus $(bus_names[ix]) changed to PSY.ACBusTypes.PQ" + within_limits = false + data.bus_type[ix] = PSY.ACBusTypes.PQ + data.bus_reactivepower_injection[ix] = data.bus_reactivepower_bounds[ix][2] + else + @debug "Within Limits" + end + end + return within_limits +end + +function _solve_powerflow!( + pf::ACPowerFlow{<:ACPowerFlowSolverType}, + data::ACPowerFlowData, + check_reactive_power_limits; + nlsolve_kwargs..., +) + if check_reactive_power_limits + for _ in 1:MAX_REACTIVE_POWER_ITERATIONS + converged, x = _newton_powerflow(pf, data; nlsolve_kwargs...) + if converged + if _check_q_limit_bounds!(data, x) + return converged, x + end + else + return converged, x + end + end + else + return _newton_powerflow(pf, data; nlsolve_kwargs...) + end +end + +function _newton_powerflow( + pf::ACPowerFlow{NLSolveACPowerFlow}, + data::ACPowerFlowData; + nlsolve_kwargs..., +) + pf = PolarPowerFlow(data) + J = PowerFlows.PolarPowerFlowJacobian(data, pf.x0) + + df = NLsolve.OnceDifferentiable(pf, J, pf.x0, pf.residual, J.Jv) + res = NLsolve.nlsolve(df, pf.x0; nlsolve_kwargs...) + if !res.f_converged + @error( + "The powerflow solver NLSolve did not converge (returned convergence = $(res.f_converged))" + ) + end + return res.f_converged, res.zero +end + +function _newton_powerflow( + pf::ACPowerFlow{KLUACPowerFlow}, + data::ACPowerFlowData; + nlsolve_kwargs..., +) + # Fetch maxIter and tol from kwargs, or use defaults if not provided + maxIter = get(nlsolve_kwargs, :maxIter, DEFAULT_NR_MAX_ITER) + tol = get(nlsolve_kwargs, :tol, DEFAULT_NR_TOL) + i = 0 + + # Find indices for each bus type + ref = findall(x -> x == PowerSystems.ACBusTypesModule.ACBusTypes.REF, data.bus_type) + pv = findall(x -> x == PowerSystems.ACBusTypesModule.ACBusTypes.PV, data.bus_type) + pq = findall(x -> x == PowerSystems.ACBusTypesModule.ACBusTypes.PQ, data.bus_type) + pvpq = [pv; pq] + + #nref = length(ref) + npv = length(pv) + npq = length(pq) + npvpq = npv + npq + n_buses = length(data.bus_type) + + Vm = data.bus_magnitude[:] + # prevent unfeasible starting values for Vm; for pv and ref buses we cannot do this: + @. Vm[pq] = clamp.(Vm[pq], 0.9, 1.1) + Va = data.bus_angles[:] + V = zeros(Complex{Float64}, length(Vm)) + @. V = Vm .* exp.(1im * Va) + + Va_pv = view(Va, pv) + Va_pq = view(Va, pq) + Vm_pq = view(Vm, pq) + + # pre-allocate dx + dx = zeros(Float64, npv + 2 * npq) + + dx_Va_pv = view(dx, 1:npv) + dx_Va_pq = view(dx, (npv + 1):(npv + npq)) + dx_Vm_pq = view(dx, (npv + npq + 1):(npv + 2 * npq)) + + Ybus = data.power_network_matrix.data + + Sbus = + data.bus_activepower_injection[:] - data.bus_activepower_withdrawals[:] + + 1im * (data.bus_reactivepower_injection[:] - data.bus_reactivepower_withdrawals[:]) + + # Pre-allocate mis and F and create views for the respective real and imaginary sections of the arrays: + mis = zeros(Complex{Float64}, length(V)) + mis_pvpq = view(mis, pvpq) + mis_pq = view(mis, pq) + + F = zeros(Float64, npvpq + npq) + F_real = view(F, 1:npvpq) + F_imag = view(F, npvpq + 1:npvpq + npq) + + mis .= V .* conj.(Ybus * V) .- Sbus + @. F_real = real(mis_pvpq) # In-place assignment to the real part, using views + @. F_imag = imag(mis_pq) # In-place assignment to the imaginary part, using views + + converged = npvpq == 0 # if only ref buses present, we do not need to enter the loop + + # preallocate Jacobian matrix + rows = vcat(1:npvpq, 1:npvpq, npvpq+1:npvpq+npq, npvpq+1:npvpq+npq) + cols = vcat(1:npvpq, npvpq+1:npvpq+npq, 1:npvpq, npvpq+1:npvpq+npq) + J = sparse(rows, cols, Float64(0)) + + # we need to define lookups for mappings of pv, pq buses onto the internal J indexing + pvpq_lookup = zeros(Int64, maximum([ref; pvpq]) + 1) + pvpq_lookup[pvpq] .= 1:npvpq + pq_lookup = zeros(Int64, maximum([ref; pvpq]) + 1) + pq_lookup[pq] .= 1:npq + + # with the pre-allocated J and lookups, we can define views into the sub-matrices of the J matrix for updating the J matrix in the NR loop + j11 = view(J, pvpq_lookup[pvpq], pvpq_lookup[pvpq]) + j12 = view(J, pvpq_lookup[pvpq], npvpq .+ pq_lookup[pq]) + j21 = view(J, npvpq .+ pq_lookup[pq], pvpq_lookup[pvpq]) + j22 = view(J, npvpq .+ pq_lookup[pq], npvpq .+ pq_lookup[pq]) + + # we need views of the diagonals to avoid using LinearAlgebra.Diagonal: + diagV = sparse(1:n_buses, 1:n_buses, Complex{Float64}(1)) + diag_idx = LinearAlgebra.diagind(diagV) + diagV_diag = view(diagV, diag_idx) + + diagIbus = sparse(1:n_buses, 1:n_buses, Complex{Float64}(1)) + diagIbus_diag = view(diagIbus, diag_idx) + + diagVnorm = sparse(1:n_buses, 1:n_buses, Complex{Float64}(1)) + diagVnorm_diag = view(diagVnorm, diag_idx) + + # pre-allocate the dSbus_dVm, dSbus_dVa to have the same structure as Ybus + # they will follow the structure of Ybus except maybe when Ybus has zero values in its diagonal, which we do not expect here + #rows, cols, _ = SparseArrays.findnz(Ybus) + #dSbus_dVm = sparse(rows, cols, Complex{Float64}(0)) + #dSbus_dVa = sparse(rows, cols, Complex{Float64}(0)) + + # preallocate dSbus_dVm, dSbus_dVa with correct structure: + dSbus_dVm = diagV * conj.(Ybus * diagVnorm) + conj.(diagIbus) * diagVnorm + dSbus_dVa = 1im * diagV * conj.(diagIbus - Ybus * diagV) + + # create views for the sub-arrays of Sbus_dVa, Sbus_dVm for updating the J: + Sbus_dVa_j11 = view(dSbus_dVa, pvpq, pvpq) + Sbus_dVm_j12 = view(dSbus_dVm, pvpq, pq) + Sbus_dVa_j21 = view(dSbus_dVa, pq, pvpq) + Sbus_dVm_j22 = view(dSbus_dVm, pq, pq) + + while i < maxIter && !converged + i += 1 + + ## use the new value of V to update dSbus_dVa, dSbus_dVm: + diagV_diag .= V + diagIbus_diag .= Ybus * V + @. diagVnorm_diag = V ./ abs.(V) + dSbus_dVm .= diagV * conj.(Ybus * diagVnorm) + conj.(diagIbus) * diagVnorm + dSbus_dVa .= 1im * diagV * conj.(diagIbus - Ybus * diagV) + + # update the Jacobian by setting values through the pre-defined views for j11, j12, j21, j22 + @. j11 = real(Sbus_dVa_j11) + @. j12 = real(Sbus_dVm_j12) + @. j21 = imag(Sbus_dVa_j21) + @. j22 = imag(Sbus_dVm_j22) + + factor_J = KLU.klu(J) + dx .= -(factor_J \ F) + + Va_pv .+= dx_Va_pv + Va_pq .+= dx_Va_pq + Vm_pq .+= dx_Vm_pq + + @. V = Vm .* exp.(1im * Va) + + Vm .= abs.(V) + Va .= angle.(V) + + mis .= V .* conj.(Ybus * V) .- Sbus + @. F_real = real(mis_pvpq) # In-place assignment to the real part + @. F_imag = imag(mis_pq) # In-place assignment to the imaginary part + converged = LinearAlgebra.norm(F, Inf) < tol + end + + if !converged + @error("The powerflow solver with KLU did not converge after $i iterations") + else + @info("The powerflow solver with KLU converged after $i iterations") + end + + # mock the expected x format, where the values depend on the type of the bus: + n_buses = length(data.bus_type) + x = zeros(Float64, 2 * n_buses) + Sbus_result = V .* conj(Ybus * V) + for (ix, b) in enumerate(data.bus_type) + if b == PSY.ACBusTypes.REF + # When bustype == REFERENCE PSY.Bus, state variables are Active and Reactive Power Generated + x[2 * ix - 1] = real(Sbus_result[ix]) + data.bus_activepower_withdrawals[ix] + x[2 * ix] = imag(Sbus_result[ix]) + data.bus_reactivepower_withdrawals[ix] + elseif b == PSY.ACBusTypes.PV + # When bustype == PV PSY.Bus, state variables are Reactive Power Generated and Voltage Angle + x[2 * ix - 1] = imag(Sbus_result[ix]) + data.bus_reactivepower_withdrawals[ix] + x[2 * ix] = Va[ix] + elseif b == PSY.ACBusTypes.PQ + # When bustype == PQ PSY.Bus, state variables are Voltage Magnitude and Voltage Angle + x[2 * ix - 1] = Vm[ix] + x[2 * ix] = Va[ix] + end + end + + return converged, x +end diff --git a/src/nlsolve_ac_powerflow.jl b/src/nlsolve_ac_powerflow.jl deleted file mode 100644 index 813848b0..00000000 --- a/src/nlsolve_ac_powerflow.jl +++ /dev/null @@ -1,157 +0,0 @@ -""" -Solves a the power flow into the system and writes the solution into the relevant structs. -Updates generators active and reactive power setpoints and branches active and reactive -power flows (calculated in the From - To direction) (see -[`flow_val`](@ref)) - -Supports passing NLsolve kwargs in the args. By default shows the solver trace. - -Arguments available for `nlsolve`: - - - `get_connectivity::Bool`: Checks if the network is connected. Default true - - `method` : See NLSolve.jl documentation for available solvers - - `xtol`: norm difference in `x` between two successive iterates under which - convergence is declared. Default: `0.0`. - - `ftol`: infinite norm of residuals under which convergence is declared. - Default: `1e-8`. - - `iterations`: maximum number of iterations. Default: `1_000`. - - `store_trace`: should a trace of the optimization algorithm's state be - stored? Default: `false`. - - `show_trace`: should a trace of the optimization algorithm's state be shown - on `STDOUT`? Default: `false`. - - `extended_trace`: should additifonal algorithm internals be added to the state - trace? Default: `false`. - -## Examples - -```julia -solve_ac_powerflow!(sys) -# Passing NLsolve arguments -solve_ac_powerflow!(sys, method=:newton) -``` -""" -function solve_ac_powerflow!(system::PSY.System; kwargs...) - #Save per-unit flag - settings_unit_cache = deepcopy(system.units_settings.unit_system) - #Work in System per unit - PSY.set_units_base_system!(system, "SYSTEM_BASE") - check_reactive_power_limits = get(kwargs, :check_reactive_power_limits, false) - data = PowerFlowData( - ACPowerFlow(; check_reactive_power_limits = check_reactive_power_limits), - system; - check_connectivity = get(kwargs, :check_connectivity, true), - ) - max_iterations = DEFAULT_MAX_REDISTRIBUTION_ITERATIONS - res = _solve_powerflow!(data, check_reactive_power_limits; kwargs...) - if res.f_converged - write_powerflow_solution!(system, res.zero, max_iterations) - @info("PowerFlow solve converged, the results have been stored in the system") - #Restore original per unit base - PSY.set_units_base_system!(system, settings_unit_cache) - return res.f_converged - end - @error("The powerflow solver returned convergence = $(res.f_converged)") - PSY.set_units_base_system!(system, settings_unit_cache) - return res.f_converged -end - -""" -Similar to solve_powerflow!(sys) but does not update the system struct with results. -Returns the results in a dictionary of dataframes. - -## Examples - -```julia -res = solve_powerflow(sys) -# Passing NLsolve arguments -res = solve_powerflow(sys, method=:newton) -``` -""" -function solve_powerflow( - pf::ACPowerFlow, - system::PSY.System; - kwargs..., -) - #Save per-unit flag - settings_unit_cache = deepcopy(system.units_settings.unit_system) - #Work in System per unit - PSY.set_units_base_system!(system, "SYSTEM_BASE") - data = PowerFlowData( - pf, - system; - check_connectivity = get(kwargs, :check_connectivity, true), - ) - - res = _solve_powerflow!(data, pf.check_reactive_power_limits; kwargs...) - - if res.f_converged - @info("PowerFlow solve converged, the results are exported in DataFrames") - df_results = write_results(pf, system, data, res.zero) - #Restore original per unit base - PSY.set_units_base_system!(system, settings_unit_cache) - return df_results - end - @error("The powerflow solver returned convergence = $(res.f_converged)") - PSY.set_units_base_system!(system, settings_unit_cache) - return res.f_converged -end - -function _check_q_limit_bounds!(data::ACPowerFlowData, zero::Vector{Float64}) - bus_names = data.power_network_matrix.axes[1] - within_limits = true - for (ix, b) in enumerate(data.bus_type) - if b == PSY.ACBusTypes.PV - Q_gen = zero[2 * ix - 1] - else - continue - end - - if Q_gen <= data.bus_reactivepower_bounds[ix][1] - @info "Bus $(bus_names[ix]) changed to PSY.ACBusTypes.PQ" - within_limits = false - data.bus_type[ix] = PSY.ACBusTypes.PQ - data.bus_reactivepower_injection[ix] = data.bus_reactivepower_bounds[ix][1] - elseif Q_gen >= data.bus_reactivepower_bounds[ix][2] - @info "Bus $(bus_names[ix]) changed to PSY.ACBusTypes.PQ" - within_limits = false - data.bus_type[ix] = PSY.ACBusTypes.PQ - data.bus_reactivepower_injection[ix] = data.bus_reactivepower_bounds[ix][2] - else - @debug "Within Limits" - end - end - return within_limits -end - -function _solve_powerflow!( - data::ACPowerFlowData, - check_reactive_power_limits; - nlsolve_kwargs..., -) - if check_reactive_power_limits - for _ in 1:MAX_REACTIVE_POWER_ITERATIONS - res = _nlsolve_powerflow(data; nlsolve_kwargs...) - if res.f_converged - if _check_q_limit_bounds!(data, res.zero) - return res - end - else - return res - end - end - else - return _nlsolve_powerflow(data; nlsolve_kwargs...) - end -end - -function _nlsolve_powerflow(data::ACPowerFlowData; nlsolve_kwargs...) - pf = PolarPowerFlow(data) - J = PowerFlows.PolarPowerFlowJacobian(data, pf.x0) - - df = NLsolve.OnceDifferentiable(pf, J, pf.x0, pf.residual, J.Jv) - res = NLsolve.nlsolve(df, pf.x0; nlsolve_kwargs...) - if !res.f_converged - @error("The powerflow solver returned convergence = $(res.f_converged)") - end - return res -end diff --git a/src/post_processing.jl b/src/post_processing.jl index 6c500a25..c8229201 100644 --- a/src/post_processing.jl +++ b/src/post_processing.jl @@ -608,7 +608,7 @@ dictionary will therefore feature just one key linked to one DataFrame. vector containing the reults for one single time-period. """ function write_results( - ::ACPowerFlow, + ::ACPowerFlow{<:ACPowerFlowSolverType}, sys::PSY.System, data::ACPowerFlowData, result::Vector{Float64}, diff --git a/src/powerflow_types.jl b/src/powerflow_types.jl index e1b9828a..e490e1f5 100644 --- a/src/powerflow_types.jl +++ b/src/powerflow_types.jl @@ -1,9 +1,21 @@ abstract type PowerFlowEvaluationModel end +abstract type ACPowerFlowSolverType end -Base.@kwdef struct ACPowerFlow <: PowerFlowEvaluationModel +struct KLUACPowerFlow <: ACPowerFlowSolverType end +struct NLSolveACPowerFlow <: ACPowerFlowSolverType end + +Base.@kwdef struct ACPowerFlow{ACSolver <: ACPowerFlowSolverType} <: + PowerFlowEvaluationModel check_reactive_power_limits::Bool = false end +# Create a constructor that defaults to KLUACPowerFlow +function ACPowerFlow(ACSolver::Type{<:ACPowerFlowSolverType} = KLUACPowerFlow; + check_reactive_power_limits::Bool = false, +) + return ACPowerFlow{ACSolver}(check_reactive_power_limits) +end + struct DCPowerFlow <: PowerFlowEvaluationModel end struct PTDFDCPowerFlow <: PowerFlowEvaluationModel end struct vPTDFDCPowerFlow <: PowerFlowEvaluationModel end diff --git a/test/test_nlsolve_powerflow.jl b/test/test_newton_ac_powerflow.jl similarity index 74% rename from test/test_nlsolve_powerflow.jl rename to test/test_newton_ac_powerflow.jl index eb2a2813..7a0b31d8 100644 --- a/test/test_nlsolve_powerflow.jl +++ b/test/test_newton_ac_powerflow.jl @@ -1,5 +1,5 @@ -@testset "NLsolve Power Flow 14-Bus testing" begin - sys = PSB.build_system(PSB.PSITestSystems, "c_sys14"; add_forecasts = false) +@testset "AC Power Flow 14-Bus testing" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) result_14 = [ 2.3255081760423684 -0.15529254415401786 @@ -30,60 +30,70 @@ 1.0213119628726421 -0.2803812119374241 ] - data = PowerFlows.PowerFlowData(ACPowerFlow(), sys; check_connectivity = true) + + sys = PSB.build_system(PSB.PSITestSystems, "c_sys14"; add_forecasts = false) + pf = ACPowerFlow{ACSolver}() + data = PowerFlows.PowerFlowData(pf, sys; check_connectivity = true) #Compare results between finite diff methods and Jacobian method - res1 = PowerFlows._solve_powerflow!(data, false) - @test LinearAlgebra.norm(result_14 - res1.zero) <= 1e-6 - @test solve_ac_powerflow!(sys; method = :newton) + converged1, x1 = PowerFlows._solve_powerflow!(pf, data, false) + @test LinearAlgebra.norm(result_14 - x1, Inf) <= 1e-6 + @test solve_ac_powerflow!(pf, sys; method = :newton) # Test enforcing the reactive power Limits set_reactive_power!(get_component(PowerLoad, sys, "Bus4"), 0.0) - data = PowerFlows.PowerFlowData(ACPowerFlow(), sys; check_connectivity = true) - res2 = PowerFlows._solve_powerflow!(data, true) - @test LinearAlgebra.norm(result_14 - res2.zero) >= 1e-6 - @test 1.08 <= res2.zero[15] <= 1.09 + data = PowerFlows.PowerFlowData(pf, sys; check_connectivity = true) + converged2, x2 = PowerFlows._solve_powerflow!(pf, data, true) + @test LinearAlgebra.norm(result_14 - x2, Inf) >= 1e-6 + @test 1.08 <= x2[15] <= 1.09 end -@testset "NLsolve Power Flow 14-Bus Line Configurations" begin +@testset "AC Power Flow 14-Bus Line Configurations" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) sys = PSB.build_system(PSB.PSITestSystems, "c_sys14"; add_forecasts = false) - base_res = solve_powerflow(ACPowerFlow(), sys) + pf = ACPowerFlow{ACSolver}() + base_res = solve_powerflow(pf, sys) branch = first(PSY.get_components(Line, sys)) dyn_branch = DynamicBranch(branch) add_component!(sys, dyn_branch) - @test dyn_pf = solve_ac_powerflow!(sys) - dyn_pf = solve_powerflow(ACPowerFlow(), sys) - @test LinearAlgebra.norm(dyn_pf["bus_results"].Vm - base_res["bus_results"].Vm) <= + @test dyn_pf = solve_ac_powerflow!(pf, sys) + dyn_pf = solve_powerflow(pf, sys) + @test LinearAlgebra.norm(dyn_pf["bus_results"].Vm - base_res["bus_results"].Vm, Inf) <= 1e-6 sys = PSB.build_system(PSB.PSITestSystems, "c_sys14"; add_forecasts = false) line = get_component(Line, sys, "Line4") PSY.set_available!(line, false) - solve_ac_powerflow!(sys) + solve_ac_powerflow!(pf, sys) @test PSY.get_active_power_flow(line) == 0.0 test_bus = get_component(PSY.Bus, sys, "Bus 4") - @test isapprox(PSY.get_magnitude(test_bus), 1.002; atol = 1e-3) + @test isapprox(PSY.get_magnitude(test_bus), 1.002; atol = 1e-3, rtol = 0) sys = PSB.build_system(PSB.PSITestSystems, "c_sys14"; add_forecasts = false) line = get_component(Line, sys, "Line4") PSY.set_available!(line, false) - res = solve_powerflow(ACPowerFlow(), sys) + res = solve_powerflow(pf, sys) @test res["flow_results"].P_from_to[4] == 0.0 @test res["flow_results"].P_to_from[4] == 0.0 end -@testset "NLsolve Power Flow 3-Bus Fixed FixedAdmittance testing" begin +@testset "AC Power Flow 3-Bus Fixed FixedAdmittance testing" for ACSolver in ( + NLSolveACPowerFlow, + KLUACPowerFlow, +) p_gen_matpower_3bus = [20.3512373930753, 100.0, 100.0] q_gen_matpower_3bus = [45.516916781567232, 10.453799727283879, -31.992561631394636] sys_3bus = PSB.build_system(PSB.PSYTestSystems, "psse_3bus_gen_cls_sys") bus_103 = get_component(PSY.Bus, sys_3bus, "BUS 3") fix_shunt = PSY.FixedAdmittance("FixAdmBus3", true, bus_103, 0.0 + 0.2im) add_component!(sys_3bus, fix_shunt) - df = solve_powerflow(ACPowerFlow(), sys_3bus) + pf = ACPowerFlow{ACSolver}() + df = solve_powerflow(pf, sys_3bus) @test isapprox(df["bus_results"].P_gen, p_gen_matpower_3bus, atol = 1e-4) @test isapprox(df["bus_results"].Q_gen, q_gen_matpower_3bus, atol = 1e-4) end -@testset "NLsolve Power Flow convergence fail testing" begin +@testset "AC Power Flow convergence fail testing" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) pf_sys5_re = PSB.build_system(PSB.PSITestSystems, "c_sys5_re"; add_forecasts = false) remove_component!(Line, pf_sys5_re, "1") remove_component!(Line, pf_sys5_re, "2") @@ -91,15 +101,18 @@ end PSY.set_x!(br, 20.0) PSY.set_r!(br, 2.0) + pf = ACPowerFlow{ACSolver}() + # This is a negative test. The data passed for sys5_re is known to be infeasible. @test_logs( (:error, "The powerflow solver returned convergence = false"), match_mode = :any, - @test !solve_ac_powerflow!(pf_sys5_re) + @test !solve_ac_powerflow!(pf, pf_sys5_re) ) end -@testset "Test 240 Case PSS/e results" begin +@testset "AC Test 240 Case PSS/e results" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) file = joinpath( TEST_FILES_DIR, "test_data", @@ -114,9 +127,11 @@ end pf_bus_result_file = joinpath(TEST_FILES_DIR, "test_data", "pf_bus_results.csv") pf_gen_result_file = joinpath(TEST_FILES_DIR, "test_data", "pf_gen_results.csv") - pf = solve_ac_powerflow!(system) - @test pf - pf_result_df = solve_powerflow(ACPowerFlow(), system) + pf = ACPowerFlow{ACSolver}() + + pf1 = solve_ac_powerflow!(pf, system) + @test pf1 + pf_result_df = solve_powerflow(pf, system) v_diff, angle_diff, number = psse_bus_results_compare(pf_bus_result_file, pf_result_df) p_diff, q_diff, names = psse_gen_results_compare(pf_gen_result_file, system) @@ -132,7 +147,8 @@ end @test norm(q_diff, 2) / length(q_diff) < DIFF_L2_TOLERANCE end -@testset "Multiple sources at ref" begin +@testset "AC Multiple sources at ref" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) sys = System(100.0) b = ACBus(; number = 1, @@ -166,16 +182,18 @@ end X_th = 1e-5, ) add_component!(sys, s2) - @test solve_ac_powerflow!(sys) + pf = ACPowerFlow{ACSolver}() + @test solve_ac_powerflow!(pf, sys) #Create power mismatch, test for error set_active_power!(get_component(Source, sys, "source_1"), -0.4) @test_throws ErrorException( "Sources do not match P and/or Q requirements for reference bus.", - ) solve_ac_powerflow!(sys) + ) solve_ac_powerflow!(pf, sys) end -@testset "AC PowerFlow with Multiple sources at PV" begin +@testset "AC PowerFlow with Multiple sources at PV" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) sys = System(100.0) b1 = ACBus(; number = 1, @@ -245,16 +263,20 @@ end ) add_component!(sys, s3) - @test solve_ac_powerflow!(sys) + pf = ACPowerFlow{ACSolver}() + + @test solve_ac_powerflow!(pf, sys) #Create power mismatch, test for error set_reactive_power!(get_component(Source, sys, "source_3"), -0.5) @test_throws ErrorException("Sources do not match Q requirements for PV bus.") solve_ac_powerflow!( + pf, sys, ) end -@testset "AC PowerFlow Source + non-source at Ref" begin +@testset "AC PowerFlow Source + non-source at Ref" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) sys = System(100.0) b = ACBus(; number = 1, @@ -300,7 +322,9 @@ end ) add_component!(sys, g1) - @test solve_ac_powerflow!(sys) + pf = ACPowerFlow{ACSolver}() + + @test solve_ac_powerflow!(pf, sys) @test isapprox( get_active_power(get_component(Source, sys, "source_1")), 0.5; @@ -313,7 +337,8 @@ end ) end -@testset "AC PowerFlow Source + non-source at PV" begin +@testset "AC PowerFlow Source + non-source at PV" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) sys = System(100.0) b1 = ACBus(; number = 1, @@ -394,7 +419,9 @@ end ) add_component!(sys, g1) - @test solve_ac_powerflow!(sys) + pf = ACPowerFlow{ACSolver}() + + @test solve_ac_powerflow!(pf, sys) @test isapprox( get_active_power(get_component(Source, sys, "source_2")), 0.5; @@ -406,3 +433,23 @@ end atol = 0.001, ) end + + +@testset "Compare larger grid results KLU vs NLSolve" begin + sys = build_system(MatpowerTestSystems, "matpower_ACTIVSg2000_sys") + + pf_default = ACPowerFlow() + pf_klu = ACPowerFlow(KLUACPowerFlow) + pf_nlsolve = ACPowerFlow(NLSolveACPowerFlow) + + res_default = solve_powerflow(pf_default, sys) # must be the same as KLU + res_klu = solve_powerflow(pf_klu, sys) + res_nlsolve = solve_powerflow(pf_nlsolve, sys) + + + @test all(isapprox.(res_klu["bus_results"][!, :Vm], res_default["bus_results"][!, :Vm], rtol=0, atol=1e-12)) + @test all(isapprox.(res_klu["bus_results"][!, :θ], res_default["bus_results"][!, :θ], rtol=0, atol=1e-12)) + + @test all(isapprox.(res_klu["bus_results"][!, :Vm], res_nlsolve["bus_results"][!, :Vm], rtol=0, atol=1e-8)) + @test all(isapprox.(res_klu["bus_results"][!, :θ], res_nlsolve["bus_results"][!, :θ], rtol=0, atol=1e-8)) +end \ No newline at end of file diff --git a/test/test_powerflow_data.jl b/test/test_powerflow_data.jl index 75964cb7..2d45a148 100644 --- a/test/test_powerflow_data.jl +++ b/test/test_powerflow_data.jl @@ -1,6 +1,7 @@ @testset "PowerFlowData" begin sys = PSB.build_system(PSB.PSITestSystems, "c_sys14"; add_forecasts = false) - @test PowerFlowData(ACPowerFlow(), sys) isa PF.ACPowerFlowData + @test PowerFlowData(ACPowerFlow{NLSolveACPowerFlow}(), sys) isa PF.ACPowerFlowData + @test PowerFlowData(ACPowerFlow{KLUACPowerFlow}(), sys) isa PF.ACPowerFlowData @test PowerFlowData(DCPowerFlow(), sys) isa PF.ABAPowerFlowData @test PowerFlowData(PTDFDCPowerFlow(), sys) isa PF.PTDFPowerFlowData @test PowerFlowData(vPTDFDCPowerFlow(), sys) isa PF.vPTDFPowerFlowData @@ -17,23 +18,24 @@ end PF.vPTDFPowerFlowData end -@testset "System <-> PowerFlowData round trip" begin +@testset "System <-> PowerFlowData round trip" for ACSolver in + (NLSolveACPowerFlow, KLUACPowerFlow) # TODO currently only tested with ACPowerFlow # TODO test that update_system! errors if the PowerFlowData doesn't correspond to the system sys_original = build_system(PSISystems, "RTS_GMLC_DA_sys") - data_original = PowerFlowData(ACPowerFlow(), sys_original) + data_original = PowerFlowData(ACPowerFlow{ACSolver}(), sys_original) sys_modified = deepcopy(sys_original) modify_rts_system!(sys_modified) - data_modified = PowerFlowData(ACPowerFlow(), sys_original) + data_modified = PowerFlowData(ACPowerFlow{ACSolver}(), sys_original) modify_rts_powerflow!(data_modified) # update_system! with unmodified PowerFlowData should result in system that yields unmodified PowerFlowData # (NOTE does NOT necessarily yield original system due to power redistribution) sys_null_updated = deepcopy(sys_original) PF.update_system!(sys_null_updated, data_original) - data_null_updated = PowerFlowData(ACPowerFlow(), sys_null_updated) + data_null_updated = PowerFlowData(ACPowerFlow{ACSolver}(), sys_null_updated) @test IS.compare_values(powerflow_match_fn, data_null_updated, data_original) # Modified versions should not be the same as unmodified versions @@ -47,7 +49,7 @@ end # Constructing PowerFlowData from modified system should result in data_modified @test IS.compare_values( powerflow_match_fn, - PowerFlowData(ACPowerFlow(), sys_modified), + PowerFlowData(ACPowerFlow{ACSolver}(), sys_modified), data_modified, ) @@ -56,6 +58,9 @@ end sys_modify_updated = deepcopy(sys_original) PF.update_system!(sys_modify_updated, data_modified) sys_mod_redist = deepcopy(sys_modified) - PF.update_system!(sys_mod_redist, PowerFlowData(ACPowerFlow(), sys_mod_redist)) + PF.update_system!( + sys_mod_redist, + PowerFlowData(ACPowerFlow{ACSolver}(), sys_mod_redist), + ) @test IS.compare_values(powerflow_match_fn, sys_modify_updated, sys_mod_redist) end diff --git a/test/test_psse_export.jl b/test/test_psse_export.jl index d6abd32d..802eee3f 100644 --- a/test/test_psse_export.jl +++ b/test/test_psse_export.jl @@ -222,9 +222,14 @@ function compare_systems_loosely(sys1::PSY.System, sys2::PSY.System; return result end -function test_power_flow(sys1::System, sys2::System; exclude_reactive_flow = false) - result1 = solve_powerflow(ACPowerFlow(), sys1) - result2 = solve_powerflow(ACPowerFlow(), sys2) +function test_power_flow( + pf::ACPowerFlow{<:ACPowerFlowSolverType}, + sys1::System, + sys2::System; + exclude_reactive_flow = false, +) + result1 = solve_powerflow(pf, sys1) + result2 = solve_powerflow(pf, sys2) reactive_power_tol = exclude_reactive_flow ? nothing : POWERFLOW_COMPARISON_TOLERANCE @test compare_df_within_tolerance("bus_results", result1["bus_results"], @@ -246,6 +251,7 @@ read_system_and_metadata(export_subdir) = read_system_and_metadata( get_psse_export_paths(export_subdir)...) function test_psse_round_trip( + pf::ACPowerFlow{<:ACPowerFlowSolverType}, sys::System, exporter::PSSEExporter, scenario_name::AbstractString, @@ -265,7 +271,7 @@ function test_psse_round_trip( sys2, sys2_metadata = read_system_and_metadata(raw_path, metadata_path) @test compare_systems_loosely(sys, sys2) do_power_flow_test && - test_power_flow(sys, sys2; exclude_reactive_flow = exclude_reactive_flow) + test_power_flow(pf, sys, sys2; exclude_reactive_flow = exclude_reactive_flow) end "Test that the two raw files are exactly identical and the two metadata files parse to identical JSON" @@ -318,17 +324,21 @@ end @test compare_systems_loosely(sys, deepcopy(sys)) end -@testset "PSSE Exporter with system_240[32].json, v33" begin +@testset "PSSE Exporter with system_240[32].json, v33" for (ACSolver, folder_name) in ( + (NLSolveACPowerFlow, "system_240_NLSolve"), + (KLUACPowerFlow, "system_240_KLU"), +) sys = load_test_system() + pf = ACPowerFlow{ACSolver}() isnothing(sys) && return # PSS/E version must be one of the supported ones @test_throws ArgumentError PSSEExporter(sys, :vNonexistent, test_psse_export_dir) # Reimported export should be comparable to original system - export_location = joinpath(test_psse_export_dir, "v33", "system_240") + export_location = joinpath(test_psse_export_dir, "v33", folder_name) exporter = PSSEExporter(sys, :v33, export_location) - test_psse_round_trip(sys, exporter, "basic", export_location; + test_psse_round_trip(pf, sys, exporter, "basic", export_location; exclude_reactive_flow = true) # TODO why is reactive flow not matching? # Exporting the exact same thing again should result in the exact same files @@ -360,20 +370,24 @@ end @test_logs((:error, r"values do not match"), match_mode = :any, min_level = Logging.Error, compare_systems_loosely(sys, reread_sys2)) - test_power_flow(sys2, reread_sys2; exclude_reactive_flow = true) # TODO why is reactive flow not matching? + test_power_flow(pf, sys2, reread_sys2; exclude_reactive_flow = true) # TODO why is reactive flow not matching? end -@testset "PSSE Exporter with RTS_GMLC_DA_sys, v33" begin +@testset "PSSE Exporter with RTS_GMLC_DA_sys, v33" for (ACSolver, folder_name) in ( + (NLSolveACPowerFlow, "rts_gmlc_NLSolve"), + (KLUACPowerFlow, "rts_gmlc_KLU"), +) sys = create_pf_friendly_rts_gmlc() + pf = ACPowerFlow{ACSolver}() set_units_base_system!(sys, UnitSystem.SYSTEM_BASE) # PSS/E version must be one of the supported ones @test_throws ArgumentError PSSEExporter(sys, :vNonexistent, test_psse_export_dir) # Reimported export should be comparable to original system - export_location = joinpath(test_psse_export_dir, "v33", "rts_gmlc") + export_location = joinpath(test_psse_export_dir, "v33", folder_name) exporter = PSSEExporter(sys, :v33, export_location) - test_psse_round_trip(sys, exporter, "basic", export_location; + test_psse_round_trip(pf, sys, exporter, "basic", export_location; exclude_reactive_flow = true) # TODO why is reactive flow not matching? # Exporting the exact same thing again should result in the exact same files @@ -404,11 +418,11 @@ end @test_logs((:error, r"values do not match"), match_mode = :any, min_level = Logging.Error, compare_systems_loosely(sys, reread_sys2)) - test_power_flow(sys2, reread_sys2; exclude_reactive_flow = true) # TODO why is reactive flow not matching? + test_power_flow(pf, sys2, reread_sys2; exclude_reactive_flow = true) # TODO why is reactive flow not matching? # Updating with changed value should result in a different reimport (PowerFlowData version) exporter = PSSEExporter(sys, :v33, export_location) - pf2 = PowerFlowData(ACPowerFlow(), sys) + pf2 = PowerFlowData(pf, sys) # This modifies the PowerFlowData in the same way that modify_rts_system! modifies the # system, so the reimport should be comparable to sys2 from above modify_rts_powerflow!(pf2) @@ -421,11 +435,11 @@ end @test_logs((:error, r"values do not match"), match_mode = :any, min_level = Logging.Error, compare_systems_loosely(sys, reread_sys3)) - test_power_flow(sys2, reread_sys3; exclude_reactive_flow = true) # TODO why is reactive flow not matching? + test_power_flow(pf, sys2, reread_sys3; exclude_reactive_flow = true) # TODO why is reactive flow not matching? # Exporting with write_comments should be comparable to original system exporter = PSSEExporter(sys, :v33, export_location; write_comments = true) - test_psse_round_trip(sys, exporter, "basic6", export_location; + test_psse_round_trip(pf, sys, exporter, "basic6", export_location; exclude_reactive_flow = true) # TODO why is reactive flow not matching? end