Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Merge branch 'hrgks/psse_exporter_psy4' into feature/nr_pf2
Browse files Browse the repository at this point in the history
GabrielKS committed Jan 16, 2025
2 parents 2f90d82 + 4be46d8 commit f0b1566
Showing 5 changed files with 95 additions and 54 deletions.
34 changes: 33 additions & 1 deletion src/common.jl
Original file line number Diff line number Diff line change
@@ -14,6 +14,38 @@ function get_total_q(l::PSY.StandardLoad)
PSY.get_impedance_reactive_power(l)
end

"""
Return the reactive power limits that should be used in power flow calculations and PSS/E
exports. Redirects to `PSY.get_reactive_power_limits` in all but special cases.
"""
get_reactive_power_limits_for_power_flow(gen::PSY.Device) =
PSY.get_reactive_power_limits(gen)

function get_reactive_power_limits_for_power_flow(gen::PSY.RenewableNonDispatch)
val = PSY.get_reactive_power(gen)
return (min = val, max = val)
end

"""
Return the active power limits that should be used in power flow calculations and PSS/E
exports. Redirects to `PSY.get_active_power_limits` in all but special cases.
"""
get_active_power_limits_for_power_flow(gen::PSY.Device) = PSY.get_active_power_limits(gen)

get_active_power_limits_for_power_flow(::PSY.Source) = (min = -Inf, max = Inf)

function get_active_power_limits_for_power_flow(gen::PSY.RenewableNonDispatch)
val = PSY.get_active_power(gen)
return (min = val, max = val)
end

get_active_power_limits_for_power_flow(gen::PSY.RenewableDispatch) =
(min = 0.0, max = PSY.get_rating(gen))

# TODO verify whether this is the correct behavior for Storage, (a) for redistribution and (b) for exporting
get_active_power_limits_for_power_flow(gen::PSY.Storage) =
(min = 0.0, max = PSY.get_output_active_power_limits(gen).max)

