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
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Hmm. Is differentiating between zero and nonzero admittance sufficiently important to merit this? Returning the general answer for the component type is more compiler-friendly


# 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