Skip to content
Open
3 changes: 3 additions & 0 deletions src/PowerSystems.jl
Original file line number Diff line number Diff line change
Expand Up @@ -488,6 +488,9 @@ export get_time_series_array
export get_time_series_resolutions
export supports_time_series
export supports_supplemental_attributes
export supports_active_power
export supports_reactive_power
export supports_voltage_control
export get_time_series_timestamps
export get_time_series_values
export get_time_series_counts
Expand Down
5 changes: 5 additions & 0 deletions src/definitions.jl
Original file line number Diff line number Diff line change
Expand Up @@ -579,6 +579,11 @@ const PARSER_TAP_RATIO_CORRECTION_TOL = 1e-5

const ZERO_IMPEDANCE_REACTANCE_THRESHOLD = 1e-4

# Absolute threshold below which a shunt admittance component (conductance or
# susceptance) is treated as zero for capability detection, so negligible
# admittances do not force their host bus to be kept during network reduction.
const ZERO_ADMITTANCE_THRESHOLD = 1e-4

const WINDING_NAMES = Dict(
WindingCategory.PRIMARY_WINDING => "primary",
WindingCategory.SECONDARY_WINDING => "secondary",
Expand Down
15 changes: 15 additions & 0 deletions src/models/static_models.jl
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,21 @@ function get_services(device::Device)
return Vector{Service}()
end

"""
Return `true` if the device has active power as a controllable parameter.
"""
supports_active_power(::StaticInjection) = true

"""
Return `true` if the device has reactive power as a controllable parameter.
"""
supports_reactive_power(::StaticInjection) = true

"""
Return `true` if the device can control voltage at its connected bus.
"""
supports_voltage_control(::StaticInjection) = false

get_dynamic_injector(d::StaticInjection) = nothing

function get_frequency_droop(static_injector::StaticInjection)
Expand Down
62 changes: 62 additions & 0 deletions src/models/supplemental_accessors.jl
Original file line number Diff line number Diff line change
Expand Up @@ -380,3 +380,65 @@ end
function supports_services(::AreaInterchange)
return true
end

# supports_active_power overrides for types without controllable active power
supports_active_power(::SynchronousCondenser) = false

# supports_reactive_power overrides for types without controllable reactive power
supports_reactive_power(::InterconnectingConverter) = false

# A shunt-admittance component counts as power support only above an absolute
# threshold, so negligible admittances do not force their host bus to be kept.
_nonzero_admittance(x::Real) = abs(x) > ZERO_ADMITTANCE_THRESHOLD

# FixedAdmittance / SwitchedAdmittance support active power via conductance
# (real(Y)) and reactive power via susceptance (imag(Y)), so capability is
# parameter-dependent rather than a fixed type property.
supports_active_power(d::FixedAdmittance) = _nonzero_admittance(real(get_Y(d)))
supports_reactive_power(d::FixedAdmittance) = _nonzero_admittance(imag(get_Y(d)))

# SwitchedAdmittance can also shift admittance via per-block switchable steps, so
# capability includes the base Y and any block with steps and an above-threshold
# increment in the relevant component.
function supports_active_power(d::SwitchedAdmittance)
_nonzero_admittance(real(get_Y(d))) && return true
return any(
n > 0 && _nonzero_admittance(real(yi))
for (n, yi) in zip(get_number_of_steps(d), get_Y_increase(d))
)
end

function supports_reactive_power(d::SwitchedAdmittance)
_nonzero_admittance(imag(get_Y(d))) && return true
return any(
n > 0 && _nonzero_admittance(imag(yi))
for (n, yi) in zip(get_number_of_steps(d), get_Y_increase(d))
)
end

# FACTSControlDevice reactive power and voltage control depend on control_mode.
# control_mode is nothing for uninitialized devices (e.g. FACTSControlDevice(nothing)).
_facts_is_active(d::FACTSControlDevice) =
(mode = get_control_mode(d); !isnothing(mode) && mode != FACTSOperationModes.OOS)

# In NML mode both Series and Shunt links operate, enabling active power control.
# In BYP mode the Series link is bypassed and the Shunt acts as a STATCOM (reactive only).
function supports_active_power(d::FACTSControlDevice)
mode = get_control_mode(d)
return !isnothing(mode) && mode == FACTSOperationModes.NML
end

supports_reactive_power(d::FACTSControlDevice) = _facts_is_active(d)

# supports_voltage_control overrides for types that can control voltage
supports_voltage_control(::Generator) = true
supports_voltage_control(::Source) = true
supports_voltage_control(::Storage) = true
supports_voltage_control(::StaticInjectionSubsystem) = true

supports_voltage_control(d::FACTSControlDevice) = _facts_is_active(d)

function supports_voltage_control(d::SynchronousCondenser)
bustype = get_bustype(get_bus(d))
return bustype ∈ (ACBusTypes.PV, ACBusTypes.REF, ACBusTypes.SLACK)
end
158 changes: 158 additions & 0 deletions test/test_devices.jl
Original file line number Diff line number Diff line change
Expand Up @@ -88,3 +88,161 @@ end
get_component(ShiftablePowerLoad, sys2, "ShiftableLoadBus4"),
).max == 0.10
end

