From e240748ade26b286cb5601dbc94c5252b56dfa29 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Tue, 11 Aug 2026 13:15:13 -0700 Subject: [PATCH 1/4] Migrate 5-bus reserve fixtures to the psy6 reserve tree ConstantReserve/VariableReserve -> OnlineReserve (keyword form where the positional order changed), ReserveDemandCurve -> OnlineReserve carrying its demand curve on `variable`, VariableReserveNonSpinning -> OfflineReserve. --- psy_data/data_5bus_pu.jl | 59 ++++++++++++++++++++++------------------ 1 file changed, 33 insertions(+), 26 deletions(-) diff --git a/psy_data/data_5bus_pu.jl b/psy_data/data_5bus_pu.jl index dd2d926..473615e 100644 --- a/psy_data/data_5bus_pu.jl +++ b/psy_data/data_5bus_pu.jl @@ -1017,64 +1017,71 @@ interruptible(nodes5) = [InterruptiblePowerLoad( # Natural Units: First vector: Power in MW, Second Vector: Slopes in $/MWh ORDC_cost = CostCurve(PiecewiseIncrementalCurve(0.0, [0.0, 20.0, 40.0, 60.0, 80.0], [150.0, 27.5, 24.5, 0.5])) +# Keyword form: `OnlineReserve`'s positional order differs from the old `ConstantReserve`. constant_reserve5() = - ConstantReserve{ReserveUp}( - "ReserveUp", - true, - 300, - 1.0, - 3600.0, - 1.0, - 1.0, - 0.0, + OnlineReserve{ReserveUp}(; + name = "ReserveUp", + available = true, + time_frame = 300, + requirement = 1.0, + sustained_time = 3600.0, + max_output_fraction = 1.0, + max_participation_factor = 1.0, + deployed_fraction = 0.0, ) +# The ORDC carries its demand curve on `variable`; non-spinning is `OfflineReserve` (no direction). reserve5(thermal_generators5) = [ - VariableReserve{ReserveUp}( + OnlineReserve{ReserveUp}( "Reserve1", true, 0.6, maximum([gen.active_power_limits[:max] for gen in thermal_generators5]) .* 0.001, ), - VariableReserve{ReserveDown}( + OnlineReserve{ReserveDown}( "Reserve2", true, 0.3, maximum([gen.active_power_limits[:max] for gen in thermal_generators5]) .* 0.005, ), - VariableReserve{ReserveUp}( + OnlineReserve{ReserveUp}( "Reserve11", true, 0.8, maximum([gen.active_power_limits[:max] for gen in thermal_generators5]) .* 0.001, ), - ReserveDemandCurve{ReserveUp}(nothing, "ORDC1", true, 0.6), - VariableReserveNonSpinning("NonSpinningReserve", true, 0.5, maximum([gen.active_power_limits[:max] for gen in thermal_generators5]) .* 0.001), + OnlineReserve{ReserveUp}(; + name = "ORDC1", + available = true, + time_frame = 0.6, + variable = ORDC_cost, + ), + OfflineReserve("NonSpinningReserve", true, 0.5, maximum([gen.active_power_limits[:max] for gen in thermal_generators5]) .* 0.001), ] reserve5_re(renewable_generators5) = [ - VariableReserve{ReserveUp}("Reserve3", true, 30, 100), - VariableReserve{ReserveDown}("Reserve4", true, 5, 50), - ReserveDemandCurve{ReserveUp}(nothing, "ORDC1", true, 0.6), + OnlineReserve{ReserveUp}("Reserve3", true, 30, 100), + OnlineReserve{ReserveDown}("Reserve4", true, 5, 50), + OnlineReserve{ReserveUp}(; name = "ORDC1", available = true, time_frame = 0.6, variable = ORDC_cost), ] reserve5_hy(hydro_generators5) = [ - VariableReserve{ReserveUp}("Reserve5", true, 30, 100), - VariableReserve{ReserveDown}("Reserve6", true, 5, 50), - ReserveDemandCurve{ReserveUp}(nothing, "ORDC1", true, 0.6), + OnlineReserve{ReserveUp}("Reserve5", true, 30, 100), + OnlineReserve{ReserveDown}("Reserve6", true, 5, 50), + OnlineReserve{ReserveUp}(; name = "ORDC1", available = true, time_frame = 0.6, variable = ORDC_cost), ] reserve5_il(interruptible_loads) = [ - VariableReserve{ReserveUp}("Reserve7", true, 30, 100), - VariableReserve{ReserveDown}("Reserve8", true, 5, 50), - ReserveDemandCurve{ReserveUp}(nothing, "ORDC1", true, 0.6), + OnlineReserve{ReserveUp}("Reserve7", true, 30, 100), + OnlineReserve{ReserveDown}("Reserve8", true, 5, 50), + OnlineReserve{ReserveUp}(; name = "ORDC1", available = true, time_frame = 0.6, variable = ORDC_cost), ] reserve5_phes(phes5) = [ - VariableReserve{ReserveUp}("Reserve9", true, 30, 100), - VariableReserve{ReserveDown}("Reserve10", true, 5, 50), - ReserveDemandCurve{ReserveUp}(nothing, "ORDC1", true, 0.6), + OnlineReserve{ReserveUp}("Reserve9", true, 30, 100), + OnlineReserve{ReserveDown}("Reserve10", true, 5, 50), + OnlineReserve{ReserveUp}(; name = "ORDC1", available = true, time_frame = 0.6, variable = ORDC_cost), ] # TODO: add a sensible cost for hybrid devices From 447d67ffea973327378d194b3702ffc822728528 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Tue, 11 Aug 2026 13:15:13 -0700 Subject: [PATCH 2/4] Add required base_current to TModelHVDCLine fixtures TModelHVDCLine now requires base_current (A); its per-unit impedances are normalized against a current base. Set to S_base/V_base = 100 MVA / 500 kV = 200 A for the 10-bus AC/DC system. --- psy_data/data_10bus_ac_dc_pu.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/psy_data/data_10bus_ac_dc_pu.jl b/psy_data/data_10bus_ac_dc_pu.jl index 9f3f12a..0d1a50f 100644 --- a/psy_data/data_10bus_ac_dc_pu.jl +++ b/psy_data/data_10bus_ac_dc_pu.jl @@ -172,6 +172,7 @@ branches10_ac(nodes10) = [ ), ] +# base_current (A) = S_base / V_base = 100 MVA / 500 kV (the DC bus base voltage) = 200 A. branches10_dc(nodes10_dc) = [ TModelHVDCLine( name = "nodeC_DC-nodeC2_DC", @@ -183,6 +184,7 @@ branches10_dc(nodes10_dc) = [ c = 0.0, active_power_limits_from=(min=-10.0, max=10.0), active_power_limits_to=(min=-10.0, max=10.0), + base_current = 200.0, ), TModelHVDCLine( name = "nodeD_DC-nodeD2_DC", @@ -194,6 +196,7 @@ branches10_dc(nodes10_dc) = [ c = 0.0, active_power_limits_from=(min=-10.0, max=10.0), active_power_limits_to=(min=-10.0, max=10.0), + base_current = 200.0, ), TModelHVDCLine( name = "nodeC_DC-nodeD2_DC", @@ -205,6 +208,7 @@ branches10_dc(nodes10_dc) = [ c = 0.0, active_power_limits_from=(min=-10.0, max=10.0), active_power_limits_to=(min=-10.0, max=10.0), + base_current = 200.0, ), ] From 0cd9cf29d80930128b511bc324584e16f63f04eb Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Tue, 11 Aug 2026 13:36:14 -0700 Subject: [PATCH 3/4] Migrate market-bid fixtures to the psy6 cost API Static fixture: keyword-form MarketBidCost (offer-curve defaults replace the removed Nothing positional signatures). TS fixture: restore the original intent with MarketBidTimeSeriesCost - every field references an attached series by key - replacing the dead static-cost-plus-loose-series shape left behind when PSY removed the TimeSeriesData set_variable_cost! overload. Unblocks deleting PSB's psy6_compat.jl shims. --- psy_data/generation_cost_function_data.jl | 86 ++++++++++++++--------- 1 file changed, 54 insertions(+), 32 deletions(-) diff --git a/psy_data/generation_cost_function_data.jl b/psy_data/generation_cost_function_data.jl index e3d9944..ea05af2 100644 --- a/psy_data/generation_cost_function_data.jl +++ b/psy_data/generation_cost_function_data.jl @@ -338,13 +338,11 @@ end ## Single Bid MarketBid Cost Function function thermal_generators_market_bid(node) - market_bid1 = MarketBidCost( - 30.0, - (hot = 1.5, warm = 1.5, cold = 1.5), - 0.75, - CostCurve(PiecewiseIncrementalCurve(0.0, [10.0, 30.0, 50.0, 100.0], [63.37, 98.155, 102.276])), - nothing, - Vector{Service}(), + market_bid1 = MarketBidCost(; + no_load_cost = LinearCurve(30.0), + start_up = (hot = 1.5, warm = 1.5, cold = 1.5), + shut_down = LinearCurve(0.75), + incremental_offer_curves = CostCurve(PiecewiseIncrementalCurve(0.0, [10.0, 30.0, 50.0, 100.0], [63.37, 98.155, 102.276])), ) gen1 = ThermalStandard( @@ -365,13 +363,11 @@ function thermal_generators_market_bid(node) base_power=100.0, ) - market_bid2 = MarketBidCost( - 50.0, - (hot = 1.5, warm = 1.5, cold = 1.5), - 0.75, - CostCurve(PiecewiseIncrementalCurve(0.0, [10.0, 30.0, 50.0, 100.0], [66.37, 88.155, 109.276])), - nothing, - Vector{Service}(), + market_bid2 = MarketBidCost(; + no_load_cost = LinearCurve(50.0), + start_up = (hot = 1.5, warm = 1.5, cold = 1.5), + shut_down = LinearCurve(0.75), + incremental_offer_curves = CostCurve(PiecewiseIncrementalCurve(0.0, [10.0, 30.0, 50.0, 100.0], [66.37, 88.155, 109.276])), ) gen2 = ThermalStandard( @@ -395,6 +391,44 @@ function thermal_generators_market_bid(node) return [gen1, gen2] end +# Constant-valued forecast matching the market-bid windows (2 windows x 5 steps, hourly). +_mbts_constant_forecast(ini_time, val) = Dict( + ini_time => fill(val, 5), + ini_time + Hour(1) => fill(val, 5), +) + +# Attach the series backing a `MarketBidTimeSeriesCost` and set it as the operation cost. +# psy6 pattern for time-varying bids: every field references an attached series by key, +# so the component must already be in the system. The decremental side gets a zero-width +# (trivial) curve, the TS analogue of `ZERO_OFFER_CURVE`. +function _set_ts_market_bid_cost!(sys, gen, incremental_bid, ini_time, no_load) + _det(name, val) = PSY.Deterministic(; + name = name, + data = _mbts_constant_forecast(ini_time, val), + resolution = Hour(1), + ) + pwl_key = PSY.add_time_series!(sys, gen, incremental_bid) + init_key = PSY.add_time_series!(sys, gen, _det("initial_input_incremental", 0.0)) + dec_key = PSY.add_time_series!( + sys, gen, _det("variable_cost_decremental", PiecewiseStepData([0.0, 0.0], [0.0])), + ) + dec_init_key = PSY.add_time_series!(sys, gen, _det("initial_input_decremental", 0.0)) + nl_key = PSY.add_time_series!(sys, gen, _det("no_load_cost", no_load)) + su_key = PSY.add_time_series!(sys, gen, _det("start_up", (1.5, 1.5, 1.5))) + sd_key = PSY.add_time_series!(sys, gen, _det("shut_down", 0.75)) + PSY.set_operation_cost!( + gen, + PSY.MarketBidTimeSeriesCost(; + no_load_cost = PSY.TimeSeriesLinearCurve(nl_key), + start_up = PSY.IS.TupleTimeSeries{PSY.StartUpStages}(su_key), + shut_down = PSY.TimeSeriesLinearCurve(sd_key), + incremental_offer_curves = PSY.make_market_bid_ts_curve(pwl_key, init_key), + decremental_offer_curves = PSY.make_market_bid_ts_curve(dec_key, dec_init_key), + ), + ) + return +end + function thermal_generators_market_bid_ts(sys, node) ini_time = DateTime("1/1/2024 0:00:00", "d/m/y H:M:S") market_bid_gen1_data = Dict( @@ -441,14 +475,9 @@ function thermal_generators_market_bid_ts(sys, node) resolution = Hour(1), ) - market_bid1 = MarketBidCost( - 30.0, - (hot = 1.5, warm = 1.5, cold = 1.5), - 0.75, - nothing, - nothing, - Vector{Service}(), - ) + # Placeholder cost, replaced by the TS-backed `MarketBidTimeSeriesCost` once the + # component is in the system (the series keys require an attached component). + market_bid1 = MarketBidCost(nothing) gen1 = ThermalStandard( name="Test Unit1", @@ -468,14 +497,7 @@ function thermal_generators_market_bid_ts(sys, node) base_power=100.0, ) - market_bid2 = MarketBidCost( - 50.0, - (hot = 1.5, warm = 1.5, cold = 1.5), - 0.75, - nothing, - nothing, - Vector{Service}(), - ) + market_bid2 = MarketBidCost(nothing) gen2 = ThermalStandard( name="Test Unit2", @@ -498,8 +520,8 @@ function thermal_generators_market_bid_ts(sys, node) PSY.add_component!(sys, gen1) PSY.add_component!(sys, gen2) - PSY.set_variable_cost!(sys, gen1, market_bid_gen1, PSY.UnitSystem.NATURAL_UNITS) - PSY.set_variable_cost!(sys, gen2, market_bid_gen2, PSY.UnitSystem.NATURAL_UNITS) + _set_ts_market_bid_cost!(sys, gen1, market_bid_gen1, ini_time, 30.0) + _set_ts_market_bid_cost!(sys, gen2, market_bid_gen2, ini_time, 50.0) return [gen1, gen2] end From fe9dd1c4f2fd7b5c6a4538e2d7af144893781db2 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Tue, 11 Aug 2026 13:40:23 -0700 Subject: [PATCH 4/4] Tighten fixture comments --- psy_data/generation_cost_function_data.jl | 9 +++------ 1 file changed, 3 insertions(+), 6 deletions(-) diff --git a/psy_data/generation_cost_function_data.jl b/psy_data/generation_cost_function_data.jl index ea05af2..37f2248 100644 --- a/psy_data/generation_cost_function_data.jl +++ b/psy_data/generation_cost_function_data.jl @@ -397,10 +397,8 @@ _mbts_constant_forecast(ini_time, val) = Dict( ini_time + Hour(1) => fill(val, 5), ) -# Attach the series backing a `MarketBidTimeSeriesCost` and set it as the operation cost. -# psy6 pattern for time-varying bids: every field references an attached series by key, -# so the component must already be in the system. The decremental side gets a zero-width -# (trivial) curve, the TS analogue of `ZERO_OFFER_CURVE`. +# Attach the series backing a `MarketBidTimeSeriesCost` (fields reference them by key, +# so `gen` must already be in the system) and set it as the operation cost. function _set_ts_market_bid_cost!(sys, gen, incremental_bid, ini_time, no_load) _det(name, val) = PSY.Deterministic(; name = name, @@ -475,8 +473,7 @@ function thermal_generators_market_bid_ts(sys, node) resolution = Hour(1), ) - # Placeholder cost, replaced by the TS-backed `MarketBidTimeSeriesCost` once the - # component is in the system (the series keys require an attached component). + # Placeholder, replaced by the TS-backed cost after `add_component!`. market_bid1 = MarketBidCost(nothing) gen1 = ThermalStandard(