Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions src/simulation/simulation_models.jl
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,15 @@ function determine_horizons!(models::SimulationModels)
return horizons
end

# A forecast with a single window has no interval: InfrastructureSystems records it as an empty
# period, because there is no next window to be an interval away. The one window covers the
# forecast horizon, so that is the step a model on it advances by. Whether the simulation asks
# for more steps than the windows it has is checked against the initial times at build.
function _single_window_interval(system::PSY.System)
length(PSY.get_forecast_initial_times(system)) == 1 || return Dates.Millisecond(0)
return IS.time_period_conversion(PSY.get_forecast_horizon(system))
end

function determine_intervals(models::SimulationModels)
intervals = OrderedDict{Symbol, Dates.Millisecond}()
for model in models.decision_models
Expand All @@ -125,6 +134,9 @@ function determine_intervals(models::SimulationModels)
else
system = get_system(model)
interval = PSY.get_forecast_interval(system)
if interval == Dates.Millisecond(0)
interval = _single_window_interval(system)
end
end
if interval == Dates.Millisecond(0)
throw(IS.InvalidValue("Model $(get_name(model)) interval not set correctly"))
Expand Down
66 changes: 66 additions & 0 deletions test/test_simulation_build.jl
Original file line number Diff line number Diff line change
Expand Up @@ -393,3 +393,69 @@ end
@test !haskey(root, "systems")
end
end

"""One bus, one thermal unit and one load with 24 h of hourly load data, transformed to a
single 24 h forecast window. InfrastructureSystems records the interval of a one-window
forecast as an empty period."""
function _single_window_system()
sys = PSY.System(100.0)
bus = PSY.ACBus(; number = 1, name = "bus1", available = true,
bustype = PSY.ACBusTypes.REF, angle = 0.0, magnitude = 1.0,
voltage_limits = (min = 0.9, max = 1.1), base_voltage = 230.0,
input_basis = u"CU")
PSY.add_component!(sys, bus)
PSY.add_component!(
sys,
PSY.ThermalStandard(; name = "gen1", available = true,
status = PSY.OperationalStates.ONLINE, bus, active_power = 1.0,
reactive_power = 0.0,
rating = 2.0, active_power_limits = (min = 0.0, max = 2.0),
reactive_power_limits = nothing, ramp_limits = nothing,
operation_cost = PSY.ThermalGenerationCost(;
variable_operation_cost = PSY.CostCurve(PSY.LinearCurve(20.0)), fixed = 0.0,
start_up = 0.0, shut_down = 0.0),
base_power = 100.0, prime_mover_type = PSY.PrimeMovers.CT,
fuel = PSY.ThermalFuels.NATURAL_GAS, input_basis = u"CU"),
)
load = PSY.PowerLoad(; name = "load1", available = true, bus, active_power = 1.0,
reactive_power = 0.0, base_power = 100.0, max_active_power = 1.0,
max_reactive_power = 0.0, input_basis = u"CU")
PSY.add_component!(sys, load)
timestamps =
collect(range(DateTime("2024-01-01T00:00:00"); step = Hour(1), length = 24))
profile = [0.6 + 0.4 * sin(pi * h / 24) for h in 0:23]
PSY.add_time_series!(sys, load,
IS.SingleTimeSeries("max_active_power", TimeSeries.TimeArray(timestamps, profile);
unit_system = IS.CU, units = nothing, quantity_kind = "active_power"))
PSY.transform_single_time_series!(sys, Hour(24), Hour(24))
return sys
end

function _single_window_simulation(; steps = 1, interval = nothing)
template = POM.PowerOperationsProblemTemplate(NetworkModel(CopperPlateNetworkModel))
set_device_model!(template, ThermalStandard, ThermalBasicDispatch)
set_device_model!(template, PowerLoad, StaticPowerLoad)
kwargs = interval === nothing ? (;) : (; interval)
model = DecisionModel(template, _single_window_system(); name = "UC",
optimizer = HiGHS_optimizer, kwargs...)
models = SimulationModels(; decision_models = [model])
sequence = SimulationSequence(; models)
return Simulation(; name = "single_window", steps, models, sequence,
simulation_folder = mktempdir(; cleanup = true))
end

@testset "Simulation from a single forecast window" begin
# A one-step simulation of a one-day model needs only the one window the system has.
sim = _single_window_simulation()
@test build!(sim) == PSI.SimulationBuildStatus.BUILT
@test execute!(sim) == PSI.RunStatus.SUCCESSFULLY_FINALIZED

# A second step would need a second window; say so rather than fail on the interval.
sim = _single_window_simulation(; steps = 2)
@test_throws IS.ConflictingInputsError build!(sim)
end

@testset "Explicit model interval on a single forecast window" begin
# The system has no forecast at a 24 h interval, so the request cannot be honored.
@test_throws IS.ConflictingInputsError _single_window_simulation(; interval = Hour(24))
end
Loading