@testset "Test static injection traits" begin
# supports_active_power
@test supports_active_power(ThermalStandard(nothing)) == true
@test supports_active_power(ThermalMultiStart(nothing)) == true
@test supports_active_power(RenewableDispatch(nothing)) == true
@test supports_active_power(RenewableNonDispatch(nothing)) == true
@test supports_active_power(HydroDispatch(nothing)) == true
@test supports_active_power(HydroTurbine(nothing)) == true
@test supports_active_power(HydroPumpTurbine(nothing)) == true
@test supports_active_power(Source(nothing)) == true
@test supports_active_power(InterconnectingConverter(nothing)) == true
@test supports_active_power(EnergyReservoirStorage(nothing)) == true
@test supports_active_power(PowerLoad(nothing)) == true
@test supports_active_power(StandardLoad(nothing)) == true
@test supports_active_power(ExponentialLoad(nothing)) == true
@test supports_active_power(InterruptiblePowerLoad(nothing)) == true
@test supports_active_power(ShiftablePowerLoad(nothing)) == true
@test supports_active_power(HybridSystem(nothing)) == true
@test supports_active_power(SynchronousCondenser(nothing)) == false
@test supports_active_power(FixedAdmittance(nothing)) == false # Y = 0
@test supports_active_power(
FixedAdmittance(; name = "fa_g", available = true, bus = ACBus(nothing),
Y = 1.0 + 0.0im),
) == true
@test supports_active_power(
FixedAdmittance(; name = "fa_b", available = true, bus = ACBus(nothing),
Y = 0.0 + 1.0im),
) == false
# Below ZERO_ADMITTANCE_THRESHOLD (1e-4): treated as no support.
@test supports_active_power(
FixedAdmittance(; name = "fa_tiny", available = true, bus = ACBus(nothing),
Y = 1e-6 + 1e-6im),
) == false
@test supports_active_power(SwitchedAdmittance(nothing)) == false # Y = 0, no blocks
@test supports_active_power(
SwitchedAdmittance(; name = "sa_g", available = true, bus = ACBus(nothing),
Y = 1.0 + 0.0im),
) == true
# Zero base Y, but a switchable block adds real (active) admittance.
@test supports_active_power(
SwitchedAdmittance(; name = "sa_gstep", available = true, bus = ACBus(nothing),
Y = 0.0 + 0.0im, number_of_steps = [2], Y_increase = [1.0 + 0.0im]),
) == true
@test supports_active_power(
SwitchedAdmittance(; name = "sa_bstep", available = true, bus = ACBus(nothing),
Y = 0.0 + 0.0im, number_of_steps = [2], Y_increase = [0.0 + 1.0im]),
) == false

# FACTSControlDevice active power depends on control_mode (true only for NML)
@test supports_active_power(FACTSControlDevice(nothing)) == false
facts_nml = FACTSControlDevice(nothing)
set_control_mode!(facts_nml, FACTSOperationModes.NML)
@test supports_active_power(facts_nml) == true
facts_byp = FACTSControlDevice(nothing)
set_control_mode!(facts_byp, FACTSOperationModes.BYP)
@test supports_active_power(facts_byp) == false
facts_oos = FACTSControlDevice(nothing)
set_control_mode!(facts_oos, FACTSOperationModes.OOS)
@test supports_active_power(facts_oos) == false

