diff --git a/Artifacts.toml b/Artifacts.toml index 9e97aa2..df7cd04 100644 --- a/Artifacts.toml +++ b/Artifacts.toml @@ -1,10 +1,10 @@ [CaseData] -git-tree-sha1 = "afb608473cf4d5eb22147856de1a1a651f36d40b" +git-tree-sha1 = "790e4a66bbce8b8bafd4fb2fbf9e494545211264" lazy = true [[CaseData.download]] - url = "https://github.com/NREL-Sienna/PowerSystemsTestData/archive/refs/tags/3.1.tar.gz" - sha256 = "4ac6ccd9dc9690b52ad6d0f46eeb759608e04ef8bc871732ff54bdbb0493dcea" + url = "https://github.com/NREL-Sienna/PowerSystemsTestData/archive/refs/tags/3.3.tar.gz" + sha256 = "93f103ea62de8026760368de3f959cd8cd52d8ccd27178c3df873bff6b204af0" [rts] git-tree-sha1 = "5098f357bad765bfefcff58f080818863ca776bd" diff --git a/Project.toml b/Project.toml index d055b1e..f7ae56b 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "PowerSystemCaseBuilder" uuid = "f00506e0-b84f-492a-93c2-c0a9afc4364e" authors = ["Sourabh Dalvi", "Jose Daniel Lara"] -version = "1.3.5" +version = "1.3.11" [deps] CSV = "336ed68f-0bac-5ca0-87d4-7b16caf5d00b" @@ -29,7 +29,7 @@ HDF5 = "0.17" InfrastructureSystems = "2" JSON3 = "1" LazyArtifacts = "1" -PowerSystems = "^4.1.1" +PowerSystems = "^4.5" PrettyTables = "2" Random = "1" SHA = "0.7" diff --git a/src/build_system.jl b/src/build_system.jl index cd3cd01..a60c6ab 100644 --- a/src/build_system.jl +++ b/src/build_system.jl @@ -58,7 +58,10 @@ function _build_system( assign_new_uuids::Bool = false, skip_serialization::Bool = false, ) - if !is_serialized(name, case_args) || force_build + # We skip serialization/de-serialization if sys_args are passed because we currently + # cannot encode information about some of them into file paths + # (such as lambda functions). + if !isempty(sys_args) || !is_serialized(name, case_args) || force_build check_serialized_storage() download_function = get_download_function(sys_descriptor) if !isnothing(download_function) @@ -76,7 +79,7 @@ function _build_system( #construct_time = time() - start serialized_filepath = get_serialized_filepath(name, case_args) start = time() - if !skip_serialization + if !skip_serialization && isempty(sys_args) PSY.to_json(sys, serialized_filepath; force = true) #serialize_time = time() - start serialize_case_parameters(case_args) diff --git a/src/definitions.jl b/src/definitions.jl index 814e540..d1d1b4f 100644 --- a/src/definitions.jl +++ b/src/definitions.jl @@ -1,6 +1,6 @@ const PACKAGE_DIR = joinpath(dirname(dirname(pathof(PowerSystemCaseBuilder)))) const DATA_DIR = - joinpath(LazyArtifacts.artifact"CaseData", "PowerSystemsTestData-3.1") + joinpath(LazyArtifacts.artifact"CaseData", "PowerSystemsTestData-3.3") const RTS_DIR = joinpath(LazyArtifacts.artifact"rts", "RTS-GMLC-0.2.2") diff --git a/src/library/psi_library.jl b/src/library/psi_library.jl index b012129..3f13bf3 100644 --- a/src/library/psi_library.jl +++ b/src/library/psi_library.jl @@ -805,7 +805,7 @@ function build_two_zone_5_bus(; kwargs...) (min = -2.0, max = 2.0), (min = -2.0, max = 2.0), (min = -2.0, max = 2.0), - (l0 = 0.0, l1 = 0.0), + LinearCurve(0.0), ), ] @@ -1361,7 +1361,7 @@ function _duplicate_system(main_sys::PSY.System, twin_sys::PSY.System, HVDC_line active_power_limits_to = (min = -1000.0, max = 1000.0), reactive_power_limits_from = (min = -1000.0, max = 1000.0), reactive_power_limits_to = (min = -1000.0, max = 1000.0), - loss = (l0 = 0.0, l1 = 0.1), + loss = PSY.LinearCurve(0.1), services = Vector{Service}[], ext = Dict{String, Any}(), ) diff --git a/src/library/psid_library.jl b/src/library/psid_library.jl index 3b3d99b..934b6aa 100644 --- a/src/library/psid_library.jl +++ b/src/library/psid_library.jl @@ -62,6 +62,92 @@ function build_3bus_inverter(; raw_data, kwargs...) return sys end +function build_psid_wecc_9_dynamic(; raw_data, kwargs...) + sys_kwargs = filter_kwargs(; kwargs...) + sys = System(raw_data; runchecks = false, sys_kwargs...) + + # Manually change reactance of three branches to match Sauer & Pai (2007) Figure 7.4 + set_x!(get_component(Branch, sys, "Bus 5-Bus 4-i_1"), 0.085) + set_x!(get_component(Branch, sys, "Bus 9-Bus 6-i_1"), 0.17) + set_x!(get_component(Branch, sys, "Bus 7-Bus 8-i_1"), 0.072) + + # Loads from raw file are constant power, consistent with Sauer & Pai (p169) + + ############### Data Dynamic devices ######################## + + # --- Machine models --- + # All parameters are from Sauer & Pai (2007) Table 7.3 M/C columns 1,2,3 + function machine_sauerpai(i) + R = [0.0, 0.0, 0.0] # <-- not specified in Table 7.3 + Xd = [0.146, 0.8958, 1.3125] + Xq = [0.0969, 0.8645, 1.2578] + Xd_p = [0.0608, 0.1198, 0.1813] + Xq_p = [0.0969, 0.1969, 0.25] + Td0_p = [8.96, 6.0, 5.89] + Tq0_p = [0.31, 0.535, 0.6] + return PSY.OneDOneQMachine(; + R = R[i], + Xd = Xd[i], + Xq = Xq[i], + Xd_p = Xd_p[i], + Xq_p = Xq_p[i], + Td0_p = Td0_p[i], + Tq0_p = Tq0_p[i], + ) + end + + # --- Shaft models --- + # All parameters are from Sauer & Pai (2007) + function shaft_sauerpai(i) + D_M = [0.1, 0.2, 0.3] # D/M from bottom of p165 + H = [23.64, 6.4, 3.01] # H from Table 7.3 + D = (2 * D_M .* H) / get_frequency(sys) + return PSY.SingleMass(; + H = H[i], + D = D[i], + ) + end + + # --- AVR models --- + # All parameters are from Sauer & Pai (2007) Table 7.3 exciter columns 1,2,3 + # All S&P exciters are IEEE-Type I (p165) + # NOTE: In S&P, terminal voltage seen by AVR is same as the bus voltage. + # In AVRTypeI, it is a measurement if the bus voltage with a sampling rate. + # Thus, Tr is set to be very small to account for this difference. + avr_typei() = PSY.AVRTypeI(; + Ka = 20, + Ke = 1.0, + Kf = 0.063, + Ta = 0.2, + Te = 0.314, + Tf = 0.35, + Tr = 0.0001, # <-- not specified in Table 7.3 + Va_lim = (-0.5, 0.5), # <-- not specified in Table 7.3 + Ae = 0.0039, + Be = 1.555, + ) + + function dyn_gen_sauerpai(generator) + i = get_number(get_bus(generator)) + return PSY.DynamicGenerator(; + name = PSY.get_name(generator), + ω_ref = 1.0, + machine = machine_sauerpai(i), + shaft = shaft_sauerpai(i), + avr = avr_typei(), + prime_mover = tg_none(), + pss = pss_none(), + ) + end + + for g in get_components(Generator, sys) + case_gen = dyn_gen_sauerpai(g) + add_component!(sys, case_gen, g) + end + + return sys +end + ################################## # Add Load tutorial systems here # ################################## diff --git a/src/library/psitest_library.jl b/src/library/psitest_library.jl index 38e8d90..98fda31 100644 --- a/src/library/psitest_library.jl +++ b/src/library/psitest_library.jl @@ -227,6 +227,152 @@ function build_c_sys5_re(; return c_sys5_re end +function build_c_sys5_re_fuel_cost(; + add_forecasts, + add_single_time_series, + add_reserves, + raw_data, + sys_kwargs..., +) + nodes = nodes5() + c_sys5_re = PSY.System( + 100.0, + nodes, + thermal_generators5(nodes), + renewable_generators5(nodes), + loads5(nodes), + branches5(nodes); + time_series_in_memory = get(sys_kwargs, :time_series_in_memory, true), + sys_kwargs..., + ) + + if add_forecasts + for (ix, l) in enumerate(PSY.get_components(PSY.PowerLoad, c_sys5_re)) + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + for t in 1:2 + ini_time = TimeSeries.timestamp(load_timeseries_DA[t][ix])[1] + forecast_data[ini_time] = load_timeseries_DA[t][ix] + end + PSY.add_time_series!( + c_sys5_re, + l, + PSY.Deterministic("max_active_power", forecast_data), + ) + end + for (ix, r) in enumerate(PSY.get_components(RenewableGen, c_sys5_re)) + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + for t in 1:2 + ini_time = TimeSeries.timestamp(ren_timeseries_DA[t][ix])[1] + forecast_data[ini_time] = ren_timeseries_DA[t][ix] + end + PSY.add_time_series!( + c_sys5_re, + r, + PSY.Deterministic("max_active_power", forecast_data), + ) + end + end + if add_single_time_series + for (ix, l) in enumerate(PSY.get_components(PSY.PowerLoad, c_sys5_re)) + PSY.add_time_series!( + c_sys5_re, + l, + PSY.SingleTimeSeries( + "max_active_power", + vcat(load_timeseries_DA[1][ix], load_timeseries_DA[2][ix]), + ), + ) + end + for (ix, r) in enumerate(PSY.get_components(RenewableGen, c_sys5_re)) + PSY.add_time_series!( + c_sys5_re, + r, + PSY.SingleTimeSeries( + "max_active_power", + vcat(ren_timeseries_DA[1][ix], ren_timeseries_DA[2][ix]), + ), + ) + end + end + if add_reserves + reserve_re = reserve5_re(PSY.get_components(PSY.RenewableDispatch, c_sys5_re)) + PSY.add_service!( + c_sys5_re, + reserve_re[1], + PSY.get_components(PSY.RenewableDispatch, c_sys5_re), + ) + PSY.add_service!( + c_sys5_re, + reserve_re[2], + [collect(PSY.get_components(PSY.RenewableDispatch, c_sys5_re))[end]], + ) + # ORDC + PSY.add_service!( + c_sys5_re, + reserve_re[3], + PSY.get_components(PSY.RenewableDispatch, c_sys5_re), + ) + for (ix, serv) in enumerate(PSY.get_components(PSY.VariableReserve, c_sys5_re)) + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + for t in 1:2 + ini_time = TimeSeries.timestamp(Reserve_ts[t])[1] + forecast_data[ini_time] = Reserve_ts[t] + end + PSY.add_time_series!( + c_sys5_re, + serv, + PSY.Deterministic("requirement", forecast_data), + ) + end + for (ix, serv) in enumerate(PSY.get_components(PSY.ReserveDemandCurve, c_sys5_re)) + PSY.set_variable_cost!( + c_sys5_re, + serv, + ORDC_cost, + ) + end + end + ### Update FuelCost ### + th_solitude = get_component(ThermalStandard, c_sys5_re, "Solitude") + th_brighton = get_component(ThermalStandard, c_sys5_re, "Brighton") + + ### Update Brighton Cost ### + DayAhead = collect( + DateTime("1/1/2024 0:00:00", "d/m/y H:M:S"):Hour(1):DateTime( + "1/1/2024 23:00:00", + "d/m/y H:M:S", + ), + ) + DayAhead2 = DayAhead + Day(1) + + fuel_cost_day1 = Float64.(collect(101:124)) # Expensive Day 1 + fuel_cost_day2 = ones(24) # Cheap Day 2 + + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + forecast_data[DayAhead[1]] = TimeArray(DayAhead, fuel_cost_day1) + forecast_data[DayAhead2[1]] = TimeArray(DayAhead2, fuel_cost_day2) + + operation_cost_brighton = th_brighton.operation_cost + io_curve = + PiecewisePointCurve([(0.0, 0.0), (200.0, 2000.0), (400.0, 4800.0), (600.0, 8400.0)]) + operation_cost_brighton.variable = FuelCurve(io_curve, 1.0) # Use PWL for Brighton + set_fuel_cost!(c_sys5_re, th_brighton, Deterministic("fuel_cost", forecast_data)) + + ### Update Solitude Cost ### + fuel_cost_day1 = ones(24) # Cheap Day 1 + fuel_cost_day2 = Float64.(collect(101:124)) # Expensive Day 2 + + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + forecast_data[DayAhead[1]] = TimeArray(DayAhead, fuel_cost_day1) + forecast_data[DayAhead2[1]] = TimeArray(DayAhead2, fuel_cost_day2) + + operation_cost_solitude = th_solitude.operation_cost + operation_cost_solitude.variable = FuelCurve(LinearCurve(1.0), 1.0) # Use Linear for Solitude + set_fuel_cost!(c_sys5_re, th_solitude, Deterministic("fuel_cost", forecast_data)) + + return c_sys5_re +end + function build_c_sys5_re_only(; add_forecasts, raw_data, kwargs...) sys_kwargs = filter_kwargs(; kwargs...) nodes = nodes5() @@ -1436,7 +1582,7 @@ function build_c_sys5_pwl_uc(; raw_data, kwargs...) return c_sys5_uc end -function build_c_sys5_ed(; add_forecasts, kwargs...) +function build_c_sys5_ed(; add_forecasts, add_reserves, kwargs...) sys_kwargs = filter_kwargs(; kwargs...) nodes = nodes5() c_sys5_ed = PSY.System( @@ -1501,11 +1647,49 @@ function build_c_sys5_ed(; add_forecasts, kwargs...) ) end end + if add_reserves + reserve_ed = reserve5(PSY.get_components(PSY.ThermalStandard, c_sys5_ed)) + PSY.add_service!( + c_sys5_ed, + reserve_ed[1], + PSY.get_components(PSY.ThermalStandard, c_sys5_ed), + ) + PSY.add_service!( + c_sys5_ed, + reserve_ed[2], + [collect(PSY.get_components(PSY.ThermalStandard, c_sys5_ed))[end]], + ) + PSY.add_service!( + c_sys5_ed, + reserve_ed[3], + PSY.get_components(PSY.ThermalStandard, c_sys5_ed), + ) + for serv in PSY.get_components(PSY.VariableReserve, c_sys5_ed) + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + for t in 1:2 # loop over days + ta_DA = Reserve_ts[t] + data_5min = repeat(values(ta_DA); inner = 12) + reserve_timeseries_RT = + TimeSeries.TimeArray(RealTime + Day(t - 1), data_5min) + # loop over hours + for ini_time in timestamp(ta_DA) #get the initial hour + # Construct TimeSeries + data = when(reserve_timeseries_RT, hour, hour(ini_time)) # get the subset ts for that hour + forecast_data[ini_time] = data + end + end + PSY.add_time_series!( + c_sys5_ed, + serv, + PSY.Deterministic("requirement", forecast_data), + ) + end + end return c_sys5_ed end -function build_c_sys5_pwl_ed(; add_forecasts, raw_data, kwargs...) - c_sys5_ed = build_c_sys5_ed(; add_forecasts, raw_data, kwargs...) +function build_c_sys5_pwl_ed(; add_forecasts, add_reserves, raw_data, kwargs...) + c_sys5_ed = build_c_sys5_ed(; add_forecasts, add_reserves, raw_data, kwargs...) thermal = thermal_generators5_pwl(collect(PSY.get_components(PSY.ACBus, c_sys5_ed))) for d in thermal PSY.add_component!(c_sys5_ed, d) @@ -2062,7 +2246,7 @@ function build_c_sys5_hy_ems_ed(; add_forecasts, raw_data, kwargs...) return c_sys5_hy_ed end -function build_c_sys5_phes_ed(; add_forecasts, raw_data, kwargs...) +function build_c_sys5_phes_ed(; add_forecasts, add_reserves, raw_data, kwargs...) sys_kwargs = filter_kwargs(; kwargs...) nodes = nodes5() c_sys5_phes_ed = PSY.System( @@ -2182,6 +2366,41 @@ function build_c_sys5_phes_ed(; add_forecasts, raw_data, kwargs...) ) end end + if add_reserves + reserve_uc = + reserve5_phes(PSY.get_components(PSY.HydroPumpedStorage, c_sys5_phes_ed)) + PSY.add_service!( + c_sys5_phes_ed, + reserve_uc[1], + PSY.get_components(PSY.HydroPumpedStorage, c_sys5_phes_ed), + ) + PSY.add_service!( + c_sys5_phes_ed, + reserve_uc[2], + [collect(PSY.get_components(PSY.HydroPumpedStorage, c_sys5_phes_ed))[end]], + ) + + for serv in PSY.get_components(PSY.VariableReserve, c_sys5_phes_ed) + forecast_data = SortedDict{Dates.DateTime, TimeSeries.TimeArray}() + for t in 1:2 # loop over days + ta_DA = Reserve_ts[t] + data_5min = repeat(values(ta_DA); inner = 12) + reserve_timeseries_RT = + TimeSeries.TimeArray(RealTime + Day(t - 1), data_5min) + # loop over hours + for ini_time in timestamp(ta_DA) #get the initial hour + # Construct TimeSeries + data = when(reserve_timeseries_RT, hour, hour(ini_time)) # get the subset ts for that hour + forecast_data[ini_time] = data + end + end + PSY.add_time_series!( + c_sys5_phes_ed, + serv, + PSY.Deterministic("requirement", forecast_data), + ) + end + end return c_sys5_phes_ed end diff --git a/src/system_descriptor_data.jl b/src/system_descriptor_data.jl index 8e700d4..84e5461 100644 --- a/src/system_descriptor_data.jl +++ b/src/system_descriptor_data.jl @@ -133,6 +133,11 @@ const SYSTEM_CATALOG = [ default = true, allowed_values = Set([true, false]), ), + SystemArgument(; + name = :add_reserves, + default = false, + allowed_values = Set([true, false]), + ), ], ), SystemDescriptor(; @@ -189,6 +194,11 @@ const SYSTEM_CATALOG = [ default = true, allowed_values = Set([true, false]), ), + SystemArgument(; + name = :add_reserves, + default = false, + allowed_values = Set([true, false]), + ), ], ), SystemDescriptor(; @@ -324,6 +334,30 @@ const SYSTEM_CATALOG = [ ), ], ), + SystemDescriptor(; + name = "c_sys5_re_fuel_cost", + description = "5-Bus system with Renewable Energy and Fuel Cost TimeSeries", + category = PSITestSystems, + raw_data = joinpath(DATA_DIR, "psy_data", "data_5bus_pu.jl"), + build_function = build_c_sys5_re_fuel_cost, + supported_arguments = [ + SystemArgument(; + name = :add_forecasts, + default = true, + allowed_values = Set([true, false]), + ), + SystemArgument(; + name = :add_reserves, + default = false, + allowed_values = Set([true, false]), + ), + SystemArgument(; + name = :add_single_time_series, + default = false, + allowed_values = Set([true, false]), + ), + ], + ), SystemDescriptor(; name = "c_sys5_re_only", description = "5-Bus system with only Renewable Energy", @@ -470,6 +504,11 @@ const SYSTEM_CATALOG = [ default = true, allowed_values = Set([true, false]), ), + SystemArgument(; + name = :add_reserves, + default = false, + allowed_values = Set([true, false]), + ), ], ), SystemDescriptor(; @@ -484,6 +523,11 @@ const SYSTEM_CATALOG = [ default = true, allowed_values = Set([true, false]), ), + SystemArgument(; + name = :add_reserves, + default = false, + allowed_values = Set([true, false]), + ), ], ), #= @@ -1718,6 +1762,13 @@ const SYSTEM_CATALOG = [ raw_data = joinpath(DATA_DIR, "psid_tests", "data_examples"), build_function = build_3bus_inverter, ), + SystemDescriptor(; + name = "WECC 9 Bus", + description = "WECC 9 Bus System with dynamic gens from Sauer and Pai", + category = PSIDSystems, + raw_data = joinpath(DATA_DIR, "psid_tests", "data_tests", "WSCC 9 bus.raw"), + build_function = build_psid_wecc_9_dynamic, + ), SystemDescriptor(; name = "2 Bus Load Tutorial", description = "2 Bus Base System for load tutorials",