function _get_injections!(
bus_activepower_injection::Vector{Float64},
bus_reactivepower_injection::Vector{Float64},
@@ -58,7 +90,7 @@ function _get_reactive_power_bound!(
!PSY.get_available(source) && continue
bus = PSY.get_bus(source)
bus_ix = bus_lookup[PSY.get_number(bus)]
reactive_power_limits = PSY.get_reactive_power_limits(source)
reactive_power_limits = get_reactive_power_limits_for_power_flow(source)
if reactive_power_limits !== nothing
bus_reactivepower_bounds[bus_ix][1] += min(0, reactive_power_limits.min)
bus_reactivepower_bounds[bus_ix][2] += max(0, reactive_power_limits.max)
30 changes: 9 additions & 21 deletions src/post_processing.jl
Original file line number Diff line number Diff line change
@@ -158,18 +158,6 @@ function _get_fixed_admittance_power(
return active_power, reactive_power
end

function _get_limits_for_power_distribution(gen::PSY.StaticInjection)
return PSY.get_active_power_limits(gen)
end

function _get_limits_for_power_distribution(gen::PSY.RenewableDispatch)
return (min = 0.0, max = PSY.get_max_active_power(gen))
end

function _get_limits_for_power_distribution(gen::PSY.Storage)
return (min = 0.0, max = PSY.get_output_active_power_limits(gen).max)
end

function _power_redistribution_ref(
sys::PSY.System,
P_gen::Float64,
@@ -204,16 +192,16 @@ function _power_redistribution_ref(
return
elseif length(devices_) > 1
devices =
sort(collect(devices_); by = x -> _get_limits_for_power_distribution(x).max)
sort(collect(devices_); by = x -> get_active_power_limits_for_power_flow(x).max)
else
error("No devices in bus $(PSY.get_name(bus))")
end

sum_basepower = sum([g.max for g in _get_limits_for_power_distribution.(devices)])
sum_basepower = sum([g.max for g in get_active_power_limits_for_power_flow.(devices)])
p_residual = P_gen
units_at_limit = Vector{Int}()
for (ix, d) in enumerate(devices)
p_limits = _get_limits_for_power_distribution(d)
p_limits = get_active_power_limits_for_power_flow(d)
part_factor = p_limits.max / sum_basepower
p_frac = P_gen * part_factor
p_set_point = clamp(p_frac, p_limits.min, p_limits.max)
@@ -229,7 +217,7 @@ function _power_redistribution_ref(
if !isapprox(p_residual, 0.0; atol = ISAPPROX_ZERO_TOLERANCE)
@debug "Ref Bus voltage residual $p_residual"
removed_power = sum([
g.max for g in _get_limits_for_power_distribution.(devices[units_at_limit])
g.max for g in get_active_power_limits_for_power_flow.(devices[units_at_limit])
])
reallocated_p = 0.0
it = 0
@@ -240,7 +228,7 @@ function _power_redistribution_ref(
end
for (ix, d) in enumerate(devices)
ix units_at_limit && continue
p_limits = PSY.get_active_power_limits(d)
p_limits = get_active_power_limits_for_power_flow(d)
part_factor = p_limits.max / (sum_basepower - removed_power)
p_frac = p_residual * part_factor
current_p = PSY.get_active_power(d)
@@ -270,7 +258,7 @@ function _power_redistribution_ref(
@debug "Remaining residual $q_residual, $(PSY.get_name(bus))"
p_set_point = PSY.get_active_power(device) + p_residual
PSY.set_active_power!(device, p_set_point)
p_limits = PSY.get_reactive_power_limits(device)
p_limits = get_reactive_power_limits_for_power_flow(device) # TODO should this be active_power_limits? It was reactive in the existing codebase
if (p_set_point >= p_limits.max + BOUNDS_TOLERANCE) ||
(p_set_point <= p_limits.min - BOUNDS_TOLERANCE)
@error "Unit $(PSY.get_name(device)) P=$(p_set_point) above limits. P_max = $(p_limits.max) P_min = $(p_limits.min)"
@@ -332,7 +320,7 @@ function _reactive_power_redistribution_pv(
units_at_limit = Vector{Int}()

for (ix, d) in enumerate(devices)
q_limits = PSY.get_reactive_power_limits(d)
q_limits = get_reactive_power_limits_for_power_flow(d)
if isapprox(q_limits.max, 0.0; atol = BOUNDS_TOLERANCE) &&
isapprox(q_limits.min, 0.0; atol = BOUNDS_TOLERANCE)
push!(units_at_limit, ix)
@@ -377,7 +365,7 @@ function _reactive_power_redistribution_pv(
reallocated_q = 0.0
for (ix, d) in enumerate(devices)
ix units_at_limit && continue
q_limits = PSY.get_reactive_power_limits(d)
q_limits = get_reactive_power_limits_for_power_flow(d)

if removed_power < total_active_power
fraction =
@@ -426,7 +414,7 @@ function _reactive_power_redistribution_pv(
@debug "Remaining residual $q_residual, $(PSY.get_name(bus))"
q_set_point = PSY.get_reactive_power(device) + q_residual
PSY.set_reactive_power!(device, q_set_point)
q_limits = PSY.get_reactive_power_limits(device)
q_limits = get_reactive_power_limits_for_power_flow(device)
if (q_set_point >= q_limits.max + BOUNDS_TOLERANCE) ||
(q_set_point <= q_limits.min - BOUNDS_TOLERANCE)
@error "Unit $(PSY.get_name(device)) Q=$(q_set_point) above limits. Q_max = $(q_limits.max) Q_min = $(q_limits.min)"
18 changes: 10 additions & 8 deletions src/powerflow_types.jl
Original file line number Diff line number Diff line change
@@ -4,17 +4,19 @@ abstract type ACPowerFlowSolverType end
struct KLUACPowerFlow <: ACPowerFlowSolverType end
struct NLSolveACPowerFlow <: ACPowerFlowSolverType end

Base.@kwdef struct ACPowerFlow{ACSolver <: ACPowerFlowSolverType} <:
PowerFlowEvaluationModel
check_reactive_power_limits::Bool = false
struct ACPowerFlow{ACSolver <: ACPowerFlowSolverType} <: PowerFlowEvaluationModel
check_reactive_power_limits::Bool
end

# Create a constructor for ACPowerFlow that defaults to KLUACPowerFlow
function ACPowerFlow(ACSolver::Type{<:ACPowerFlowSolverType} = KLUACPowerFlow;
ACPowerFlow{ACSolver}(;
check_reactive_power_limits::Bool = false,
)
return ACPowerFlow{ACSolver}(check_reactive_power_limits)
end
) where {ACSolver <: ACPowerFlowSolverType} =
ACPowerFlow{ACSolver}(check_reactive_power_limits)

ACPowerFlow(
ACSolver::Type{<:ACPowerFlowSolverType} = KLUACPowerFlow;
check_reactive_power_limits::Bool = false,
) = ACPowerFlow{ACSolver}(check_reactive_power_limits)

struct DCPowerFlow <: PowerFlowEvaluationModel end
struct PTDFDCPowerFlow <: PowerFlowEvaluationModel end
61 changes: 43 additions & 18 deletions src/psse_export.jl
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
const PSSE_EXPORT_SUPPORTED_VERSIONS = [:v33] # TODO add :v34
const PSSE_EXPORT_SUPPORTED_VERSIONS = [:v33]
const PSSE_DEFAULT = "" # Used below in cases where we want to insert an empty field to signify the PSSE default
const PSSE_BUS_TYPE_MAP = Dict(
PSY.ACBusTypes.PQ => 1,
@@ -133,14 +133,14 @@ function reset_caches(exporter::PSSEExporter)
# We do not clear the md_buffer here, but !md_valid implies that its contents are not valid
end

# TODO solidify the notion of sameness we care about here
"""
Update the `PSSEExporter` with new `data`.
# Arguments:
- `exporter::PSSEExporter`: the exporter to update
- `data::PSY.System`: system containing the new data. Must be fundamentally the same
`System` as the one with which the exporter was constructed, just with different values
`System` as the one with which the exporter was constructed, just with different values —
this is the user's responsibility, we do not exhaustively verify it.
"""
function update_exporter!(exporter::PSSEExporter, data::PSY.System)
_validate_same_system(exporter.system, data) || throw(
@@ -243,6 +243,20 @@ function _permissive_parse_int(x)
return Int64(n)
end

"""
If `val` is empty, returns `T()`; if not, asserts that `val isa T` and returns `val`. Has nice type checker semantics.
# Examples
```julia
convert_empty(Vector{String}, []) # -> String[]
convert_empty(Vector{String}, ["a"]) # -> ["a"]
convert_empty(Vector{String}, [2]) # -> TypeError: in typeassert, expected Vector{String}, got a value of type Vector{Int64}
Base.return_types(Base.Fix1(convert_empty, Vector{String})) # -> [Vector{String}]
```
"""
convert_empty(::Type{T}, val) where {T} = isempty(val) ? T() : val::T
convert_empty_stringvec = Base.Fix1(convert_empty, Vector{String})

# PERF could be improved by appending to the buffer rather than doing string interpolation, seems unnecessary
_psse_quote_string(s::String) = "'$s'"

@@ -307,7 +321,6 @@ function write_to_buffers!(
exporter.write_comments && (BASFRQ = "$BASFRQ / $md_string")

# PERF we use manually unrolled loops because the vector/tuple allocation was a performance issue
# TODO this could almost certaintly be done more elegantly using a macro
fastprintdelim(io, IC)
fastprintdelim(io, SBASE)
fastprintdelim(io, REV)
@@ -427,7 +440,11 @@ function write_to_buffers!(
if !exporter.md_valid
md["bus_number_mapping"] = _psse_bus_numbers(old_bus_numbers)
md["bus_name_mapping"] =
_psse_bus_names(PSY.get_name.(buses), old_bus_numbers, md["bus_number_mapping"])
_psse_bus_names(
convert_empty_stringvec(PSY.get_name.(buses)),
old_bus_numbers,
md["bus_number_mapping"],
)
end
bus_number_mapping = md["bus_number_mapping"]
bus_name_mapping = md["bus_name_mapping"]
@@ -583,7 +600,7 @@ function write_to_buffers!(
end
load_name_mapping = get!(exporter.components_cache, "load_name_mapping") do
create_component_ids(
PSY.get_name.(loads),
convert_empty_stringvec(PSY.get_name.(loads)),
PSY.get_number.(PSY.get_bus.(loads));
singles_to_1 = true,
)
@@ -639,7 +656,7 @@ function write_to_buffers!(
end
shunt_name_mapping = get!(exporter.components_cache, "shunt_name_mapping") do
create_component_ids(
PSY.get_name.(shunts),
convert_empty_stringvec(PSY.get_name.(shunts)),
PSY.get_number.(PSY.get_bus.(shunts));
singles_to_1 = true,
)
@@ -694,7 +711,7 @@ function write_to_buffers!(
end
generator_name_mapping = get!(exporter.components_cache, "generator_name_mapping") do
create_component_ids(
PSY.get_name.(generators),
convert_empty_stringvec(PSY.get_name.(generators)),
PSY.get_number.(PSY.get_bus.(generators));
singles_to_1 = false,
)
@@ -711,10 +728,9 @@ function write_to_buffers!(
PSY.get_active_power(generator) * PSY.get_base_power(exporter.system),
PSY.get_reactive_power(generator) * PSY.get_base_power(exporter.system)
end
# TODO approximate a QT for generators that don't have it set
# (this is needed to run power flows also)
# TODO maybe have a better default here
reactive_power_limits = with_units_base(
() -> PSY.get_reactive_power_limits(generator),
() -> get_reactive_power_limits_for_power_flow(generator),
exporter.system,
PSY.UnitSystem.NATURAL_UNITS,
)
@@ -733,12 +749,14 @@ function write_to_buffers!(
# TODO maybe have a better default here
active_power_limits =
with_units_base(
() -> PSY.get_active_power_limits(generator),
() -> get_active_power_limits_for_power_flow(generator),
exporter.system,
PSY.UnitSystem.NATURAL_UNITS,
)
PT = active_power_limits.max
isfinite(PT) || (PT = PSSE_DEFAULT)
PB = active_power_limits.min
isfinite(PB) || (PB = PSSE_DEFAULT)
WMOD = get(PSY.get_ext(generator), "WMOD", PSSE_DEFAULT)
WPF = get(PSY.get_ext(generator), "WPF", PSSE_DEFAULT)

@@ -790,7 +808,7 @@ function write_to_buffers!(
end
branch_name_mapping = get!(exporter.components_cache, "branch_name_mapping") do
create_component_ids(
PSY.get_name.(first.(branches_with_numbers)),
convert_empty_stringvec(PSY.get_name.(first.(branches_with_numbers))),
last.(branches_with_numbers);
singles_to_1 = false,
)
@@ -891,7 +909,6 @@ function _psse_transformer_names(
return mapping
end

# TODO support three-winding transformers
"""
Currently only supports two-winding transformers
@@ -915,14 +932,14 @@ function write_to_buffers!(
end
transformer_ckt_mapping = get!(exporter.components_cache, "transformer_ckt_mapping") do
create_component_ids(
PSY.get_name.(first.(transformers_with_numbers)),
convert_empty_stringvec(PSY.get_name.(first.(transformers_with_numbers))),
last.(transformers_with_numbers);
singles_to_1 = false,
)
end
if !exporter.md_valid
md["transformer_name_mapping"] = _psse_transformer_names(
PSY.get_name.(first.(transformers_with_numbers)),
convert_empty_stringvec(PSY.get_name.(first.(transformers_with_numbers))),
last.(transformers_with_numbers),
md["bus_number_mapping"],
transformer_ckt_mapping,
@@ -1124,11 +1141,19 @@ function write_export(
# These mappings are accessed in e.g. _write_bus_data via the metadata
md["area_mapping"] = _map_psse_container_names(
sort!(
collect(PSY.get_name.(PSY.get_components(PSY.Area, exporter.system)))),
collect(
convert_empty_stringvec(
PSY.get_name.(PSY.get_components(PSY.Area, exporter.system)),
),
)),
)
md["zone_number_mapping"] = _map_psse_container_names(
sort!(
collect(PSY.get_name.(PSY.get_components(PSY.LoadZone, exporter.system)))),
collect(
convert_empty_stringvec(
PSY.get_name.(PSY.get_components(PSY.LoadZone, exporter.system)),
),
)),
)
md["record_groups"] = OrderedDict{String, Bool}() # Keep track of which record groups we actually write to and which we skip
end
6 changes: 0 additions & 6 deletions test/test_utils/common.jl
Original file line number Diff line number Diff line change
@@ -8,12 +8,6 @@ powerflow_match_fn(
isapprox(a, b; atol = POWERFLOW_COMPARISON_TOLERANCE) || IS.isequivalent(a, b)
powerflow_match_fn(a, b) = IS.isequivalent(a, b)

# TODO temporary hacks, see https://github.com/NREL-Sienna/PowerFlows.jl/issues/39
PowerSystems.get_reactive_power_limits(::RenewableNonDispatch) = (min = 0.0, max = 0.0)
PowerSystems.get_active_power_limits(
::Union{RenewableDispatch, RenewableNonDispatch, Source},
) = (min = 0.0, max = 0.0)

# TODO another temporary hack
"Create a version of the RTS_GMLC system that plays nice with the current implementation of AC power flow"
function create_pf_friendly_rts_gmlc()

0 comments on commit f0b1566

Please sign in to comment.