# supports_reactive_power
@test supports_reactive_power(ThermalStandard(nothing)) == true
@test supports_reactive_power(RenewableDispatch(nothing)) == true
@test supports_reactive_power(Source(nothing)) == true
@test supports_reactive_power(SynchronousCondenser(nothing)) == true
@test supports_reactive_power(PowerLoad(nothing)) == true
@test supports_reactive_power(EnergyReservoirStorage(nothing)) == true
@test supports_reactive_power(HybridSystem(nothing)) == true
@test supports_reactive_power(InterconnectingConverter(nothing)) == false
@test supports_reactive_power(FixedAdmittance(nothing)) == false # Y = 0
@test supports_reactive_power(
FixedAdmittance(; name = "fa_b2", available = true, bus = ACBus(nothing),
Y = 0.0 + 1.0im),
) == true
@test supports_reactive_power(
FixedAdmittance(; name = "fa_g2", available = true, bus = ACBus(nothing),
Y = 1.0 + 0.0im),
) == false
@test supports_reactive_power(
FixedAdmittance(; name = "fa_tiny2", available = true, bus = ACBus(nothing),
Y = 1e-6 + 1e-6im),
) == false
@test supports_reactive_power(SwitchedAdmittance(nothing)) == false # Y = 0, no blocks
@test supports_reactive_power(
SwitchedAdmittance(; name = "sa_b", available = true, bus = ACBus(nothing),
Y = 0.0 + 1.0im),
) == true
# Zero base Y, but a switchable block adds susceptance (reactive).
@test supports_reactive_power(
SwitchedAdmittance(; name = "sa_bstep2", available = true, bus = ACBus(nothing),
Y = 0.0 + 0.0im, number_of_steps = [2], Y_increase = [0.0 + 1.0im]),
) == true
# A block with steps but a below-threshold increment does not count.
@test supports_reactive_power(
SwitchedAdmittance(; name = "sa_tinystep", available = true, bus = ACBus(nothing),
Y = 0.0 + 0.0im, number_of_steps = [2], Y_increase = [0.0 + 1e-6im]),
) == false

# FACTSControlDevice reactive power depends on control_mode
@test supports_reactive_power(FACTSControlDevice(nothing)) == false
facts_nml = FACTSControlDevice(nothing)
set_control_mode!(facts_nml, FACTSOperationModes.NML)
@test supports_reactive_power(facts_nml) == true
facts_byp = FACTSControlDevice(nothing)
set_control_mode!(facts_byp, FACTSOperationModes.BYP)
@test supports_reactive_power(facts_byp) == true
facts_oos = FACTSControlDevice(nothing)
set_control_mode!(facts_oos, FACTSOperationModes.OOS)
@test supports_reactive_power(facts_oos) == false

# supports_voltage_control
@test supports_voltage_control(ThermalStandard(nothing)) == true
@test supports_voltage_control(ThermalMultiStart(nothing)) == true
@test supports_voltage_control(RenewableDispatch(nothing)) == true
@test supports_voltage_control(RenewableNonDispatch(nothing)) == true
@test supports_voltage_control(HydroDispatch(nothing)) == true
@test supports_voltage_control(Source(nothing)) == true
@test supports_voltage_control(EnergyReservoirStorage(nothing)) == true
@test supports_voltage_control(HybridSystem(nothing)) == true
@test supports_voltage_control(PowerLoad(nothing)) == false
@test supports_voltage_control(StandardLoad(nothing)) == false
@test supports_voltage_control(ExponentialLoad(nothing)) == false
@test supports_voltage_control(InterruptiblePowerLoad(nothing)) == false
@test supports_voltage_control(ShiftablePowerLoad(nothing)) == false
@test supports_voltage_control(InterconnectingConverter(nothing)) == false
@test supports_voltage_control(FixedAdmittance(nothing)) == false
@test supports_voltage_control(SwitchedAdmittance(nothing)) == false

# FACTSControlDevice voltage control depends on control_mode
@test supports_voltage_control(FACTSControlDevice(nothing)) == false
@test supports_voltage_control(facts_nml) == true
@test supports_voltage_control(facts_byp) == true
@test supports_voltage_control(facts_oos) == false

# SynchronousCondenser voltage control depends on bus type
sc_pv = SynchronousCondenser(nothing)
sc_pv.bus = ACBus(;
number = 1, name = "pv_bus", available = true, bustype = ACBusTypes.PV,
angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.9, max = 1.1),
base_voltage = 230.0,
)
@test supports_voltage_control(sc_pv) == true
sc_ref = SynchronousCondenser(nothing)
sc_ref.bus = ACBus(;
number = 2, name = "ref_bus", available = true, bustype = ACBusTypes.REF,
angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.9, max = 1.1),
base_voltage = 230.0,
)
@test supports_voltage_control(sc_ref) == true
sc_pq = SynchronousCondenser(nothing)
sc_pq.bus = ACBus(;
number = 2, name = "pq_bus", available = true, bustype = ACBusTypes.PQ,
angle = 0.0, magnitude = 1.0, voltage_limits = (min = 0.9, max = 1.1),
base_voltage = 230.0,
)
@test supports_voltage_control(sc_pq) == false
end
Loading