From d969b858682d4715c185cdc51beaedf0bdb42a13 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 13 Apr 2026 13:51:10 -0600 Subject: [PATCH 01/19] refactor MBC/IEC for PSY static/time-series type split MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Adapt IOM to handle PSY's split of MarketBidCost → MarketBidCost + MarketBidTimeSeriesCost, and ImportExportCost → ImportExportCost + ImportExportTimeSeriesCost. Core changes: - Add MBC_TYPES/IEC_TYPES/TS_OFFER_CURVE_COST_TYPES union aliases - Add accessor overloads for TS cost types - Replace removed PSY.get_incremental_initial_input with curve traversal - Simplify _has_parameter_time_series to dispatch on cost type - Split _get_pwl_data into dispatched _get_raw_pwl_data (static vs TS) - Split validate_occ_breakpoints_slopes into dispatched _validate_occ_curves - Remove validate_initial_input_time_series (type guarantees consistency) - Add is_time_variant overloads for IS ValueCurve/CostCurve types - Add _shutdown_cost_value for LinearCurve → Float64 extraction Test utilities updated for new PSY types (LinearCurve fields, MarketBidTimeSeriesCost/ImportExportTimeSeriesCost construction). New integration tests for offer curve cost code paths. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/core/definitions.jl | 6 + src/objective_function/value_curve_cost.jl | 341 +++++++-------- src/utils/powersystems_utils.jl | 2 + test/InfrastructureOptimizationModelsTests.jl | 2 + test/test_offer_curve_cost.jl | 387 ++++++++++++++++++ test/test_utils/add_market_bid_cost.jl | 64 +-- test/test_utils/iec_simulation_utils.jl | 20 +- test/test_utils/mbc_system_utils.jl | 170 +++++--- 8 files changed, 716 insertions(+), 276 deletions(-) create mode 100644 test/test_offer_curve_cost.jl diff --git a/src/core/definitions.jl b/src/core/definitions.jl index 74dd0af7..359c596f 100644 --- a/src/core/definitions.jl +++ b/src/core/definitions.jl @@ -28,6 +28,12 @@ struct DecrementalOffer <: OfferDirection end Base.string(::IncrementalOffer) = "incremental" Base.string(::DecrementalOffer) = "decremental" +# Union type aliases for static + time-series cost variants +const MBC_TYPES = Union{PSY.MarketBidCost, PSY.MarketBidTimeSeriesCost} +const IEC_TYPES = Union{PSY.ImportExportCost, PSY.ImportExportTimeSeriesCost} +const TS_OFFER_CURVE_COST_TYPES = + Union{PSY.MarketBidTimeSeriesCost, PSY.ImportExportTimeSeriesCost} + # Type alias for decision model indices - used for indexing into output stores const DecisionModelIndexType = Dates.DateTime const EmulationModelIndexType = Int diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index 96d1dcb4..3c5071ea 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -23,34 +23,55 @@ ################################################################################# ####################### get_{output/input}_offer_curves ######################### -# these 1-argument getters turn into straight getfield calls -get_output_offer_curves(cost::PSY.ImportExportCost) = PSY.get_import_offer_curves(cost) -get_output_offer_curves(cost::PSY.MarketBidCost) = PSY.get_incremental_offer_curves(cost) -get_input_offer_curves(cost::PSY.ImportExportCost) = PSY.get_export_offer_curves(cost) -get_input_offer_curves(cost::PSY.MarketBidCost) = PSY.get_decremental_offer_curves(cost) - -# these 2-argument getters return either a TimeArray of curves or a single curve, -# depending on whether the cost is time varying or not. +# 1-argument getters: straight getfield calls (same PSY getter for static and TS variants) +get_output_offer_curves(cost::IEC_TYPES) = PSY.get_import_offer_curves(cost) +get_output_offer_curves(cost::MBC_TYPES) = PSY.get_incremental_offer_curves(cost) +get_input_offer_curves(cost::IEC_TYPES) = PSY.get_export_offer_curves(cost) +get_input_offer_curves(cost::MBC_TYPES) = PSY.get_decremental_offer_curves(cost) + +# 2-argument getters: resolve time series if needed, return static curve(s). +# Static types: delegate to 1-arg getter (no resolution needed). get_output_offer_curves( - component::PSY.Component, + ::PSY.Component, cost::PSY.ImportExportCost; kwargs..., -) = PSY.get_import_offer_curves(component, cost; kwargs...) +) = PSY.get_import_offer_curves(cost) get_output_offer_curves( - component::PSY.Component, + ::PSY.Component, cost::PSY.MarketBidCost; kwargs..., -) = PSY.get_incremental_offer_curves(component, cost; kwargs...) +) = PSY.get_incremental_offer_curves(cost) get_input_offer_curves( - component::PSY.Component, + ::PSY.Component, cost::PSY.ImportExportCost; kwargs..., -) = PSY.get_export_offer_curves(component, cost; kwargs...) +) = PSY.get_export_offer_curves(cost) get_input_offer_curves( - component::PSY.Component, + ::PSY.Component, cost::PSY.MarketBidCost; kwargs..., -) = PSY.get_decremental_offer_curves(component, cost; kwargs...) +) = PSY.get_decremental_offer_curves(cost) +# TS types: resolve via PSY's 2-arg getters. +get_output_offer_curves( + component::PSY.Component, + cost::PSY.ImportExportTimeSeriesCost; + kwargs..., +) = PSY.get_import_variable_cost(component, cost; kwargs...) +get_output_offer_curves( + component::PSY.Component, + cost::PSY.MarketBidTimeSeriesCost; + kwargs..., +) = PSY.get_incremental_variable_cost(component, cost; kwargs...) +get_input_offer_curves( + component::PSY.Component, + cost::PSY.ImportExportTimeSeriesCost; + kwargs..., +) = PSY.get_export_variable_cost(component, cost; kwargs...) +get_input_offer_curves( + component::PSY.Component, + cost::PSY.MarketBidTimeSeriesCost; + kwargs..., +) = PSY.get_decremental_variable_cost(component, cost; kwargs...) ######################### get_offer_curves(direction, ...) ############################## @@ -60,9 +81,9 @@ get_offer_curves(::DecrementalOffer, device::PSY.StaticInjection) = get_offer_curves(::IncrementalOffer, device::PSY.StaticInjection) = get_output_offer_curves(PSY.get_operation_cost(device)) get_initial_input(::DecrementalOffer, device::PSY.StaticInjection) = - PSY.get_decremental_initial_input(PSY.get_operation_cost(device)) + IS.get_initial_input(PSY.get_value_curve(get_input_offer_curves(PSY.get_operation_cost(device)))) get_initial_input(::IncrementalOffer, device::PSY.StaticInjection) = - PSY.get_incremental_initial_input(PSY.get_operation_cost(device)) + IS.get_initial_input(PSY.get_value_curve(get_output_offer_curves(PSY.get_operation_cost(device)))) # direction and cost curve (needed for VOM code path): get_offer_curves(::DecrementalOffer, op_cost::PSY.OfferCurveCost) = @@ -98,9 +119,9 @@ _objective_sign(::DecrementalOffer) = OBJECTIVE_FUNCTION_NEGATIVE _get_parameter_field(::StartupCostParameter, op_cost) = PSY.get_start_up(op_cost) _get_parameter_field(::ShutdownCostParameter, op_cost) = PSY.get_shut_down(op_cost) _get_parameter_field(::IncrementalCostAtMinParameter, op_cost) = - PSY.get_incremental_initial_input(op_cost) + IS.get_initial_input(PSY.get_value_curve(get_output_offer_curves(op_cost))) _get_parameter_field(::DecrementalCostAtMinParameter, op_cost) = - PSY.get_decremental_initial_input(op_cost) + IS.get_initial_input(PSY.get_value_curve(get_input_offer_curves(op_cost))) _get_parameter_field( ::Union{ IncrementalPiecewiseLinearSlopeParameter, @@ -122,41 +143,19 @@ _get_parameter_field( ################################################################################# _has_market_bid_cost(device::PSY.StaticInjection) = - PSY.get_operation_cost(device) isa PSY.MarketBidCost + PSY.get_operation_cost(device) isa MBC_TYPES _has_import_export_cost(device::PSY.Source) = - PSY.get_operation_cost(device) isa PSY.ImportExportCost + PSY.get_operation_cost(device) isa IEC_TYPES _has_import_export_cost(::PSY.StaticInjection) = false _has_offer_curve_cost(device::PSY.Component) = _has_market_bid_cost(device) || _has_import_export_cost(device) -_has_parameter_time_series(::StartupCostParameter, device::PSY.StaticInjection) = - is_time_variant(PSY.get_start_up(PSY.get_operation_cost(device))) - -_has_parameter_time_series(::ShutdownCostParameter, device::PSY.StaticInjection) = - is_time_variant(PSY.get_shut_down(PSY.get_operation_cost(device))) - -_has_parameter_time_series( - ::T, - device::PSY.StaticInjection, -) where {T <: AbstractCostAtMinParameter} = - _has_offer_curve_cost(device) && - is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device))) - -_has_parameter_time_series( - ::T, - device::PSY.StaticInjection, -) where {T <: AbstractPiecewiseLinearSlopeParameter} = - _has_offer_curve_cost(device) && - is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device))) - -_has_parameter_time_series( - ::T, - device::PSY.StaticInjection, -) where {T <: AbstractPiecewiseLinearBreakpointParameter} = - _has_offer_curve_cost(device) && - is_time_variant(_get_parameter_field(T(), PSY.get_operation_cost(device))) +# With the static/TS type split, time-series parameters are determined by cost type: +# TS cost types always have time-series parameters; static types never do. +_has_parameter_time_series(::ParameterType, device::PSY.StaticInjection) = + PSY.get_operation_cost(device) isa TS_OFFER_CURVE_COST_TYPES ################################################################################# # Section 5: _consider_parameter (generic versions) @@ -200,35 +199,6 @@ _consider_parameter( # (ThermalMultiStart, RenewableDispatch, Storage) are in POM. ################################################################################# -function validate_initial_input_time_series( - device::PSY.StaticInjection, - dir::OfferDirection, -) - initial_input = get_initial_input(dir, device) - initial_is_ts = is_time_variant(initial_input) - variable_is_ts = is_time_variant(get_offer_curves(dir, device)) - label = string(dir) - - (initial_is_ts && !variable_is_ts) && - @warn "In `MarketBidCost` for $(get_name(device)), found time series for `$(label)_initial_input` but non-time-series `$(label)_offer_curves`; will ignore `initial_input` of `$(label)_offer_curves`" - (variable_is_ts && !initial_is_ts) && - throw( - ArgumentError( - "In `MarketBidCost` for $(get_name(device)), if providing time series for `$(label)_offer_curves`, must also provide time series for `$(label)_initial_input`", - ), - ) - - if !variable_is_ts && !initial_is_ts - _validate_eltype( - Union{Float64, Nothing}, device, initial_input, " $(label)_initial_input", - ) - else - _validate_eltype( - Float64, device, initial_input, " $(label)_initial_input", - ) - end -end - curvity_check(::IncrementalOffer, x) = PSY.is_convex(x) curvity_check(::DecrementalOffer, x) = PSY.is_concave(x) expected_curvity(::IncrementalOffer) = "convex" @@ -236,97 +206,51 @@ expected_curvity(::DecrementalOffer) = "concave" function validate_occ_breakpoints_slopes(device::PSY.StaticInjection, dir::OfferDirection) offer_curves = get_offer_curves(dir, device) - device_name = get_name(device) - is_ts = is_time_variant(offer_curves) - expected_type = if is_ts - IS.PiecewiseStepData - else - PSY.CostCurve{PSY.PiecewiseIncrementalCurve} - end - p1 = nothing - apply_maybe_across_time_series(device, offer_curves) do x - _validate_eltype(expected_type, device, x, " $(string(dir)) offer curves") - cost_curve_name = nameof(typeof(PSY.get_operation_cost(device))) - curvity_check(dir, x) || - throw( - ArgumentError( - "$(uppercasefirst(string(dir))) $cost_curve_name for component $(device_name) is non-$(expected_curvity(dir))", - ), - ) - - p1 = _validate_occ_subtype( - PSY.get_operation_cost(device), - dir, - is_ts, - x, - device_name, - p1, - ) - end + _validate_occ_curves(device, dir, offer_curves) end -function _validate_occ_subtype( - ::PSY.MarketBidCost, +# Static: validate convexity/concavity and cost-type-specific constraints +function _validate_occ_curves( + device::PSY.StaticInjection, dir::OfferDirection, - is_ts, - curve::PSY.PiecewiseStepData, - device_name::String, - p1::Union{Nothing, Float64}, + cost_curve::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, ) - @assert is_ts - my_p1 = first(PSY.get_x_coords(curve)) - if isnothing(p1) - p1 = my_p1 - elseif !isapprox(p1, my_p1) + device_name = get_name(device) + cost_curve_name = nameof(typeof(PSY.get_operation_cost(device))) + curvity_check(dir, cost_curve) || throw( ArgumentError( - "Inconsistent minimum breakpoint values in time series MarketBidCost for $(device_name) offer curves. For time-variable MarketBidCost, all first x-coordinates must be equal across the entire time series.", + "$(uppercasefirst(string(dir))) $cost_curve_name for component $(device_name) is non-$(expected_curvity(dir))", ), ) - end - return p1 + _validate_occ_subtype(PSY.get_operation_cost(device), dir, cost_curve, device_name) end -_validate_occ_subtype( - ::PSY.MarketBidCost, - dir::OfferDirection, - is_ts, - ::PSY.CostCurve, - args..., -) = - @assert !is_ts +# TS-backed: validated at parameter population time, not here +_validate_occ_curves(::PSY.StaticInjection, ::OfferDirection, + ::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}) = nothing + +_validate_occ_subtype(::PSY.MarketBidCost, ::OfferDirection, ::PSY.CostCurve, args...) = + nothing function _validate_occ_subtype( - cost::PSY.ImportExportCost, - dir::OfferDirection, - is_ts, + ::PSY.ImportExportCost, + ::OfferDirection, curve::PSY.CostCurve, args..., ) - @assert !is_ts !iszero(PSY.get_vom_cost(curve)) && throw( ArgumentError( "For ImportExportCost, VOM cost must be zero.", ), ) - vc = PSY.get_value_curve(curve) !iszero(PSY.get_initial_input(curve)) && throw( ArgumentError( "For ImportExportCost, initial input must be zero.", ), ) - _validate_occ_subtype(cost, dir, true, PSY.get_function_data(vc)) -end - -function _validate_occ_subtype( - ::PSY.ImportExportCost, - dir::OfferDirection, - is_ts, - curve::PSY.PiecewiseStepData, - args..., -) - @assert is_ts - if !iszero(first(PSY.get_x_coords(curve))) + fd = PSY.get_function_data(PSY.get_value_curve(curve)) + if !iszero(first(PSY.get_x_coords(fd))) throw( ArgumentError( "For ImportExportCost, the first breakpoint must be zero.", @@ -339,44 +263,50 @@ end # Device-specific overloads (ThermalMultiStart, RenewableDispatch, Storage) are in POM. function validate_occ_component(::StartupCostParameter, device::PSY.StaticInjection) - startup = PSY.get_start_up(PSY.get_operation_cost(device)) - contains_multistart = false - apply_maybe_across_time_series(device, startup) do x - if x isa Float64 - return - elseif x isa Union{NTuple{3, Float64}, StartUpStages} - contains_multistart = true - else - location = - is_time_variant(startup) ? " in time series $(get_name(startup))" : "" - throw( - ArgumentError( - "Expected Float64 or NTuple{3, Float64} or StartUpStages startup cost but got $(typeof(x))$location for $(get_name(device))", - ), - ) - end - end - if contains_multistart - location = is_time_variant(startup) ? " in time series $(get_name(startup))" : "" - @warn "Multi-start costs detected$location for non-multi-start unit $(get_name(device)), will take the maximum" + op_cost = PSY.get_operation_cost(device) + # TS types are validated at parameter population time + op_cost isa PSY.MarketBidTimeSeriesCost && return + startup = PSY.get_start_up(op_cost) + if startup isa Union{NTuple{3, Float64}, StartUpStages} + @warn "Multi-start costs detected for non-multi-start unit $(get_name(device)), will take the maximum" + elseif !(startup isa Float64) + throw( + ArgumentError( + "Expected Float64 or StartUpStages startup cost but got $(typeof(startup)) for $(get_name(device))", + ), + ) end return end function validate_occ_component(::ShutdownCostParameter, device::PSY.StaticInjection) - shutdown = PSY.get_shut_down(PSY.get_operation_cost(device)) - _validate_eltype(Float64, device, shutdown, " for shutdown cost") + op_cost = PSY.get_operation_cost(device) + # TS types are validated at parameter population time + op_cost isa PSY.MarketBidTimeSeriesCost && return + # Static MBC: shut_down is LinearCurve; ThermalGenerationCost: shut_down is Float64 + shutdown = PSY.get_shut_down(op_cost) + if shutdown isa IS.LinearCurve + return # valid + elseif shutdown isa Float64 + return # valid (e.g. ThermalGenerationCost) + else + throw( + ArgumentError( + "Expected Float64 or LinearCurve shutdown cost but got $(typeof(shutdown)) for $(get_name(device))", + ), + ) + end end validate_occ_component( ::IncrementalCostAtMinParameter, device::PSY.StaticInjection, -) = validate_initial_input_time_series(device, IncrementalOffer()) +) = nothing # consistency guaranteed by the static/TS type split validate_occ_component( ::DecrementalCostAtMinParameter, device::PSY.StaticInjection, -) = validate_initial_input_time_series(device, DecrementalOffer()) +) = nothing # consistency guaranteed by the static/TS type split validate_occ_component( ::IncrementalPiecewiseLinearBreakpointParameter, @@ -481,35 +411,8 @@ function _get_pwl_data( time::Int, ) where {T <: PSY.Component} cost_data = get_offer_curves(dir, component) - - if is_time_variant(cost_data) - name = PSY.get_name(component) - - SlopeParam = _slope_param(dir) - slope_param_arr = get_parameter_array(container, SlopeParam, T) - slope_param_mult = get_parameter_multiplier_array(container, SlopeParam, T) - @assert size(slope_param_arr) == size(slope_param_mult) - slope_cost_component = - slope_param_arr[name, :, time] .* slope_param_mult[name, :, time] - slope_cost_component = slope_cost_component.data - - BreakpointParam = _breakpoint_param(dir) - breakpoint_param_container = get_parameter(container, BreakpointParam, T) - breakpoint_param_arr = get_parameter_column_refs(breakpoint_param_container, name) - breakpoint_param_mult = get_multiplier_array(breakpoint_param_container) - @assert size(breakpoint_param_arr) == size(breakpoint_param_mult[name, :, :]) - breakpoint_cost_component = - breakpoint_param_arr[:, time] .* breakpoint_param_mult[name, :, time] - breakpoint_cost_component = breakpoint_cost_component.data - - @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 - unit_system = PSY.UnitSystem.NATURAL_UNITS - else - cost_component = PSY.get_function_data(PSY.get_value_curve(cost_data)) - breakpoint_cost_component = PSY.get_x_coords(cost_component) - slope_cost_component = PSY.get_y_coords(cost_component) - unit_system = PSY.get_power_units(cost_data) - end + breakpoint_cost_component, slope_cost_component, unit_system = + _get_raw_pwl_data(dir, container, component, cost_data, time) breakpoints, slopes = get_piecewise_curve_per_system_unit( breakpoint_cost_component, @@ -518,10 +421,54 @@ function _get_pwl_data( get_model_base_power(container), PSY.get_base_power(component), ) - return breakpoints, slopes end +# static curve: read directly from the cost curve +function _get_raw_pwl_data( + ::OfferDirection, + ::OptimizationContainer, + ::T, + cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, + ::Int, +) where {T <: PSY.Component} + cost_component = PSY.get_function_data(PSY.get_value_curve(cost_data)) + return PSY.get_x_coords(cost_component), + PSY.get_y_coords(cost_component), + PSY.get_power_units(cost_data) +end + +# time-series curve: read from parameter arrays (already initialized) +function _get_raw_pwl_data( + dir::OfferDirection, + container::OptimizationContainer, + component::T, + ::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}, + time::Int, +) where {T <: PSY.Component} + name = PSY.get_name(component) + + SlopeParam = _slope_param(dir) + slope_param_arr = get_parameter_array(container, SlopeParam, T) + slope_param_mult = get_parameter_multiplier_array(container, SlopeParam, T) + @assert size(slope_param_arr) == size(slope_param_mult) + slope_cost_component = + slope_param_arr[name, :, time] .* slope_param_mult[name, :, time] + slope_cost_component = slope_cost_component.data + + BreakpointParam = _breakpoint_param(dir) + breakpoint_param_container = get_parameter(container, BreakpointParam, T) + breakpoint_param_arr = get_parameter_column_refs(breakpoint_param_container, name) + breakpoint_param_mult = get_multiplier_array(breakpoint_param_container) + @assert size(breakpoint_param_arr) == size(breakpoint_param_mult[name, :, :]) + breakpoint_cost_component = + breakpoint_param_arr[:, time] .* breakpoint_param_mult[name, :, time] + breakpoint_cost_component = breakpoint_cost_component.data + + @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 + return breakpoint_cost_component, slope_cost_component, PSY.UnitSystem.NATURAL_UNITS +end + ################################################################################# # Section 11: PWL Objective Terms + Variable Objective Formulation (generic) # Load formulation overloads (AbstractControllablePowerLoadFormulation) are in POM. diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 194d8f80..92ae44e4 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -376,6 +376,8 @@ function _get_piecewise_curve_per_system_unit( end is_time_variant(::IS.TimeSeriesKey) = true +is_time_variant(x::IS.ProductionVariableCostCurve) = IS.is_time_series_backed(x) +is_time_variant(x::IS.ValueCurve) = IS.is_time_series_backed(x) is_time_variant(::Any) = false function create_temporary_cost_function_in_system_per_unit( diff --git a/test/InfrastructureOptimizationModelsTests.jl b/test/InfrastructureOptimizationModelsTests.jl index 82970458..3a53856c 100644 --- a/test/InfrastructureOptimizationModelsTests.jl +++ b/test/InfrastructureOptimizationModelsTests.jl @@ -171,6 +171,8 @@ function run_tests() @info "Starting integration tests..." # --- operation/ subfolder --- include(joinpath(TEST_DIR, "test_model_store.jl")) + # --- objective_function/ subfolder --- + include(joinpath(TEST_DIR, "test_offer_curve_cost.jl")) end end diff --git a/test/test_offer_curve_cost.jl b/test/test_offer_curve_cost.jl new file mode 100644 index 00000000..4ad96214 --- /dev/null +++ b/test/test_offer_curve_cost.jl @@ -0,0 +1,387 @@ +#= +Tests for MarketBidCost / MarketBidTimeSeriesCost / ImportExportCost / +ImportExportTimeSeriesCost code paths in value_curve_cost.jl and start_up_shut_down.jl. + +Uses c_sys5_uc as a base system and attaches MBC/IEC costs with real time series. + +Exercises: accessor wrappers, detection predicates, _has_parameter_time_series, +validation, _shutdown_cost_value, _get_parameter_field, get_initial_input, +validate_occ_breakpoints_slopes, validate_occ_component. +=# + +import PowerSystemCaseBuilder: PSITestSystems +using DataStructures: OrderedDict + +# ─── system builders ────────────────────────────────────────────────────────── + +"""Build a deterministic time series matching the system's forecast parameters.""" +function _make_ts(sys::PSY.System, name::String, value::Float64) + init_time = first(PSY.get_forecast_initial_times(sys)) + horizon = PSY.get_forecast_horizon(sys) + interval = PSY.get_forecast_interval(sys) + resolution = first(PSY.get_time_series_resolutions(sys)) + count = PSY.get_forecast_window_count(sys) + horizon_count = IS.get_horizon_count(horizon, resolution) + data = OrderedDict{Dates.DateTime, Vector{Float64}}() + for i in 0:(count - 1) + data[init_time + i * interval] = fill(value, horizon_count) + end + return PSY.Deterministic(; name = name, data = data, resolution = resolution) +end + +"""Build a deterministic time series of PiecewiseStepData matching the system's forecast params.""" +function _make_pwl_ts( + sys::PSY.System, + name::String, + breakpoints::Vector{Float64}, + slopes::Vector{Float64}, +) + init_time = first(PSY.get_forecast_initial_times(sys)) + horizon = PSY.get_forecast_horizon(sys) + interval = PSY.get_forecast_interval(sys) + resolution = first(PSY.get_time_series_resolutions(sys)) + count = PSY.get_forecast_window_count(sys) + horizon_count = IS.get_horizon_count(horizon, resolution) + psd = PSY.PiecewiseStepData(breakpoints, slopes) + data = OrderedDict{Dates.DateTime, Vector{PSY.PiecewiseStepData}}() + for i in 0:(count - 1) + data[init_time + i * interval] = fill(psd, horizon_count) + end + return PSY.Deterministic(; name = name, data = data, resolution = resolution) +end + +""" +Load c_sys5_uc, pick a ThermalStandard, give it a static MarketBidCost, +and return (sys, component). +""" +function _make_static_mbc_system(; + slopes = [25.0, 30.0], + breakpoints = [0.0, 50.0, 100.0], + initial_input = 10.0, + no_load = 5.0, + start_up = (hot = 100.0, warm = 200.0, cold = 300.0), + shut_down = 50.0, +) + sys = Logging.with_logger(Logging.NullLogger()) do + build_system(PSITestSystems, "c_sys5_uc") + end + comp = first(PSY.get_components(PSY.ThermalStandard, sys)) + incr_vc = PSY.PiecewiseIncrementalCurve( + PSY.PiecewiseStepData(breakpoints, slopes), initial_input, nothing) + decr_vc = PSY.PiecewiseIncrementalCurve( + PSY.PiecewiseStepData(breakpoints, reverse(slopes)), initial_input, nothing) + mbc = PSY.MarketBidCost(; + no_load_cost = PSY.LinearCurve(no_load), + start_up = start_up, + shut_down = PSY.LinearCurve(shut_down), + incremental_offer_curves = PSY.CostCurve(incr_vc), + decremental_offer_curves = PSY.CostCurve(decr_vc), + ) + PSY.set_operation_cost!(comp, mbc) + return sys, comp +end + +""" +Load c_sys5_uc, pick a ThermalStandard, give it a MarketBidTimeSeriesCost +with real time series attached, and return (sys, component). +""" +function _make_ts_mbc_system(; + slopes = [25.0, 30.0], + breakpoints = [0.0, 50.0, 100.0], + initial_input = 10.0, + no_load = 5.0, + start_up_val = 100.0, + shut_down = 50.0, +) + sys = Logging.with_logger(Logging.NullLogger()) do + build_system(PSITestSystems, "c_sys5_uc") + end + comp = first(PSY.get_components(PSY.ThermalStandard, sys)) + + # Create and attach time series + incr_ts = _make_pwl_ts(sys, "variable_cost incremental", breakpoints, slopes) + incr_key = PSY.add_time_series!(sys, comp, incr_ts) + incr_init_ts = _make_ts(sys, "initial_input incremental", initial_input) + incr_init_key = PSY.add_time_series!(sys, comp, incr_init_ts) + + decr_ts = _make_pwl_ts(sys, "variable_cost decremental", breakpoints, reverse(slopes)) + decr_key = PSY.add_time_series!(sys, comp, decr_ts) + decr_init_ts = _make_ts(sys, "initial_input decremental", initial_input) + decr_init_key = PSY.add_time_series!(sys, comp, decr_init_ts) + + no_load_ts = _make_ts(sys, "no_load_cost", no_load) + no_load_key = PSY.add_time_series!(sys, comp, no_load_ts) + + shut_down_ts = _make_ts(sys, "shut_down", shut_down) + shut_down_key = PSY.add_time_series!(sys, comp, shut_down_ts) + + startup_ts = _make_ts(sys, "start_up", start_up_val) + startup_key = PSY.add_time_series!(sys, comp, startup_ts) + + ts_mbc = PSY.MarketBidTimeSeriesCost(; + no_load_cost = PSY.TimeSeriesLinearCurve(no_load_key), + start_up = startup_key, + shut_down = PSY.TimeSeriesLinearCurve(shut_down_key), + incremental_offer_curves = PSY.make_market_bid_ts_curve(incr_key, incr_init_key), + decremental_offer_curves = PSY.make_market_bid_ts_curve(decr_key, decr_init_key), + ) + PSY.set_operation_cost!(comp, ts_mbc) + return sys, comp +end + +""" +Load c_sys5_uc, add a Source with static ImportExportCost, return (sys, source). +""" +function _make_static_iec_system() + sys = Logging.with_logger(Logging.NullLogger()) do + build_system(PSITestSystems, "c_sys5_uc") + end + bus = first(PSY.get_components(PSY.ACBus, sys)) + source = PSY.Source(; + name = "test_source", + available = true, + bus = bus, + active_power = 0.0, + reactive_power = 0.0, + active_power_limits = (min = -2.0, max = 2.0), + reactive_power_limits = (min = -2.0, max = 2.0), + R_th = 0.01, + X_th = 0.02, + internal_voltage = 1.0, + internal_angle = 0.0, + base_power = 100.0, + ) + import_curve = PSY.make_import_curve( + [0.0, 100.0, 105.0, 120.0, 200.0], [5.0, 10.0, 20.0, 40.0]) + export_curve = PSY.make_export_curve( + [0.0, 100.0, 105.0, 120.0, 200.0], [12.0, 8.0, 4.0, 1.0]) + iec = PSY.ImportExportCost(; + import_offer_curves = import_curve, + export_offer_curves = export_curve, + ) + PSY.set_operation_cost!(source, iec) + PSY.add_component!(sys, source) + return sys, source +end + +""" +Load c_sys5_uc, add a Source with ImportExportTimeSeriesCost backed by real TS, +return (sys, source). +""" +function _make_ts_iec_system() + sys = Logging.with_logger(Logging.NullLogger()) do + build_system(PSITestSystems, "c_sys5_uc") + end + bus = first(PSY.get_components(PSY.ACBus, sys)) + source = PSY.Source(; + name = "test_source", + available = true, + bus = bus, + active_power = 0.0, + reactive_power = 0.0, + active_power_limits = (min = -2.0, max = 2.0), + reactive_power_limits = (min = -2.0, max = 2.0), + R_th = 0.01, + X_th = 0.02, + internal_voltage = 1.0, + internal_angle = 0.0, + base_power = 100.0, + ) + PSY.add_component!(sys, source) + + im_ts = _make_pwl_ts( + sys, "variable_cost_import", + [0.0, 100.0, 105.0, 120.0, 200.0], [5.0, 10.0, 20.0, 40.0]) + im_key = PSY.add_time_series!(sys, source, im_ts) + ex_ts = _make_pwl_ts( + sys, "variable_cost_export", + [0.0, 100.0, 105.0, 120.0, 200.0], [12.0, 8.0, 4.0, 1.0]) + ex_key = PSY.add_time_series!(sys, source, ex_ts) + + ts_iec = PSY.ImportExportTimeSeriesCost(; + import_offer_curves = PSY.make_import_export_ts_curve(im_key), + export_offer_curves = PSY.make_import_export_ts_curve(ex_key), + ) + PSY.set_operation_cost!(source, ts_iec) + return sys, source +end + +# ─── tests ──────────────────────────────────────────────────────────────────── + +@testset "Offer Curve Cost: is_time_variant with new types" begin + _, comp_mbc = _make_static_mbc_system() + mbc = PSY.get_operation_cost(comp_mbc) + _, comp_ts = _make_ts_mbc_system() + ts_mbc = PSY.get_operation_cost(comp_ts) + _, source_iec = _make_static_iec_system() + iec = PSY.get_operation_cost(source_iec) + _, source_ts = _make_ts_iec_system() + ts_iec = PSY.get_operation_cost(source_ts) + + # Static → not time variant + @test !IOM.is_time_variant(PSY.get_incremental_offer_curves(mbc)) + @test !IOM.is_time_variant(PSY.get_import_offer_curves(iec)) + @test !IOM.is_time_variant(PSY.get_shut_down(mbc)) + @test !IOM.is_time_variant(PSY.get_start_up(mbc)) + + # TS → time variant + @test IOM.is_time_variant(PSY.get_incremental_offer_curves(ts_mbc)) + @test IOM.is_time_variant(PSY.get_import_offer_curves(ts_iec)) + @test IOM.is_time_variant(PSY.get_shut_down(ts_mbc)) + @test IOM.is_time_variant(PSY.get_start_up(ts_mbc)) +end + +@testset "Offer Curve Cost: _shutdown_cost_value" begin + @test IOM._shutdown_cost_value(42.0) == 42.0 + @test IOM._shutdown_cost_value(PSY.LinearCurve(99.0)) ≈ 99.0 + @test IOM._shutdown_cost_value(PSY.LinearCurve(0.0)) ≈ 0.0 +end + +@testset "Offer Curve Cost: Detection predicates on devices" begin + _, comp_mbc = _make_static_mbc_system() + @test IOM._has_market_bid_cost(comp_mbc) + @test !IOM._has_import_export_cost(comp_mbc) + @test IOM._has_offer_curve_cost(comp_mbc) + + _, comp_ts = _make_ts_mbc_system() + @test IOM._has_market_bid_cost(comp_ts) + @test !IOM._has_import_export_cost(comp_ts) + @test IOM._has_offer_curve_cost(comp_ts) + + _, source_iec = _make_static_iec_system() + @test !IOM._has_market_bid_cost(source_iec) + @test IOM._has_import_export_cost(source_iec) + @test IOM._has_offer_curve_cost(source_iec) + + _, source_ts = _make_ts_iec_system() + @test !IOM._has_market_bid_cost(source_ts) + @test IOM._has_import_export_cost(source_ts) + @test IOM._has_offer_curve_cost(source_ts) +end + +@testset "Offer Curve Cost: _has_parameter_time_series on devices" begin + _, comp_static = _make_static_mbc_system() + _, comp_ts = _make_ts_mbc_system() + _, source_static = _make_static_iec_system() + _, source_ts = _make_ts_iec_system() + + param = IOM.IncrementalPiecewiseLinearSlopeParameter() + @test !IOM._has_parameter_time_series(param, comp_static) + @test IOM._has_parameter_time_series(param, comp_ts) + @test !IOM._has_parameter_time_series(param, source_static) + @test IOM._has_parameter_time_series(param, source_ts) + + startup_param = IOM.StartupCostParameter() + @test !IOM._has_parameter_time_series(startup_param, comp_static) + @test IOM._has_parameter_time_series(startup_param, comp_ts) +end + +@testset "Offer Curve Cost: get_initial_input on devices" begin + _, comp = _make_static_mbc_system(; initial_input = 7.5) + @test IOM.get_initial_input(IOM.IncrementalOffer(), comp) ≈ 7.5 + @test IOM.get_initial_input(IOM.DecrementalOffer(), comp) ≈ 7.5 + + _, comp_ts = _make_ts_mbc_system(; initial_input = 12.0) + # TS path: initial_input is a TimeSeriesKey, not a Float64 + @test IOM.get_initial_input(IOM.IncrementalOffer(), comp_ts) isa IS.TimeSeriesKey + @test IOM.get_initial_input(IOM.DecrementalOffer(), comp_ts) isa IS.TimeSeriesKey +end + +@testset "Offer Curve Cost: validate_occ_breakpoints_slopes on devices" begin + # Static MBC: convex incremental curve should validate without error + _, comp = _make_static_mbc_system(; slopes = [25.0, 30.0, 35.0], + breakpoints = [0.0, 50.0, 80.0, 100.0]) + IOM.validate_occ_breakpoints_slopes(comp, IOM.IncrementalOffer()) + + # Static MBC: concave decremental curve should validate without error + IOM.validate_occ_breakpoints_slopes(comp, IOM.DecrementalOffer()) + + # TS MBC: should return immediately (no validation for TS) + _, comp_ts = _make_ts_mbc_system() + IOM.validate_occ_breakpoints_slopes(comp_ts, IOM.IncrementalOffer()) + IOM.validate_occ_breakpoints_slopes(comp_ts, IOM.DecrementalOffer()) +end + +@testset "Offer Curve Cost: validate_occ_component on devices" begin + _, comp = _make_static_mbc_system() + # Startup: static StartUpStages, should warn about multistart but not error + @test_logs (:warn, r"Multi-start") IOM.validate_occ_component( + IOM.StartupCostParameter(), comp) + # Shutdown: LinearCurve is valid + IOM.validate_occ_component(IOM.ShutdownCostParameter(), comp) + + # CostAtMin: should not error (simplified to no-op) + IOM.validate_occ_component(IOM.IncrementalCostAtMinParameter(), comp) + IOM.validate_occ_component(IOM.DecrementalCostAtMinParameter(), comp) + + # Breakpoints: validates the static curve + IOM.validate_occ_component( + IOM.IncrementalPiecewiseLinearBreakpointParameter(), comp) + IOM.validate_occ_component( + IOM.DecrementalPiecewiseLinearBreakpointParameter(), comp) + + # TS MBC: startup validation should return immediately (skip TS) + _, comp_ts = _make_ts_mbc_system() + IOM.validate_occ_component(IOM.StartupCostParameter(), comp_ts) + IOM.validate_occ_component(IOM.ShutdownCostParameter(), comp_ts) +end + +@testset "Offer Curve Cost: Validation errors (static IEC)" begin + _, source = _make_static_iec_system() + iec = PSY.get_operation_cost(source) + + # Valid IEC should not error + IOM._validate_occ_subtype(iec, IOM.IncrementalOffer(), + PSY.get_import_offer_curves(iec), "test") + + # IEC with non-zero VOM: error + bad_import = PSY.CostCurve( + PSY.PiecewiseIncrementalCurve( + PSY.PiecewiseStepData([0.0, 100.0], [10.0]), 0.0, nothing), + PSY.UnitSystem.NATURAL_UNITS, + PSY.LinearCurve(5.0), + ) + bad_iec = PSY.ImportExportCost(; import_offer_curves = bad_import, + export_offer_curves = PSY.make_export_curve(100.0, 10.0)) + @test_throws ArgumentError IOM._validate_occ_subtype( + bad_iec, IOM.IncrementalOffer(), bad_import, "test") + + # IEC with non-zero first breakpoint: error + bad_import2 = PSY.CostCurve( + PSY.PiecewiseIncrementalCurve( + PSY.PiecewiseStepData([10.0, 100.0], [10.0]), 0.0, nothing)) + bad_iec2 = PSY.ImportExportCost(; import_offer_curves = bad_import2, + export_offer_curves = PSY.make_export_curve(100.0, 10.0)) + @test_throws ArgumentError IOM._validate_occ_subtype( + bad_iec2, IOM.IncrementalOffer(), bad_import2, "test") +end + +@testset "Offer Curve Cost: TS curve properties (MBC)" begin + _, comp = _make_ts_mbc_system() + ts_mbc = PSY.get_operation_cost(comp) + incr = PSY.get_incremental_offer_curves(ts_mbc) + decr = PSY.get_decremental_offer_curves(ts_mbc) + + @test IS.is_time_series_backed(incr) + @test IS.is_time_series_backed(decr) + @test IOM.is_time_variant(incr) + @test IOM.is_time_variant(decr) + + vc = PSY.get_value_curve(incr) + @test IS.get_time_series_key(vc) isa IS.TimeSeriesKey + @test IS.get_initial_input(vc) isa IS.TimeSeriesKey +end + +@testset "Offer Curve Cost: TS curve properties (IEC)" begin + _, source = _make_ts_iec_system() + ts_iec = PSY.get_operation_cost(source) + im = PSY.get_import_offer_curves(ts_iec) + ex = PSY.get_export_offer_curves(ts_iec) + + @test IS.is_time_series_backed(im) + @test IS.is_time_series_backed(ex) + + # IEC curves have no initial_input + @test IS.get_initial_input(PSY.get_value_curve(im)) === nothing + @test IS.get_initial_input(PSY.get_value_curve(ex)) === nothing +end diff --git a/test/test_utils/add_market_bid_cost.jl b/test/test_utils/add_market_bid_cost.jl index 5a70f9d1..3fc010c4 100644 --- a/test/test_utils/add_market_bid_cost.jl +++ b/test/test_utils/add_market_bid_cost.jl @@ -14,9 +14,9 @@ function add_mbc_inner!( error("At least one of incr_curve or decr_curve must be provided") end mbc = MarketBidCost(; - no_load_cost = 0.0, + no_load_cost = LinearCurve(0.0), start_up = (hot = 0.0, warm = 0.0, cold = 0.0), - shut_down = 0.0, + shut_down = LinearCurve(0.0), ) if !isnothing(decr_curve) set_decremental_offer_curves!(mbc, CostCurve(decr_curve)) @@ -82,7 +82,9 @@ function get_deterministic_ts(sys::PSY.System) end """ -Extend the MarketBidCost objects attached to the selected components such that they're determined by a time series. +Convert the static `MarketBidCost` on each selected component to a +`MarketBidTimeSeriesCost` whose offer curves, initial inputs, no-load cost, and +shut-down cost are backed by time series. # Arguments: @@ -112,9 +114,10 @@ function extend_mbc!( do_override_min_x::Bool = false, ) @assert !isempty(get_components(active_components, sys)) "No components selected" - # incremental_initial_input is cost at minimum generation, NOT cost at zero generation for comp in get_components(active_components, sys) op_cost = get_operation_cost(comp) + @assert op_cost isa MarketBidCost + if do_override_min_x && :active_power_limits in fieldnames(typeof(comp)) min_power = with_units_base(sys, UnitSystem.NATURAL_UNITS) do get_active_power_limits(comp).min @@ -123,29 +126,15 @@ function extend_mbc!( min_power = nothing end - @assert op_cost isa MarketBidCost - for (getter, setter_initial, setter_curves, incr_or_decr) in ( - ( - get_incremental_offer_curves, - set_incremental_initial_input!, - set_incremental_offer_curves!, - "incremental", - ), - ( - get_decremental_offer_curves, - set_decremental_initial_input!, - set_decremental_offer_curves!, - "decremental", - ), + # Build TS-backed offer curves for each direction + ts_curves = Dict{String, CostCurve{TimeSeriesPiecewiseIncrementalCurve}}() + for (getter, incr_or_decr) in ( + (get_incremental_offer_curves, "incremental"), + (get_decremental_offer_curves, "decremental"), ) cost_curve = getter(op_cost) - isnothing(cost_curve) && continue - baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve - baseline_initial = get_initial_input(baseline) - if zero_cost_at_min - baseline_initial = 0.0 - end + baseline_initial = zero_cost_at_min ? 0.0 : get_initial_input(baseline) baseline_pwl = get_function_data(baseline) if do_override_min_x && isnothing(min_power) min_power = first(get_x_coords(baseline_pwl)) @@ -153,7 +142,7 @@ function extend_mbc!( !isnothing(modify_baseline_pwl) && (baseline_pwl = modify_baseline_pwl(baseline_pwl)) - # primes for easier attribution + incr_initial = initial_varies ? (0.11, 0.05) : (0.0, 0.0) incr_x = breakpoints_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0) incr_y = slopes_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0) @@ -183,9 +172,30 @@ function extend_mbc!( ) initial_key = add_time_series!(sys, comp, my_initial_ts) curve_key = add_time_series!(sys, comp, my_pwl_ts) - setter_initial(op_cost, initial_key) - setter_curves(op_cost, curve_key) + + ts_curves[incr_or_decr] = + make_market_bid_ts_curve(curve_key, initial_key) end + + # Build constant TS for no_load_cost and shut_down (wrapped as TimeSeriesLinearCurve) + baseline_no_load = IS.get_proportional_term(get_no_load_cost(op_cost)) + no_load_ts = make_deterministic_ts(sys, "no_load_cost", baseline_no_load, 0.0, 0.0) + no_load_key = add_time_series!(sys, comp, no_load_ts) + + baseline_shut_down = IS.get_proportional_term(get_shut_down(op_cost)) + shut_down_ts = + make_deterministic_ts(sys, "shut_down", baseline_shut_down, 0.0, 0.0) + shut_down_key = add_time_series!(sys, comp, shut_down_ts) + + new_cost = MarketBidTimeSeriesCost(; + no_load_cost = TimeSeriesLinearCurve(no_load_key), + start_up = get_start_up(op_cost), + shut_down = TimeSeriesLinearCurve(shut_down_key), + incremental_offer_curves = ts_curves["incremental"], + decremental_offer_curves = ts_curves["decremental"], + ancillary_service_offers = get_ancillary_service_offers(op_cost), + ) + set_operation_cost!(comp, new_cost) end end diff --git a/test/test_utils/iec_simulation_utils.jl b/test/test_utils/iec_simulation_utils.jl index af967731..044172aa 100644 --- a/test/test_utils/iec_simulation_utils.jl +++ b/test/test_utils/iec_simulation_utils.jl @@ -104,8 +104,17 @@ function make_5_bus_with_ie_ts( im_key = add_time_series!(sys, source, im_ts) ex_key = add_time_series!(sys, source, ex_ts) - set_import_offer_curves!(oc, im_key) - set_export_offer_curves!(oc, ex_key) + # Build ImportExportTimeSeriesCost with TS-backed curves + im_ts_curve = make_import_export_ts_curve(im_key) + ex_ts_curve = make_import_export_ts_curve(ex_key) + ts_cost = ImportExportTimeSeriesCost(; + import_offer_curves = im_ts_curve, + export_offer_curves = ex_ts_curve, + energy_import_weekly_limit = get_energy_import_weekly_limit(oc), + energy_export_weekly_limit = get_energy_export_weekly_limit(oc), + ancillary_service_offers = get_ancillary_service_offers(oc), + ) + set_operation_cost!(source, ts_cost) return sys end @@ -206,13 +215,14 @@ function cost_due_to_time_varying_iec( @assert all(power_in_df.DateTime .== power_out_df.DateTime) @assert any([ - get_operation_cost(comp) isa ImportExportCost for + get_operation_cost(comp) isa + Union{ImportExportCost, ImportExportTimeSeriesCost} for comp in get_components(T, sys) ]) for gen_name in gen_names comp = get_component(T, sys, gen_name) cost = PSY.get_operation_cost(comp) - (cost isa ImportExportCost) || continue + (cost isa Union{ImportExportCost, ImportExportTimeSeriesCost}) || continue step_df[!, gen_name] .= 0.0 # imports = addition of power = power flowing out of the device # exports = reduction of power = power flowing into the device @@ -221,7 +231,7 @@ function cost_due_to_time_varying_iec( (-1.0, power_in_df, PSY.get_export_offer_curves), ) offer_curves = getter(cost) - if PSI.is_time_variant(offer_curves) + if IS.is_time_series_backed(offer_curves) vc_ts = getter(comp, cost; start_time = step_dt) @assert all(unique(power_df.DateTime) .== TimeSeries.timestamp(vc_ts)) step_df[!, gen_name] .+= diff --git a/test/test_utils/mbc_system_utils.jl b/test/test_utils/mbc_system_utils.jl index 561f2d26..6b058e50 100644 --- a/test/test_utils/mbc_system_utils.jl +++ b/test/test_utils/mbc_system_utils.jl @@ -102,7 +102,7 @@ function tweak_system!(sys::System, load_pow_mult, therm_pow_mult, therm_price_m # replace with type of component? for therm in get_components(ThermalStandard, sys) op_cost = get_operation_cost(therm) - op_cost isa MarketBidCost && continue + op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} && continue with_units_base(sys, UnitSystem.DEVICE_BASE) do old_limits = get_active_power_limits(therm) new_limits = (min = old_limits.min, max = old_limits.max * therm_pow_mult) @@ -130,22 +130,14 @@ tweak_for_startup_shutdown!(sys::System) = tweak_system!(sys::System, 0.8, 1.0, tweak_for_decremental_initial!(sys::PSY.System) = tweak_system!(sys, 1.0, 1.2, 0.5) -"""Transfer the market bid cost from old_comp to new_comp, copying any time series in the process.""" +"""Transfer the market bid cost from old_comp to new_comp.""" function transfer_mbc!( new_comp::PSY.Device, old_comp::PSY.Device, - new_sys::PSY.System, + ::PSY.System, ) mbc = deepcopy(get_operation_cost(old_comp)) - @assert mbc isa PSY.MarketBidCost - for field in fieldnames(PSY.MarketBidCost) - val = getfield(mbc, field) - if val isa IS.TimeSeriesKey - ts = PSY.get_time_series(old_comp, val) - new_ts_key = add_time_series!(new_sys, new_comp, deepcopy(ts)) - setfield!(mbc, field, new_ts_key) - end - end + @assert mbc isa PSY.MarketBidCost # static MBC has no embedded TS keys to transfer set_operation_cost!(new_comp, mbc) return end @@ -153,53 +145,50 @@ end function zero_out_startup_shutdown_costs!(comp::PSY.Device) op_cost = get_operation_cost(comp)::MarketBidCost set_start_up!(op_cost, (hot = 0.0, warm = 0.0, cold = 0.0)) - set_shut_down!(op_cost, 0.0) + set_shut_down!(op_cost, LinearCurve(0.0)) end """Set everything except the incremental_offer_curves to zero on the MarketBidCost attached to the unit.""" function zero_out_non_incremental_curve!(sys::PSY.System, unit::PSY.Component) cost = deepcopy(get_operation_cost(unit)::MarketBidCost) - set_no_load_cost!(cost, 0.0) + set_no_load_cost!(cost, LinearCurve(0.0)) set_start_up!(cost, (hot = 0.0, warm = 0.0, cold = 0.0)) - set_shut_down!(cost, 0.0) + set_shut_down!(cost, LinearCurve(0.0)) # set minimum generation cost (but not min gen power) to zero. - if get_incremental_offer_curves(cost) isa IS.TimeSeriesKey - zero_ts = make_deterministic_ts(sys, "initial_input", 0.0, 0.0, 0.0) - zero_ts_key = add_time_series!(sys, unit, zero_ts) - set_incremental_initial_input!(cost, zero_ts_key) - else - base_curve = get_value_curve(get_incremental_offer_curves(cost)) - x_coords = get_x_coords(base_curve) - slopes = get_slopes(base_curve) - new_curve = PiecewiseIncrementalCurve(0.0, x_coords, slopes) - set_incremental_offer_curves!(cost, CostCurve(new_curve)) - end + base_curve = get_value_curve(get_incremental_offer_curves(cost)) + x_coords = get_x_coords(base_curve) + slopes = get_slopes(base_curve) + new_curve = PiecewiseIncrementalCurve(0.0, x_coords, slopes) + set_incremental_offer_curves!(cost, CostCurve(new_curve)) set_operation_cost!(unit, cost) end -"Set the no_load_cost to `nothing` and the initial_input to the old no_load_cost. Not designed for time series" +"Move the no_load_cost into the initial_input of the incremental offer curve. Not designed for time series." function no_load_to_initial_input!(comp::Generator) cost = get_operation_cost(comp)::MarketBidCost - no_load = PSY.get_no_load_cost(cost) + no_load = IS.get_proportional_term(PSY.get_no_load_cost(cost)) old_fd = get_function_data( get_value_curve(get_incremental_offer_curves(get_operation_cost(comp))), )::IS.PiecewiseStepData new_vc = PiecewiseIncrementalCurve(old_fd, no_load, nothing) set_incremental_offer_curves!(get_operation_cost(comp), CostCurve(new_vc)) - set_no_load_cost!(get_operation_cost(comp), nothing) + set_no_load_cost!(get_operation_cost(comp), LinearCurve(0.0)) return end no_load_to_initial_input!( sys::PSY.System, - sel = make_selector(x -> get_operation_cost(x) isa MarketBidCost, Generator), + sel = make_selector( + x -> get_operation_cost(x) isa Union{MarketBidCost, MarketBidTimeSeriesCost}, + Generator, + ), ) = no_load_to_initial_input!.(get_components(sel, sys)) "Set all MBC thermal unit min active powers to their min breakpoints" function adjust_min_power!(sys) for comp in get_components(Union{ThermalStandard, ThermalMultiStart}, sys) op_cost = get_operation_cost(comp) - op_cost isa MarketBidCost || continue + op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} || continue cost_curve = get_incremental_offer_curves(op_cost)::CostCurve baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve x_coords = get_x_coords(get_function_data(baseline)) @@ -210,15 +199,16 @@ function adjust_min_power!(sys) end """ -Add startup and shutdown time series to a certain component. `with_increments`: whether the -elements should be increasing over time or constant. Version A: designed for -`c_fixed_market_bid_cost`. +Convert a component's MarketBidCost to MarketBidTimeSeriesCost with startup and shutdown +time series. `with_increments`: whether the elements should be increasing over time or +constant. Version A: designed for `c_fixed_market_bid_cost`. """ function add_startup_shutdown_ts_a!(sys::System, with_increments::Bool) res_incr = with_increments ? 0.05 : 0.0 interval_incr = with_increments ? 0.01 : 0.0 unit1 = get_component(ThermalStandard, sys, "Test Unit1") - @assert get_operation_cost(unit1) isa MarketBidCost + op_cost = get_operation_cost(unit1) + @assert op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} startup_ts_1 = make_deterministic_ts( sys, "start_up", @@ -226,24 +216,29 @@ function add_startup_shutdown_ts_a!(sys::System, with_increments::Bool) res_incr, interval_incr, ) - set_start_up!(sys, unit1, startup_ts_1) shutdown_ts_1 = make_deterministic_ts(sys, "shut_down", 0.5, res_incr, interval_incr) - set_shut_down!(sys, unit1, shutdown_ts_1) + _convert_to_ts_mbc!(sys, unit1, op_cost, startup_ts_1, shutdown_ts_1) return startup_ts_1, shutdown_ts_1 end """ -Add startup and shutdown time series to a certain component. `with_increments`: whether the -elements should be increasing over time or constant. Version B: designed for `c_sys5_pglib`. +Convert a component's MarketBidCost to MarketBidTimeSeriesCost with startup and shutdown +time series. `with_increments`: whether the elements should be increasing over time or +constant. Version B: designed for `c_sys5_pglib`. """ function add_startup_shutdown_ts_b!(sys::System, with_increments::Bool) res_incr = with_increments ? 0.05 : 0.0 interval_incr = with_increments ? 0.01 : 0.0 unit1 = get_component(ThermalMultiStart, sys, "115_STEAM_1") - base_startup = Tuple(get_start_up(get_operation_cost(unit1))) - base_shutdown = get_shut_down(get_operation_cost(unit1)) - @assert get_operation_cost(unit1) isa MarketBidCost + op_cost = get_operation_cost(unit1) + @assert op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} + base_startup = Tuple(get_start_up(op_cost)) + base_shutdown = if op_cost isa MarketBidCost + IS.get_proportional_term(get_shut_down(op_cost)) + else + get_shut_down(op_cost) # already TS or scalar + end startup_ts_1 = make_deterministic_ts( sys, "start_up", @@ -251,7 +246,6 @@ function add_startup_shutdown_ts_b!(sys::System, with_increments::Bool) res_incr, interval_incr, ) - set_start_up!(sys, unit1, startup_ts_1) shutdown_ts_1 = make_deterministic_ts( sys, @@ -260,10 +254,92 @@ function add_startup_shutdown_ts_b!(sys::System, with_increments::Bool) res_incr, interval_incr, ) - set_shut_down!(sys, unit1, shutdown_ts_1) + _convert_to_ts_mbc!(sys, unit1, op_cost, startup_ts_1, shutdown_ts_1) return startup_ts_1, shutdown_ts_1 end +""" +Helper: convert a static MarketBidCost to MarketBidTimeSeriesCost, attaching the given +startup and shutdown time series. If already a MarketBidTimeSeriesCost, update in place. +Offer curves are converted to TS-backed with constant values; no_load_cost gets a constant TS. +""" +function _convert_to_ts_mbc!( + sys::System, + comp::PSY.Device, + op_cost::MarketBidCost, + startup_ts::Deterministic, + shutdown_ts::Deterministic, +) + startup_key = add_time_series!(sys, comp, startup_ts) + shutdown_key = add_time_series!(sys, comp, shutdown_ts) + + # Convert offer curves to TS-backed with constant values + local incr_curve, decr_curve + for (getter, incr_or_decr) in ( + (get_incremental_offer_curves, "incremental"), + (get_decremental_offer_curves, "decremental"), + ) + cost_curve = getter(op_cost) + baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve + baseline_pwl = get_function_data(baseline) + baseline_initial = get_initial_input(baseline) + + curve_ts = make_deterministic_ts( + sys, + "variable_cost $(incr_or_decr)", + baseline_pwl, + (0.0, 0.0, 0.0), + (0.0, 0.0, 0.0), + ) + curve_key = add_time_series!(sys, comp, curve_ts) + + if !isnothing(baseline_initial) + initial_ts = make_deterministic_ts( + sys, "initial_input $(incr_or_decr)", baseline_initial, 0.0, 0.0) + initial_key = add_time_series!(sys, comp, initial_ts) + else + initial_key = nothing + end + + if incr_or_decr == "incremental" + incr_curve = make_market_bid_ts_curve(curve_key, initial_key) + else + decr_curve = make_market_bid_ts_curve(curve_key, initial_key) + end + end + + # no_load_cost as constant TS + baseline_no_load = IS.get_proportional_term(get_no_load_cost(op_cost)) + no_load_ts = make_deterministic_ts(sys, "no_load_cost", baseline_no_load, 0.0, 0.0) + no_load_key = add_time_series!(sys, comp, no_load_ts) + + new_cost = MarketBidTimeSeriesCost(; + no_load_cost = TimeSeriesLinearCurve(no_load_key), + start_up = startup_key, + shut_down = TimeSeriesLinearCurve(shutdown_key), + incremental_offer_curves = incr_curve, + decremental_offer_curves = decr_curve, + ancillary_service_offers = get_ancillary_service_offers(op_cost), + ) + set_operation_cost!(comp, new_cost) + return +end + +function _convert_to_ts_mbc!( + sys::System, + comp::PSY.Device, + op_cost::MarketBidTimeSeriesCost, + startup_ts::Deterministic, + shutdown_ts::Deterministic, +) + # Already a TS cost — just update startup/shutdown + startup_key = add_time_series!(sys, comp, startup_ts) + shutdown_key = add_time_series!(sys, comp, shutdown_ts) + set_start_up!(op_cost, startup_key) + set_shut_down!(op_cost, TimeSeriesLinearCurve(shutdown_key)) + return +end + # functions for building the systems: calls the above function load_and_fix_system(args...; kwargs...) @@ -352,7 +428,7 @@ function remove_thermal_mbcs!(sys::PSY.System) new_op_cost = ThermalGenerationCost(; variable = get_incremental_offer_curves(old_cost), start_up = get_start_up(old_cost), - shut_down = get_shut_down(old_cost), + shut_down = IS.get_proportional_term(get_shut_down(old_cost)), fixed = 0.0, ) set_operation_cost!(comp, new_op_cost) @@ -462,9 +538,9 @@ function create_multistart_sys( set_operation_cost!( ms_comp, MarketBidCost(; - no_load_cost = nothing, + no_load_cost = LinearCurve(0.0), start_up = (hot = 300.0, warm = 450.0, cold = 500.0), - shut_down = 100.0, + shut_down = LinearCurve(100.0), incremental_offer_curves = CostCurve(new_ic), ), ) From cc2f88d05d4cc4c54cffa9b71c3019da1ac4e324 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 13 Apr 2026 13:51:28 -0600 Subject: [PATCH 02/19] add _shutdown_cost_value for LinearCurve shutdown field Static MarketBidCost.shut_down is now LinearCurve (was Float64). Extract the proportional term for the static shutdown cost path. ThermalGenerationCost.shut_down remains Float64. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/objective_function/start_up_shut_down.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/objective_function/start_up_shut_down.jl b/src/objective_function/start_up_shut_down.jl index cffa61fd..971752b8 100644 --- a/src/objective_function/start_up_shut_down.jl +++ b/src/objective_function/start_up_shut_down.jl @@ -5,6 +5,11 @@ struct StartupCostParameter <: ObjectiveFunctionParameter end "Parameter to define shutdown cost time series" struct ShutdownCostParameter <: ObjectiveFunctionParameter end +# Extract the scalar shutdown cost from either a Float64 (ThermalGenerationCost) or +# a LinearCurve (MarketBidCost) whose proportional term is the cost. +_shutdown_cost_value(x::Float64) = x +_shutdown_cost_value(x::IS.LinearCurve) = IS.get_proportional_term(x) + function add_shut_down_cost!( container::OptimizationContainer, ::U, @@ -27,7 +32,7 @@ function add_shut_down_cost!( get_parameter_multiplier_array(container, ShutdownCostParameter, T) param[name, t] * mult[name, t] else - get_shut_down(get_operation_cost(d)) + _shutdown_cost_value(get_shut_down(get_operation_cost(d))) end iszero(cost_term) && continue rate = cost_term * multiplier @@ -115,6 +120,6 @@ function get_startup_cost_value( else get_start_up(get_operation_cost(component)) end - # possible types for raw_startup_cost: Float, AffExpr, NamedTuple, or StartUpStages. + # possible types for raw_startup_cost: Float, AffExpr, NamedTuple, or StartUpStages. return start_up_cost(raw_startup_cost, V, T(), U()) end From 8a9b33ffe69d93fcb7f6d25fa85e37408406f65c Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 13 Apr 2026 14:12:26 -0600 Subject: [PATCH 03/19] refactor startup/shutdown costs: trait-based dispatch, split mock cost types Replace is_time_variant runtime checks in startup/shutdown with _is_time_series_cost trait dispatched at the function barrier. Julia specializes on the op_cost type parameter, so the branch is resolved at compile time for concrete cost types. Split MockOperationCost into static MockOperationCost (Float64 fields) and MockTimeSeriesOperationCost (signals "read from parameters"). The mock TS type opts into the TS path via _is_time_series_cost trait, avoiding coupling to PSY types. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/objective_function/start_up_shut_down.jl | 120 ++++++++++--------- test/mocks/mock_components.jl | 30 ++++- test/test_start_up_shut_down.jl | 29 ++--- 3 files changed, 101 insertions(+), 78 deletions(-) diff --git a/src/objective_function/start_up_shut_down.jl b/src/objective_function/start_up_shut_down.jl index 971752b8..5dbcbf4b 100644 --- a/src/objective_function/start_up_shut_down.jl +++ b/src/objective_function/start_up_shut_down.jl @@ -10,6 +10,15 @@ struct ShutdownCostParameter <: ObjectiveFunctionParameter end _shutdown_cost_value(x::Float64) = x _shutdown_cost_value(x::IS.LinearCurve) = IS.get_proportional_term(x) +# Trait: does this cost type store startup/shutdown in time-series parameters? +# Overridden for PSY.MarketBidTimeSeriesCost; duck-typeable by mocks. +_is_time_series_cost(::PSY.MarketBidTimeSeriesCost) = true +_is_time_series_cost(::IS.DeviceParameter) = false + +################################################################################# +# Shutdown cost +################################################################################# + function add_shut_down_cost!( container::OptimizationContainer, ::U, @@ -23,32 +32,48 @@ function add_shut_down_cost!( multiplier = objective_function_multiplier(U(), V()) for d in devices get_must_run(d) && continue - name = get_name(d) - add_as_time_variant = is_time_variant(get_shut_down(get_operation_cost(d))) + # Function barrier: op_cost type becomes a compile-time parameter + _add_shut_down_cost_per_device!( + container, U, T, get_name(d), get_operation_cost(d), multiplier) + end + return +end + +function _add_shut_down_cost_per_device!( + container::OptimizationContainer, + ::Type{U}, + ::Type{T}, + name::String, + op_cost::IS.DeviceParameter, + multiplier, +) where {U <: VariableType, T <: IS.InfrastructureSystemsComponent} + if _is_time_series_cost(op_cost) + param = get_parameter_array(container, ShutdownCostParameter, T) + mult = get_parameter_multiplier_array(container, ShutdownCostParameter, T) for t in get_time_steps(container) - cost_term = if add_as_time_variant - param = get_parameter_array(container, ShutdownCostParameter, T) - mult = - get_parameter_multiplier_array(container, ShutdownCostParameter, T) - param[name, t] * mult[name, t] - else - _shutdown_cost_value(get_shut_down(get_operation_cost(d))) - end + cost_term = param[name, t] * mult[name, t] iszero(cost_term) && continue rate = cost_term * multiplier variable = get_variable(container, U, T)[name, t] - if add_as_time_variant - add_cost_term_variant!( - container, variable, rate, ProductionCostExpression, T, name, t) - else - add_cost_term_invariant!( - container, variable, rate, ProductionCostExpression, T, name, t) - end + add_cost_term_variant!( + container, variable, rate, ProductionCostExpression, T, name, t) + end + else + cost_term = _shutdown_cost_value(get_shut_down(op_cost)) + iszero(cost_term) && return + rate = cost_term * multiplier + for t in get_time_steps(container) + variable = get_variable(container, U, T)[name, t] + add_cost_term_invariant!( + container, variable, rate, ProductionCostExpression, T, name, t) end end - return end +################################################################################# +# Startup cost +################################################################################# + function add_start_up_cost!( container::OptimizationContainer, ::U, @@ -60,19 +85,18 @@ function add_start_up_cost!( V <: AbstractDeviceFormulation, } for d in devices - op_cost_data = get_operation_cost(d) - _add_start_up_cost_to_objective!(container, U(), d, op_cost_data, V()) + # Function barrier: op_cost type becomes a compile-time parameter + _add_start_up_cost_to_objective!( + container, U(), d, get_operation_cost(d), V()) end return end -# op_cost is PSY.ThermalGenerationCost or PSY.MarketBidCost, but trying -# to avoid PSY types here. function _add_start_up_cost_to_objective!( container::OptimizationContainer, ::T, component::C, - op_cost, + op_cost::IS.DeviceParameter, ::U, ) where { T <: VariableType, @@ -82,44 +106,28 @@ function _add_start_up_cost_to_objective!( multiplier = objective_function_multiplier(T(), U()) get_must_run(component) && return name = get_name(component) - add_as_time_variant = is_time_variant(get_start_up(op_cost)) - for t in get_time_steps(container) - cost_term = get_startup_cost_value( - container, T(), component, U(), t, add_as_time_variant) - iszero(cost_term) && continue - rate = cost_term * multiplier - variable = get_variable(container, T, C)[name, t] - if add_as_time_variant + if _is_time_series_cost(op_cost) + param = get_parameter_array(container, StartupCostParameter, C) + mult = get_parameter_multiplier_array(container, StartupCostParameter, C) + for t in get_time_steps(container) + raw_startup_cost = param[name, t] * mult[name, t] + cost_term = start_up_cost(raw_startup_cost, C, T(), U()) + iszero(cost_term) && continue + rate = cost_term * multiplier + variable = get_variable(container, T, C)[name, t] add_cost_term_variant!( container, variable, rate, ProductionCostExpression, C, name, t) - else + end + else + raw_startup_cost = get_start_up(op_cost) + for t in get_time_steps(container) + cost_term = start_up_cost(raw_startup_cost, C, T(), U()) + iszero(cost_term) && continue + rate = cost_term * multiplier + variable = get_variable(container, T, C)[name, t] add_cost_term_invariant!( container, variable, rate, ProductionCostExpression, C, name, t) end end return end - -function get_startup_cost_value( - container::OptimizationContainer, - ::T, - component::V, - ::U, - time_period::Int, - is_time_variant_::Bool, -) where { - T <: VariableType, - V <: IS.InfrastructureSystemsComponent, - U <: AbstractDeviceFormulation, -} - raw_startup_cost = if is_time_variant_ - name = get_name(component) - param = get_parameter_array(container, StartupCostParameter, V) - mult = get_parameter_multiplier_array(container, StartupCostParameter, V) - param[name, time_period] * mult[name, time_period] - else - get_start_up(get_operation_cost(component)) - end - # possible types for raw_startup_cost: Float, AffExpr, NamedTuple, or StartUpStages. - return start_up_cost(raw_startup_cost, V, T(), U()) -end diff --git a/test/mocks/mock_components.jl b/test/mocks/mock_components.jl index dcc35947..b78fce95 100644 --- a/test/mocks/mock_components.jl +++ b/test/mocks/mock_components.jl @@ -17,13 +17,16 @@ const IS = InfrastructureSystems struct TestDeviceFormulation <: PSI.AbstractDeviceFormulation end struct TestPowerModel <: IS.Optimization.AbstractPowerModel end -# Mock operation cost for testing proportional cost functions -struct MockOperationCost +# Mock operation costs for testing objective function construction. +# Mirrors the PSY pattern: separate static and time-series types. + +"Static mock cost — all fields are scalars." +struct MockOperationCost <: IS.DeviceParameter proportional_term::Float64 is_time_variant::Bool fuel_cost::Float64 - start_up::Union{Float64, IS.TimeSeriesKey} - shut_down::Union{Float64, IS.TimeSeriesKey} + start_up::Float64 + shut_down::Float64 end MockOperationCost(proportional_term::Float64) = @@ -36,6 +39,21 @@ MockOperationCost(proportional_term::Float64, is_time_variant::Bool, fuel_cost:: IOM.get_start_up(c::MockOperationCost) = c.start_up IOM.get_shut_down(c::MockOperationCost) = c.shut_down +"Time-series mock cost — startup/shutdown come from parameter arrays, not fields." +struct MockTimeSeriesOperationCost <: IS.DeviceParameter + proportional_term::Float64 + fuel_cost::Float64 +end + +MockTimeSeriesOperationCost() = MockTimeSeriesOperationCost(0.0, 0.0) + +# Startup/shutdown values aren't stored on the cost object; they live in parameter containers. +# Return sentinel values that would error if accidentally used as costs. +IOM.get_start_up(::MockTimeSeriesOperationCost) = + error("MockTimeSeriesOperationCost: start_up should be read from parameters, not the cost object") +IOM.get_shut_down(::MockTimeSeriesOperationCost) = + error("MockTimeSeriesOperationCost: shut_down should be read from parameters, not the cost object") + # Abstract mock device type for testing rejection of abstract types in DeviceModel # Subtypes IS.InfrastructureSystemsComponent so they work with DeviceModel and container keys abstract type AbstractMockDevice <: IS.InfrastructureSystemsComponent end @@ -52,6 +70,8 @@ get_name(b::MockBus) = b.name get_number(b::MockBus) = b.number get_bustype(b::MockBus) = b.bustype +const MockOperationCostTypes = Union{MockOperationCost, MockTimeSeriesOperationCost} + # Mock Thermal Generator struct MockThermalGen <: AbstractMockGenerator name::String @@ -59,7 +79,7 @@ struct MockThermalGen <: AbstractMockGenerator bus::MockBus active_power_limits::NamedTuple{(:min, :max), Tuple{Float64, Float64}} base_power::Float64 - operation_cost::MockOperationCost + operation_cost::MockOperationCostTypes must_run::Bool end diff --git a/test/test_start_up_shut_down.jl b/test/test_start_up_shut_down.jl index 73d09f10..38a8dfe9 100644 --- a/test/test_start_up_shut_down.jl +++ b/test/test_start_up_shut_down.jl @@ -24,27 +24,24 @@ IOM.start_up_cost( ::TestDeviceFormulation, ) = cost -# Helper to create a MockThermalGen with specified startup/shutdown costs +# MockTimeSeriesOperationCost is the mock equivalent of MarketBidTimeSeriesCost +IOM._is_time_series_cost(::MockTimeSeriesOperationCost) = true + +# Helper to create a MockThermalGen with specified startup/shutdown costs (static) function make_thermal_with_costs( name::String; - startup_cost::Union{Float64, IS.TimeSeriesKey} = 0.0, - shutdown_cost::Union{Float64, IS.TimeSeriesKey} = 0.0, + startup_cost::Float64 = 0.0, + shutdown_cost::Float64 = 0.0, must_run::Bool = false, ) op_cost = MockOperationCost(0.0, false, 0.0, startup_cost, shutdown_cost) return make_mock_thermal(name; operation_cost = op_cost, must_run = must_run) end -# Helper to create a dummy TimeSeriesKey for triggering the time-variant path -function make_dummy_ts_key() - return IS.StaticTimeSeriesKey( - IS.SingleTimeSeries, - "cost", - Dates.DateTime(2024, 1, 1), - Dates.Hour(1), - 3, - Dict{String, Any}(), - ) +# Helper to create a MockThermalGen with time-series operation cost +function make_thermal_with_ts_costs(name::String; must_run::Bool = false) + op_cost = MockTimeSeriesOperationCost() + return make_mock_thermal(name; operation_cost = op_cost, must_run = must_run) end # Helper to set up container with variables for mock devices @@ -337,8 +334,7 @@ end @testset "add_shut_down_cost! time-variant path" begin time_steps = 1:3 - ts_key = make_dummy_ts_key() - device = make_thermal_with_costs("gen1"; shutdown_cost = ts_key) + device = make_thermal_with_ts_costs("gen1") devices = [device] container = setup_startup_shutdown_test_container( time_steps, @@ -379,8 +375,7 @@ end @testset "add_start_up_cost! time-variant path" begin time_steps = 1:3 - ts_key = make_dummy_ts_key() - device = make_thermal_with_costs("gen1"; startup_cost = ts_key) + device = make_thermal_with_ts_costs("gen1") devices = [device] container = setup_startup_shutdown_test_container( time_steps, From b5595e8b8e37117db917591caccef834f5c1b698 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 13 Apr 2026 16:19:11 -0600 Subject: [PATCH 04/19] format, cleanup: remove POM test utils, type and compile-time fixes - Remove mbc_system_utils.jl, add_market_bid_cost.jl, iec_simulation_utils.jl (POM test utilities, not called within IOM) - Make is_time_variant compile-time: dispatch on ValueCurve{<:TimeSeriesFunctionData} type parameter instead of calling IS.is_time_series_backed at runtime - Type op_cost as IS.DeviceParameter in startup/shutdown function signatures - Mock costs subtype IS.DeviceParameter to match PSY hierarchy - Consolidate CostAtMin validate_occ_component to single AbstractCostAtMinParameter method - Pass (Type, name) instead of component instance in _get_raw_pwl_data - Format Co-Authored-By: Claude Opus 4.6 (1M context) --- src/objective_function/value_curve_cost.jl | 34 +- src/utils/powersystems_utils.jl | 6 +- test/includes.jl | 2 - test/mocks/mock_components.jl | 21 +- test/test_utils/add_market_bid_cost.jl | 336 ------------- test/test_utils/iec_simulation_utils.jl | 280 ----------- test/test_utils/mbc_system_utils.jl | 550 --------------------- 7 files changed, 32 insertions(+), 1197 deletions(-) delete mode 100644 test/test_utils/add_market_bid_cost.jl delete mode 100644 test/test_utils/iec_simulation_utils.jl delete mode 100644 test/test_utils/mbc_system_utils.jl diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index 3c5071ea..dc7a594b 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -81,9 +81,13 @@ get_offer_curves(::DecrementalOffer, device::PSY.StaticInjection) = get_offer_curves(::IncrementalOffer, device::PSY.StaticInjection) = get_output_offer_curves(PSY.get_operation_cost(device)) get_initial_input(::DecrementalOffer, device::PSY.StaticInjection) = - IS.get_initial_input(PSY.get_value_curve(get_input_offer_curves(PSY.get_operation_cost(device)))) + IS.get_initial_input( + PSY.get_value_curve(get_input_offer_curves(PSY.get_operation_cost(device))), + ) get_initial_input(::IncrementalOffer, device::PSY.StaticInjection) = - IS.get_initial_input(PSY.get_value_curve(get_output_offer_curves(PSY.get_operation_cost(device)))) + IS.get_initial_input( + PSY.get_value_curve(get_output_offer_curves(PSY.get_operation_cost(device))), + ) # direction and cost curve (needed for VOM code path): get_offer_curves(::DecrementalOffer, op_cost::PSY.OfferCurveCost) = @@ -298,15 +302,8 @@ function validate_occ_component(::ShutdownCostParameter, device::PSY.StaticInjec end end -validate_occ_component( - ::IncrementalCostAtMinParameter, - device::PSY.StaticInjection, -) = nothing # consistency guaranteed by the static/TS type split - -validate_occ_component( - ::DecrementalCostAtMinParameter, - device::PSY.StaticInjection, -) = nothing # consistency guaranteed by the static/TS type split +# Consistency of initial_input vs offer curves is guaranteed by the static/TS type split +validate_occ_component(::AbstractCostAtMinParameter, ::PSY.StaticInjection) = nothing validate_occ_component( ::IncrementalPiecewiseLinearBreakpointParameter, @@ -410,9 +407,10 @@ function _get_pwl_data( component::T, time::Int, ) where {T <: PSY.Component} + name = PSY.get_name(component) cost_data = get_offer_curves(dir, component) breakpoint_cost_component, slope_cost_component, unit_system = - _get_raw_pwl_data(dir, container, component, cost_data, time) + _get_raw_pwl_data(dir, container, T, name, cost_data, time) breakpoints, slopes = get_piecewise_curve_per_system_unit( breakpoint_cost_component, @@ -428,26 +426,26 @@ end function _get_raw_pwl_data( ::OfferDirection, ::OptimizationContainer, - ::T, + ::Type{<:PSY.Component}, + ::String, cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, ::Int, -) where {T <: PSY.Component} +) cost_component = PSY.get_function_data(PSY.get_value_curve(cost_data)) return PSY.get_x_coords(cost_component), PSY.get_y_coords(cost_component), PSY.get_power_units(cost_data) end -# time-series curve: read from parameter arrays (already initialized) +# time-series curve: read from parameter arrays function _get_raw_pwl_data( dir::OfferDirection, container::OptimizationContainer, - component::T, + ::Type{T}, + name::String, ::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}, time::Int, ) where {T <: PSY.Component} - name = PSY.get_name(component) - SlopeParam = _slope_param(dir) slope_param_arr = get_parameter_array(container, SlopeParam, T) slope_param_mult = get_parameter_multiplier_array(container, SlopeParam, T) diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 92ae44e4..9dfcc429 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -376,8 +376,10 @@ function _get_piecewise_curve_per_system_unit( end is_time_variant(::IS.TimeSeriesKey) = true -is_time_variant(x::IS.ProductionVariableCostCurve) = IS.is_time_series_backed(x) -is_time_variant(x::IS.ValueCurve) = IS.is_time_series_backed(x) +is_time_variant(::IS.ValueCurve{<:IS.TimeSeriesFunctionData}) = true +is_time_variant( + ::IS.ProductionVariableCostCurve{<:IS.ValueCurve{<:IS.TimeSeriesFunctionData}}, +) = true is_time_variant(::Any) = false function create_temporary_cost_function_in_system_per_unit( diff --git a/test/includes.jl b/test/includes.jl index dfaa6914..3467496c 100644 --- a/test/includes.jl +++ b/test/includes.jl @@ -48,8 +48,6 @@ include("test_utils/mock_operation_models.jl") include("test_utils/solver_definitions.jl") include("test_utils/operations_problem_templates.jl") include("test_utils/run_simulation.jl") -include("test_utils/add_market_bid_cost.jl") -include("test_utils/mbc_system_utils.jl") ENV["RUNNING_SIENNA_TESTS"] = "true" ENV["SIENNA_RANDOM_SEED"] = 1234 # Set a fixed seed for reproducibility in tests diff --git a/test/mocks/mock_components.jl b/test/mocks/mock_components.jl index b78fce95..b4d35c02 100644 --- a/test/mocks/mock_components.jl +++ b/test/mocks/mock_components.jl @@ -39,20 +39,23 @@ MockOperationCost(proportional_term::Float64, is_time_variant::Bool, fuel_cost:: IOM.get_start_up(c::MockOperationCost) = c.start_up IOM.get_shut_down(c::MockOperationCost) = c.shut_down -"Time-series mock cost — startup/shutdown come from parameter arrays, not fields." -struct MockTimeSeriesOperationCost <: IS.DeviceParameter - proportional_term::Float64 - fuel_cost::Float64 -end - -MockTimeSeriesOperationCost() = MockTimeSeriesOperationCost(0.0, 0.0) +""" +Time-series mock cost, paralleling PSY.MarketBidTimeSeriesCost. Has no fields because all +cost data (startup, shutdown, offer curves) lives in parameter containers populated +externally — by POM in real use, or by `add_test_parameter!` in the tests. +""" +struct MockTimeSeriesOperationCost <: IS.DeviceParameter end # Startup/shutdown values aren't stored on the cost object; they live in parameter containers. # Return sentinel values that would error if accidentally used as costs. IOM.get_start_up(::MockTimeSeriesOperationCost) = - error("MockTimeSeriesOperationCost: start_up should be read from parameters, not the cost object") + error( + "MockTimeSeriesOperationCost: start_up should be read from parameters, not the cost object", + ) IOM.get_shut_down(::MockTimeSeriesOperationCost) = - error("MockTimeSeriesOperationCost: shut_down should be read from parameters, not the cost object") + error( + "MockTimeSeriesOperationCost: shut_down should be read from parameters, not the cost object", + ) # Abstract mock device type for testing rejection of abstract types in DeviceModel # Subtypes IS.InfrastructureSystemsComponent so they work with DeviceModel and container keys diff --git a/test/test_utils/add_market_bid_cost.jl b/test/test_utils/add_market_bid_cost.jl deleted file mode 100644 index 3fc010c4..00000000 --- a/test/test_utils/add_market_bid_cost.jl +++ /dev/null @@ -1,336 +0,0 @@ -# WARNING: included in HydroPowerSimulations's tests as well. -# If you make changes, run those tests too! -""" -Add a MarketBidCost object to the selected components, with specified incremental and/or decremental cost curves. -""" -function add_mbc_inner!( - sys::PSY.System, - active_components::ComponentSelector; - incr_curve::Union{Nothing, PiecewiseIncrementalCurve} = nothing, - decr_curve::Union{Nothing, PiecewiseIncrementalCurve} = nothing, -) - @assert !isempty(get_components(active_components, sys)) "No components selected" - if isnothing(incr_curve) && isnothing(decr_curve) - error("At least one of incr_curve or decr_curve must be provided") - end - mbc = MarketBidCost(; - no_load_cost = LinearCurve(0.0), - start_up = (hot = 0.0, warm = 0.0, cold = 0.0), - shut_down = LinearCurve(0.0), - ) - if !isnothing(decr_curve) - set_decremental_offer_curves!(mbc, CostCurve(decr_curve)) - end - if !isnothing(incr_curve) - set_incremental_offer_curves!(mbc, CostCurve(incr_curve)) - end - for comp in get_components(active_components, sys) - set_operation_cost!(comp, mbc) - end -end - -""" -Add a MarketBidCost object to the selected components, with an incremental cost curve and/or -a decremental cost curve defined by hard-coded values. -""" -function add_mbc!( - sys::PSY.System, - active_components::ComponentSelector; - incremental::Bool = true, - decremental::Bool = false, -) - incr_slopes = 100 .* [0.3, 0.5, 0.7] - decr_slopes = 100 .* [0.7, 0.5, 0.3] - x_coords = [10.0, 30.0, 50.0, 100.0] - initial_input = 20.0 - - if !incremental && !decremental - error("At least one of incremental or decremental must be true") - end - if incremental - incr_curve = - PiecewiseIncrementalCurve(initial_input, x_coords, incr_slopes) - else - incr_curve = nothing - end - - if decremental - decr_curve = - PiecewiseIncrementalCurve(initial_input, x_coords, decr_slopes) - else - decr_curve = nothing - end - add_mbc_inner!(sys, active_components; incr_curve = incr_curve, decr_curve = decr_curve) -end - -""" -Get a deterministic or DeterministicSingleTimeSeries time series from the system. -""" -function get_deterministic_ts(sys::PSY.System) - for device in get_components(PSY.Device, sys) - if has_time_series(device, Union{DeterministicSingleTimeSeries, Deterministic}) - for key in PSY.get_time_series_keys(device) - ts = get_time_series(device, key) - if ts isa DeterministicSingleTimeSeries || ts isa Deterministic - return ts - end - end - end - end - @assert false "No Deterministic or DeterministicSingleTimeSeries found in system" - return DeterministicSingleTimeSeries(nothing) -end - -""" -Convert the static `MarketBidCost` on each selected component to a -`MarketBidTimeSeriesCost` whose offer curves, initial inputs, no-load cost, and -shut-down cost are backed by time series. - -# Arguments: - - - `initial_varies`: whether the initial input time series should have values that vary - over time (as opposed to a time series with constant values over time) - - `breakpoints_vary`: whether the breakpoints in the variable cost time series should vary - over time - - `slopes_vary`: whether the slopes of the variable cost time series should vary over time - - `active_components`: a `ComponentSelector` specifying which components should get time - series - - `initial_input_names_vary`: whether the initial input time series names should vary over - components - - `variable_cost_names_vary`: whether the variable cost time series names should vary over - components -""" -function extend_mbc!( - sys::PSY.System, - active_components::ComponentSelector; - modify_baseline_pwl = nothing, - initial_varies::Bool = false, - breakpoints_vary::Bool = false, - slopes_vary::Bool = false, - initial_input_names_vary::Bool = false, - variable_cost_names_vary::Bool = false, - zero_cost_at_min::Bool = false, - create_extra_tranches::Bool = false, - do_override_min_x::Bool = false, -) - @assert !isempty(get_components(active_components, sys)) "No components selected" - for comp in get_components(active_components, sys) - op_cost = get_operation_cost(comp) - @assert op_cost isa MarketBidCost - - if do_override_min_x && :active_power_limits in fieldnames(typeof(comp)) - min_power = with_units_base(sys, UnitSystem.NATURAL_UNITS) do - get_active_power_limits(comp).min - end - else - min_power = nothing - end - - # Build TS-backed offer curves for each direction - ts_curves = Dict{String, CostCurve{TimeSeriesPiecewiseIncrementalCurve}}() - for (getter, incr_or_decr) in ( - (get_incremental_offer_curves, "incremental"), - (get_decremental_offer_curves, "decremental"), - ) - cost_curve = getter(op_cost) - baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve - baseline_initial = zero_cost_at_min ? 0.0 : get_initial_input(baseline) - baseline_pwl = get_function_data(baseline) - if do_override_min_x && isnothing(min_power) - min_power = first(get_x_coords(baseline_pwl)) - end - - !isnothing(modify_baseline_pwl) && - (baseline_pwl = modify_baseline_pwl(baseline_pwl)) - - incr_initial = initial_varies ? (0.11, 0.05) : (0.0, 0.0) - incr_x = breakpoints_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0) - incr_y = slopes_vary ? (0.02, 0.07, 0.03) : (0.0, 0.0, 0.0) - - name_modifier = "_$(replace(get_name(comp), " " => "_"))_" - - initial_name = - "initial_input $(incr_or_decr)" * - (initial_input_names_vary ? name_modifier : "") - my_initial_ts = make_deterministic_ts( - sys, - initial_name, - baseline_initial, - incr_initial...; - ) - variable_name = - "variable_cost $(incr_or_decr)" * - (variable_cost_names_vary ? name_modifier : "") - my_pwl_ts = make_deterministic_ts( - sys, - variable_name, - baseline_pwl, - incr_x, - incr_y; - create_extra_tranches = create_extra_tranches, - override_min_x = do_override_min_x ? min_power : nothing, - ) - initial_key = add_time_series!(sys, comp, my_initial_ts) - curve_key = add_time_series!(sys, comp, my_pwl_ts) - - ts_curves[incr_or_decr] = - make_market_bid_ts_curve(curve_key, initial_key) - end - - # Build constant TS for no_load_cost and shut_down (wrapped as TimeSeriesLinearCurve) - baseline_no_load = IS.get_proportional_term(get_no_load_cost(op_cost)) - no_load_ts = make_deterministic_ts(sys, "no_load_cost", baseline_no_load, 0.0, 0.0) - no_load_key = add_time_series!(sys, comp, no_load_ts) - - baseline_shut_down = IS.get_proportional_term(get_shut_down(op_cost)) - shut_down_ts = - make_deterministic_ts(sys, "shut_down", baseline_shut_down, 0.0, 0.0) - shut_down_key = add_time_series!(sys, comp, shut_down_ts) - - new_cost = MarketBidTimeSeriesCost(; - no_load_cost = TimeSeriesLinearCurve(no_load_key), - start_up = get_start_up(op_cost), - shut_down = TimeSeriesLinearCurve(shut_down_key), - incremental_offer_curves = ts_curves["incremental"], - decremental_offer_curves = ts_curves["decremental"], - ancillary_service_offers = get_ancillary_service_offers(op_cost), - ) - set_operation_cost!(comp, new_cost) - end -end - -""" -Make a deterministic time series from a tuple or a float value. See below function for -details about the arguments. -""" -function make_deterministic_ts( - name::String, - ini_val::T, - res_incr::Number, - interval_incr::Number, - init_time::DateTime, - horizon::Period, - interval::Period, - window_count::Int, - resolution::Period, -) where {T <: Union{Number, Tuple}} - horizon_count = IS.get_horizon_count(horizon, resolution) - ts_data = OrderedDict{DateTime, Vector{T}}() - for i in 0:(window_count - 1) - if ini_val isa Tuple - series = [ - ini_val .+ (res_incr * j + i * interval_incr) for - j in 0:(horizon_count - 1) - ] - else - series = ini_val .+ res_incr .* (0:(horizon_count - 1)) .+ i * interval_incr - end - ts_data[init_time + i * interval] = series - end - return Deterministic(; - name = name, - data = ts_data, - resolution = resolution, - interval = interval, - ) -end - -""" -Create a deterministic time series with increments to the initial values, breakpoints, and slopes. -Here, the elements of `incrs_x` and `incrs_y` are tuples of three values, corresponding to: - -`tranche_incr`: increment between tranche breakpoints. -`res_incr`: increment within the forecast horizon window. -`interval_incr`: increment in baseline, between horizon windows. - -`override_min_x`: if provided, overrides the minimum x value in all piecewise curves. -`create_extra_tranches`: if true, split the first tranche of the first timestep into two; -split the last tranche of the last timestep of into three. -""" -function make_deterministic_ts( - name::String, - ini_val::PiecewiseStepData, - incrs_x::NTuple{3, Float64}, - incrs_y::NTuple{3, Float64}, - init_time::DateTime, - horizon::Period, - interval::Period, - count::Int, - resolution::Period; - override_min_x = nothing, - override_max_x = nothing, - create_extra_tranches = false, -) - (tranche_incr_x, res_incr_x, interval_incr_x) = incrs_x - (tranche_incr_y, res_incr_y, interval_incr_y) = incrs_y - - horizon_count = IS.get_horizon_count(horizon, resolution) - - # Perturb the baseline curves by the tranche increments - xs1, ys1 = deepcopy(get_x_coords(ini_val)), deepcopy(get_y_coords(ini_val)) - xs1 .+= [i * tranche_incr_x for i in 0:(length(xs1) - 1)] - ys1 .+= [i * tranche_incr_y for i in 0:(length(ys1) - 1)] - - ts_data = OrderedDict{DateTime, Vector{PiecewiseStepData}}() - for i in 0:(count - 1) - xs = [deepcopy(xs1) .+ i * interval_incr_x for _ in 1:horizon_count] - ys = [deepcopy(ys1) .+ i * interval_incr_y for _ in 1:horizon_count] - for j in 1:horizon_count - xs[j] .+= (j - 1) * res_incr_x - ys[j] .+= (j - 1) * res_incr_y - end - if !isnothing(override_min_x) - for j in 1:horizon_count - xs[j][1] = override_min_x - end - end - if !isnothing(override_max_x) - for j in 1:horizon_count - xs[j][end] = override_max_x - end - end - if i == 0 && create_extra_tranches - xs[1] = [xs[1][1], (xs[1][1] + xs[1][2]) / 2, xs[1][2:end]...] - ys[1] = [ys[1][1], ys[1][1], ys[1][2:end]...] - elseif i == count - 1 && create_extra_tranches - xs[end] = [ - xs[end][1:(end - 1)]..., - (2 * xs[end][end - 1] + xs[end][end]) / 3, - (xs[end][end - 1] + 2 * xs[end][end]) / 3, - xs[end][end], - ] - ys[end] = [ys[end][1:(end - 1)]..., ys[end][end], ys[end][end], ys[end][end]] - end - ts_data[init_time + i * interval] = PiecewiseStepData.(xs, ys) - end - - return Deterministic(; - name = name, - data = ts_data, - resolution = resolution, - interval = interval, - ) -end - -""" -Create a deterministic time series as above, with the same horizon, count, and interval as an existing time series. -""" -function make_deterministic_ts( - sys::PSY.System, - args...; - kwargs..., -) - @assert all( - PSY.get_time_series_resolutions(sys) .== - first(PSY.get_time_series_resolutions(sys)), - ) - return make_deterministic_ts( - args..., - first(PSY.get_forecast_initial_times(sys)), - PSY.get_forecast_horizon(sys), - PSY.get_forecast_interval(sys), - PSY.get_forecast_window_count(sys), - first(PSY.get_time_series_resolutions(sys)); - kwargs..., - ) -end diff --git a/test/test_utils/iec_simulation_utils.jl b/test/test_utils/iec_simulation_utils.jl deleted file mode 100644 index 044172aa..00000000 --- a/test/test_utils/iec_simulation_utils.jl +++ /dev/null @@ -1,280 +0,0 @@ -const IECComponentType = Source -const IEC_COMPONENT_NAME = "source" -const SEL_IEC = make_selector(IECComponentType, IEC_COMPONENT_NAME) - -function make_5_bus_with_import_export(; - add_single_time_series::Bool = false, - name = nothing, -) - sys = build_system( - PSITestSystems, - "c_sys5_uc"; - add_single_time_series = add_single_time_series, - ) - - source = IECComponentType(; - name = IEC_COMPONENT_NAME, - available = true, - bus = get_component(ACBus, sys, "nodeC"), - active_power = 0.0, - reactive_power = 0.0, - active_power_limits = (min = -2.0, max = 2.0), - reactive_power_limits = (min = -2.0, max = 2.0), - R_th = 0.01, - X_th = 0.02, - internal_voltage = 1.0, - internal_angle = 0.0, - base_power = 100.0, - ) - - import_curve = make_import_curve( - [0.0, 100.0, 105.0, 120.0, 200.0], - [5.0, 10.0, 20.0, 40.0], - ) - - export_curve = make_export_curve( - [0.0, 100.0, 105.0, 120.0, 200.0], - [12.0, 8.0, 4.0, 1.0], # elsewhere the final slope is 0.0 but that's problematic here - ) - - ie_cost = ImportExportCost(; - import_offer_curves = import_curve, - export_offer_curves = export_curve, - ancillary_service_offers = Vector{Service}(), - energy_import_weekly_limit = 1e6, - energy_export_weekly_limit = 1e6, - ) - - set_operation_cost!(source, ie_cost) - add_component!(sys, source) - @assert get_component(SEL_IEC, sys) == source - - isnothing(name) || set_name!(sys, name) - return sys -end - -function make_5_bus_with_ie_ts( - import_breakpoints_vary::Bool, - import_slopes_vary::Bool, - export_breakpoints_vary::Bool, - export_slopes_vary::Bool; - zero_min_power::Bool = true, - unperturb_max_power::Bool = false, - add_single_time_series::Bool = false, - import_scalar = 1.0, - export_scalar = 1.0, - name = nothing) - im_incr_x = import_breakpoints_vary ? (0.02, 0.11, 0.05) : (0.0, 0.0, 0.0) - im_incr_y = import_slopes_vary ? (0.02, 0.11, 0.05) : (0.0, 0.0, 0.0) - - ex_incr_x = export_breakpoints_vary ? (0.03, 0.13, 0.07) : (0.0, 0.0, 0.0) - ex_incr_y = export_slopes_vary ? (0.03, 0.13, 0.07) : (0.0, 0.0, 0.0) - - sys = make_5_bus_with_import_export(; - add_single_time_series = add_single_time_series, - name = name, - ) - - source = get_component(SEL_IEC, sys) - oc = get_operation_cost(source)::ImportExportCost - im_oc = get_import_offer_curves(oc) - ex_oc = get_export_offer_curves(oc) - im_fd = get_function_data(im_oc) * import_scalar - ex_fd = get_function_data(ex_oc) * export_scalar - - im_ts = make_deterministic_ts( - sys, - "variable_cost_import", - im_fd, - im_incr_x, - im_incr_y; - override_min_x = zero_min_power ? 0.0 : nothing, - override_max_x = unperturb_max_power ? last(get_x_coords(im_fd)) : nothing, - ) - ex_ts = make_deterministic_ts( - sys, - "variable_cost_export", - ex_fd, - ex_incr_x, - ex_incr_y; - override_min_x = zero_min_power ? 0.0 : nothing, - override_max_x = unperturb_max_power ? last(get_x_coords(ex_fd)) : nothing, - ) - - im_key = add_time_series!(sys, source, im_ts) - ex_key = add_time_series!(sys, source, ex_ts) - - # Build ImportExportTimeSeriesCost with TS-backed curves - im_ts_curve = make_import_export_ts_curve(im_key) - ex_ts_curve = make_import_export_ts_curve(ex_key) - ts_cost = ImportExportTimeSeriesCost(; - import_offer_curves = im_ts_curve, - export_offer_curves = ex_ts_curve, - energy_import_weekly_limit = get_energy_import_weekly_limit(oc), - energy_export_weekly_limit = get_energy_export_weekly_limit(oc), - ancillary_service_offers = get_ancillary_service_offers(oc), - ) - set_operation_cost!(source, ts_cost) - - return sys -end - -# Analogous to run_mbc_obj_fun_test in test_utils/mbc_simulation_utils.jl -function run_iec_obj_fun_test(sys1, sys2, comp_name::String, ::Type{T}; - simulation = true, in_memory_store = false, reservation = false, -) where {T <: PSY.Component} - _, res1, decisions1, nullable_decisions1 = run_iec_sim(sys1, comp_name, T; - simulation = simulation, - in_memory_store = in_memory_store, - reservation = reservation, - ) - _, res2, decisions2, nullable_decisions2 = run_iec_sim(sys2, comp_name, T; - simulation = simulation, - in_memory_store = in_memory_store, - reservation = reservation, - ) - - all_decisions1 = (decisions1..., nullable_decisions1...) - all_decisions2 = (decisions2..., nullable_decisions2...) - if !all(isapprox.(all_decisions1, all_decisions2)) - @error all_decisions1 - @error all_decisions2 - end - @assert all(isapprox.(all_decisions1, all_decisions2)) - - ground_truth_1 = cost_due_to_time_varying_iec(sys1, res1, T) - ground_truth_2 = cost_due_to_time_varying_iec(sys2, res2, T) - - success = obj_fun_test_helper(ground_truth_1, ground_truth_2, res1, res2) - return decisions1, decisions2 -end - -function run_iec_sim(sys::System, comp_name::String, ::Type{T}; - simulation = true, in_memory_store = false, reservation = false, -) where {T <: PSY.Component} - device_to_formulation = FormulationDict( - Source => DeviceModel( - Source, - ImportExportSourceModel; - attributes = Dict("reservation" => reservation), - ), - ) - model, res = if simulation - run_generic_mbc_sim( - sys; - in_memory_store = in_memory_store, - device_to_formulation = device_to_formulation, - ) - else - run_generic_mbc_prob(sys; device_to_formulation = device_to_formulation) - end - - # TODO test slope, breakpoint written parameters against time series values - # (https://github.com/NREL-Sienna/PowerSimulations.jl/issues/1429) - - decisions = ( - _read_one_value(res, PSI.ActivePowerOutVariable, T, comp_name), - _read_one_value(res, PSI.ActivePowerInVariable, T, comp_name), - ) - - output_var = read_variable_dict(res, PSI.ActivePowerOutVariable, T) - input_var = read_variable_dict(res, PSI.ActivePowerInVariable, T) - - for key in keys(output_var) - output_on = output_var[key][!, "value"] .> PSI.COST_EPSILON - input_on = input_var[key][!, "value"] .> PSI.COST_EPSILON - if reservation - @test all(.~(output_on .& input_on)) # no simultaneous import/export - else - @test any(output_on .& input_on) # some simultaneous import/export - end - end - - return model, res, decisions, () # return format follows the MBC run_startup_shutdown_test convention -end - -# Analogous to cost_due_to_time_varying_mbc in test_utils/mbc_simulation_utils.jl -# TODO deduplicate after initial time-sensitive merge -function cost_due_to_time_varying_iec( - sys::System, - res::IS.Outputs, - ::Type{T}, -) where {T <: PSY.Component} - power_in_vars = read_variable_dict(res, PSI.ActivePowerInVariable, T) - power_out_vars = read_variable_dict(res, PSI.ActivePowerOutVariable, T) - output = SortedDict{DateTime, DataFrame}() - - for step_dt in keys(power_in_vars) - power_in_df = power_in_vars[step_dt] - step_df = DataFrame(:DateTime => unique(power_in_df.DateTime)) - gen_names = unique(power_in_df.name) - @assert !isempty(gen_names) - - power_out_df = power_out_vars[step_dt] - @assert names(power_in_df) == names(power_out_df) - @assert all(power_in_df.DateTime .== power_out_df.DateTime) - - @assert any([ - get_operation_cost(comp) isa - Union{ImportExportCost, ImportExportTimeSeriesCost} for - comp in get_components(T, sys) - ]) - for gen_name in gen_names - comp = get_component(T, sys, gen_name) - cost = PSY.get_operation_cost(comp) - (cost isa Union{ImportExportCost, ImportExportTimeSeriesCost}) || continue - step_df[!, gen_name] .= 0.0 - # imports = addition of power = power flowing out of the device - # exports = reduction of power = power flowing into the device - for (multiplier, power_df, getter) in ( - (1.0, power_out_df, PSY.get_import_offer_curves), - (-1.0, power_in_df, PSY.get_export_offer_curves), - ) - offer_curves = getter(cost) - if IS.is_time_series_backed(offer_curves) - vc_ts = getter(comp, cost; start_time = step_dt) - @assert all(unique(power_df.DateTime) .== TimeSeries.timestamp(vc_ts)) - step_df[!, gen_name] .+= - multiplier * - _calc_pwi_cost.( - @rsubset(power_df, :name == gen_name).value, - TimeSeries.values(vc_ts), - ) - end - end - end - - measure_vars = [x for x in names(step_df) if x != "DateTime"] - # rows represent: [time, component, time-varying MBC cost for {component} at {time}] - output[step_dt] = - DataFrames.stack( - step_df, - measure_vars; - variable_name = :name, - value_name = :value, - ) - end - return output -end - -function iec_obj_fun_test_wrapper(sys_constant, sys_varying; reservation = false) - for use_simulation in (false, true) - for in_memory_store in (use_simulation ? (false, true) : (false,)) - decisions1, decisions2 = run_iec_obj_fun_test( - sys_constant, - sys_varying, - IEC_COMPONENT_NAME, - IECComponentType; - simulation = use_simulation, - in_memory_store = in_memory_store, - reservation = reservation, - ) - - if !all(isapprox.(decisions1, decisions2)) - @error decisions1 - @error decisions2 - end - @assert all(approx_geq_1.(decisions1)) - end - end -end diff --git a/test/test_utils/mbc_system_utils.jl b/test/test_utils/mbc_system_utils.jl deleted file mode 100644 index 6b058e50..00000000 --- a/test/test_utils/mbc_system_utils.jl +++ /dev/null @@ -1,550 +0,0 @@ -# WARNING: included in HydroPowerSimulations's tests as well. -# If you make changes, run those tests too! -const SEL_INCR = make_selector(ThermalStandard, "Test Unit1") -const SEL_DECR = make_selector(InterruptiblePowerLoad, "Bus1_interruptible") -const SEL_MULTISTART = make_selector(ThermalMultiStart, "115_STEAM_1") - -# functions for replacing components in the system -function replace_with_renewable!( - sys::PSY.System, - unit1::PSY.Generator; - use_thermal_max_power = false, - magnitude = 1.0, - random_variation = 0.1, -) - rg1 = PSY.RenewableDispatch(; - name = "RG1", - available = true, - bus = get_bus(unit1), - active_power = get_active_power(unit1), - reactive_power = get_reactive_power(unit1), - rating = get_rating(unit1), - prime_mover_type = PSY.PrimeMovers.PVe, - reactive_power_limits = get_reactive_power_limits(unit1), - power_factor = 0.9, - # the start up, shunt down, and no-load cost of renewables should be zero, - # but we'll use the unit's operation cost as-is for simplicity. - operation_cost = deepcopy(get_operation_cost(unit1)), - base_power = get_base_power(unit1), - ) - add_component!(sys, rg1) - transfer_mbc!(rg1, unit1, sys) - remove_component!(sys, unit1) - zero_out_startup_shutdown_costs!(rg1) - - # add a max_active_power time series to the component - load = first(PSY.get_components(PSY.PowerLoad, sys)) - load_ts = get_time_series(Deterministic, load, "max_active_power") - num_windows = length(get_data(load_ts)) - num_forecast_steps = - floor(Int, get_horizon(load_ts) / get_interval(load_ts)) - total_steps = num_windows + num_forecast_steps - 1 - dates = range( - get_initial_timestamp(load_ts); - step = get_interval(load_ts), - length = total_steps, - ) - if use_thermal_max_power - rg_data = fill(get_active_power_limits(unit1).max, total_steps) - else - rg_data = magnitude .* ones(total_steps) .+ random_variation .* rand(total_steps) - end - rg_ts = SingleTimeSeries("max_active_power", TimeArray(dates, rg_data)) - add_time_series!(sys, rg1, rg_ts) - transform_single_time_series!( - sys, - get_horizon(load_ts), - get_interval(load_ts), - ) -end - -function replace_load_with_interruptible!(sys::System) - @assert !isempty(get_components(PSY.PowerLoad, sys)) - load1 = first(get_components(PSY.PowerLoad, sys)) - interruptible_load = PSY.InterruptiblePowerLoad(; - name = get_name(load1) * "_interruptible", - bus = get_bus(load1), - available = get_available(load1), - active_power = get_active_power(load1), - reactive_power = get_reactive_power(load1), - max_active_power = get_max_active_power(load1), - max_reactive_power = get_max_reactive_power(load1), - operation_cost = PSY.LoadCost(nothing), - base_power = get_base_power(load1), - conformity = get_conformity(load1), - ) - add_component!(sys, interruptible_load) - for ts_key in get_time_series_keys(load1) - ts = get_time_series(load1, ts_key) - add_time_series!( - sys, - interruptible_load, - ts, - ) - end - remove_component!(sys, load1) -end - -# functions for adjusting power/cost curves and manipulating time series -""" -Helper function to tweak load powers, non-MBC generator powers, and non-MBC generator costs -to exercise the generators we want to test. - -Multiplies {} for {} by {}: -- max active power, all loads, load_pow_mult -- active power limits, non-MBC ThermalStandard, therm_pow_mult -- operational costs, non-MBC ThermalStandard, therm_price_mult -""" -function tweak_system!(sys::System, load_pow_mult, therm_pow_mult, therm_price_mult) - for load in get_components(PowerLoad, sys) - set_max_active_power!(load, get_max_active_power(load) * load_pow_mult) - end - # replace with type of component? - for therm in get_components(ThermalStandard, sys) - op_cost = get_operation_cost(therm) - op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} && continue - with_units_base(sys, UnitSystem.DEVICE_BASE) do - old_limits = get_active_power_limits(therm) - new_limits = (min = old_limits.min, max = old_limits.max * therm_pow_mult) - set_active_power_limits!(therm, new_limits) - end - if get_variable(op_cost) isa CostCurve{LinearCurve} || - get_variable(op_cost) isa CostCurve{QuadraticCurve} - prop = get_proportional_term(get_value_curve(get_variable(op_cost))) - set_variable!(op_cost, CostCurve(LinearCurve(prop * therm_price_mult))) - elseif get_variable(op_cost) isa CostCurve{PiecewiseIncrementalCurve} - pwl = get_value_curve(get_variable(op_cost)) - new_pwl = PiecewiseIncrementalCurve( - therm_price_mult * get_initial_input(pwl), - get_x_coords(pwl), - therm_price_mult * get_slopes(pwl), - ) - set_variable!(op_cost, CostCurve(new_pwl)) - else - error("Unhandled operation cost variable type $(typeof(get_variable(op_cost)))") - end - end -end - -tweak_for_startup_shutdown!(sys::System) = tweak_system!(sys::System, 0.8, 1.0, 1.0) - -tweak_for_decremental_initial!(sys::PSY.System) = tweak_system!(sys, 1.0, 1.2, 0.5) - -"""Transfer the market bid cost from old_comp to new_comp.""" -function transfer_mbc!( - new_comp::PSY.Device, - old_comp::PSY.Device, - ::PSY.System, -) - mbc = deepcopy(get_operation_cost(old_comp)) - @assert mbc isa PSY.MarketBidCost # static MBC has no embedded TS keys to transfer - set_operation_cost!(new_comp, mbc) - return -end - -function zero_out_startup_shutdown_costs!(comp::PSY.Device) - op_cost = get_operation_cost(comp)::MarketBidCost - set_start_up!(op_cost, (hot = 0.0, warm = 0.0, cold = 0.0)) - set_shut_down!(op_cost, LinearCurve(0.0)) -end - -"""Set everything except the incremental_offer_curves to zero on the MarketBidCost attached to the unit.""" -function zero_out_non_incremental_curve!(sys::PSY.System, unit::PSY.Component) - cost = deepcopy(get_operation_cost(unit)::MarketBidCost) - set_no_load_cost!(cost, LinearCurve(0.0)) - set_start_up!(cost, (hot = 0.0, warm = 0.0, cold = 0.0)) - set_shut_down!(cost, LinearCurve(0.0)) - # set minimum generation cost (but not min gen power) to zero. - base_curve = get_value_curve(get_incremental_offer_curves(cost)) - x_coords = get_x_coords(base_curve) - slopes = get_slopes(base_curve) - new_curve = PiecewiseIncrementalCurve(0.0, x_coords, slopes) - set_incremental_offer_curves!(cost, CostCurve(new_curve)) - set_operation_cost!(unit, cost) -end - -"Move the no_load_cost into the initial_input of the incremental offer curve. Not designed for time series." -function no_load_to_initial_input!(comp::Generator) - cost = get_operation_cost(comp)::MarketBidCost - no_load = IS.get_proportional_term(PSY.get_no_load_cost(cost)) - old_fd = get_function_data( - get_value_curve(get_incremental_offer_curves(get_operation_cost(comp))), - )::IS.PiecewiseStepData - new_vc = PiecewiseIncrementalCurve(old_fd, no_load, nothing) - set_incremental_offer_curves!(get_operation_cost(comp), CostCurve(new_vc)) - set_no_load_cost!(get_operation_cost(comp), LinearCurve(0.0)) - return -end - -no_load_to_initial_input!( - sys::PSY.System, - sel = make_selector( - x -> get_operation_cost(x) isa Union{MarketBidCost, MarketBidTimeSeriesCost}, - Generator, - ), -) = no_load_to_initial_input!.(get_components(sel, sys)) - -"Set all MBC thermal unit min active powers to their min breakpoints" -function adjust_min_power!(sys) - for comp in get_components(Union{ThermalStandard, ThermalMultiStart}, sys) - op_cost = get_operation_cost(comp) - op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} || continue - cost_curve = get_incremental_offer_curves(op_cost)::CostCurve - baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve - x_coords = get_x_coords(get_function_data(baseline)) - with_units_base(sys, UnitSystem.NATURAL_UNITS) do - set_active_power_limits!(comp, (min = first(x_coords), max = last(x_coords))) - end - end -end - -""" -Convert a component's MarketBidCost to MarketBidTimeSeriesCost with startup and shutdown -time series. `with_increments`: whether the elements should be increasing over time or -constant. Version A: designed for `c_fixed_market_bid_cost`. -""" -function add_startup_shutdown_ts_a!(sys::System, with_increments::Bool) - res_incr = with_increments ? 0.05 : 0.0 - interval_incr = with_increments ? 0.01 : 0.0 - unit1 = get_component(ThermalStandard, sys, "Test Unit1") - op_cost = get_operation_cost(unit1) - @assert op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} - startup_ts_1 = make_deterministic_ts( - sys, - "start_up", - (1.0, 1.5, 2.0), - res_incr, - interval_incr, - ) - shutdown_ts_1 = - make_deterministic_ts(sys, "shut_down", 0.5, res_incr, interval_incr) - _convert_to_ts_mbc!(sys, unit1, op_cost, startup_ts_1, shutdown_ts_1) - return startup_ts_1, shutdown_ts_1 -end - -""" -Convert a component's MarketBidCost to MarketBidTimeSeriesCost with startup and shutdown -time series. `with_increments`: whether the elements should be increasing over time or -constant. Version B: designed for `c_sys5_pglib`. -""" -function add_startup_shutdown_ts_b!(sys::System, with_increments::Bool) - res_incr = with_increments ? 0.05 : 0.0 - interval_incr = with_increments ? 0.01 : 0.0 - unit1 = get_component(ThermalMultiStart, sys, "115_STEAM_1") - op_cost = get_operation_cost(unit1) - @assert op_cost isa Union{MarketBidCost, MarketBidTimeSeriesCost} - base_startup = Tuple(get_start_up(op_cost)) - base_shutdown = if op_cost isa MarketBidCost - IS.get_proportional_term(get_shut_down(op_cost)) - else - get_shut_down(op_cost) # already TS or scalar - end - startup_ts_1 = make_deterministic_ts( - sys, - "start_up", - base_startup, - res_incr, - interval_incr, - ) - shutdown_ts_1 = - make_deterministic_ts( - sys, - "shut_down", - base_shutdown, - res_incr, - interval_incr, - ) - _convert_to_ts_mbc!(sys, unit1, op_cost, startup_ts_1, shutdown_ts_1) - return startup_ts_1, shutdown_ts_1 -end - -""" -Helper: convert a static MarketBidCost to MarketBidTimeSeriesCost, attaching the given -startup and shutdown time series. If already a MarketBidTimeSeriesCost, update in place. -Offer curves are converted to TS-backed with constant values; no_load_cost gets a constant TS. -""" -function _convert_to_ts_mbc!( - sys::System, - comp::PSY.Device, - op_cost::MarketBidCost, - startup_ts::Deterministic, - shutdown_ts::Deterministic, -) - startup_key = add_time_series!(sys, comp, startup_ts) - shutdown_key = add_time_series!(sys, comp, shutdown_ts) - - # Convert offer curves to TS-backed with constant values - local incr_curve, decr_curve - for (getter, incr_or_decr) in ( - (get_incremental_offer_curves, "incremental"), - (get_decremental_offer_curves, "decremental"), - ) - cost_curve = getter(op_cost) - baseline = get_value_curve(cost_curve)::PiecewiseIncrementalCurve - baseline_pwl = get_function_data(baseline) - baseline_initial = get_initial_input(baseline) - - curve_ts = make_deterministic_ts( - sys, - "variable_cost $(incr_or_decr)", - baseline_pwl, - (0.0, 0.0, 0.0), - (0.0, 0.0, 0.0), - ) - curve_key = add_time_series!(sys, comp, curve_ts) - - if !isnothing(baseline_initial) - initial_ts = make_deterministic_ts( - sys, "initial_input $(incr_or_decr)", baseline_initial, 0.0, 0.0) - initial_key = add_time_series!(sys, comp, initial_ts) - else - initial_key = nothing - end - - if incr_or_decr == "incremental" - incr_curve = make_market_bid_ts_curve(curve_key, initial_key) - else - decr_curve = make_market_bid_ts_curve(curve_key, initial_key) - end - end - - # no_load_cost as constant TS - baseline_no_load = IS.get_proportional_term(get_no_load_cost(op_cost)) - no_load_ts = make_deterministic_ts(sys, "no_load_cost", baseline_no_load, 0.0, 0.0) - no_load_key = add_time_series!(sys, comp, no_load_ts) - - new_cost = MarketBidTimeSeriesCost(; - no_load_cost = TimeSeriesLinearCurve(no_load_key), - start_up = startup_key, - shut_down = TimeSeriesLinearCurve(shutdown_key), - incremental_offer_curves = incr_curve, - decremental_offer_curves = decr_curve, - ancillary_service_offers = get_ancillary_service_offers(op_cost), - ) - set_operation_cost!(comp, new_cost) - return -end - -function _convert_to_ts_mbc!( - sys::System, - comp::PSY.Device, - op_cost::MarketBidTimeSeriesCost, - startup_ts::Deterministic, - shutdown_ts::Deterministic, -) - # Already a TS cost — just update startup/shutdown - startup_key = add_time_series!(sys, comp, startup_ts) - shutdown_key = add_time_series!(sys, comp, shutdown_ts) - set_start_up!(op_cost, startup_key) - set_shut_down!(op_cost, TimeSeriesLinearCurve(shutdown_key)) - return -end - -# functions for building the systems: calls the above - -function load_and_fix_system(args...; kwargs...) - sys = Logging.with_logger(Logging.NullLogger()) do - build_system(args...; kwargs...) - end - no_load_to_initial_input!(sys) - adjust_min_power!(sys) - return sys -end - -"""Create a system with for testing fixed market bid costs on thermal get_components.""" -function load_sys_incr() - # NOTE we are using the fixed one so we can add time series ourselves - sys = load_and_fix_system( - PSITestSystems, - "c_fixed_market_bid_cost", - ) - tweak_system!(sys, 1.05, 1.0, 1.0) - get_y_coords( - get_function_data( - get_value_curve( - get_incremental_offer_curves( - get_operation_cost(get_component(ThermalStandard, sys, "Test Unit2")), - ), - ), - ), - )[1] *= 0.9 - return sys -end - -""" -Create a system with initial input and variable cost time series. Lots of options: - -# Arguments: - - `initial_varies`: whether the initial input time series should have values that vary - over time (as opposed to a time series with constant values over time) - - `breakpoints_vary`: whether the breakpoints in the variable cost time series should vary - over time - - `slopes_vary`: whether the slopes of the variable cost time series should vary over time - - `modify_baseline_pwl`: optional, a function to modify the baseline piecewise linear cost - `FunctionData` from which the variable cost time series is calculated - - `do_override_min_x`: whether to override the P1 to be equal to the minimum power in all - time steps - - `create_extra_tranches`: whether to create extra tranches in some time steps by - splitting one tranche into two - - `active_components`: a `ComponentSelector` specifying which components should get time - series - - `initial_input_names_vary`: whether the initial input time series names should vary over - components - - `variable_cost_names_vary`: whether the variable cost time series names should vary over - components -""" -function build_sys_incr( - initial_varies::Bool, - breakpoints_vary::Bool, - slopes_vary::Bool; - modify_baseline_pwl = nothing, - do_override_min_x = true, - create_extra_tranches = false, - active_components = SEL_INCR, - initial_input_names_vary = false, - variable_cost_names_vary = false, -) - sys = load_sys_incr() - @assert !isempty(get_components(active_components, sys)) "No components selected" - extend_mbc!( - sys, - active_components; - initial_varies = initial_varies, - breakpoints_vary = breakpoints_vary, - slopes_vary = slopes_vary, - modify_baseline_pwl = modify_baseline_pwl, - do_override_min_x = do_override_min_x, - create_extra_tranches = create_extra_tranches, - initial_input_names_vary = initial_input_names_vary, - variable_cost_names_vary = variable_cost_names_vary, - ) - return sys -end - -function remove_thermal_mbcs!(sys::PSY.System) - for comp in get_components(ThermalStandard, sys) - old_cost = get_operation_cost(comp) - old_cost isa MarketBidCost || continue - new_op_cost = ThermalGenerationCost(; - variable = get_incremental_offer_curves(old_cost), - start_up = get_start_up(old_cost), - shut_down = IS.get_proportional_term(get_shut_down(old_cost)), - fixed = 0.0, - ) - set_operation_cost!(comp, new_op_cost) - end -end - -function zero_out_thermal_costs!(sys) - for comp in get_components(ThermalStandard, sys) - set_operation_cost!( - comp, - ThermalGenerationCost(; - variable = CostCurve( - LinearCurve(0.0), - ), - start_up = (hot = 0.0, warm = 0.0, cold = 0.0), - shut_down = 0.0, - fixed = 0.0, - ), - ) - end -end - -"""Like `load_sys_incr` but for decremental MarketBidCost on ControllableLoad components.""" -function load_sys_decr2() - sys = load_and_fix_system( - PSITestSystems, - "c_fixed_market_bid_cost", - ) - replace_load_with_interruptible!(sys) - interruptible_load = first(get_components(PSY.InterruptiblePowerLoad, sys)) - selector = make_selector(PSY.InterruptiblePowerLoad, get_name(interruptible_load)) - add_mbc!(sys, selector; incremental = false, decremental = true) - # replace the MBCs on the thermals with ThermalCost objects. - remove_thermal_mbcs!(sys) - # makes the objective function/constraints simpler, easier to track down issues, - # but not actually needed. - zero_out_thermal_costs!(sys) - return sys -end - -"""Like `build_sys_incr` but for decremental MarketBidCost on ControllableLoad components.""" -function build_sys_decr2( - initial_varies::Bool, - breakpoints_vary::Bool, - slopes_vary::Bool; - modify_baseline_pwl = nothing, - do_override_min_x = true, - create_extra_tranches = false, - active_components = SEL_DECR, - initial_input_names_vary = false, - variable_cost_names_vary = false, -) - sys = load_sys_decr2() - @assert !isempty(get_components(active_components, sys)) "No components selected" - extend_mbc!( - sys, - active_components; - initial_varies = initial_varies, - breakpoints_vary = breakpoints_vary, - slopes_vary = slopes_vary, - modify_baseline_pwl = modify_baseline_pwl, - do_override_min_x = do_override_min_x, - create_extra_tranches = create_extra_tranches, - initial_input_names_vary = initial_input_names_vary, - variable_cost_names_vary = variable_cost_names_vary, - ) - - # make the max_active_power time series constant. - il = first(get_components(PSY.InterruptiblePowerLoad, sys)) - for ts_key in get_time_series_keys(il) - if get_name(ts_key) == "max_active_power" - max_active_power_ts = get_time_series( - first(get_components(PSY.InterruptiblePowerLoad, sys)), - ts_key, - ) - max_max_active_power = maximum(maximum(values(max_active_power_ts.data))) - remove_time_series!(sys, Deterministic, il, "max_active_power") - new_ts = make_deterministic_ts( - sys, - "max_active_power", - max_max_active_power, - 0.0, - 0.0, - ) - add_time_series!(sys, il, new_ts) - break - end - end - return sys -end - -function create_multistart_sys( - with_increments::Bool, - load_pow_mult, - therm_pow_mult, - therm_price_mult; - add_ts = true, -) - @assert add_ts || !with_increments - c_sys5_pglib = load_and_fix_system(PSITestSystems, "c_sys5_pglib") - tweak_system!(c_sys5_pglib, load_pow_mult, therm_pow_mult, therm_price_mult) - ms_comp = get_component(SEL_MULTISTART, c_sys5_pglib) - old_op = get_operation_cost(ms_comp) - old_ic = IncrementalCurve(get_value_curve(get_variable(old_op))) - new_ii = get_initial_input(old_ic) + get_fixed(old_op) - new_ic = IncrementalCurve(get_function_data(old_ic), new_ii, nothing) - set_operation_cost!( - ms_comp, - MarketBidCost(; - no_load_cost = LinearCurve(0.0), - start_up = (hot = 300.0, warm = 450.0, cold = 500.0), - shut_down = LinearCurve(100.0), - incremental_offer_curves = CostCurve(new_ic), - ), - ) - - add_ts && add_startup_shutdown_ts_b!(c_sys5_pglib, with_increments) - return c_sys5_pglib -end From 4d1d69cf70e49fc3cf02cc160cc3882233d870c3 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Tue, 14 Apr 2026 11:32:43 -0600 Subject: [PATCH 05/19] fix latent bugs surfaced by POM unit tests - _get_raw_pwl_data (TS path): stale 2D/3D assertion and hardcoded NATURAL_UNITS; now reads power_units from the cost curve. - is_nontrivial_offer: shared predicate to replace dead isnothing(get_input_offer_curves(...)) checks (static MBC's default is ZERO_OFFER_CURVE, never nothing). - _add_start_up_cost_to_objective!: broadcast param * mult so Tuple-valued (multi-start) startup costs round-trip correctly. - is_time_variant_term: collapse to single-arg form (output depends only on cost type). Co-Authored-By: Claude Opus 4.6 (1M context) --- src/common_models/interfaces.jl | 9 +--- src/objective_function/proportional.jl | 5 +- src/objective_function/start_up_shut_down.jl | 4 +- src/objective_function/value_curve_cost.jl | 50 +++++++++++++------- test/test_proportional.jl | 10 +--- 5 files changed, 41 insertions(+), 37 deletions(-) diff --git a/src/common_models/interfaces.jl b/src/common_models/interfaces.jl index f9a3e2d0..273101c9 100644 --- a/src/common_models/interfaces.jl +++ b/src/common_models/interfaces.jl @@ -135,14 +135,7 @@ end Extension point: Check if proportional cost term is time-variant. Returns true if the cost should be added to the variant objective expression. """ -is_time_variant_term( - ::OptimizationContainer, - ::PSY.OperationalCost, - ::VariableType, - ::Type{<:IS.InfrastructureSystemsComponent}, - ::AbstractDeviceFormulation, - ::Int, -) = false +is_time_variant_term(::PSY.OperationalCost) = false # corresponds to get_must_run for thermals, but avoiding device specific code here. """ diff --git a/src/objective_function/proportional.jl b/src/objective_function/proportional.jl index 13132e9b..1d6634b2 100644 --- a/src/objective_function/proportional.jl +++ b/src/objective_function/proportional.jl @@ -60,6 +60,8 @@ function add_proportional_cost_maybe_time_variant!( multiplier = objective_function_multiplier(U(), V()) for d in devices op_cost_data = get_operation_cost(d) + # FIXME this should really be in its own function for compilation reasons: + # is_time_variant_term only depends on the type of op_cost_data. name = get_name(d) for t in get_time_steps(container) cost_term = proportional_cost(container, op_cost_data, U(), d, V(), t) @@ -78,8 +80,7 @@ function add_proportional_cost_maybe_time_variant!( ) else variable = get_variable(container, U, T)[name, t] - add_as_time_variant = - is_time_variant_term(container, op_cost_data, U(), T, V(), t) + add_as_time_variant = is_time_variant_term(op_cost_data) if add_as_time_variant add_cost_term_variant!( container, variable, rate, ProductionCostExpression, T, name, t) diff --git a/src/objective_function/start_up_shut_down.jl b/src/objective_function/start_up_shut_down.jl index 5dbcbf4b..79056d75 100644 --- a/src/objective_function/start_up_shut_down.jl +++ b/src/objective_function/start_up_shut_down.jl @@ -110,7 +110,9 @@ function _add_start_up_cost_to_objective!( param = get_parameter_array(container, StartupCostParameter, C) mult = get_parameter_multiplier_array(container, StartupCostParameter, C) for t in get_time_steps(container) - raw_startup_cost = param[name, t] * mult[name, t] + # Broadcast so Tuple-valued parameters (for multi-start formulations) work + # alongside Float64-valued ones. + raw_startup_cost = param[name, t] .* mult[name, t] cost_term = start_up_cost(raw_startup_cost, C, T(), U()) iszero(cost_term) && continue rate = cost_term * multiplier diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index dc7a594b..929643e9 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -437,34 +437,34 @@ function _get_raw_pwl_data( PSY.get_power_units(cost_data) end -# time-series curve: read from parameter arrays +# time-series curve: read from parameter arrays. Parameter containers for +# Slope/Breakpoint are allocated with axes `(names, segments|points, times)`, so the +# 3-index lookup mirrors `_fill_pwl_data_from_arrays!`. The parameter values carry the +# units declared on the `CostCurve`, so we forward those through (don't hardcode). function _get_raw_pwl_data( dir::OfferDirection, container::OptimizationContainer, ::Type{T}, name::String, - ::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}, + cost_data::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}, time::Int, ) where {T <: PSY.Component} SlopeParam = _slope_param(dir) - slope_param_arr = get_parameter_array(container, SlopeParam, T) - slope_param_mult = get_parameter_multiplier_array(container, SlopeParam, T) - @assert size(slope_param_arr) == size(slope_param_mult) + slope_arr = get_parameter_array(container, SlopeParam, T) + slope_mult = get_parameter_multiplier_array(container, SlopeParam, T) + @assert size(slope_arr) == size(slope_mult) slope_cost_component = - slope_param_arr[name, :, time] .* slope_param_mult[name, :, time] - slope_cost_component = slope_cost_component.data + (slope_arr[name, :, time] .* slope_mult[name, :, time]).data BreakpointParam = _breakpoint_param(dir) - breakpoint_param_container = get_parameter(container, BreakpointParam, T) - breakpoint_param_arr = get_parameter_column_refs(breakpoint_param_container, name) - breakpoint_param_mult = get_multiplier_array(breakpoint_param_container) - @assert size(breakpoint_param_arr) == size(breakpoint_param_mult[name, :, :]) + bp_arr = get_parameter_array(container, BreakpointParam, T) + bp_mult = get_parameter_multiplier_array(container, BreakpointParam, T) + @assert size(bp_arr) == size(bp_mult) breakpoint_cost_component = - breakpoint_param_arr[:, time] .* breakpoint_param_mult[name, :, time] - breakpoint_cost_component = breakpoint_cost_component.data + (bp_arr[name, :, time] .* bp_mult[name, :, time]).data @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 - return breakpoint_cost_component, slope_cost_component, PSY.UnitSystem.NATURAL_UNITS + return breakpoint_cost_component, slope_cost_component, PSY.get_power_units(cost_data) end ################################################################################# @@ -551,10 +551,20 @@ function add_pwl_term_delta!( end end +# FIXME better validation: for static, != ZERO_OFFER_CURVE would be clearer +# and for time series, actually check. """ -Generic: incremental offers only (most device formulations). -Decremental-only overload for load formulations is in POM. +Is this offer curve carrying meaningful data, as opposed to the default `ZERO_OFFER_CURVE` +placeholder that PSY assigns to unused sides of a `MarketBidCost` / `ImportExportCost`? +Only used for load formulations, to decide whether to throw an error about a non-trivial +supply offer curve. """ +function is_nontrivial_offer(curve::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}) + xs = PSY.get_x_coords(PSY.get_function_data(PSY.get_value_curve(curve))) + return last(xs) > first(xs) +end +is_nontrivial_offer(::PSY.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}) = false + function add_variable_cost_to_objective!( container::OptimizationContainer, ::T, @@ -564,8 +574,12 @@ function add_variable_cost_to_objective!( ) where {T <: VariableType, U <: AbstractDeviceFormulation} component_name = PSY.get_name(component) @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name - if !isnothing(get_input_offer_curves(cost_function)) - error("Component $(component_name) is not allowed to participate as a demand.") + if is_nontrivial_offer(get_input_offer_curves(cost_function)) + throw( + ArgumentError( + "Component $(component_name) is not allowed to participate as a demand.", + ), + ) end add_pwl_term_delta!( IncrementalOffer(), diff --git a/test/test_proportional.jl b/test/test_proportional.jl index 92b8af5f..757dae2c 100644 --- a/test/test_proportional.jl +++ b/test/test_proportional.jl @@ -37,14 +37,8 @@ InfrastructureOptimizationModels.proportional_cost( ) = op_cost.proportional_term # is_time_variant_term: return the is_time_variant flag from MockOperationCost -InfrastructureOptimizationModels.is_time_variant_term( - ::InfrastructureOptimizationModels.OptimizationContainer, - op_cost::MockOperationCost, - ::TestProportionalVariable, - ::Type{MockThermalGen}, - ::TestProportionalFormulation, - ::Int, -) = op_cost.is_time_variant +InfrastructureOptimizationModels.is_time_variant_term(op_cost::MockOperationCost) = + op_cost.is_time_variant # Helper to set up container with variables for devices function setup_proportional_test_container( From ed5e7bb159aa584cc7a555bbbda515c0e58f4f4f Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Tue, 14 Apr 2026 14:52:57 -0600 Subject: [PATCH 06/19] update for `TupleTimeSeries` --- src/utils/powersystems_utils.jl | 1 + test/test_offer_curve_cost.jl | 25 ++++++++++++++++++++----- 2 files changed, 21 insertions(+), 5 deletions(-) diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 9dfcc429..aec57ca2 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -380,6 +380,7 @@ is_time_variant(::IS.ValueCurve{<:IS.TimeSeriesFunctionData}) = true is_time_variant( ::IS.ProductionVariableCostCurve{<:IS.ValueCurve{<:IS.TimeSeriesFunctionData}}, ) = true +is_time_variant(::IS.TupleTimeSeries) = true is_time_variant(::Any) = false function create_temporary_cost_function_in_system_per_unit( diff --git a/test/test_offer_curve_cost.jl b/test/test_offer_curve_cost.jl index 4ad96214..ea24741c 100644 --- a/test/test_offer_curve_cost.jl +++ b/test/test_offer_curve_cost.jl @@ -29,6 +29,21 @@ function _make_ts(sys::PSY.System, name::String, value::Float64) return PSY.Deterministic(; name = name, data = data, resolution = resolution) end +"""Build a deterministic time series of NTuple{3, Float64} matching the system's forecast params.""" +function _make_tuple_ts(sys::PSY.System, name::String, value::NTuple{3, Float64}) + init_time = first(PSY.get_forecast_initial_times(sys)) + horizon = PSY.get_forecast_horizon(sys) + interval = PSY.get_forecast_interval(sys) + resolution = first(PSY.get_time_series_resolutions(sys)) + count = PSY.get_forecast_window_count(sys) + horizon_count = IS.get_horizon_count(horizon, resolution) + data = OrderedDict{Dates.DateTime, Vector{NTuple{3, Float64}}}() + for i in 0:(count - 1) + data[init_time + i * interval] = fill(value, horizon_count) + end + return PSY.Deterministic(; name = name, data = data, resolution = resolution) +end + """Build a deterministic time series of PiecewiseStepData matching the system's forecast params.""" function _make_pwl_ts( sys::PSY.System, @@ -90,7 +105,7 @@ function _make_ts_mbc_system(; breakpoints = [0.0, 50.0, 100.0], initial_input = 10.0, no_load = 5.0, - start_up_val = 100.0, + start_up_val = (100.0, 100.0, 100.0), shut_down = 50.0, ) sys = Logging.with_logger(Logging.NullLogger()) do @@ -115,12 +130,12 @@ function _make_ts_mbc_system(; shut_down_ts = _make_ts(sys, "shut_down", shut_down) shut_down_key = PSY.add_time_series!(sys, comp, shut_down_ts) - startup_ts = _make_ts(sys, "start_up", start_up_val) + startup_ts = _make_tuple_ts(sys, "start_up", start_up_val) startup_key = PSY.add_time_series!(sys, comp, startup_ts) ts_mbc = PSY.MarketBidTimeSeriesCost(; no_load_cost = PSY.TimeSeriesLinearCurve(no_load_key), - start_up = startup_key, + start_up = IS.TupleTimeSeries{PSY.StartUpStages}(startup_key), shut_down = PSY.TimeSeriesLinearCurve(shut_down_key), incremental_offer_curves = PSY.make_market_bid_ts_curve(incr_key, incr_init_key), decremental_offer_curves = PSY.make_market_bid_ts_curve(decr_key, decr_init_key), @@ -342,7 +357,7 @@ end PSY.LinearCurve(5.0), ) bad_iec = PSY.ImportExportCost(; import_offer_curves = bad_import, - export_offer_curves = PSY.make_export_curve(100.0, 10.0)) + export_offer_curves = PSY.make_export_curve([0.0, 100.0], [10.0])) @test_throws ArgumentError IOM._validate_occ_subtype( bad_iec, IOM.IncrementalOffer(), bad_import, "test") @@ -351,7 +366,7 @@ end PSY.PiecewiseIncrementalCurve( PSY.PiecewiseStepData([10.0, 100.0], [10.0]), 0.0, nothing)) bad_iec2 = PSY.ImportExportCost(; import_offer_curves = bad_import2, - export_offer_curves = PSY.make_export_curve(100.0, 10.0)) + export_offer_curves = PSY.make_export_curve([0.0, 100.0], [10.0])) @test_throws ArgumentError IOM._validate_occ_subtype( bad_iec2, IOM.IncrementalOffer(), bad_import2, "test") end From 0ce9382702def7981ac44a62c61097e140ec55fb Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Wed, 15 Apr 2026 13:20:20 -0600 Subject: [PATCH 07/19] handle TupleTimeSeries in apply_maybe_across_time_series Lets POM's Renewable/Storage startup-nonzero validation traverse a TupleTimeSeries-backed `start_up` field on MarketBidTimeSeriesCost by delegating to the underlying TimeSeriesKey lookup. Co-Authored-By: Claude Opus 4.6 (1M context) --- src/utils/time_series_utils.jl | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/utils/time_series_utils.jl b/src/utils/time_series_utils.jl index 3a72840b..268e066c 100644 --- a/src/utils/time_series_utils.jl +++ b/src/utils/time_series_utils.jl @@ -21,6 +21,13 @@ apply_maybe_across_time_series( ) = apply_maybe_across_time_series(fn, PSY.get_time_series(component, ts_key)) +apply_maybe_across_time_series( + fn::Function, + component::PSY.Component, + tts::IS.TupleTimeSeries, +) = + apply_maybe_across_time_series(fn, component, IS.get_time_series_key(tts)) + # case where the element isn't a time series apply_maybe_across_time_series(fn::Function, ::PSY.Component, elem) = fn(elem) From ce498b6605153b4dd6d3f93836a7d1a0dab6ee40 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 17 Apr 2026 09:54:33 -0600 Subject: [PATCH 08/19] replace isa with dispatch --- src/objective_function/value_curve_cost.jl | 19 +++++++++++++------ test/test_offer_curve_cost.jl | 12 ++++++------ 2 files changed, 19 insertions(+), 12 deletions(-) diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index 196bb7d3..c7cfcfaf 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -151,19 +151,26 @@ _get_parameter_field( ################################################################################# _has_market_bid_cost(device::PSY.StaticInjection) = - PSY.get_operation_cost(device) isa MBC_TYPES + _has_market_bid_cost(PSY.get_operation_cost(device)) +_has_market_bid_cost(::MBC_TYPES) = true +_has_market_bid_cost(::PSY.OperationalCost) = false -_has_import_export_cost(device::PSY.Source) = - PSY.get_operation_cost(device) isa IEC_TYPES _has_import_export_cost(::PSY.StaticInjection) = false +_has_import_export_cost(device::PSY.Source) = + _has_import_export_cost(PSY.get_operation_cost(device)) +_has_import_export_cost(::IEC_TYPES) = true +_has_import_export_cost(::PSY.OperationalCost) = false _has_offer_curve_cost(device::PSY.Component) = _has_market_bid_cost(device) || _has_import_export_cost(device) # With the static/TS type split, time-series parameters are determined by cost type: # TS cost types always have time-series parameters; static types never do. -_has_parameter_time_series(::Type{<:ParameterType}, device::PSY.StaticInjection) = - PSY.get_operation_cost(device) isa TS_OFFER_CURVE_COST_TYPES +_has_parameter_time_series(device::PSY.StaticInjection) = + _has_parameter_time_series(PSY.get_operation_cost(device)) + +_has_parameter_time_series(::TS_OFFER_CURVE_COST_TYPES) = true +_has_parameter_time_series(::PSY.OperationalCost) = false ################################################################################# # Section 5: _consider_parameter (generic versions) @@ -344,7 +351,7 @@ function _process_occ_parameters_helper( end if _consider_parameter(P, container, model) ts_devices = - filter(device -> _has_parameter_time_series(P, device), devices) + filter(device -> _has_parameter_time_series(device), devices) (length(ts_devices) > 0) && add_parameters!(container, P, ts_devices, model) end end diff --git a/test/test_offer_curve_cost.jl b/test/test_offer_curve_cost.jl index 891f8e3d..ef150dfc 100644 --- a/test/test_offer_curve_cost.jl +++ b/test/test_offer_curve_cost.jl @@ -281,14 +281,14 @@ end _, source_ts = _make_ts_iec_system() param = IOM.IncrementalPiecewiseLinearSlopeParameter - @test !IOM._has_parameter_time_series(param, comp_static) - @test IOM._has_parameter_time_series(param, comp_ts) - @test !IOM._has_parameter_time_series(param, source_static) - @test IOM._has_parameter_time_series(param, source_ts) + @test !IOM._has_parameter_time_series(comp_static) + @test IOM._has_parameter_time_series(comp_ts) + @test !IOM._has_parameter_time_series(source_static) + @test IOM._has_parameter_time_series(source_ts) startup_param = IOM.StartupCostParameter - @test !IOM._has_parameter_time_series(startup_param, comp_static) - @test IOM._has_parameter_time_series(startup_param, comp_ts) + @test !IOM._has_parameter_time_series(comp_static) + @test IOM._has_parameter_time_series(comp_ts) end @testset "Offer Curve Cost: get_initial_input on devices" begin From f7830afdf0e7155ec76d96249e6df8da573fc24f Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 17 Apr 2026 14:08:13 -0600 Subject: [PATCH 09/19] copilot comments --- src/objective_function/value_curve_cost.jl | 2 +- test/mocks/mock_components.jl | 4 +++- 2 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index c7cfcfaf..16f4ef4c 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -287,7 +287,7 @@ function validate_occ_component(::Type{<:StartupCostParameter}, device::PSY.Stat elseif !(startup isa Float64) throw( ArgumentError( - "Expected Float64 or StartUpStages startup cost but got $(typeof(startup)) for $(get_name(device))", + "Expected Float64, NTuple{3, Float64}, or StartUpStages startup cost but got $(typeof(startup)) for $(get_name(device))", ), ) end diff --git a/test/mocks/mock_components.jl b/test/mocks/mock_components.jl index 694753ed..75b990d4 100644 --- a/test/mocks/mock_components.jl +++ b/test/mocks/mock_components.jl @@ -101,7 +101,9 @@ IOM.get_active_power_limits(g::MockThermalGen) = g.active_power_limits IOM.get_base_power(g::MockThermalGen) = g.base_power IOM.get_operation_cost(g::MockThermalGen) = g.operation_cost IOM.get_must_run(g::MockThermalGen) = g.must_run -IS.get_fuel_cost(g::MockThermalGen) = g.operation_cost.fuel_cost +IS.get_fuel_cost(g::MockThermalGen) = _mock_fuel_cost(g.operation_cost) +_mock_fuel_cost(c::MockOperationCost) = c.fuel_cost +_mock_fuel_cost(::MockTimeSeriesOperationCost) = 0.0 # Mock Renewable Generator struct MockRenewableGen <: AbstractMockGenerator From 7df716eed6e03b004d1321d9ccccf69568152f59 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Fri, 17 Apr 2026 16:51:54 -0600 Subject: [PATCH 10/19] light refactor of `powersystems_utils.jl` --- src/InfrastructureOptimizationModels.jl | 2 - src/core/optimization_problem_outputs.jl | 3 + src/operation/decision_model.jl | 19 +++++ src/utils/powersystems_utils.jl | 103 ----------------------- 4 files changed, 22 insertions(+), 105 deletions(-) diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index ac6cbc50..808100b4 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -168,7 +168,6 @@ export get_power_flow_evaluation, has_subnetworks, get_subsystem export set_subsystem!, add_dual! export requires_all_branch_models, supports_branch_filtering, ignores_branch_filtering export validate_network_model -export validate_available_devices export BranchReductionOptimizationTracker export get_variable_dict, get_constraint_dict, get_constraint_map_by_type export get_number_of_steps, set_number_of_steps! @@ -340,7 +339,6 @@ export add_variable_container!, add_constraint_dual! export add_to_objective_invariant_expression!, lazy_container_addition! export get_parameter_multiplier_array, get_aux_variable, get_condition export supports_milp, get_quadratic_cost_per_system_unit -export check_hvdc_line_limits_unidirectional, check_hvdc_line_limits_consistency export add_sparse_pwl_interpolation_variables! export JuMPOrFloat # Constraint helpers diff --git a/src/core/optimization_problem_outputs.jl b/src/core/optimization_problem_outputs.jl index 96db0836..e75c39fe 100644 --- a/src/core/optimization_problem_outputs.jl +++ b/src/core/optimization_problem_outputs.jl @@ -104,6 +104,9 @@ get_optimizer_stats(res::OptimizationProblemOutputs) = res.optimizer_stats get_parameter_values(res::OptimizationProblemOutputs) = res.parameter_values get_source_data(res::OptimizationProblemOutputs) = res.source_data +make_system_filename(sys::PSY.System) = make_system_filename(IS.get_uuid(sys)) +make_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json" + """ Load the system from disk if not already set, and return it. diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index ffd5d876..01e8cd16 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -1,3 +1,22 @@ +function get_deterministic_time_series_type(sys::PSY.System) + time_series_types = IS.get_time_series_counts_by_type(sys.data) + existing_types = Set(d["type"] for d in time_series_types) + if Set(["Deterministic", "DeterministicSingleTimeSeries"]) ∈ existing_types + error( + "The System contains a combination of forecast data and transformed time series data. Currently this is not supported.", + ) + end + if "Deterministic" ∈ existing_types + return PSY.Deterministic + elseif "DeterministicSingleTimeSeries" ∈ existing_types + return PSY.DeterministicSingleTimeSeries + else + error( + "The System does not contain any forecast data or transformed time series data.", + ) + end +end + """ Abstract type for models that use default InfrastructureOptimizationModels formulations. For custom decision problems use DecisionProblem as the super type. diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index aec57ca2..09feb18e 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -44,22 +44,6 @@ function get_available_components( end end -""" -Default implementation for validating that a device model has available devices. -Can be extended in downstream packages for additional validation logic. -""" -function validate_available_devices( - model::DeviceModel{T, <:AbstractDeviceFormulation}, - sys::PSY.System, -) where {T <: PSY.Component} - devices = get_available_components(model, sys) - if isempty(devices) - return false - end - PSY.check_components(sys, devices) - return true -end - _filter_function(x::PSY.ACBus) = PSY.get_bustype(x) != PSY.ACBusTypes.ISOLATED && PSY.get_available(x) @@ -103,50 +87,6 @@ function get_available_components( end =# -make_system_filename(sys::PSY.System) = make_system_filename(IS.get_uuid(sys)) -make_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json" - -function check_hvdc_line_limits_consistency( - d::Union{PSY.TwoTerminalHVDC, PSY.TModelHVDCLine}, -) - from_min = PSY.get_active_power_limits_from(d).min - to_min = PSY.get_active_power_limits_to(d).min - from_max = PSY.get_active_power_limits_from(d).max - to_max = PSY.get_active_power_limits_to(d).max - - if from_max < to_min - throw( - IS.ConflictingInputsError( - "From Max $(from_max) can't be a smaller value than To Min $(to_min)", - ), - ) - elseif to_max < from_min - throw( - IS.ConflictingInputsError( - "To Max $(to_max) can't be a smaller value than From Min $(from_min)", - ), - ) - end - return -end - -function check_hvdc_line_limits_unidirectional(d::PSY.TwoTerminalHVDC) - from_min = PSY.get_active_power_limits_from(d).min - to_min = PSY.get_active_power_limits_to(d).min - from_max = PSY.get_active_power_limits_from(d).max - to_max = PSY.get_active_power_limits_to(d).max - - if from_min < 0 || to_min < 0 || from_max < 0 || to_max < 0 - throw( - IS.ConflictingInputsError( - "Changing flow direction on HVDC Line $(PSY.get_name(d)) is not compatible with non-linear network formulations. \ - Bi-directional models with losses are only compatible with linear network models like DCPPowerModel.", - ), - ) - end - return -end - ################################################## ########### Cost Function Utilities ############## ################################################## @@ -382,46 +322,3 @@ is_time_variant( ) = true is_time_variant(::IS.TupleTimeSeries) = true is_time_variant(::Any) = false - -function create_temporary_cost_function_in_system_per_unit( - original_cost_function::PSY.CostCurve, - new_data::PSY.PiecewiseLinearData, -) - return PSY.CostCurve( - PSY.PiecewisePointCurve(new_data), - PSY.UnitSystem.SYSTEM_BASE, - PSY.get_vom_cost(original_cost_function), - ) -end - -function create_temporary_cost_function_in_system_per_unit( - original_cost_function::PSY.FuelCurve, - new_data::PSY.PiecewiseLinearData, -) - return PSY.FuelCurve( - PSY.PiecewisePointCurve(new_data), - PSY.UnitSystem.SYSTEM_BASE, - PSY.get_fuel_cost(original_cost_function), - IS.LinearCurve(0.0), # setting fuel offtake cost to default value of 0 - PSY.get_vom_cost(original_cost_function), - ) -end - -function get_deterministic_time_series_type(sys::PSY.System) - time_series_types = IS.get_time_series_counts_by_type(sys.data) - existing_types = Set(d["type"] for d in time_series_types) - if Set(["Deterministic", "DeterministicSingleTimeSeries"]) ∈ existing_types - error( - "The System contains a combination of forecast data and transformed time series data. Currently this is not supported.", - ) - end - if "Deterministic" ∈ existing_types - return PSY.Deterministic - elseif "DeterministicSingleTimeSeries" ∈ existing_types - return PSY.DeterministicSingleTimeSeries - else - error( - "The System does not contain any forecast data or transformed time series data.", - ) - end -end From 2cee375712085a0da58441e76e150eae8f784502 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Sun, 19 Apr 2026 23:25:35 -0600 Subject: [PATCH 11/19] fix NDMT typing --- src/quadratic_approximations/nmdt_common.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/quadratic_approximations/nmdt_common.jl b/src/quadratic_approximations/nmdt_common.jl index 3f45695b..889d6a72 100644 --- a/src/quadratic_approximations/nmdt_common.jl +++ b/src/quadratic_approximations/nmdt_common.jl @@ -29,10 +29,10 @@ Fields: - `beta_var`: binary variables β_i ∈ {0,1} indexed by (name, i, t) - `delta_var`: residual variables δ ∈ [0, 2^{−depth}] indexed by (name, t) """ -struct NMDTDiscretization - norm_expr::Any - beta_var::Any - delta_var::Any +struct NMDTDiscretization{NE, BV, DV} + norm_expr::NE + beta_var::BV + delta_var::DV end """ From 6d1e755ea1870a0a22586de275a9ec118421ba68 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Sun, 19 Apr 2026 23:26:05 -0600 Subject: [PATCH 12/19] fix time variant logic --- src/objective_function/proportional.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/objective_function/proportional.jl b/src/objective_function/proportional.jl index 88dba510..8ed45497 100644 --- a/src/objective_function/proportional.jl +++ b/src/objective_function/proportional.jl @@ -60,15 +60,16 @@ function add_proportional_cost_maybe_time_variant!( multiplier = objective_function_multiplier(U, V) for d in devices op_cost_data = get_operation_cost(d) - # FIXME this should really be in its own function for compilation reasons: - # is_time_variant_term only depends on the type of op_cost_data. name = get_name(d) + # is_time_variant_term depends only on typeof(op_cost_data); hoist out of the time loop. + add_as_time_variant = is_time_variant_term(op_cost_data) + skip = skip_proportional_cost(d) for t in get_time_steps(container) cost_term = proportional_cost(container, op_cost_data, U, d, V, t) iszero(cost_term) && continue rate = cost_term * multiplier - if skip_proportional_cost(d) + if skip # Only add to expression, not objective add_cost_to_expression!( container, @@ -80,7 +81,6 @@ function add_proportional_cost_maybe_time_variant!( ) else variable = get_variable(container, U, T)[name, t] - add_as_time_variant = is_time_variant_term(op_cost_data) if add_as_time_variant add_cost_term_variant!( container, variable, rate, ProductionCostExpression, T, name, t) From 528215b247b47f7fb4622e28dfd5c90a9309746b Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Sun, 19 Apr 2026 23:26:58 -0600 Subject: [PATCH 13/19] clean up value curve cost --- src/objective_function/value_curve_cost.jl | 34 ++++++++++++++++------ 1 file changed, 25 insertions(+), 9 deletions(-) diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index 16f4ef4c..77901928 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -280,7 +280,7 @@ end function validate_occ_component(::Type{<:StartupCostParameter}, device::PSY.StaticInjection) op_cost = PSY.get_operation_cost(device) # TS types are validated at parameter population time - op_cost isa PSY.MarketBidTimeSeriesCost && return + _is_time_series_cost(op_cost) && return startup = PSY.get_start_up(op_cost) if startup isa Union{NTuple{3, Float64}, StartUpStages} @warn "Multi-start costs detected for non-multi-start unit $(get_name(device)), will take the maximum" @@ -300,7 +300,7 @@ function validate_occ_component( ) op_cost = PSY.get_operation_cost(device) # TS types are validated at parameter population time - op_cost isa PSY.MarketBidTimeSeriesCost && return + _is_time_series_cost(op_cost) && return # Static MBC: shut_down is LinearCurve; ThermalGenerationCost: shut_down is Float64 shutdown = PSY.get_shut_down(op_cost) if shutdown isa IS.LinearCurve @@ -362,7 +362,7 @@ function process_import_export_parameters!( devices_in, model::DeviceModel, ) - devices = filter(_has_import_export_cost, collect(devices_in)) + devices = [d for d in devices_in if _has_import_export_cost(d)] for param in ( IncrementalPiecewiseLinearSlopeParameter, @@ -382,7 +382,7 @@ function process_market_bid_parameters!( incremental::Bool = true, decremental::Bool = false, ) - devices = filter(_has_market_bid_cost, collect(devices_in)) + devices = [d for d in devices_in if _has_market_bid_cost(d)] isempty(devices) && return for param in ( @@ -467,15 +467,21 @@ function _get_raw_pwl_data( slope_arr = get_parameter_array(container, SlopeParam, T) slope_mult = get_parameter_multiplier_array(container, SlopeParam, T) @assert size(slope_arr) == size(slope_mult) - slope_cost_component = - (slope_arr[name, :, time] .* slope_mult[name, :, time]).data + seg_axis = axes(slope_arr)[2] + slope_cost_component = Vector{Float64}(undef, length(seg_axis)) + for (i, seg) in enumerate(seg_axis) + slope_cost_component[i] = slope_arr[name, seg, time] * slope_mult[name, seg, time] + end BreakpointParam = _breakpoint_param(dir) bp_arr = get_parameter_array(container, BreakpointParam, T) bp_mult = get_parameter_multiplier_array(container, BreakpointParam, T) @assert size(bp_arr) == size(bp_mult) - breakpoint_cost_component = - (bp_arr[name, :, time] .* bp_mult[name, :, time]).data + point_axis = axes(bp_arr)[2] + breakpoint_cost_component = Vector{Float64}(undef, length(point_axis)) + for (i, pt) in enumerate(point_axis) + breakpoint_cost_component[i] = bp_arr[name, pt, time] * bp_mult[name, pt, time] + end @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 return breakpoint_cost_component, slope_cost_component, PSY.get_power_units(cost_data) @@ -523,8 +529,18 @@ function add_pwl_term_delta!( dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR time_steps = get_time_steps(container) is_variant = is_time_variant(get_offer_curves(dir, component)) + # Static offer curves are time-invariant: compute breakpoints/slopes once. + static_breakpoints, static_slopes = if is_variant + (Float64[], Float64[]) + else + _get_pwl_data(dir, container, component, first(time_steps)) + end for t in time_steps - breakpoints, slopes = _get_pwl_data(dir, container, component, t) + breakpoints, slopes = if is_variant + _get_pwl_data(dir, container, component, t) + else + (static_breakpoints, static_slopes) + end pwl_vars = add_pwl_variables_delta!( container, From 5b4af9beb13f3d66940d77d12caff98223bca965 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Mon, 20 Apr 2026 08:21:49 -0600 Subject: [PATCH 14/19] remove PSY references --- .claude/claude.md | 2 +- Project.toml | 2 - src/InfrastructureOptimizationModels.jl | 37 +- src/common_models/add_auxiliary_variable.jl | 2 +- src/common_models/add_constraint_dual.jl | 24 +- src/common_models/add_param_container.jl | 2 +- src/common_models/add_variable.jl | 12 +- src/common_models/constraint_helpers.jl | 2 +- src/common_models/duration_constraints.jl | 8 +- src/common_models/get_time_series.jl | 6 +- src/common_models/interfaces.jl | 18 +- src/common_models/range_constraint.jl | 55 +- src/common_models/rateofchange_constraints.jl | 44 +- src/common_models/set_expression.jl | 4 +- src/core/definitions.jl | 6 - src/core/initial_conditions.jl | 8 +- src/core/network_model.jl | 6 +- src/core/network_reductions.jl | 28 +- src/core/optimization_container.jl | 45 +- src/core/optimization_problem_outputs.jl | 24 +- src/core/parameter_container.jl | 6 +- src/core/service_model.jl | 16 +- src/core/standard_variables_expressions.jl | 2 +- src/objective_function/common.jl | 26 +- src/objective_function/import_export.jl | 56 -- .../objective_function_pwl_lambda.jl | 3 +- src/objective_function/start_up_shut_down.jl | 3 +- src/objective_function/value_curve_cost.jl | 673 +++--------------- src/operation/decision_model.jl | 34 +- src/operation/emulation_model.jl | 30 +- ...itial_conditions_update_in_memory_store.jl | 2 +- src/operation/operation_model_interface.jl | 6 +- src/operation/time_series_interface.jl | 4 +- src/quadratic_approximations/incremental.jl | 8 +- ...wersystems_utils.jl => component_utils.jl} | 98 +-- src/utils/print_pt_v3.jl | 59 +- src/utils/time_series_utils.jl | 14 +- test/InfrastructureOptimizationModelsTests.jl | 38 +- test/Project.toml | 2 - test/includes.jl | 53 -- test/test_offer_curve_cost.jl | 402 ----------- test/test_utils/mock_operation_models.jl | 167 ----- test/test_utils/model_checks.jl | 550 -------------- 43 files changed, 429 insertions(+), 2158 deletions(-) delete mode 100644 src/objective_function/import_export.jl rename src/utils/{powersystems_utils.jl => component_utils.jl} (77%) delete mode 100644 test/includes.jl delete mode 100644 test/test_offer_curve_cost.jl delete mode 100644 test/test_utils/mock_operation_models.jl delete mode 100644 test/test_utils/model_checks.jl diff --git a/.claude/claude.md b/.claude/claude.md index 917625b2..ff4ff67e 100644 --- a/.claude/claude.md +++ b/.claude/claude.md @@ -106,7 +106,7 @@ src/ file_utils.jl # File I/O utilities logging.jl # Logging setup indexing.jl # Index/key utilities - powersystems_utils.jl # PowerSystems integration utilities + component_utils.jl # Component filtering and unit-system conversion helpers (IS-only) time_series_utils.jl # Time series helpers generate_valid_formulations.jl # Formulation validation print_pt_v2.jl / print_pt_v3.jl # Pretty-printing diff --git a/Project.toml b/Project.toml index e61229c9..f84750c3 100644 --- a/Project.toml +++ b/Project.toml @@ -18,7 +18,6 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" MathOptInterface = "b8f27783-ece8-5eb3-8dc8-9495eed66fee" PowerNetworkMatrices = "bed98974-b02a-5e2f-9fe0-a103f5c450dd" -PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" PrettyTables = "08abe8d2-0d0c-5749-adfa-8a2ac140af0d" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Serialization = "9e88b42a-f829-5b0c-bbe9-9e923198166b" @@ -41,7 +40,6 @@ LinearAlgebra = "1" Logging = "1" MathOptInterface = "1" PowerNetworkMatrices = "^0.19" -PowerSystems = "5" PrettyTables = "3.1" Random = "^1.10" Serialization = "1" diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index 808100b4..d671be70 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -14,7 +14,6 @@ import JuMP.Containers: DenseAxisArray, SparseAxisArray import MathOptInterface import LinearAlgebra import JSON3 -import PowerSystems import InfrastructureSystems import PowerNetworkMatrices import PowerNetworkMatrices: PTDF, VirtualPTDF, LODF, VirtualLODF @@ -90,22 +89,30 @@ import InfrastructureSystems: InvalidValue, ConflictingInputsError -# PowerSystems imports -import PowerSystems: +# IS re-exports of generic component/time-series accessors +import InfrastructureSystems: get_components, get_component, get_available_components, get_available_component, get_groups, get_available_groups, - stores_time_series_in_memory, - get_base_power, - get_active_power_limits, - get_start_up, - get_shut_down, - get_must_run, - get_operation_cost -import PowerSystems: StartUpStages + stores_time_series_in_memory + +# Extension-point stubs for accessors that downstream packages (e.g. POM) provide +# methods for when operating on PSY types. +function get_base_power end +function get_active_power_limits end +function get_max_active_power end +function get_ramp_limits end +function get_start_up end +function get_shut_down end +function get_must_run end +function get_operation_cost end +function get_dc_bus end +function get_bustype end +function has_service end +function set_units_base_system! end import TimerOutputs @@ -131,7 +138,6 @@ import PrettyTables ################################################################################ # Type Aliases -const PSY = PowerSystems const POM = InfrastructureOptimizationModels const IS = InfrastructureSystems const ISOPT = InfrastructureSystems.Optimization @@ -224,7 +230,6 @@ export add_pwl_linking_constraint! export add_pwl_normalization_constraint! export add_pwl_sos2_constraint! export get_pwl_cost_expression_delta -export process_market_bid_parameters! ## Outputs interfaces export get_variable_values @@ -360,7 +365,6 @@ export get_min_max_limits export AbstractThermalDispatchFormulation, AbstractThermalUnitCommitment # Service/misc helpers # NOTE: get_time_series NOT exported — conflicts with IS.get_time_series. Use IOM.get_time_series. -export process_import_export_parameters!, process_market_bid_parameters! # Extension point functions export add_service_variables!, requires_initialization # End bulk-added @@ -375,7 +379,7 @@ export get_incompatible_devices export OptimizationContainer, OperationModel, AbstractPowerFlowEvaluationModel export ArgumentConstructStage, ModelConstructStage export EmulationModelStore, DeviceModelForBranches -export StartUpStages, SOSStatusVariable +export SOSStatusVariable # Parameter types export FuelCostParameter, VariableValueParameter, FixValueParameter # Offer curve types (parameter, variable, constraint) @@ -584,7 +588,6 @@ include("objective_function/start_up_shut_down.jl") # add_{start_up, shut_down}_ # same 5 arguments: container, variable, component, cost_curve, formulation. include("objective_function/linear_curve.jl") include("objective_function/quadratic_curve.jl") -include("objective_function/import_export.jl") # Offer curve types (pure type definitions, no dependencies) include("objective_function/offer_curve_types.jl") @@ -642,7 +645,7 @@ include("utils/file_utils.jl") include("utils/logging.jl") include("utils/dataframes_utils.jl") include("utils/jump_utils.jl") -include("utils/powersystems_utils.jl") +include("utils/component_utils.jl") include("utils/time_series_utils.jl") include("utils/datetime_utils.jl") end diff --git a/src/common_models/add_auxiliary_variable.jl b/src/common_models/add_auxiliary_variable.jl index fbd75e1e..e4495057 100644 --- a/src/common_models/add_auxiliary_variable.jl +++ b/src/common_models/add_auxiliary_variable.jl @@ -16,7 +16,7 @@ function add_variables!( container, T, D, - PSY.get_name.(devices), + IS.get_name.(devices), time_steps, ) return diff --git a/src/common_models/add_constraint_dual.jl b/src/common_models/add_constraint_dual.jl index 1c16eefd..b30fc823 100644 --- a/src/common_models/add_constraint_dual.jl +++ b/src/common_models/add_constraint_dual.jl @@ -1,9 +1,9 @@ # Device model function add_constraint_dual!( container::OptimizationContainer, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, model::DeviceModel{T, D}, -) where {T <: PSY.Component, D <: AbstractDeviceFormulation} +) where {T <: IS.InfrastructureSystemsComponent, D <: AbstractDeviceFormulation} if !isempty(get_duals(model)) devices = get_available_components(model, sys) for constraint_type in get_duals(model) @@ -16,11 +16,11 @@ end # Network model function add_constraint_dual!( container::OptimizationContainer, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, model::NetworkModel{T}, ) where {T <: AbstractPowerModel} if !isempty(get_duals(model)) - devices = get_available_components(model, PSY.ACBus, sys) + devices = get_available_components(model, IS.InfrastructureSystemsComponent, sys) for constraint_type in get_duals(model) assign_dual_variable!(container, constraint_type, devices, model) end @@ -31,9 +31,9 @@ end # Service model function add_constraint_dual!( container::OptimizationContainer, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, model::ServiceModel{T, D}, -) where {T <: PSY.Service, D <: AbstractServiceFormulation} +) where {T <: IS.InfrastructureSystemsComponent, D <: AbstractServiceFormulation} if !isempty(get_duals(model)) service = get_available_components(model, sys) for constraint_type in get_duals(model) @@ -49,9 +49,9 @@ function assign_dual_variable!( constraint_type::Type{<:ConstraintType}, service::D, ::Type{<:AbstractServiceFormulation}, -) where {D <: PSY.Service} +) where {D <: IS.InfrastructureSystemsComponent} time_steps = get_time_steps(container) - service_name = PSY.get_name(service) + service_name = IS.get_name(service) add_dual_container!( container, constraint_type, @@ -69,14 +69,14 @@ function assign_dual_variable!( constraint_type::Type{<:ConstraintType}, devices::U, ::Type{<:AbstractDeviceFormulation}, -) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.Device} +) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: IS.InfrastructureSystemsComponent} @assert !isempty(devices) time_steps = get_time_steps(container) add_dual_container!( container, constraint_type, D, - PSY.get_name.(devices), + IS.get_name.(devices), time_steps, ) return @@ -88,14 +88,14 @@ function assign_dual_variable!( constraint_type::Type{<:ConstraintType}, devices::U, ::NetworkModel{<:AbstractPowerModel}, -) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: PSY.ACBus} +) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: IS.InfrastructureSystemsComponent} @assert !isempty(devices) time_steps = get_time_steps(container) add_dual_container!( container, constraint_type, D, - PSY.get_name.(devices), + IS.get_name.(devices), time_steps, ) return diff --git a/src/common_models/add_param_container.jl b/src/common_models/add_param_container.jl index ae97e9dd..37cfa052 100644 --- a/src/common_models/add_param_container.jl +++ b/src/common_models/add_param_container.jl @@ -90,7 +90,7 @@ function add_param_container!( axs...; sparse = false, meta = CONTAINER_KEY_EMPTY_META, -) where {T <: EventParameter, U <: IS.InfrastructureSystemsComponent, V <: PSY.Contingency} +) where {T <: EventParameter, U <: IS.InfrastructureSystemsComponent, V <: IS.InfrastructureSystemsComponent} param_key = ParameterKey(T, U, meta) attributes = EventParametersAttributes(V) return add_param_container_shared_axes!( diff --git a/src/common_models/add_variable.jl b/src/common_models/add_variable.jl index ead64dbf..952ac602 100644 --- a/src/common_models/add_variable.jl +++ b/src/common_models/add_variable.jl @@ -87,10 +87,10 @@ function add_service_variables!( ::Type{F}, ) where { T <: VariableType, - U <: PSY.Service, + U <: IS.InfrastructureSystemsComponent, V <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, F <: AbstractServiceFormulation, -} where {D <: PSY.Component} +} where {D <: IS.InfrastructureSystemsComponent} @assert !isempty(contributing_devices) time_steps = get_time_steps(container) @@ -100,16 +100,16 @@ function add_service_variables!( container, T, U, - PSY.get_name(service), - [PSY.get_name(d) for d in contributing_devices], + IS.get_name(service), + [IS.get_name(d) for d in contributing_devices], time_steps, ) for t in time_steps, d in contributing_devices - name = PSY.get_name(d) + name = IS.get_name(d) variable[name, t] = JuMP.@variable( get_jump_model(container), - base_name = "$(T)_$(U)_$(PSY.get_name(service))_{$(name), $(t)}", + base_name = "$(T)_$(U)_$(IS.get_name(service))_{$(name), $(t)}", binary = binary ) diff --git a/src/common_models/constraint_helpers.jl b/src/common_models/constraint_helpers.jl index b4273540..519c90e5 100644 --- a/src/common_models/constraint_helpers.jl +++ b/src/common_models/constraint_helpers.jl @@ -117,7 +117,7 @@ function add_updown_constraints_containers!( ::Type{V}, names, time_steps, -) where {T <: ConstraintType, V <: PSY.Component} +) where {T <: ConstraintType, V <: IS.InfrastructureSystemsComponent} return ( up = add_constraints_container!(container, T, V, names, time_steps; meta = "up"), down = add_constraints_container!( diff --git a/src/common_models/duration_constraints.jl b/src/common_models/duration_constraints.jl index ebea926c..9336b2c8 100644 --- a/src/common_models/duration_constraints.jl +++ b/src/common_models/duration_constraints.jl @@ -40,7 +40,7 @@ function device_duration_retrospective!( initial_duration::Matrix{InitialCondition}, ::Type{C}, ::Type{T}, -) where {C <: ConstraintType, T <: PSY.Component} +) where {C <: ConstraintType, T <: IS.InfrastructureSystemsComponent} time_steps = get_time_steps(container) varon = get_variable(container, OnVariable, T) @@ -145,7 +145,7 @@ function device_duration_look_ahead!( ::Type{C_up}, ::Type{C_dn}, ::Type{T}, -) where {C_up <: ConstraintType, C_dn <: ConstraintType, T <: PSY.Component} +) where {C_up <: ConstraintType, C_dn <: ConstraintType, T <: IS.InfrastructureSystemsComponent} time_steps = get_time_steps(container) varon = get_variable(container, OnVariable, T) varstart = get_variable(container, StartVariable, T) @@ -243,7 +243,7 @@ function device_duration_parameters!( initial_duration::Matrix{InitialCondition}, ::Type{C}, ::Type{T}, -) where {C <: ConstraintType, T <: PSY.Component} +) where {C <: ConstraintType, T <: IS.InfrastructureSystemsComponent} time_steps = get_time_steps(container) varon = get_variable(container, OnVariable, T) @@ -365,7 +365,7 @@ function device_duration_compact_retrospective!( initial_duration::Matrix{InitialCondition}, ::Type{C}, ::Type{T}, -) where {C <: ConstraintType, T <: PSY.Component} +) where {C <: ConstraintType, T <: IS.InfrastructureSystemsComponent} time_steps = get_time_steps(container) varon = get_variable(container, OnVariable, T) diff --git a/src/common_models/get_time_series.jl b/src/common_models/get_time_series.jl index 154d4f30..95d7e15d 100644 --- a/src/common_models/get_time_series.jl +++ b/src/common_models/get_time_series.jl @@ -1,7 +1,7 @@ # NOTE not included currently. function _get_time_series( container::OptimizationContainer, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, attributes::TimeSeriesAttributes{T}, ) where {T <: IS.TimeSeriesData} return get_time_series_initial_values!( @@ -17,7 +17,7 @@ function get_time_series( component::T, ::Type{P}, meta = CONTAINER_KEY_EMPTY_META, -) where {T <: PSY.Component, P <: TimeSeriesParameter} +) where {T <: IS.InfrastructureSystemsComponent, P <: TimeSeriesParameter} parameter_container = get_parameter(container, P, T, meta) return _get_time_series(container, component, parameter_container.attributes) end @@ -26,7 +26,7 @@ end # refactor is done. function get_time_series( container::OptimizationContainer, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, forecast_name::String, ) ts_type = get_default_time_series_type(container) diff --git a/src/common_models/interfaces.jl b/src/common_models/interfaces.jl index 336d5451..8e5c9ef5 100644 --- a/src/common_models/interfaces.jl +++ b/src/common_models/interfaces.jl @@ -99,7 +99,7 @@ function proportional_cost( ::C, ::Type{F}, ) where { - O <: PSY.OperationalCost, + O <: IS.DeviceParameter, V <: VariableType, C <: IS.InfrastructureSystemsComponent, F <: AbstractDeviceFormulation, @@ -121,7 +121,7 @@ function proportional_cost( ::Type{F}, ::Int, ) where { - O <: PSY.OperationalCost, + O <: IS.DeviceParameter, V <: VariableType, C <: IS.InfrastructureSystemsComponent, F <: AbstractDeviceFormulation, @@ -135,7 +135,7 @@ end Extension point: Check if proportional cost term is time-variant. Returns true if the cost should be added to the variant objective expression. """ -is_time_variant_term(::PSY.OperationalCost) = false +is_time_variant_term(::IS.DeviceParameter) = false # corresponds to get_must_run for thermals, but avoiding device specific code here. """ @@ -203,14 +203,20 @@ The one exception where it isn't just `get_variable(cost)`: storage devices, whe need to map `ActivePower{In/Out}` to {charge/discharge} variable cost. """ function variable_cost( - cost::PSY.OperationalCost, + cost::IS.DeviceParameter, ::Type{<:VariableType}, ::Type{<:IS.InfrastructureSystemsComponent}, ::Type{<:AbstractDeviceFormulation}, ) - return PSY.get_variable(cost) + return get_variable_cost(cost) end +""" +Extension point: read the primary variable cost from an operation-cost object. +POM provides methods (typically delegating to `PSY.get_variable`). +""" +function get_variable_cost end + variable_cost( ::Nothing, ::Type{<:VariableType}, @@ -224,7 +230,7 @@ Concrete implementations in POM. Used for ramp constraints. """ _get_initial_condition_type( X::Type{<:ConstraintType}, - Y::Type{<:PSY.Component}, + Y::Type{<:IS.InfrastructureSystemsComponent}, Z::Type{<:AbstractDeviceFormulation}, ) = error("`_get_initial_condition_type` not implemented for $X , $Y and $Z") diff --git a/src/common_models/range_constraint.jl b/src/common_models/range_constraint.jl index 60adc60c..2b10439a 100644 --- a/src/common_models/range_constraint.jl +++ b/src/common_models/range_constraint.jl @@ -94,14 +94,14 @@ function _add_bound_range_constraints_impl!( W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) - device_names = PSY.get_name.(devices) + device_names = IS.get_name.(devices) jump_model = get_jump_model(container) con = add_constraints_container!( container, T, V, device_names, time_steps; meta = constraint_meta(dir)) for device in devices, t in time_steps - ci_name = PSY.get_name(device) + ci_name = IS.get_name(device) limits = get_min_max_limits(device, T, W) add_range_bound_constraint!( dir, jump_model, con, ci_name, t, array[ci_name, t], get_bound(dir, limits)) @@ -209,14 +209,14 @@ function _add_semicontinuous_bound_range_constraints_impl!( W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) - names = PSY.get_name.(devices) + names = IS.get_name.(devices) jump_model = get_jump_model(container) con = add_constraints_container!( container, T, V, names, time_steps; meta = constraint_meta(dir)) varbin = get_variable(container, OnVariable, V) for device in devices, t in time_steps - ci_name = PSY.get_name(device) + ci_name = IS.get_name(device) limits = get_min_max_limits(device, T, W) add_range_bound_constraint!( dir, jump_model, con, ci_name, t, @@ -225,35 +225,6 @@ function _add_semicontinuous_bound_range_constraints_impl!( return end -# ThermalGen version - checks must_run to decide whether to use binary variable -function _add_semicontinuous_bound_range_constraints_impl!( - container::OptimizationContainer, - ::Type{T}, - dir::BoundDirection, - array, - devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, - ::DeviceModel{V, W}, -) where {T <: ConstraintType, V <: PSY.ThermalGen, W <: AbstractDeviceFormulation} - time_steps = get_time_steps(container) - names = PSY.get_name.(devices) - jump_model = get_jump_model(container) - con = add_constraints_container!( - container, T, V, names, time_steps; meta = constraint_meta(dir)) - varbin = get_variable(container, OnVariable, V) - - for device in devices - ci_name = PSY.get_name(device) - limits = get_min_max_limits(device, T, W) - for t in time_steps - bin = PSY.get_must_run(device) ? 1.0 : varbin[ci_name, t] - add_range_bound_constraint!( - dir, jump_model, con, ci_name, t, - array[ci_name, t], get_bound(dir, limits), bin) - end - end - return -end - # Unified reserve range constraints impl # invert_binary: true for InputActivePower (uses 1-varbin), false for others (uses varbin) function add_reserve_bound_range_constraints!( @@ -270,7 +241,7 @@ function add_reserve_bound_range_constraints!( W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) - names = PSY.get_name.(devices) + names = IS.get_name.(devices) jump_model = get_jump_model(container) con = add_constraints_container!( @@ -278,7 +249,7 @@ function add_reserve_bound_range_constraints!( varbin = get_variable(container, ReservationVariable, V) for device in devices, t in time_steps - ci_name = PSY.get_name(device) + ci_name = IS.get_name(device) limits = get_min_max_limits(device, T, W) bin = invert_binary ? (1 - varbin[ci_name, t]) : varbin[ci_name, t] add_range_bound_constraint!( @@ -416,7 +387,7 @@ function _add_parameterized_bound_range_constraints_impl!( ts_name = get_time_series_names(model)[P] ts_type = get_default_time_series_type(container) # PERF: compilation hotspot. Switch to TSC. - names = [PSY.get_name(d) for d in devices if PSY.has_time_series(d, ts_type, ts_name)] + names = [IS.get_name(d) for d in devices if IS.has_time_series(d, ts_type, ts_name)] if isempty(names) @debug "There are no $V devices with time series data $ts_type, $ts_name" return @@ -445,7 +416,7 @@ function _add_parameterized_bound_range_constraints_impl!( W <: AbstractDeviceFormulation, } time_steps = get_time_steps(container) - names = PSY.get_name.(devices) + names = IS.get_name.(devices) constraint = add_constraints_container!( container, T, V, names, time_steps; meta = constraint_meta(dir)) @@ -473,7 +444,7 @@ function _bound_range_with_parameter!( jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] for device in devices, t in time_steps - name = PSY.get_name(device) + name = IS.get_name(device) rhs = param_multiplier[name, t] * param_array[name, t] constraint_container[name, t] = _make_bound_constraint(dir, jump_model, lhs_array[name, t], rhs) @@ -503,8 +474,8 @@ function _bound_range_with_parameter!( jump_model = get_jump_model(container) time_steps = axes(constraint_container)[2] for device in devices, t in time_steps - ub = PSY.get_max_active_power(device) - name = PSY.get_name(device) + ub = get_max_active_power(device) + name = IS.get_name(device) rhs = ub * param_array[name, t] constraint_container[name, t] = _make_bound_constraint(dir, jump_model, lhs_array[name, t], rhs) @@ -533,8 +504,8 @@ function _bound_range_with_parameter!( ts_name = get_time_series_names(model)[P] ts_type = get_default_time_series_type(container) for device in devices - name = PSY.get_name(device) - if !(PSY.has_time_series(device, ts_type, ts_name)) + name = IS.get_name(device) + if !(IS.has_time_series(device, ts_type, ts_name)) continue end param_col = get_parameter_column_refs(param_container, name) diff --git a/src/common_models/rateofchange_constraints.jl b/src/common_models/rateofchange_constraints.jl index 327844ab..3785d6de 100644 --- a/src/common_models/rateofchange_constraints.jl +++ b/src/common_models/rateofchange_constraints.jl @@ -13,16 +13,16 @@ end function _get_ramp_constraint_devices( container::OptimizationContainer, devices::Union{Vector{U}, IS.FlattenIteratorWrapper{U}}, -) where {U <: PSY.Component} +) where {U <: IS.InfrastructureSystemsComponent} minutes_per_period = _get_minutes_per_period(container) filtered_device = Vector{U}() for d in devices - ramp_limits = PSY.get_ramp_limits(d) + ramp_limits = get_ramp_limits(d) if ramp_limits !== nothing - p_lims = PSY.get_active_power_limits(d) + p_lims = get_active_power_limits(d) max_rate = abs(p_lims.min - p_lims.max) / minutes_per_period if (ramp_limits.up >= max_rate) & (ramp_limits.down >= max_rate) - @debug "Generator has a nonbinding ramp limits. Constraints Skipped" PSY.get_name( + @debug "Generator has a nonbinding ramp limits. Constraints Skipped" IS.get_name( d, ) continue @@ -39,7 +39,7 @@ function _get_ramp_slack_vars( model::DeviceModel{V, W}, name::String, t::Int, -) where {V <: PSY.Component, W <: AbstractDeviceFormulation} +) where {V <: IS.InfrastructureSystemsComponent, W <: AbstractDeviceFormulation} if get_use_slacks(model) slack_up = get_variable(container, RateofChangeConstraintSlackUp, V) slack_dn = get_variable(container, RateofChangeConstraintSlackDown, V) @@ -81,7 +81,7 @@ function add_linear_ramp_constraints!( ::Type{<:AbstractPowerModel}, ) where { S <: Union{PowerAboveMinimumVariable, ActivePowerVariable}, - V <: PSY.Component, + V <: IS.InfrastructureSystemsComponent, W <: AbstractDeviceFormulation, } # common setup for all ramp constraints @@ -92,7 +92,7 @@ function add_linear_ramp_constraints!( IC = _get_initial_condition_type(T, V, W) initial_conditions_power = get_initial_condition(container, IC, V) jump_model = get_jump_model(container) - device_name_set = PSY.get_name.(ramp_devices) + device_name_set = IS.get_name.(ramp_devices) cons = add_updown_constraints_containers!(container, T, V, device_name_set, time_steps) expr_dn = get_expression(container, ActivePowerRangeExpressionLB, V) @@ -102,7 +102,7 @@ function add_linear_ramp_constraints!( name = get_component_name(ic) # This is to filter out devices that dont need a ramping constraint name ∉ device_name_set && continue - ramp_limits = PSY.get_ramp_limits(get_component(ic)) + ramp_limits = get_ramp_limits(get_component(ic)) ic_power = get_value(ic) @debug "add rate_of_change_constraint" name ic_power @@ -132,7 +132,7 @@ function _add_linear_ramp_constraints_impl!( U::Type{<:VariableType}, devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, -) where {V <: PSY.Component, W <: AbstractDeviceFormulation} +) where {V <: IS.InfrastructureSystemsComponent, W <: AbstractDeviceFormulation} # common setup for all ramp constraints time_steps = get_time_steps(container) variable = get_variable(container, U, V) @@ -141,7 +141,7 @@ function _add_linear_ramp_constraints_impl!( IC = _get_initial_condition_type(T, V, W) initial_conditions_power = get_initial_condition(container, IC, V) jump_model = get_jump_model(container) - device_name_set = PSY.get_name.(ramp_devices) + device_name_set = IS.get_name.(ramp_devices) cons = add_updown_constraints_containers!(container, T, V, device_name_set, time_steps) parameters = built_for_recurrent_solves(container) @@ -150,7 +150,7 @@ function _add_linear_ramp_constraints_impl!( name = get_component_name(ic) # This is to filter out devices that dont need a ramping constraint name ∉ device_name_set && continue - ramp_limits = PSY.get_ramp_limits(get_component(ic)) + ramp_limits = get_ramp_limits(get_component(ic)) ic_power = get_value(ic) @debug "add rate_of_change_constraint" name ic_power @assert (parameters && isa(ic_power, JuMP.VariableRef)) || !parameters @@ -179,7 +179,7 @@ function add_linear_ramp_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, X::Type{<:AbstractPowerModel}, -) where {V <: PSY.Component, W <: AbstractDeviceFormulation} +) where {V <: IS.InfrastructureSystemsComponent, W <: AbstractDeviceFormulation} return _add_linear_ramp_constraints_impl!(container, T, U, devices, model) end @@ -194,7 +194,7 @@ function add_linear_ramp_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::DeviceModel{V, W}, X::Type{<:AbstractPowerModel}, -) where {V <: PSY.ThermalGen, W <: AbstractThermalDispatchFormulation} +) where {V <: IS.InfrastructureSystemsComponent, W <: AbstractThermalDispatchFormulation} # Fallback to generic implementation if OnStatusParameter is not present if !has_container_key(container, OnStatusParameter, V) @@ -209,7 +209,7 @@ function add_linear_ramp_constraints!( IC = _get_initial_condition_type(T, V, W) initial_conditions_power = get_initial_condition(container, IC, V) jump_model = get_jump_model(container) - device_name_set = [PSY.get_name(r) for r in ramp_devices] + device_name_set = [IS.get_name(r) for r in ramp_devices] cons = add_updown_constraints_containers!(container, T, V, device_name_set, time_steps) # Commitment path from UC as a PARAMETER (fixed 0/1) @@ -221,9 +221,9 @@ function add_linear_ramp_constraints!( ) for dev in ramp_devices - name = PSY.get_name(dev) - ramp_limits = PSY.get_ramp_limits(dev) - power_limits = PSY.get_active_power_limits(dev) + name = IS.get_name(dev) + ramp_limits = get_ramp_limits(dev) + power_limits = get_active_power_limits(dev) # --- t = 1: Use ic_power to determine starting ramp condition ic_power = ic_power_by_name[name] @@ -285,7 +285,7 @@ function add_semicontinuous_ramp_constraints!( ::Type{<:AbstractPowerModel}, ) where { S <: Union{PowerAboveMinimumVariable, ActivePowerVariable}, - V <: PSY.Component, + V <: IS.InfrastructureSystemsComponent, W <: AbstractDeviceFormulation, } # common setup for all ramp constraints @@ -296,7 +296,7 @@ function add_semicontinuous_ramp_constraints!( IC = _get_initial_condition_type(T, V, W) initial_conditions_power = get_initial_condition(container, IC, V) jump_model = get_jump_model(container) - device_name_set = PSY.get_name.(ramp_devices) + device_name_set = IS.get_name.(ramp_devices) cons = add_updown_constraints_containers!(container, T, V, device_name_set, time_steps) varstart = get_variable(container, StartVariable, V) @@ -309,12 +309,12 @@ function add_semicontinuous_ramp_constraints!( # This is to filter out devices that dont need a ramping constraint name ∉ device_name_set && continue device = get_component(ic) - ramp_limits = PSY.get_ramp_limits(device) - power_limits = PSY.get_active_power_limits(device) + ramp_limits = get_ramp_limits(device) + power_limits = get_active_power_limits(device) ic_power = get_value(ic) @debug "add rate_of_change_constraint" name ic_power - must_run = hasmethod(PSY.get_must_run, Tuple{V}) && PSY.get_must_run(device) + must_run = hasmethod(get_must_run, Tuple{V}) && get_must_run(device) for t in time_steps slack = _get_ramp_slack_vars(container, model, name, t) diff --git a/src/common_models/set_expression.jl b/src/common_models/set_expression.jl index 2537abd5..91b93597 100644 --- a/src/common_models/set_expression.jl +++ b/src/common_models/set_expression.jl @@ -7,10 +7,10 @@ function set_expression!( cost_expression::JuMP.AbstractJuMPScalar, component::T, time_period::Int, -) where {S <: CostExpressions, T <: PSY.Component} +) where {S <: CostExpressions, T <: IS.InfrastructureSystemsComponent} if has_container_key(container, S, T) device_cost_expression = get_expression(container, S, T) - component_name = PSY.get_name(component) + component_name = IS.get_name(component) device_cost_expression[component_name, time_period] = cost_expression end return diff --git a/src/core/definitions.jl b/src/core/definitions.jl index 359c596f..74dd0af7 100644 --- a/src/core/definitions.jl +++ b/src/core/definitions.jl @@ -28,12 +28,6 @@ struct DecrementalOffer <: OfferDirection end Base.string(::IncrementalOffer) = "incremental" Base.string(::DecrementalOffer) = "decremental" -# Union type aliases for static + time-series cost variants -const MBC_TYPES = Union{PSY.MarketBidCost, PSY.MarketBidTimeSeriesCost} -const IEC_TYPES = Union{PSY.ImportExportCost, PSY.ImportExportTimeSeriesCost} -const TS_OFFER_CURVE_COST_TYPES = - Union{PSY.MarketBidTimeSeriesCost, PSY.ImportExportTimeSeriesCost} - # Type alias for decision model indices - used for indexing into output stores const DecisionModelIndexType = Dates.DateTime const EmulationModelIndexType = Int diff --git a/src/core/initial_conditions.jl b/src/core/initial_conditions.jl index 81372422..3c0c116b 100644 --- a/src/core/initial_conditions.jl +++ b/src/core/initial_conditions.jl @@ -5,13 +5,13 @@ mutable struct InitialCondition{ T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64, Nothing}, } - component::PSY.Component + component::IS.InfrastructureSystemsComponent value::U end function InitialCondition( ::Type{T}, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, value::U, ) where {T <: InitialConditionType, U <: Union{JuMP.VariableRef, Float64}} return InitialCondition{T, U}(component, value) @@ -23,7 +23,7 @@ function InitialCondition( value::V, ) where { T <: InitialConditionType, - U <: PSY.Component, + U <: IS.InfrastructureSystemsComponent, V <: Union{JuMP.VariableRef, Float64}, } return InitialCondition{T, U}(component, value) @@ -47,7 +47,7 @@ end get_component(ic::InitialCondition) = ic.component get_value(ic::InitialCondition) = ic.value -get_component_name(ic::InitialCondition) = PSY.get_name(ic.component) +get_component_name(ic::InitialCondition) = IS.get_name(ic.component) get_component_type(ic::InitialCondition) = typeof(ic.component) get_ic_type( ::Type{InitialCondition{T, U}}, diff --git a/src/core/network_model.jl b/src/core/network_model.jl index fa9c56bd..9e33d10c 100644 --- a/src/core/network_model.jl +++ b/src/core/network_model.jl @@ -1,4 +1,4 @@ -const DeviceModelForBranches = DeviceModel{<:PSY.Branch, <:AbstractDeviceFormulation} +const DeviceModelForBranches = DeviceModel{<:IS.InfrastructureSystemsComponent, <:AbstractDeviceFormulation} const BranchModelContainer = Dict{Symbol, DeviceModelForBranches} function _check_pm_formulation(::Type{T}) where {T <: AbstractPowerModel} @@ -60,7 +60,7 @@ mutable struct NetworkModel{T <: AbstractPowerModel} PTDF_matrix::Union{Nothing, PNM.PowerNetworkMatrix} LODF_matrix::Union{Nothing, PNM.PowerNetworkMatrix} subnetworks::Dict{Int, Set{Int}} - bus_area_map::Dict{PSY.ACBus, Int} + bus_area_map::Dict{IS.InfrastructureSystemsComponent, Int} duals::Vector{DataType} network_reduction::PNM.NetworkReductionData reduce_radial_branches::Bool @@ -92,7 +92,7 @@ mutable struct NetworkModel{T <: AbstractPowerModel} PTDF_matrix, LODF_matrix, subnetworks, - Dict{PSY.ACBus, Int}(), + Dict{IS.InfrastructureSystemsComponent, Int}(), duals, PNM.NetworkReductionData(), reduce_radial_branches, diff --git a/src/core/network_reductions.jl b/src/core/network_reductions.jl index b0e9a74f..84432313 100644 --- a/src/core/network_reductions.jl +++ b/src/core/network_reductions.jl @@ -11,7 +11,7 @@ mutable struct BranchReductionOptimizationTracker constraint_map_by_type::Dict{ Type{<:ConstraintType}, Dict{ - Type{<:PSY.ACTransmission}, + Type{<:IS.InfrastructureSystemsComponent}, SortedDict{String, Tuple{Tuple{Int, Int}, String}}, }, } @@ -141,7 +141,7 @@ function get_branch_argument_parameter_axes( ::IS.FlattenIteratorWrapper{T}, ::Type{V}, ts_name::String, -) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData} +) where {T <: IS.InfrastructureSystemsComponent, V <: IS.TimeSeriesData} return get_branch_argument_parameter_axes(net_reduction_data, T, V, ts_name) end @@ -151,10 +151,10 @@ Delegates to PNM, which handles BranchesParallel, BranchesSeries, ThreeWindingTransformerWinding, and plain ACTransmission entries. """ function get_device_with_time_series( - branch::PSY.ACTransmission, + branch::IS.InfrastructureSystemsComponent, ::Type{V}, ts_name::String, -) where {V <: PSY.TimeSeriesData} +) where {V <: IS.TimeSeriesData} return PNM.get_device_with_time_series(branch, V, ts_name) end @@ -163,7 +163,7 @@ function get_branch_argument_parameter_axes( ::Type{T}, ::Type{V}, ts_name::String, -) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData} +) where {T <: IS.InfrastructureSystemsComponent, V <: IS.TimeSeriesData} name_axis = Vector{String}() ts_uuid_axis = Vector{String}() arc_map = get(net_reduction_data.name_to_arc_map, T, nothing) @@ -186,32 +186,24 @@ end function get_branch_argument_variable_axis( net_reduction_data::PNM.NetworkReductionData, ::IS.FlattenIteratorWrapper{T}, -) where {T <: PSY.ACTransmission} +) where {T <: IS.InfrastructureSystemsComponent} return get_branch_argument_variable_axis(net_reduction_data, T) end function get_branch_argument_variable_axis( net_reduction_data::PNM.NetworkReductionData, ::Type{T}, -) where {T <: PSY.ACTransmission} +) where {T <: IS.InfrastructureSystemsComponent} name_axis = net_reduction_data.name_to_arc_map[T] return collect(keys(name_axis)) end -#= function get_branch_argument_variable_axis( - net_reduction_data::PNM.NetworkReductionData, - ::Type{PowerNetworkMatrices.ThreeWindingTransformerWinding{T}}, -) where {T <: PSY.ThreeWindingTransformer} - name_axis = net_reduction_data.name_to_arc_map[T] - return collect(keys(name_axis)) -end =# - function get_branch_argument_constraint_axis( net_reduction_data::PNM.NetworkReductionData, reduced_branch_tracker::BranchReductionOptimizationTracker, ::IS.FlattenIteratorWrapper{T}, ::Type{U}, -) where {T <: PSY.ACTransmission, U <: ConstraintType} +) where {T <: IS.InfrastructureSystemsComponent, U <: ConstraintType} return get_branch_argument_constraint_axis( net_reduction_data, reduced_branch_tracker, @@ -225,7 +217,7 @@ function get_branch_argument_constraint_axis( reduced_branch_tracker::BranchReductionOptimizationTracker, ::Type{T}, ::Type{U}, -) where {T <: PSY.ACTransmission, U <: ConstraintType} +) where {T <: IS.InfrastructureSystemsComponent, U <: ConstraintType} constraint_tracker = get_constraint_dict(reduced_branch_tracker) constraint_map_by_type = get_constraint_map_by_type(reduced_branch_tracker) name_axis = net_reduction_data.name_to_arc_map[T] @@ -235,7 +227,7 @@ function get_branch_argument_constraint_axis( constraint_map_by_type, U, Dict{ - Type{<:PSY.ACTransmission}, + Type{<:IS.InfrastructureSystemsComponent}, SortedDict{String, Tuple{Tuple{Int, Int}, String}}, }(), ) diff --git a/src/core/optimization_container.jl b/src/core/optimization_container.jl index c77e57a3..4bb352ac 100644 --- a/src/core/optimization_container.jl +++ b/src/core/optimization_container.jl @@ -288,12 +288,9 @@ function finalize_jump_model!(container::OptimizationContainer, settings::Settin return end -# Dispatch helpers so init_optimization_container! works with both PSY.System and mock containers. -temp_set_units_base_system!(sys::PSY.System, base::String) = - PSY.set_units_base_system!(sys, base) +# Dispatch extension points: default behavior is no-op / sentinel. Concrete system +# implementations (e.g. PSY.System in POM) should add their own methods. temp_set_units_base_system!(::IS.InfrastructureSystemsContainer, ::String) = nothing -temp_get_forecast_initial_timestamp(sys::PSY.System) = - PSY.get_forecast_initial_timestamp(sys) temp_get_forecast_initial_timestamp(::IS.InfrastructureSystemsContainer) = Dates.DateTime(1970) @@ -302,17 +299,17 @@ function init_optimization_container!( network_model::NetworkModel{T}, sys::IS.InfrastructureSystemsContainer, ) where {T <: AbstractPowerModel} - # PSY.set_units_base_system!(sys, "SYSTEM_BASE") + # set_units_base_system!(sys, "SYSTEM_BASE") temp_set_units_base_system!(sys, "SYSTEM_BASE") # The order of operations matter settings = get_settings(container) if get_initial_time(settings) == UNSET_INI_TIME - if get_default_time_series_type(container) <: PSY.AbstractDeterministic - # set_initial_time!(settings, PSY.get_forecast_initial_timestamp(sys)) + if get_default_time_series_type(container) <: IS.AbstractDeterministic + # set_initial_time!(settings, IS.get_forecast_initial_timestamp(sys)) set_initial_time!(settings, temp_get_forecast_initial_timestamp(sys)) - elseif get_default_time_series_type(container) <: PSY.SingleTimeSeries - ini_time, _ = PSY.check_time_series_consistency(sys, PSY.SingleTimeSeries) + elseif get_default_time_series_type(container) <: IS.SingleTimeSeries + ini_time, _ = IS.check_time_series_consistency(sys, IS.SingleTimeSeries) set_initial_time!(settings, ini_time) end end @@ -328,9 +325,9 @@ function init_optimization_container!( # NOTE: Simplified to avoid referencing concrete network model types (CopperPlatePowerModel, AreaBalancePowerModel) # PowerSimulations can implement more specific logic based on concrete types total_number_of_devices = - length(get_available_components(network_model, PSY.Device, sys)) + length(get_available_components(network_model, IS.InfrastructureSystemsComponent, sys)) total_number_of_devices += - length(get_available_components(network_model, PSY.ACBranch, sys)) + length(get_available_components(network_model, IS.InfrastructureSystemsComponent, sys)) # The 10e6 limit is based on the sizes of the lp benchmark problems http://plato.asu.edu/ftp/lpcom.html # The maximum numbers of constraints and variables in the benchmark problems is 1,918,399 and 1,259,121, @@ -1161,7 +1158,7 @@ function get_initial_conditions_variable( container::OptimizationContainer, type::VariableType, ::Type{T}, -) where {T <: Union{PSY.Component, PSY.System}} +) where {T <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} return get_initial_conditions_variable(get_initial_conditions_data(container), type, T) end @@ -1169,7 +1166,7 @@ function get_initial_conditions_aux_variable( container::OptimizationContainer, type::AuxVariableType, ::Type{T}, -) where {T <: Union{PSY.Component, PSY.System}} +) where {T <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} return get_initial_conditions_aux_variable( get_initial_conditions_data(container), type, @@ -1181,7 +1178,7 @@ function get_initial_conditions_dual( container::OptimizationContainer, type::ConstraintType, ::Type{T}, -) where {T <: Union{PSY.Component, PSY.System}} +) where {T <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} return get_initial_conditions_dual(get_initial_conditions_data(container), type, T) end @@ -1189,7 +1186,7 @@ function get_initial_conditions_parameter( container::OptimizationContainer, type::ParameterType, ::Type{T}, -) where {T <: Union{PSY.Component, PSY.System}} +) where {T <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} return get_initial_conditions_parameter(get_initial_conditions_data(container), type, T) end @@ -1274,8 +1271,8 @@ end # This should be defined in PowerSimulations if needed # function _calculate_dual_variable_value!( # container::OptimizationContainer, -# key::ConstraintKey{CopperPlateBalanceConstraint, PSY.System}, -# ::PSY.System, +# key::ConstraintKey{CopperPlateBalanceConstraint, IS.InfrastructureSystemsContainer}, +# ::IS.InfrastructureSystemsContainer, # ) # constraint_container = get_constraint(container, key) # dual_variable_container = get_duals(container)[key] @@ -1290,8 +1287,8 @@ end function _calculate_dual_variable_value!( container::OptimizationContainer, key::ConstraintKey{T, D}, - ::PSY.System, -) where {T <: ConstraintType, D <: Union{PSY.Component, PSY.System}} + ::IS.InfrastructureSystemsContainer, +) where {T <: ConstraintType, D <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} constraint_duals = jump_value.(get_constraint(container, key)) dual_variable_container = get_duals(container)[key] @@ -1305,7 +1302,7 @@ end function _calculate_dual_variables_continous_model!( container::OptimizationContainer, - system::PSY.System, + system::IS.InfrastructureSystemsContainer, ) duals_vars = get_duals(container) for key in keys(duals_vars) @@ -1316,7 +1313,7 @@ end function _calculate_dual_variables_discrete_model!( container::OptimizationContainer, - ::PSY.System, + ::IS.InfrastructureSystemsContainer, ) return process_duals(container, container.settings.optimizer) end @@ -1415,12 +1412,12 @@ end function get_time_series_initial_values!( container::OptimizationContainer, ::Type{T}, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, time_series_name::AbstractString, ) where {T <: IS.TimeSeriesData} initial_time = get_initial_time(container) time_steps = get_time_steps(container) - forecast = PSY.get_time_series( + forecast = IS.get_time_series( T, component, time_series_name; diff --git a/src/core/optimization_problem_outputs.jl b/src/core/optimization_problem_outputs.jl index e75c39fe..a4a93a8f 100644 --- a/src/core/optimization_problem_outputs.jl +++ b/src/core/optimization_problem_outputs.jl @@ -104,7 +104,7 @@ get_optimizer_stats(res::OptimizationProblemOutputs) = res.optimizer_stats get_parameter_values(res::OptimizationProblemOutputs) = res.parameter_values get_source_data(res::OptimizationProblemOutputs) = res.source_data -make_system_filename(sys::PSY.System) = make_system_filename(IS.get_uuid(sys)) +make_system_filename(sys::IS.InfrastructureSystemsContainer) = make_system_filename(IS.get_uuid(sys)) make_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json" """ @@ -116,7 +116,7 @@ function load_system(res::OptimizationProblemOutputs; kwargs...) !isnothing(get_source_data(res)) && return file = joinpath(get_outputs_dir(res), make_system_filename(get_source_data_uuid(res))) if isfile(file) - sys = PSY.System(file; time_series_read_only = true) + sys = IS.InfrastructureSystemsContainer(file; time_series_read_only = true) @info "De-serialized the system from files." else error("Could not locate system file: $file") @@ -515,7 +515,7 @@ Accepts a vector of keys for the return of the values. # Arguments - `res::OptimizationProblemOutputs`: Optimization problem outputs -- `variable::Tuple{Type{<:VariableType}, Type{<:PSY.Component}`: Tuple with variable type +- `variable::Tuple{Type{<:VariableType}, Type{<:IS.InfrastructureSystemsComponent}`: Tuple with variable type and device type for the desired outputs - `start_time::Dates.DateTime`: Start time of the requested outputs - `len::Int`: length of outputs @@ -561,7 +561,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `variables::Vector{Tuple{Type{<:VariableType}, Type{<:PSY.Component}}` : Tuple with variable type and device type for the desired outputs + - `variables::Vector{Tuple{Type{<:VariableType}, Type{<:IS.InfrastructureSystemsComponent}}` : Tuple with variable type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -612,7 +612,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `dual::Tuple{Type{<:ConstraintType}, Type{<:PSY.Component}` : Tuple with dual type and device type for the desired outputs + - `dual::Tuple{Type{<:ConstraintType}, Type{<:IS.InfrastructureSystemsComponent}` : Tuple with dual type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -651,7 +651,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `duals::Vector{Tuple{Type{<:ConstraintType}, Type{<:PSY.Component}}` : Tuple with dual type and device type for the desired outputs + - `duals::Vector{Tuple{Type{<:ConstraintType}, Type{<:IS.InfrastructureSystemsComponent}}` : Tuple with dual type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -701,7 +701,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `parameter::Tuple{Type{<:ParameterType}, Type{<:PSY.Component}` : Tuple with parameter type and device type for the desired outputs + - `parameter::Tuple{Type{<:ParameterType}, Type{<:IS.InfrastructureSystemsComponent}` : Tuple with parameter type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -740,7 +740,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `parameters::Vector{Tuple{Type{<:ParameterType}, Type{<:PSY.Component}}` : Tuple with parameter type and device type for the desired outputs + - `parameters::Vector{Tuple{Type{<:ParameterType}, Type{<:IS.InfrastructureSystemsComponent}}` : Tuple with parameter type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -792,7 +792,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `aux_variable::Tuple{Type{<:AuxVariableType}, Type{<:PSY.Component}` : Tuple with aux_variable type and device type for the desired outputs + - `aux_variable::Tuple{Type{<:AuxVariableType}, Type{<:IS.InfrastructureSystemsComponent}` : Tuple with aux_variable type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -831,7 +831,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `aux_variables::Vector{Tuple{Type{<:AuxVariableType}, Type{<:PSY.Component}}` : Tuple with aux_variable type and device type for the desired outputs + - `aux_variables::Vector{Tuple{Type{<:AuxVariableType}, Type{<:IS.InfrastructureSystemsComponent}}` : Tuple with aux_variable type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -884,7 +884,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `expression::Tuple{Type{<:ExpressionType}, Type{<:PSY.Component}` : Tuple with expression type and device type for the desired outputs + - `expression::Tuple{Type{<:ExpressionType}, Type{<:IS.InfrastructureSystemsComponent}` : Tuple with expression type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ @@ -923,7 +923,7 @@ Accepts a vector of keys for the return of the values. # Arguments - - `expressions::Vector{Tuple{Type{<:ExpressionType}, Type{<:PSY.Component}}` : Tuple with expression type and device type for the desired outputs + - `expressions::Vector{Tuple{Type{<:ExpressionType}, Type{<:IS.InfrastructureSystemsComponent}}` : Tuple with expression type and device type for the desired outputs - `start_time::Dates.DateTime` : initial time of the requested outputs - `len::Int`: length of outputs """ diff --git a/src/core/parameter_container.jl b/src/core/parameter_container.jl index 06503ca5..d0a0ea39 100644 --- a/src/core/parameter_container.jl +++ b/src/core/parameter_container.jl @@ -80,13 +80,13 @@ get_sos_status(attr::CostFunctionAttributes) = attr.sos_status get_variable_types(attr::CostFunctionAttributes) = attr.variable_types get_uses_compact_power(attr::CostFunctionAttributes) = attr.uses_compact_power -struct EventParametersAttributes{T <: PSY.Outage, U <: ParameterType} <: ParameterAttributes - affected_devices::Vector{<:PSY.Component} +struct EventParametersAttributes{T <: IS.InfrastructureSystemsComponent, U <: ParameterType} <: ParameterAttributes + affected_devices::Vector{<:IS.InfrastructureSystemsComponent} end function get_param_type( ::EventParametersAttributes{T, U}, -) where {T <: PSY.Outage, U <: ParameterType} +) where {T <: IS.InfrastructureSystemsComponent, U <: ParameterType} return U end diff --git a/src/core/service_model.jl b/src/core/service_model.jl index 4a1bb727..316eabb4 100644 --- a/src/core/service_model.jl +++ b/src/core/service_model.jl @@ -28,14 +28,14 @@ model at simulation time reserves = ServiceModel(PSY.VariableReserve{PSY.ReserveUp}, RangeReserve) """ -mutable struct ServiceModel{D <: PSY.Service, B} +mutable struct ServiceModel{D <: IS.InfrastructureSystemsComponent, B} feedforwards::Vector{<:AbstractAffectFeedforward} service_name::String use_slacks::Bool duals::Vector{DataType} time_series_names::Dict{Type{<:TimeSeriesParameter}, String} attributes::Dict{String, Any} - contributing_devices_map::Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}} + contributing_devices_map::Dict{Type{<:IS.InfrastructureSystemsComponent}, Vector{<:IS.InfrastructureSystemsComponent}} subsystem::Union{Nothing, String} function ServiceModel( ::Type{D}, @@ -46,8 +46,8 @@ mutable struct ServiceModel{D <: PSY.Service, B} duals = Vector{DataType}(), time_series_names = get_default_time_series_names(D, B), attributes = Dict{String, Any}(), - contributing_devices_map = Dict{Type{<:PSY.Component}, Vector{<:PSY.Component}}(), - ) where {D <: PSY.Service, B} + contributing_devices_map = Dict{Type{<:IS.InfrastructureSystemsComponent}, Vector{<:IS.InfrastructureSystemsComponent}}(), + ) where {D <: IS.InfrastructureSystemsComponent, B} attributes_for_model = get_default_attributes(D, B) for (k, v) in attributes attributes_for_model[k] = v @@ -70,10 +70,10 @@ end get_component_type( ::ServiceModel{D, B}, -) where {D <: PSY.Service, B} = D +) where {D <: IS.InfrastructureSystemsComponent, B} = D get_formulation( ::ServiceModel{D, B}, -) where {D <: PSY.Service, B} = B +) where {D <: IS.InfrastructureSystemsComponent, B} = B get_feedforwards(m::ServiceModel) = m.feedforwards get_service_name(m::ServiceModel) = m.service_name get_use_slacks(m::ServiceModel) = m.use_slacks @@ -98,7 +98,7 @@ function ServiceModel( duals = Vector{DataType}(), time_series_names = get_default_time_series_names(D, B), attributes = get_default_attributes(D, B), -) where {D <: PSY.Service, B} +) where {D <: IS.InfrastructureSystemsComponent, B} # If more attributes are used later, move free form string to const and organize # attributes attributes_for_model = get_default_attributes(D, B) @@ -131,7 +131,7 @@ end function set_model!( dict::Dict, model::ServiceModel{D, B}, -) where {D <: PSY.Service, B} +) where {D <: IS.InfrastructureSystemsComponent, B} set_model!(dict, (get_service_name(model), Symbol(D)), model) return end diff --git a/src/core/standard_variables_expressions.jl b/src/core/standard_variables_expressions.jl index e980ab01..f4f6d25a 100644 --- a/src/core/standard_variables_expressions.jl +++ b/src/core/standard_variables_expressions.jl @@ -84,7 +84,7 @@ function add_expression_container!( axs...; sparse = false, meta = CONTAINER_KEY_EMPTY_META, -) where {T <: ProductionCostExpression, U <: Union{PSY.Component, PSY.System}} +) where {T <: ProductionCostExpression, U <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} expr_container = _add_container!(container, T, U, JuMP.QuadExpr, sparse, axs...; meta = meta) remove_undef!(expr_container) diff --git a/src/objective_function/common.jl b/src/objective_function/common.jl index 9a1f0d75..ae3c7fe4 100644 --- a/src/objective_function/common.jl +++ b/src/objective_function/common.jl @@ -43,7 +43,7 @@ function add_variable_cost!( V <: AbstractDeviceFormulation, } for d in devices - op_cost_data = PSY.get_operation_cost(d) + op_cost_data = get_operation_cost(d) add_variable_cost_to_objective!(container, U, d, op_cost_data, V) _add_vom_cost_to_objective!(container, U, d, op_cost_data, V) end @@ -59,7 +59,7 @@ function _add_vom_cost_to_objective!( container::OptimizationContainer, ::Type{T}, component::C, - op_cost::PSY.OperationalCost, + op_cost::IS.DeviceParameter, ::Type{U}, ) where { T <: VariableType, @@ -67,8 +67,8 @@ function _add_vom_cost_to_objective!( C <: IS.InfrastructureSystemsComponent, } variable_cost_data = variable_cost(op_cost, T, C, U) - power_units = PSY.get_power_units(variable_cost_data) - cost_term = PSY.get_proportional_term(PSY.get_vom_cost(variable_cost_data)) + power_units = IS.get_power_units(variable_cost_data) + cost_term = IS.get_proportional_term(IS.get_vom_cost(variable_cost_data)) add_proportional_cost_invariant!(container, T, component, cost_term, power_units) return end @@ -79,7 +79,7 @@ function add_variable_cost_to_objective!( container::OptimizationContainer, ::Type{T}, component::C, - op_cost::PSY.OperationalCost, + op_cost::IS.DeviceParameter, ::Type{U}, ) where { T <: VariableType, @@ -127,30 +127,30 @@ end # currently: ThermalGen, ControllableLoad subtypes. # FIXME only called in POM, device specific code. -function _onvar_cost(::PSY.CostCurve{PSY.PiecewisePointCurve}) +function _onvar_cost(::IS.CostCurve{IS.PiecewisePointCurve}) # OnVariableCost is included in the Point itself for PiecewisePointCurve return 0.0 end function _onvar_cost( - cost_function::Union{PSY.CostCurve{IS.LinearCurve}, PSY.CostCurve{IS.QuadraticCurve}}, + cost_function::Union{IS.CostCurve{IS.LinearCurve}, IS.CostCurve{IS.QuadraticCurve}}, ) - value_curve = PSY.get_value_curve(cost_function) - cost_component = PSY.get_function_data(value_curve) + value_curve = IS.get_value_curve(cost_function) + cost_component = IS.get_function_data(value_curve) # Always in \$/h - constant_term = PSY.get_constant_term(cost_component) + constant_term = IS.get_constant_term(cost_component) return constant_term end -function _onvar_cost(::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}) +function _onvar_cost(::IS.CostCurve{IS.PiecewiseIncrementalCurve}) # Input at min is used to transform to InputOutputCurve return 0.0 end function _onvar_cost( ::OptimizationContainer, - cost_function::PSY.CostCurve{T}, - ::PSY.Component, + cost_function::IS.CostCurve{T}, + ::IS.InfrastructureSystemsComponent, ::Int, ) where {T <: IS.ValueCurve} return _onvar_cost(cost_function) diff --git a/src/objective_function/import_export.jl b/src/objective_function/import_export.jl deleted file mode 100644 index 929e33b3..00000000 --- a/src/objective_function/import_export.jl +++ /dev/null @@ -1,56 +0,0 @@ -# FIXME requires AbstractSourceFormulation to be defined -# or, rely on PSY.Source being enough to uniquely determine which function gets called. - -#= -function add_variable_cost_to_objective!( - container::OptimizationContainer, - ::T, - component::PSY.Source, - cost_function::PSY.ImportExportCost, - ::U, -) where { - T <: ActivePowerOutVariable, - U <: AbstractSourceFormulation, -} - component_name = PSY.get_name(component) - @debug "Import Export Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name - import_cost_curves = PSY.get_import_offer_curves(cost_function) - if !isnothing(import_cost_curves) - add_pwl_term_delta!( - false, - container, - component, - cost_function, - T(), - U(), - ) - end - return -end - -function add_variable_cost_to_objective!( - container::OptimizationContainer, - ::T, - component::PSY.Source, - cost_function::PSY.ImportExportCost, - ::U, -) where { - T <: ActivePowerInVariable, - U <: AbstractSourceFormulation, -} - component_name = PSY.get_name(component) - @debug "Import Export Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name - export_cost_curves = PSY.get_export_offer_curves(cost_function) - if !isnothing(export_cost_curves) - add_pwl_term_delta!( - true, - container, - component, - cost_function, - T(), - U(), - ) - end - return -end -=# diff --git a/src/objective_function/objective_function_pwl_lambda.jl b/src/objective_function/objective_function_pwl_lambda.jl index 7d99a5a0..8a55a59e 100644 --- a/src/objective_function/objective_function_pwl_lambda.jl +++ b/src/objective_function/objective_function_pwl_lambda.jl @@ -56,10 +56,9 @@ _sos_status( """ Trait function: does device type `T` use commitment (on/off) variables? -Defaults to `false`; specialized for `PSY.ThermalGen`. +Defaults to `false`; POM specializes for thermal device types. """ uses_commitment_variables(::Type{<:IS.InfrastructureSystemsComponent}) = false -uses_commitment_variables(::Type{<:PSY.ThermalGen}) = true function _sos_status( ::Type{T}, ::Type{<:AbstractThermalUnitCommitment}, diff --git a/src/objective_function/start_up_shut_down.jl b/src/objective_function/start_up_shut_down.jl index d770a6a7..cb988603 100644 --- a/src/objective_function/start_up_shut_down.jl +++ b/src/objective_function/start_up_shut_down.jl @@ -11,8 +11,7 @@ _shutdown_cost_value(x::Float64) = x _shutdown_cost_value(x::IS.LinearCurve) = IS.get_proportional_term(x) # Trait: does this cost type store startup/shutdown in time-series parameters? -# Overridden for PSY.MarketBidTimeSeriesCost; duck-typeable by mocks. -_is_time_series_cost(::PSY.MarketBidTimeSeriesCost) = true +# POM adds an override for PSY.MarketBidTimeSeriesCost; mocks can duck-type. _is_time_series_cost(::IS.DeviceParameter) = false ################################################################################# diff --git a/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index 77901928..d06e855a 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -2,98 +2,105 @@ # Value Curve Objective Function: Delta PWL Formulation # # Objective function formulations for ValueCurve-based offer curves using the -# delta (incremental/block) PWL method. Maps ValueCurve types (static and -# time-series-backed) to slopes/breakpoints and routes to the delta formulation -# primitives in objective_function_pwl_delta.jl. +# delta (incremental/block) PWL method. IOM owns the generic pieces: +# * `OfferDirection` dispatch tables (parameter / variable / constraint types) +# * `_consider_parameter` generics +# * IS-only PWL predicates (`is_nontrivial_offer`, `curvity_check`) +# * The PSY-free time-series delta PWL path +# (`add_variable_cost_to_objective!` for `IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}` +# and `_add_ts_incremental_pwl_cost!`) +# * Abstract extension points (stubs) for the PSY orchestration that lives in +# downstream packages. # -# IOM defines objective function formulations — the mathematical structure of -# JuMP objective terms. "Costs" (production cost, fuel cost, etc.) are a -# domain concept defined in POM. This file provides the formulation machinery -# that POM routes specific cost types into. PSY cost types appear in some -# function signatures for dispatch, but the formulations themselves are -# generic over IS.InfrastructureSystemsComponent and IS.ValueCurve types. -# -# Device-specific overloads (e.g., ThermalMultiStart, ControllableLoad) are -# in POM. +# PSY-specific orchestration (accessor wrappers for MBC / IEC, validation, +# parameter processing, the static `add_pwl_term_delta!` entry point) lives in +# POM's `common_models/market_bid_plumbing.jl`. ################################################################################# ################################################################################# -# Section 1: Offer Curve Accessor Wrappers -# Map PSY cost types (MarketBidCost, ImportExportCost) to a unified interface. +# Section 1: Extension points +# Declared here so IOM can call them generically; downstream packages (POM) +# add methods for PSY-specific cost types. ################################################################################# -####################### get_{output/input}_offer_curves ######################### -# 1-argument getters: straight getfield calls (same PSY getter for static and TS variants) -get_output_offer_curves(cost::IEC_TYPES) = PSY.get_import_offer_curves(cost) -get_output_offer_curves(cost::MBC_TYPES) = PSY.get_incremental_offer_curves(cost) -get_input_offer_curves(cost::IEC_TYPES) = PSY.get_export_offer_curves(cost) -get_input_offer_curves(cost::MBC_TYPES) = PSY.get_decremental_offer_curves(cost) - -# 2-argument getters: resolve time series if needed, return static curve(s). -# Static types: delegate to 1-arg getter (no resolution needed). -get_output_offer_curves( - ::PSY.Component, - cost::PSY.ImportExportCost; - kwargs..., -) = PSY.get_import_offer_curves(cost) -get_output_offer_curves( - ::PSY.Component, - cost::PSY.MarketBidCost; - kwargs..., -) = PSY.get_incremental_offer_curves(cost) -get_input_offer_curves( - ::PSY.Component, - cost::PSY.ImportExportCost; - kwargs..., -) = PSY.get_export_offer_curves(cost) -get_input_offer_curves( - ::PSY.Component, - cost::PSY.MarketBidCost; - kwargs..., -) = PSY.get_decremental_offer_curves(cost) -# TS types: resolve via PSY's 2-arg getters. -get_output_offer_curves( - component::PSY.Component, - cost::PSY.ImportExportTimeSeriesCost; - kwargs..., -) = PSY.get_import_variable_cost(component, cost; kwargs...) -get_output_offer_curves( - component::PSY.Component, - cost::PSY.MarketBidTimeSeriesCost; - kwargs..., -) = PSY.get_incremental_variable_cost(component, cost; kwargs...) -get_input_offer_curves( - component::PSY.Component, - cost::PSY.ImportExportTimeSeriesCost; - kwargs..., -) = PSY.get_export_variable_cost(component, cost; kwargs...) -get_input_offer_curves( - component::PSY.Component, - cost::PSY.MarketBidTimeSeriesCost; - kwargs..., -) = PSY.get_decremental_variable_cost(component, cost; kwargs...) - -######################### get_offer_curves(direction, ...) ############################## - -# direction and device: -get_offer_curves(::DecrementalOffer, device::PSY.StaticInjection) = - get_input_offer_curves(PSY.get_operation_cost(device)) -get_offer_curves(::IncrementalOffer, device::PSY.StaticInjection) = - get_output_offer_curves(PSY.get_operation_cost(device)) -get_initial_input(::DecrementalOffer, device::PSY.StaticInjection) = - IS.get_initial_input( - PSY.get_value_curve(get_input_offer_curves(PSY.get_operation_cost(device))), - ) -get_initial_input(::IncrementalOffer, device::PSY.StaticInjection) = - IS.get_initial_input( - PSY.get_value_curve(get_output_offer_curves(PSY.get_operation_cost(device))), - ) - -# direction and cost curve (needed for VOM code path): -get_offer_curves(::DecrementalOffer, op_cost::PSY.OfferCurveCost) = - get_input_offer_curves(op_cost) -get_offer_curves(::IncrementalOffer, op_cost::PSY.OfferCurveCost) = - get_output_offer_curves(op_cost) +""" + get_offer_curves(direction, device_or_cost) + +Return the output/input offer curve(s) for the given direction. POM provides +methods dispatching on `PSY.StaticInjection` and `PSY.OfferCurveCost`. +""" +function get_offer_curves end + +""" + get_initial_input(direction, device) + +Return the `initial_input` scalar (cost at minimum) from the direction's side of +the offer curve. POM provides methods dispatching on `PSY.StaticInjection`. +""" +function get_initial_input end + +""" + validate_occ_component(::Type{<:ParameterType}, device) + +Validate that `device` can be processed by the given offer-curve-cost parameter. +POM provides overloads per device type. +""" +function validate_occ_component end + +""" + validate_occ_breakpoints_slopes(device, direction) + +Validate breakpoints/slopes on the given direction's offer curve. +""" +function validate_occ_breakpoints_slopes end + +""" + _get_parameter_field(::Type{<:ParameterType}, op_cost) + +Extract the raw field corresponding to this parameter type from an operation +cost object. POM provides overloads. +""" +function _get_parameter_field end + +""" + _get_pwl_data(direction, container, component, time) + +Return `(breakpoints, slopes)` for the given component at the given time step, +ready for unit conversion. POM's static-curve overload forwards to +`_get_raw_pwl_data`. +""" +function _get_pwl_data end + +""" + _get_raw_pwl_data(direction, container, ComponentType, name, cost_data, time) + +Return `(breakpoint_cost, slope_cost, unit_system)`. The IS-backed TS method +lives here in IOM; the static `CostCurve{PiecewiseIncrementalCurve}` method is +provided by POM. +""" +function _get_raw_pwl_data end + +""" + add_pwl_term_delta!(direction, container, component, cost_function, ::Type{VariableType}, ::Type{Formulation}) + +Add the delta PWL objective term for a static cost function. POM provides the +method dispatching on `PSY.OfferCurveCost`. +""" +function add_pwl_term_delta! end + +""" + _add_vom_cost_to_objective!(container, ::Type{VariableType}, component, op_cost, ::Type{Formulation}) + +Add the variable operations & maintenance (VOM) cost term. POM provides the +method dispatching on `PSY.OfferCurveCost`. +""" +function _add_vom_cost_to_objective! end + +"Default: most formulations use incremental offers. POM overrides for loads." +function _vom_offer_direction end + +"Predicate: is this op_cost time-series-backed? POM extends for TS types." +_is_time_series_cost(::Any) = false ################################################################################# # Section 2: OfferDirection Type Dispatch Table @@ -116,64 +123,7 @@ _objective_sign(::IncrementalOffer) = OBJECTIVE_FUNCTION_POSITIVE _objective_sign(::DecrementalOffer) = OBJECTIVE_FUNCTION_NEGATIVE ################################################################################# -# Section 3: _get_parameter_field Dispatch Table -# Maps parameter types to PSY getter functions. -################################################################################# - -_get_parameter_field(::Type{<:StartupCostParameter}, op_cost) = PSY.get_start_up(op_cost) -_get_parameter_field(::Type{<:ShutdownCostParameter}, op_cost) = PSY.get_shut_down(op_cost) -_get_parameter_field(::Type{<:IncrementalCostAtMinParameter}, op_cost) = - IS.get_initial_input(PSY.get_value_curve(get_output_offer_curves(op_cost))) -_get_parameter_field(::Type{<:DecrementalCostAtMinParameter}, op_cost) = - IS.get_initial_input(PSY.get_value_curve(get_input_offer_curves(op_cost))) -_get_parameter_field( - ::Type{ - <:Union{ - IncrementalPiecewiseLinearSlopeParameter, - IncrementalPiecewiseLinearBreakpointParameter, - }, - }, - op_cost, -) = get_output_offer_curves(op_cost) -_get_parameter_field( - ::Type{ - <:Union{ - DecrementalPiecewiseLinearSlopeParameter, - DecrementalPiecewiseLinearBreakpointParameter, - }, - }, - op_cost, -) = get_input_offer_curves(op_cost) - -################################################################################# -# Section 4: Device Cost Detection Predicates (generic) -# Device-specific overrides (RenewableNonDispatch, PowerLoad, etc.) are in POM. -################################################################################# - -_has_market_bid_cost(device::PSY.StaticInjection) = - _has_market_bid_cost(PSY.get_operation_cost(device)) -_has_market_bid_cost(::MBC_TYPES) = true -_has_market_bid_cost(::PSY.OperationalCost) = false - -_has_import_export_cost(::PSY.StaticInjection) = false -_has_import_export_cost(device::PSY.Source) = - _has_import_export_cost(PSY.get_operation_cost(device)) -_has_import_export_cost(::IEC_TYPES) = true -_has_import_export_cost(::PSY.OperationalCost) = false - -_has_offer_curve_cost(device::PSY.Component) = - _has_market_bid_cost(device) || _has_import_export_cost(device) - -# With the static/TS type split, time-series parameters are determined by cost type: -# TS cost types always have time-series parameters; static types never do. -_has_parameter_time_series(device::PSY.StaticInjection) = - _has_parameter_time_series(PSY.get_operation_cost(device)) - -_has_parameter_time_series(::TS_OFFER_CURVE_COST_TYPES) = true -_has_parameter_time_series(::PSY.OperationalCost) = false - -################################################################################# -# Section 5: _consider_parameter (generic versions) +# Section 3: _consider_parameter (generic versions) # Whether a parameter should be added based on what's in the container. # POM overrides for ThermalMultiStart startup (MULTI_START_VARIABLES). ################################################################################# @@ -209,252 +159,35 @@ _consider_parameter( ) where {T, D} = true ################################################################################# -# Section 6: Validation -# Generic validation for offer curve costs. Device-specific overrides -# (ThermalMultiStart, RenewableDispatch, Storage) are in POM. +# Section 4: Curvity + IS-only predicates ################################################################################# -curvity_check(::IncrementalOffer, x) = PSY.is_convex(x) -curvity_check(::DecrementalOffer, x) = PSY.is_concave(x) +curvity_check(::IncrementalOffer, x) = IS.is_convex(x) +curvity_check(::DecrementalOffer, x) = IS.is_concave(x) expected_curvity(::IncrementalOffer) = "convex" expected_curvity(::DecrementalOffer) = "concave" -function validate_occ_breakpoints_slopes(device::PSY.StaticInjection, dir::OfferDirection) - offer_curves = get_offer_curves(dir, device) - _validate_occ_curves(device, dir, offer_curves) -end - -# Static: validate convexity/concavity and cost-type-specific constraints -function _validate_occ_curves( - device::PSY.StaticInjection, - dir::OfferDirection, - cost_curve::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, -) - device_name = get_name(device) - cost_curve_name = nameof(typeof(PSY.get_operation_cost(device))) - curvity_check(dir, cost_curve) || - throw( - ArgumentError( - "$(uppercasefirst(string(dir))) $cost_curve_name for component $(device_name) is non-$(expected_curvity(dir))", - ), - ) - _validate_occ_subtype(PSY.get_operation_cost(device), dir, cost_curve, device_name) -end - -# TS-backed: validated at parameter population time, not here -_validate_occ_curves(::PSY.StaticInjection, ::OfferDirection, - ::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}) = nothing - -_validate_occ_subtype(::PSY.MarketBidCost, ::OfferDirection, ::PSY.CostCurve, args...) = - nothing - -function _validate_occ_subtype( - ::PSY.ImportExportCost, - ::OfferDirection, - curve::PSY.CostCurve, - args..., -) - !iszero(PSY.get_vom_cost(curve)) && throw( - ArgumentError( - "For ImportExportCost, VOM cost must be zero.", - ), - ) - !iszero(PSY.get_initial_input(curve)) && throw( - ArgumentError( - "For ImportExportCost, initial input must be zero.", - ), - ) - fd = PSY.get_function_data(PSY.get_value_curve(curve)) - if !iszero(first(PSY.get_x_coords(fd))) - throw( - ArgumentError( - "For ImportExportCost, the first breakpoint must be zero.", - ), - ) - end -end - -# Generic validate_occ_component overloads for PSY.StaticInjection. -# Device-specific overloads (ThermalMultiStart, RenewableDispatch, Storage) are in POM. - -function validate_occ_component(::Type{<:StartupCostParameter}, device::PSY.StaticInjection) - op_cost = PSY.get_operation_cost(device) - # TS types are validated at parameter population time - _is_time_series_cost(op_cost) && return - startup = PSY.get_start_up(op_cost) - if startup isa Union{NTuple{3, Float64}, StartUpStages} - @warn "Multi-start costs detected for non-multi-start unit $(get_name(device)), will take the maximum" - elseif !(startup isa Float64) - throw( - ArgumentError( - "Expected Float64, NTuple{3, Float64}, or StartUpStages startup cost but got $(typeof(startup)) for $(get_name(device))", - ), - ) - end - return -end - -function validate_occ_component( - ::Type{<:ShutdownCostParameter}, - device::PSY.StaticInjection, -) - op_cost = PSY.get_operation_cost(device) - # TS types are validated at parameter population time - _is_time_series_cost(op_cost) && return - # Static MBC: shut_down is LinearCurve; ThermalGenerationCost: shut_down is Float64 - shutdown = PSY.get_shut_down(op_cost) - if shutdown isa IS.LinearCurve - return # valid - elseif shutdown isa Float64 - return # valid (e.g. ThermalGenerationCost) - else - throw( - ArgumentError( - "Expected Float64 or LinearCurve shutdown cost but got $(typeof(shutdown)) for $(get_name(device))", - ), - ) - end -end - -# Consistency of initial_input vs offer curves is guaranteed by the static/TS type split -validate_occ_component(::Type{<:AbstractCostAtMinParameter}, ::PSY.StaticInjection) = - nothing - -validate_occ_component( - ::Type{<:IncrementalPiecewiseLinearBreakpointParameter}, - device::PSY.StaticInjection, -) = validate_occ_breakpoints_slopes(device, IncrementalOffer()) - -validate_occ_component( - ::Type{<:DecrementalPiecewiseLinearBreakpointParameter}, - device::PSY.StaticInjection, -) = validate_occ_breakpoints_slopes(device, DecrementalOffer()) - -# Slope and breakpoint validations are done together, nothing to do here -validate_occ_component( - ::Type{<:AbstractPiecewiseLinearSlopeParameter}, - device::PSY.StaticInjection, -) = nothing - -################################################################################# -# Section 7: Parameter Processing Orchestration -################################################################################# - -function _process_occ_parameters_helper( - ::Type{P}, - container::OptimizationContainer, - model, - devices, -) where {P <: ParameterType} - for device in devices - validate_occ_component(P, device) - end - if _consider_parameter(P, container, model) - ts_devices = - filter(device -> _has_parameter_time_series(device), devices) - (length(ts_devices) > 0) && add_parameters!(container, P, ts_devices, model) - end -end - -"Validate ImportExportCosts and add the appropriate parameters" -function process_import_export_parameters!( - container::OptimizationContainer, - devices_in, - model::DeviceModel, -) - devices = [d for d in devices_in if _has_import_export_cost(d)] - - for param in ( - IncrementalPiecewiseLinearSlopeParameter, - IncrementalPiecewiseLinearBreakpointParameter, - DecrementalPiecewiseLinearSlopeParameter, - DecrementalPiecewiseLinearBreakpointParameter, - ) - _process_occ_parameters_helper(param, container, model, devices) - end -end - -"Validate MarketBidCosts and add the appropriate parameters" -function process_market_bid_parameters!( - container::OptimizationContainer, - devices_in, - model::DeviceModel, - incremental::Bool = true, - decremental::Bool = false, -) - devices = [d for d in devices_in if _has_market_bid_cost(d)] - isempty(devices) && return - - for param in ( - StartupCostParameter, - ShutdownCostParameter, - ) - _process_occ_parameters_helper(param, container, model, devices) - end - if incremental - for param in ( - IncrementalCostAtMinParameter, - IncrementalPiecewiseLinearSlopeParameter, - IncrementalPiecewiseLinearBreakpointParameter, - ) - _process_occ_parameters_helper(param, container, model, devices) - end - end - if decremental - for param in ( - DecrementalCostAtMinParameter, - DecrementalPiecewiseLinearSlopeParameter, - DecrementalPiecewiseLinearBreakpointParameter, - ) - _process_occ_parameters_helper(param, container, model, devices) - end - end +""" +Is this offer curve carrying meaningful data, as opposed to the default +`ZERO_OFFER_CURVE` placeholder that PSY assigns to unused sides of a +`MarketBidCost` / `ImportExportCost`? Only used for load formulations. +""" +function is_nontrivial_offer(curve::IS.CostCurve{IS.PiecewiseIncrementalCurve}) + xs = IS.get_x_coords(IS.get_function_data(IS.get_value_curve(curve))) + return last(xs) > first(xs) end +is_nontrivial_offer(::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}) = false ################################################################################# -# Section 10: PWL Data Retrieval +# Section 5: TimeSeriesValueCurve Objective Formulation (PSY-free) +# Delta PWL objective for CostCurve{TimeSeriesPiecewiseIncrementalCurve}. +# Reads slopes/breakpoints from pre-populated parameter containers. ################################################################################# -function _get_pwl_data( - dir::OfferDirection, - container::OptimizationContainer, - component::T, - time::Int, -) where {T <: PSY.Component} - name = PSY.get_name(component) - cost_data = get_offer_curves(dir, component) - breakpoint_cost_component, slope_cost_component, unit_system = - _get_raw_pwl_data(dir, container, T, name, cost_data, time) - - breakpoints, slopes = get_piecewise_curve_per_system_unit( - breakpoint_cost_component, - slope_cost_component, - unit_system, - get_model_base_power(container), - PSY.get_base_power(component), - ) - return breakpoints, slopes -end - -# static curve: read directly from the cost curve -function _get_raw_pwl_data( - ::OfferDirection, - ::OptimizationContainer, - ::Type{<:PSY.Component}, - ::String, - cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, - ::Int, -) - cost_component = PSY.get_function_data(PSY.get_value_curve(cost_data)) - return PSY.get_x_coords(cost_component), - PSY.get_y_coords(cost_component), - PSY.get_power_units(cost_data) -end - -# time-series curve: read from parameter arrays. Parameter containers for -# Slope/Breakpoint are allocated with axes `(names, segments|points, times)`, so the -# 3-index lookup mirrors `_fill_pwl_data_from_arrays!`. The parameter values carry the -# units declared on the `CostCurve`, so we forward those through (don't hardcode). +# TS-backed PWL data retrieval. Parameter containers for Slope/Breakpoint are +# allocated with axes `(names, segments|points, times)`, so the 3-index lookup +# mirrors `_fill_pwl_data_from_arrays!`. The parameter values carry the units +# declared on the `CostCurve`, so we forward those through (don't hardcode). function _get_raw_pwl_data( dir::OfferDirection, container::OptimizationContainer, @@ -462,7 +195,7 @@ function _get_raw_pwl_data( name::String, cost_data::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}, time::Int, -) where {T <: PSY.Component} +) where {T <: IS.InfrastructureSystemsComponent} SlopeParam = _slope_param(dir) slope_arr = get_parameter_array(container, SlopeParam, T) slope_mult = get_parameter_multiplier_array(container, SlopeParam, T) @@ -484,184 +217,8 @@ function _get_raw_pwl_data( end @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 - return breakpoint_cost_component, slope_cost_component, PSY.get_power_units(cost_data) -end - -################################################################################# -# Section 11: PWL Objective Terms + Variable Objective Formulation (generic) -# Load formulation overloads (AbstractControllablePowerLoadFormulation) are in POM. -################################################################################# - -""" -Add PWL objective terms using the **delta (incremental/block-offer) formulation**. - -Given an offer curve with breakpoints ``P_0, P_1, \\ldots, P_n`` and slopes -``m_1, m_2, \\ldots, m_n``, this function: - -1. Creates delta variables ``\\delta_k \\geq 0`` for each segment via [`add_pwl_variables_delta!`](@ref), - with no upper bound (block sizes are enforced by constraints). -2. Adds linking and block-size constraints via [`add_pwl_constraint_delta!`](@ref): - ``p = \\sum_k \\delta_k`` and ``\\delta_k \\leq P_{k+1} - P_k``. -3. Builds the cost expression ``C = \\sum_k m_k \\, \\delta_k`` via [`get_pwl_cost_expression_delta`](@ref). - -For convex offer curves (``m_1 \\leq m_2 \\leq \\cdots \\leq m_n``), no SOS2 or binary -variables are needed — the optimizer fills cheap segments first automatically. - -Dispatches on `OfferDirection` (incremental or decremental) to select the appropriate -variable and constraint types. - -See also: [`add_pwl_term_lambda!`](@ref) for the lambda (convex combination) formulation used by -`CostCurve{PiecewisePointCurve}`. -""" -function add_pwl_term_delta!( - dir::OfferDirection, - container::OptimizationContainer, - component::T, - ::PSY.OfferCurveCost, - ::Type{U}, - ::Type{V}, -) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation} - W = _block_offer_var(dir) - X = _block_offer_constraint(dir) - - name = PSY.get_name(component) - resolution = get_resolution(container) - dt = Dates.value(resolution) / MILLISECONDS_IN_HOUR - time_steps = get_time_steps(container) - is_variant = is_time_variant(get_offer_curves(dir, component)) - # Static offer curves are time-invariant: compute breakpoints/slopes once. - static_breakpoints, static_slopes = if is_variant - (Float64[], Float64[]) - else - _get_pwl_data(dir, container, component, first(time_steps)) - end - for t in time_steps - breakpoints, slopes = if is_variant - _get_pwl_data(dir, container, component, t) - else - (static_breakpoints, static_slopes) - end - pwl_vars = - add_pwl_variables_delta!( - container, - W, - T, - name, - t, - length(slopes); - upper_bound = Inf, - ) - add_pwl_constraint_delta!( - container, - component, - U, - V, - breakpoints, - pwl_vars, - t, - X, - ) - pwl_cost = - get_pwl_cost_expression_delta(pwl_vars, slopes, _objective_sign(dir) * dt) - - add_cost_to_expression!( - container, - ProductionCostExpression, - pwl_cost, - T, - name, - t, - ) - - if is_variant - add_to_objective_variant_expression!(container, pwl_cost) - else - add_to_objective_invariant_expression!(container, pwl_cost) - end - end -end - -# FIXME better validation: for static, != ZERO_OFFER_CURVE would be clearer -# and for time series, actually check. -""" -Is this offer curve carrying meaningful data, as opposed to the default `ZERO_OFFER_CURVE` -placeholder that PSY assigns to unused sides of a `MarketBidCost` / `ImportExportCost`? -Only used for load formulations, to decide whether to throw an error about a non-trivial -supply offer curve. -""" -function is_nontrivial_offer(curve::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}) - xs = PSY.get_x_coords(PSY.get_function_data(PSY.get_value_curve(curve))) - return last(xs) > first(xs) + return breakpoint_cost_component, slope_cost_component, IS.get_power_units(cost_data) end -is_nontrivial_offer(::PSY.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}) = false - -function add_variable_cost_to_objective!( - container::OptimizationContainer, - ::Type{T}, - component::PSY.Component, - cost_function::PSY.OfferCurveCost, - ::Type{U}, -) where {T <: VariableType, U <: AbstractDeviceFormulation} - component_name = PSY.get_name(component) - @debug "Market Bid" _group = LOG_GROUP_COST_FUNCTIONS component_name - if is_nontrivial_offer(get_input_offer_curves(cost_function)) - throw( - ArgumentError( - "Component $(component_name) is not allowed to participate as a demand.", - ), - ) - end - add_pwl_term_delta!( - IncrementalOffer(), - container, - component, - cost_function, - T, - U, - ) - return -end - -# Default: most formulations use incremental offers -_vom_offer_direction(::Type{<:AbstractDeviceFormulation}) = IncrementalOffer() - -function _add_vom_cost_to_objective!( - container::OptimizationContainer, - ::Type{T}, - component::PSY.Component, - op_cost::PSY.OfferCurveCost, - ::Type{U}, -) where {T <: VariableType, U <: AbstractDeviceFormulation} - dir = _vom_offer_direction(U) - cost_curves = get_offer_curves(dir, op_cost) - if is_time_variant(cost_curves) - @warn "$(typeof(dir)) curves are time variant, there is no VOM cost source. Skipping VOM cost." - return - end - _add_vom_cost_to_objective_helper!( - container, T, component, op_cost, cost_curves, U) - return -end - -function _add_vom_cost_to_objective_helper!( - container::OptimizationContainer, - ::Type{T}, - component::PSY.Component, - ::PSY.OfferCurveCost, - cost_data::PSY.CostCurve{PSY.PiecewiseIncrementalCurve}, - ::Type{U}, -) where {T <: VariableType, U <: AbstractDeviceFormulation} - power_units = PSY.get_power_units(cost_data) - cost_term = PSY.get_proportional_term(PSY.get_vom_cost(cost_data)) - add_proportional_cost_invariant!(container, T, component, cost_term, power_units) - return -end - -################################################################################# -# Section 12: TimeSeriesValueCurve Objective Formulation -# PSY-free delta PWL objective for CostCurve{TimeSeriesPiecewiseIncrementalCurve}. -# Reads slopes/breakpoints from pre-populated parameter containers. -################################################################################# """ add_variable_cost_to_objective!(container, ::T, component, cost_function, ::U; dir) diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 01e8cd16..45325af6 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -1,4 +1,4 @@ -function get_deterministic_time_series_type(sys::PSY.System) +function get_deterministic_time_series_type(sys::IS.InfrastructureSystemsContainer) time_series_types = IS.get_time_series_counts_by_type(sys.data) existing_types = Set(d["type"] for d in time_series_types) if Set(["Deterministic", "DeterministicSingleTimeSeries"]) ∈ existing_types @@ -7,9 +7,9 @@ function get_deterministic_time_series_type(sys::PSY.System) ) end if "Deterministic" ∈ existing_types - return PSY.Deterministic + return IS.Deterministic elseif "DeterministicSingleTimeSeries" ∈ existing_types - return PSY.DeterministicSingleTimeSeries + return IS.DeterministicSingleTimeSeries else error( "The System does not contain any forecast data or transformed time series data.", @@ -31,7 +31,7 @@ struct GenericOpProblem <: DefaultDecisionProblem end mutable struct DecisionModel{M <: DecisionProblem} <: OperationModel name::Symbol template::AbstractProblemTemplate - sys::PSY.System + sys::IS.InfrastructureSystemsContainer internal::Union{Nothing, ModelInternal} simulation_info::Union{Nothing, SimulationInfo} store::DecisionModelStore @@ -41,7 +41,7 @@ end """ DecisionModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model}=nothing; kwargs...) where {M<:DecisionProblem} @@ -51,7 +51,7 @@ Build the optimization problem of type M with the specific system and template. - `::Type{M} where M<:DecisionProblem`: The abstract operation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - - `sys::PSY.System`: the system created using Power Systems + - `sys::IS.InfrastructureSystemsContainer`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care - `name = nothing`: name of model, string or symbol; defaults to the type of template converted to a symbol. - `optimizer::Union{Nothing,MOI.OptimizerWithAttributes} = nothing` : The optimizer does @@ -83,7 +83,7 @@ OpModel = DecisionModel(MockOperationProblem, template, system) """ function DecisionModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, settings::Settings, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, @@ -115,7 +115,7 @@ end function DecisionModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, optimizer = nothing, @@ -172,7 +172,7 @@ Build the optimization problem of type M with the specific system and template - `::Type{M} where M<:DecisionProblem`: The abstract operation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - - `sys::PSY.System`: the system created using Power Systems + - `sys::IS.InfrastructureSystemsContainer`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care. # Example @@ -185,7 +185,7 @@ problem = DecisionModel(MyOpProblemType, template, system, optimizer) function DecisionModel( ::Type{M}, template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: DecisionProblem} @@ -194,7 +194,7 @@ end function DecisionModel( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) @@ -202,7 +202,7 @@ function DecisionModel( end function DecisionModel{M}( - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: DefaultDecisionProblem} @@ -233,9 +233,9 @@ function init_model_store_params!(model::DecisionModel) num_executions = get_executions(model) horizon = get_horizon(model) system = get_system(model) - interval = PSY.get_forecast_interval(system) + interval = IS.get_forecast_interval(system) resolution = get_resolution(model) - base_power = PSY.get_base_power(system) + base_power = get_base_power(system) sys_uuid = IS.get_uuid(system) store_params = ModelStoreParams( num_executions, @@ -253,7 +253,7 @@ end function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem}) sys = get_system(model) settings = get_settings(model) - available_resolutions = PSY.get_time_series_resolutions(sys) + available_resolutions = IS.get_time_series_resolutions(sys) if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 throw( @@ -274,10 +274,10 @@ function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem}) end if get_horizon(settings) == UNSET_HORIZON - set_horizon!(settings, PSY.get_forecast_horizon(sys)) + set_horizon!(settings, IS.get_forecast_horizon(sys)) end - counts = PSY.get_time_series_counts(sys) + counts = IS.get_time_series_counts(sys) if counts.forecast_count < 1 error( "The system does not contain forecast data. A DecisionModel can't be built.", diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl index 33e181a1..5045fe3b 100644 --- a/src/operation/emulation_model.jl +++ b/src/operation/emulation_model.jl @@ -14,7 +14,7 @@ struct GenericEmulationProblem <: DefaultEmulationProblem end """ EmulationModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model}=nothing; kwargs...) where {M<:EmulationProblem} @@ -24,7 +24,7 @@ Build the optimization problem of type M with the specific system and template. - `::Type{M} where M<:EmulationProblem`: The abstract Emulation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - - `sys::PSY.System`: the system created using Power Systems + - `sys::IS.InfrastructureSystemsContainer`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care - `name = nothing`: name of model, string or symbol; defaults to the type of template converted to a symbol. - `optimizer::Union{Nothing,MOI.OptimizerWithAttributes} = nothing` : The optimizer does @@ -54,7 +54,7 @@ OpModel = EmulationModel(MockEmulationProblem, template, system) mutable struct EmulationModel{M <: EmulationProblem} <: OperationModel name::Symbol template::AbstractProblemTemplate - sys::PSY.System + sys::IS.InfrastructureSystemsContainer internal::ModelInternal simulation_info::SimulationInfo store::EmulationModelStore # might be extended to other stores for simulation @@ -62,7 +62,7 @@ mutable struct EmulationModel{M <: EmulationProblem} <: OperationModel function EmulationModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, settings::Settings, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, @@ -74,7 +74,7 @@ mutable struct EmulationModel{M <: EmulationProblem} <: OperationModel end finalize_template!(template, sys) internal = ModelInternal( - OptimizationContainer(sys, settings, jump_model, PSY.SingleTimeSeries), + OptimizationContainer(sys, settings, jump_model, IS.SingleTimeSeries), ) new{M}( name, @@ -90,7 +90,7 @@ end function EmulationModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; resolution = UNSET_RESOLUTION, name = nothing, @@ -145,7 +145,7 @@ Build the optimization problem of type M with the specific system and template - `::Type{M} where M<:EmulationProblem`: The abstract Emulation model type - `template::AbstractProblemTemplate`: The model reference made up of transmission, devices, branches, and services. - - `sys::PSY.System`: the system created using Power Systems + - `sys::IS.InfrastructureSystemsContainer`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}`: Enables passing a custom JuMP model. Use with care # Example @@ -158,7 +158,7 @@ problem = EmulationModel(MyEmProblemType, template, system, optimizer) function EmulationModel( ::Type{M}, template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: EmulationProblem} @@ -167,7 +167,7 @@ end function EmulationModel( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) @@ -181,7 +181,7 @@ emulation models that do not require a template. # Arguments - `::Type{M} where M<:EmulationProblem`: The abstract operation model type - - `sys::PSY.System`: the system created using Power Systems + - `sys::IS.InfrastructureSystemsContainer`: the system created using Power Systems - `jump_model::Union{Nothing, JuMP.Model}` = nothing: Enables passing a custom JuMP model. Use with care. # Example @@ -191,7 +191,7 @@ problem = EmulationModel(system, optimizer) ``` """ function EmulationModel{M}( - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: EmulationProblem} @@ -211,7 +211,7 @@ end function validate_time_series!(model::EmulationModel{<:DefaultEmulationProblem}) sys = get_system(model) settings = get_settings(model) - available_resolutions = PSY.get_time_series_resolutions(sys) + available_resolutions = IS.get_time_series_resolutions(sys) if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 throw( @@ -236,7 +236,7 @@ function validate_time_series!(model::EmulationModel{<:DefaultEmulationProblem}) set_horizon!(settings, get_resolution(settings)) end - counts = PSY.get_time_series_counts(sys) + counts = IS.get_time_series_counts(sys) if counts.static_time_series_count < 1 error( "The system does not contain Static Time Series data. A EmulationModel can't be built.", @@ -257,7 +257,7 @@ function init_model_store_params!(model::EmulationModel) system = get_system(model) settings = get_settings(model) horizon = interval = resolution = get_resolution(settings) - base_power = PSY.get_base_power(system) + base_power = get_base_power(system) sys_uuid = IS.get_uuid(system) set_store_params!( get_internal(model), @@ -333,7 +333,7 @@ function update_parameter_values!( model::EmulationModel, key::ParameterKey{T, U}, input::DatasetContainer{InMemoryDataset}, -) where {T <: ParameterType, U <: PSY.Component} +) where {T <: ParameterType, U <: IS.InfrastructureSystemsComponent} # Enable again for detailed debugging # TimerOutputs.@timeit RUN_SIMULATION_TIMER "$T $U Parameter Update" begin optimization_container = get_optimization_container(model) diff --git a/src/operation/initial_conditions_update_in_memory_store.jl b/src/operation/initial_conditions_update_in_memory_store.jl index b37fa91d..0b00970d 100644 --- a/src/operation/initial_conditions_update_in_memory_store.jl +++ b/src/operation/initial_conditions_update_in_memory_store.jl @@ -11,7 +11,7 @@ function update_initial_conditions!( model::OperationModel, key::InitialConditionKey{T, U}, source, -) where {T <: InitialConditionType, U <: PSY.Component} +) where {T <: InitialConditionType, U <: IS.InfrastructureSystemsComponent} if get_execution_count(model) < 1 return end diff --git a/src/operation/operation_model_interface.jl b/src/operation/operation_model_interface.jl index 30ac7e32..20f50e5c 100644 --- a/src/operation/operation_model_interface.jl +++ b/src/operation/operation_model_interface.jl @@ -40,7 +40,7 @@ function get_resolution(model::OperationModel) return resolution end -get_problem_base_power(model::OperationModel) = PSY.get_base_power(model.sys) +get_problem_base_power(model::OperationModel) = get_base_power(model.sys) get_settings(model::OperationModel) = get_optimization_container(model).settings get_optimizer_stats(model::OperationModel) = @@ -126,7 +126,7 @@ function get_initial_conditions( model::OperationModel, ::T, ::U, -) where {T <: InitialConditionType, U <: PSY.Device} +) where {T <: InitialConditionType, U <: IS.InfrastructureSystemsComponent} return get_initial_conditions(get_optimization_container(model), T, U) end @@ -200,8 +200,6 @@ function advance_execution_count!(model::OperationModel) return end -const _TEMPLATE_VALIDATION_EXCLUSIONS = [PSY.Arc, PSY.Area, PSY.ACBus, PSY.LoadZone] - function _check_numerical_bounds(model::OperationModel) variable_bounds = get_variable_numerical_bounds(model) if variable_bounds.bounds.max - variable_bounds.bounds.min > 1e9 diff --git a/src/operation/time_series_interface.jl b/src/operation/time_series_interface.jl index bcfea04a..bde95abb 100644 --- a/src/operation/time_series_interface.jl +++ b/src/operation/time_series_interface.jl @@ -6,7 +6,7 @@ function get_time_series_values!( initial_time::Dates.DateTime, horizon::Int; ignore_scaling_factors = true, -) where {T <: PSY.Forecast} +) where {T <: IS.Forecast} if !use_time_series_cache(get_settings(model)) return IS.get_time_series_values( T, @@ -46,7 +46,7 @@ function get_time_series_values!( initial_time::Dates.DateTime, len::Int = 1; ignore_scaling_factors = true, -) where {T <: PSY.StaticTimeSeries, U <: PSY.Component} +) where {T <: IS.StaticTimeSeries, U <: IS.InfrastructureSystemsComponent} if !use_time_series_cache(get_settings(model)) return IS.get_time_series_values( T, diff --git a/src/quadratic_approximations/incremental.jl b/src/quadratic_approximations/incremental.jl index b6c8f08a..913cba9d 100644 --- a/src/quadratic_approximations/incremental.jl +++ b/src/quadratic_approximations/incremental.jl @@ -32,7 +32,7 @@ transitions between segments. # Type Parameters - `T <: Union{InterpolationVariableType, BinaryInterpolationVariableType}`: Variable type to create -- `U <: PSY.Component`: Component type for devices +- `U <: IS.InfrastructureSystemsComponent`: Component type for devices - `V <: AbstractDeviceFormulation`: Device formulation type for bounds # Notes @@ -132,7 +132,7 @@ Binary variables z ensure the incremental property: δᵢ₊₁ ≤ zᵢ ≤ δ - `T <: VariableType`: Interpolation variable type - `U <: VariableType`: Binary interpolation variable type - `V <: ConstraintType`: Constraint type -- `W <: PSY.Component`: Component type for devices +- `W <: IS.InfrastructureSystemsComponent`: Component type for devices # Notes - Creates two types of constraints: variable interpolation and function interpolation @@ -166,7 +166,7 @@ function _add_generic_incremental_interpolation_constraint!( # Retrieve all required variables from the optimization container # Retrieve original variable for DCVoltage from the Bus x_var = if (R <: DCVoltage) - get_variable(container, R, PSY.DCBus) # Original variable (domain of function) + get_variable(container, R, IS.InfrastructureSystemsComponent) # Original variable (domain of function) else get_variable(container, R, W) # Original variable (domain of function) end # Original variable (domain of function) @@ -199,7 +199,7 @@ function _add_generic_incremental_interpolation_constraint!( for d in devices name = get_name(d) # Get proper name for x variable (if is DCVoltage or not) - x_name = (R <: DCVoltage) ? get_name(PSY.get_dc_bus(d)) : name + x_name = (R <: DCVoltage) ? get_name(get_dc_bus(d)) : name var_bkpts = dic_var_bkpts[name] # Breakpoints in domain (x-values) function_bkpts = dic_function_bkpts[name] # Function values at breakpoints (y-values) num_segments = length(var_bkpts) - 1 # Number of linear segments diff --git a/src/utils/powersystems_utils.jl b/src/utils/component_utils.jl similarity index 77% rename from src/utils/powersystems_utils.jl rename to src/utils/component_utils.jl index 09feb18e..6a4bd77d 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/component_utils.jl @@ -1,19 +1,19 @@ function get_available_components( model::DeviceModel{T, <:AbstractDeviceFormulation}, - sys::PSY.System, -) where {T <: PSY.Component} + sys::IS.InfrastructureSystemsContainer, +) where {T <: IS.InfrastructureSystemsComponent} subsystem = get_subsystem(model) filter_function = get_attribute(model, "filter_function") if filter_function === nothing - return PSY.get_components( - PSY.get_available, + return IS.get_components( + IS.get_available, T, sys; subsystem_name = subsystem, ) else - return PSY.get_components( - x -> PSY.get_available(x) && filter_function(x), + return IS.get_components( + x -> IS.get_available(x) && filter_function(x), T, sys; subsystem_name = subsystem, @@ -23,20 +23,20 @@ end function get_available_components( model::ServiceModel{T, <:AbstractServiceFormulation}, - sys::PSY.System, -) where {T <: PSY.Component} + sys::IS.InfrastructureSystemsContainer, +) where {T <: IS.InfrastructureSystemsComponent} subsystem = get_subsystem(model) filter_function = get_attribute(model, "filter_function") if filter_function === nothing - return PSY.get_components( - PSY.get_available, + return IS.get_components( + IS.get_available, T, sys; subsystem_name = subsystem, ) else - return PSY.get_components( - x -> PSY.get_available(x) && filter_function(x), + return IS.get_components( + x -> IS.get_available(x) && filter_function(x), T, sys; subsystem_name = subsystem, @@ -44,49 +44,19 @@ function get_available_components( end end -_filter_function(x::PSY.ACBus) = - PSY.get_bustype(x) != PSY.ACBusTypes.ISOLATED && PSY.get_available(x) - -function get_available_components( - model::NetworkModel, - ::Type{PSY.ACBus}, - sys::PSY.System, -) - subsystem = get_subsystem(model) - return PSY.get_components( - _filter_function, - PSY.ACBus, - sys; - subsystem_name = subsystem, - ) -end - function get_available_components( model::NetworkModel, ::Type{T}, - sys::PSY.System, -) where {T <: PSY.Component} + sys::IS.InfrastructureSystemsContainer, +) where {T <: IS.InfrastructureSystemsComponent} subsystem = get_subsystem(model) - return PSY.get_components( + return IS.get_components( T, sys; subsystem_name = subsystem, ) end -#= -function get_available_components( - ::Type{PSY.RegulationDevice{T}}, - sys::PSY.System, -) where {T <: PSY.Component} - return PSY.get_components( - x -> (PSY.get_available(x) && PSY.has_service(x, PSY.AGC)), - PSY.RegulationDevice{T}, - sys, - ) -end -=# - ################################################## ########### Cost Function Utilities ############## ################################################## @@ -189,8 +159,8 @@ Note that the costs (y-axis) are always in \$/h so they do not require transformation """ function get_piecewise_pointcurve_per_system_unit( - cost_component::PSY.PiecewiseLinearData, - unit_system::PSY.UnitSystem, + cost_component::IS.PiecewiseLinearData, + unit_system::IS.UnitSystem, system_base_power::Float64, device_base_power::Float64, ) @@ -203,8 +173,8 @@ function get_piecewise_pointcurve_per_system_unit( end function _get_piecewise_pointcurve_per_system_unit( - cost_component::PSY.PiecewiseLinearData, - ::Val{PSY.UnitSystem.SYSTEM_BASE}, + cost_component::IS.PiecewiseLinearData, + ::Val{IS.UnitSystem.SYSTEM_BASE}, system_base_power::Float64, device_base_power::Float64, ) @@ -212,8 +182,8 @@ function _get_piecewise_pointcurve_per_system_unit( end function _get_piecewise_pointcurve_per_system_unit( - cost_component::PSY.PiecewiseLinearData, - ::Val{PSY.UnitSystem.DEVICE_BASE}, + cost_component::IS.PiecewiseLinearData, + ::Val{IS.UnitSystem.DEVICE_BASE}, system_base_power::Float64, device_base_power::Float64, ) @@ -223,12 +193,12 @@ function _get_piecewise_pointcurve_per_system_unit( points_normalized[ix] = (x = point.x * (device_base_power / system_base_power), y = point.y) end - return PSY.PiecewiseLinearData(points_normalized) + return IS.PiecewiseLinearData(points_normalized) end function _get_piecewise_pointcurve_per_system_unit( - cost_component::PSY.PiecewiseLinearData, - ::Val{PSY.UnitSystem.NATURAL_UNITS}, + cost_component::IS.PiecewiseLinearData, + ::Val{IS.UnitSystem.NATURAL_UNITS}, system_base_power::Float64, device_base_power::Float64, ) @@ -237,7 +207,7 @@ function _get_piecewise_pointcurve_per_system_unit( for (ix, point) in enumerate(points) points_normalized[ix] = (x = point.x / system_base_power, y = point.y) end - return PSY.PiecewiseLinearData(points_normalized) + return IS.PiecewiseLinearData(points_normalized) end """ @@ -248,15 +218,15 @@ Note that the costs (y-axis) are in \$/MWh, \$/(sys pu h) or \$/(device pu h), s require transformation. """ function get_piecewise_curve_per_system_unit( - cost_component::PSY.PiecewiseStepData, - unit_system::PSY.UnitSystem, + cost_component::IS.PiecewiseStepData, + unit_system::IS.UnitSystem, system_base_power::Float64, device_base_power::Float64, ) - return PSY.PiecewiseStepData( + return IS.PiecewiseStepData( get_piecewise_curve_per_system_unit( - PSY.get_x_coords(cost_component), - PSY.get_y_coords(cost_component), + IS.get_x_coords(cost_component), + IS.get_y_coords(cost_component), unit_system, system_base_power, device_base_power, @@ -267,7 +237,7 @@ end function get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, - unit_system::PSY.UnitSystem, + unit_system::IS.UnitSystem, system_base_power::Float64, device_base_power::Float64, ) @@ -283,7 +253,7 @@ end function _get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, - ::Val{PSY.UnitSystem.SYSTEM_BASE}, + ::Val{IS.UnitSystem.SYSTEM_BASE}, system_base_power::Float64, device_base_power::Float64, ) @@ -293,7 +263,7 @@ end function _get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, - ::Val{PSY.UnitSystem.DEVICE_BASE}, + ::Val{IS.UnitSystem.DEVICE_BASE}, system_base_power::Float64, device_base_power::Float64, ) @@ -306,7 +276,7 @@ end function _get_piecewise_curve_per_system_unit( x_coords::AbstractVector, y_coords::AbstractVector, - ::Val{PSY.UnitSystem.NATURAL_UNITS}, + ::Val{IS.UnitSystem.NATURAL_UNITS}, system_base_power::Float64, device_base_power::Float64, ) diff --git a/src/utils/print_pt_v3.jl b/src/utils/print_pt_v3.jl index 315e7bba..743bfaca 100644 --- a/src/utils/print_pt_v3.jl +++ b/src/utils/print_pt_v3.jl @@ -1,3 +1,45 @@ +# The predefined HTML table format recipe was removed in PrettyTables v3. +# This CSS recipe mirrors PT v2 for simple HTML tables. +const tf_html_simple = PrettyTables.HtmlTableFormat(; + css = """ + table, td, th { + border-collapse: collapse; + font-family: sans-serif; + } + + td, th { + border-bottom: 0; + padding: 4px + } + + tr:nth-child(odd) { + background: #eee; + } + + tr:nth-child(even) { + background: #fff; + } + + tr.header { + background: #fff !important; + font-weight: bold; + } + + tr.subheader { + background: #fff !important; + color: dimgray; + } + + tr.headerLastRow { + border-bottom: 2px solid black; + } + + th.rowNumber, td.rowNumber { + text-align: right; + } + """, +) + function Base.show(io::IO, container::OptimizationContainer) show(io, get_jump_model(container)) end @@ -7,8 +49,7 @@ function Base.show(io::IO, ::MIME"text/plain", input::Union{ServiceModel, Device end function Base.show(io::IO, ::MIME"text/html", input::Union{ServiceModel, DeviceModel}) - # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _show_method( @@ -124,7 +165,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::NetworkModel) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _show_method(io::IO, network_model::NetworkModel, backend::Symbol; kwargs...) @@ -153,7 +194,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::OperationModel) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _show_method(io::IO, model::OperationModel, backend::Symbol; kwargs...) @@ -166,7 +207,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::SimulationModels) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end _get_model_type(::DecisionModel{T}) where {T <: DecisionProblem} = T @@ -224,7 +265,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::SimulationSequence) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _show_method(io::IO, sequence::SimulationSequence, backend::Symbol; kwargs...) @@ -290,7 +331,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::Simulation) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _get_initial_time_for_show(sim::Simulation) @@ -349,7 +390,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::SimulationOutputs) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _show_method(io::IO, outputs::SimulationOutputs, backend::Symbol; kwargs...) @@ -396,7 +437,7 @@ end function Base.show(io::IO, ::MIME"text/html", input::ProblemOutputsTypes) # The tf_html_simple format was eliminated from PrettyTables and it was added to PowerSystems - _show_method(io, input, :html; stand_alone = false, table_format = PSY.tf_html_simple) + _show_method(io, input, :html; stand_alone = false, table_format = tf_html_simple) end function _show_method( diff --git a/src/utils/time_series_utils.jl b/src/utils/time_series_utils.jl index 268e066c..7ebea046 100644 --- a/src/utils/time_series_utils.jl +++ b/src/utils/time_series_utils.jl @@ -8,7 +8,7 @@ apply_maybe_across_time_series(fn::Function, ts_data::AbstractDict) = apply_maybe_across_time_series.(Ref(fn), values(ts_data)) apply_maybe_across_time_series(fn::Function, ts_data::IS.TimeSeriesData) = - apply_maybe_across_time_series(fn, PSY.get_data(ts_data)) + apply_maybe_across_time_series(fn, IS.get_data(ts_data)) """ Helper function to look up a time series if necessary then apply a function (typically a @@ -16,20 +16,20 @@ validation routine in a `do` block) to every element in it """ apply_maybe_across_time_series( fn::Function, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, ts_key::IS.TimeSeriesKey, ) = - apply_maybe_across_time_series(fn, PSY.get_time_series(component, ts_key)) + apply_maybe_across_time_series(fn, IS.get_time_series(component, ts_key)) apply_maybe_across_time_series( fn::Function, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, tts::IS.TupleTimeSeries, ) = apply_maybe_across_time_series(fn, component, IS.get_time_series_key(tts)) # case where the element isn't a time series -apply_maybe_across_time_series(fn::Function, ::PSY.Component, elem) = fn(elem) +apply_maybe_across_time_series(fn::Function, ::IS.InfrastructureSystemsComponent, elem) = fn(elem) # success case _validate_eltype_helper(::Type{T}, element::T) where {T} = true @@ -43,7 +43,7 @@ is of the type given """ _validate_eltype( ::Type{T}, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, ts_key::IS.TimeSeriesKey, msg = "", ) where {T} = @@ -57,7 +57,7 @@ _validate_eltype( ) end -function _validate_eltype(::Type{T}, component::PSY.Component, element, msg = "") where {T} +function _validate_eltype(::Type{T}, component::IS.InfrastructureSystemsComponent, element, msg = "") where {T} component_name = get_name(component) output = _validate_eltype_helper(T, element) output || throw( diff --git a/test/InfrastructureOptimizationModelsTests.jl b/test/InfrastructureOptimizationModelsTests.jl index 3a53856c..f218b685 100644 --- a/test/InfrastructureOptimizationModelsTests.jl +++ b/test/InfrastructureOptimizationModelsTests.jl @@ -41,17 +41,8 @@ include(joinpath(TEST_DIR, "test_utils/qa_bilinear_helpers.jl")) # Environment flags for test selection const RUN_UNIT_TESTS = get(ENV, "IOM_RUN_UNIT_TESTS", "true") == "true" -const RUN_INTEGRATION_TESTS = get(ENV, "IOM_RUN_INTEGRATION_TESTS", "true") == "true" - -# Heavy dependencies - only load if we need tests that use them -if RUN_INTEGRATION_TESTS - using PowerSystems - const PSY = PowerSystems - using PowerSystemCaseBuilder - const PSB = PowerSystemCaseBuilder - # only requires JuMP, SCS, HiGHS - include(joinpath(TEST_DIR, "test_utils/solver_definitions.jl")) -end + +include(joinpath(TEST_DIR, "test_utils/solver_definitions.jl")) const LOG_FILE = "power-optimization-models-test.log" @@ -87,7 +78,6 @@ function run_tests() @info "Running InfrastructureOptimizationModels.jl tests" @info "Unit tests: $RUN_UNIT_TESTS" - @info "Integration tests: $RUN_INTEGRATION_TESTS" if RUN_UNIT_TESTS @info "Starting unit tests..." @@ -151,31 +141,17 @@ function run_tests() #= ============================================================================ - BROKEN/NEEDS-WORK TEST FILES (not included) + BROKEN/NEEDS-WORK TEST FILES (not included — rewrite with mocks) ============================================================================ - - test_basic_model_structs.jl: Uses PSY types directly, 2 failures (missing types?) - - test_model_decision.jl: uses PowerModels.jl types, needs rework or move to POM - - test_model_emulation.jl: uses PowerModels.jl types, needs rework or move to POM + - test_basic_model_structs.jl, test_model_decision.jl, test_model_emulation.jl: + previously used PSY/PowerModels types; rewrite with mocks before re-enabling. + - test_model_store.jl, test_offer_curve_cost.jl: PSY/PSB-backed integration tests + removed when IOM dropped PSY/PSB dependencies; rewrite with mocks. ============================================================================ =# end end - #= - ============================================================================ - INTEGRATION TESTS (require PowerSystems types) - ============================================================================ - =# - if RUN_INTEGRATION_TESTS - @time @testset "InfrastructureOptimizationModels Integration Tests" begin - @info "Starting integration tests..." - # --- operation/ subfolder --- - include(joinpath(TEST_DIR, "test_model_store.jl")) - # --- objective_function/ subfolder --- - include(joinpath(TEST_DIR, "test_offer_curve_cost.jl")) - end - end - @test length(IS.get_log_events(multi_logger.tracker, Logging.Error)) == 0 @info IS.report_log_summary(multi_logger) end diff --git a/test/Project.toml b/test/Project.toml index 18754c4e..11f1ba61 100644 --- a/test/Project.toml +++ b/test/Project.toml @@ -18,8 +18,6 @@ LinearAlgebra = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" Logging = "56ddb016-857b-54e1-b83d-db4d58db5568" Pkg = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" PowerNetworkMatrices = "bed98974-b02a-5e2f-9fe0-a103f5c450dd" -PowerSystemCaseBuilder = "f00506e0-b84f-492a-93c2-c0a9afc4364e" -PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" Random = "9a3f8284-a2c9-5f02-9a11-845980a1fd5c" Revise = "295af30f-e4ad-537b-8983-00126c2a3abe" SCS = "c946c3f1-0d1f-5ce8-9dea-7daa1f7e2d13" diff --git a/test/includes.jl b/test/includes.jl deleted file mode 100644 index 3467496c..00000000 --- a/test/includes.jl +++ /dev/null @@ -1,53 +0,0 @@ -# SIIP Packages -using InfrastructureOptimizationModels -using PowerSystems -using PowerSystemCaseBuilder -using InfrastructureSystems -using PowerNetworkMatrices -using HydroPowerSimulations -import PowerSystemCaseBuilder: PSITestSystems -using PowerNetworkMatrices -using StorageSystemsSimulations -using PowerFlows -using DataFramesMeta - -# Test Packages -using Test -using Logging - -# Dependencies for testing -using DataFrames -using DataFramesMeta -using Dates -using JuMP -import JuMP.MOI as MOI -import JuMP.Containers: DenseAxisArray, SparseAxisArray -using TimeSeries -using CSV -import JSON3 -using DataStructures -import UUIDs -using Random -import Serialization -import LinearAlgebra - -const PSY = PowerSystems -const IOM = InfrastructureOptimizationModels -const PFS = PowerFlows -const PSB = PowerSystemCaseBuilder -const PNM = PowerNetworkMatrices -const ISOPT = InfrastructureSystems.Optimization - -const IS = InfrastructureSystems -const BASE_DIR = string(dirname(dirname(pathof(InfrastructureOptimizationModels)))) -const DATA_DIR = joinpath(BASE_DIR, "test/test_data") - -include("test_utils/common_operation_model.jl") -include("test_utils/model_checks.jl") -include("test_utils/mock_operation_models.jl") -include("test_utils/solver_definitions.jl") -include("test_utils/operations_problem_templates.jl") -include("test_utils/run_simulation.jl") - -ENV["RUNNING_SIENNA_TESTS"] = "true" -ENV["SIENNA_RANDOM_SEED"] = 1234 # Set a fixed seed for reproducibility in tests diff --git a/test/test_offer_curve_cost.jl b/test/test_offer_curve_cost.jl deleted file mode 100644 index ef150dfc..00000000 --- a/test/test_offer_curve_cost.jl +++ /dev/null @@ -1,402 +0,0 @@ -#= -Tests for MarketBidCost / MarketBidTimeSeriesCost / ImportExportCost / -ImportExportTimeSeriesCost code paths in value_curve_cost.jl and start_up_shut_down.jl. - -Uses c_sys5_uc as a base system and attaches MBC/IEC costs with real time series. - -Exercises: accessor wrappers, detection predicates, _has_parameter_time_series, -validation, _shutdown_cost_value, _get_parameter_field, get_initial_input, -validate_occ_breakpoints_slopes, validate_occ_component. -=# - -import PowerSystemCaseBuilder: PSITestSystems -using DataStructures: OrderedDict - -# ─── system builders ────────────────────────────────────────────────────────── - -"""Build a deterministic time series matching the system's forecast parameters.""" -function _make_ts(sys::PSY.System, name::String, value::Float64) - init_time = first(PSY.get_forecast_initial_times(sys)) - horizon = PSY.get_forecast_horizon(sys) - interval = PSY.get_forecast_interval(sys) - resolution = first(PSY.get_time_series_resolutions(sys)) - count = PSY.get_forecast_window_count(sys) - horizon_count = IS.get_horizon_count(horizon, resolution) - data = OrderedDict{Dates.DateTime, Vector{Float64}}() - for i in 0:(count - 1) - data[init_time + i * interval] = fill(value, horizon_count) - end - return PSY.Deterministic(; name = name, data = data, resolution = resolution) -end - -"""Build a deterministic time series of NTuple{3, Float64} matching the system's forecast params.""" -function _make_tuple_ts(sys::PSY.System, name::String, value::NTuple{3, Float64}) - init_time = first(PSY.get_forecast_initial_times(sys)) - horizon = PSY.get_forecast_horizon(sys) - interval = PSY.get_forecast_interval(sys) - resolution = first(PSY.get_time_series_resolutions(sys)) - count = PSY.get_forecast_window_count(sys) - horizon_count = IS.get_horizon_count(horizon, resolution) - data = OrderedDict{Dates.DateTime, Vector{NTuple{3, Float64}}}() - for i in 0:(count - 1) - data[init_time + i * interval] = fill(value, horizon_count) - end - return PSY.Deterministic(; name = name, data = data, resolution = resolution) -end - -"""Build a deterministic time series of PiecewiseStepData matching the system's forecast params.""" -function _make_pwl_ts( - sys::PSY.System, - name::String, - breakpoints::Vector{Float64}, - slopes::Vector{Float64}, -) - init_time = first(PSY.get_forecast_initial_times(sys)) - horizon = PSY.get_forecast_horizon(sys) - interval = PSY.get_forecast_interval(sys) - resolution = first(PSY.get_time_series_resolutions(sys)) - count = PSY.get_forecast_window_count(sys) - horizon_count = IS.get_horizon_count(horizon, resolution) - psd = PSY.PiecewiseStepData(breakpoints, slopes) - data = OrderedDict{Dates.DateTime, Vector{PSY.PiecewiseStepData}}() - for i in 0:(count - 1) - data[init_time + i * interval] = fill(psd, horizon_count) - end - return PSY.Deterministic(; name = name, data = data, resolution = resolution) -end - -""" -Load c_sys5_uc, pick a ThermalStandard, give it a static MarketBidCost, -and return (sys, component). -""" -function _make_static_mbc_system(; - slopes = [25.0, 30.0], - breakpoints = [0.0, 50.0, 100.0], - initial_input = 10.0, - no_load = 5.0, - start_up = (hot = 100.0, warm = 200.0, cold = 300.0), - shut_down = 50.0, -) - sys = Logging.with_logger(Logging.NullLogger()) do - build_system(PSITestSystems, "c_sys5_uc") - end - comp = first(PSY.get_components(PSY.ThermalStandard, sys)) - incr_vc = PSY.PiecewiseIncrementalCurve( - PSY.PiecewiseStepData(breakpoints, slopes), initial_input, nothing) - decr_vc = PSY.PiecewiseIncrementalCurve( - PSY.PiecewiseStepData(breakpoints, reverse(slopes)), initial_input, nothing) - mbc = PSY.MarketBidCost(; - no_load_cost = PSY.LinearCurve(no_load), - start_up = start_up, - shut_down = PSY.LinearCurve(shut_down), - incremental_offer_curves = PSY.CostCurve(incr_vc), - decremental_offer_curves = PSY.CostCurve(decr_vc), - ) - PSY.set_operation_cost!(comp, mbc) - return sys, comp -end - -""" -Load c_sys5_uc, pick a ThermalStandard, give it a MarketBidTimeSeriesCost -with real time series attached, and return (sys, component). -""" -function _make_ts_mbc_system(; - slopes = [25.0, 30.0], - breakpoints = [0.0, 50.0, 100.0], - initial_input = 10.0, - no_load = 5.0, - start_up_val = (100.0, 100.0, 100.0), - shut_down = 50.0, -) - sys = Logging.with_logger(Logging.NullLogger()) do - build_system(PSITestSystems, "c_sys5_uc") - end - comp = first(PSY.get_components(PSY.ThermalStandard, sys)) - - # Create and attach time series - incr_ts = _make_pwl_ts(sys, "variable_cost incremental", breakpoints, slopes) - incr_key = PSY.add_time_series!(sys, comp, incr_ts) - incr_init_ts = _make_ts(sys, "initial_input incremental", initial_input) - incr_init_key = PSY.add_time_series!(sys, comp, incr_init_ts) - - decr_ts = _make_pwl_ts(sys, "variable_cost decremental", breakpoints, reverse(slopes)) - decr_key = PSY.add_time_series!(sys, comp, decr_ts) - decr_init_ts = _make_ts(sys, "initial_input decremental", initial_input) - decr_init_key = PSY.add_time_series!(sys, comp, decr_init_ts) - - no_load_ts = _make_ts(sys, "no_load_cost", no_load) - no_load_key = PSY.add_time_series!(sys, comp, no_load_ts) - - shut_down_ts = _make_ts(sys, "shut_down", shut_down) - shut_down_key = PSY.add_time_series!(sys, comp, shut_down_ts) - - startup_ts = _make_tuple_ts(sys, "start_up", start_up_val) - startup_key = PSY.add_time_series!(sys, comp, startup_ts) - - ts_mbc = PSY.MarketBidTimeSeriesCost(; - no_load_cost = PSY.TimeSeriesLinearCurve(no_load_key), - start_up = IS.TupleTimeSeries{PSY.StartUpStages}(startup_key), - shut_down = PSY.TimeSeriesLinearCurve(shut_down_key), - incremental_offer_curves = PSY.make_market_bid_ts_curve(incr_key, incr_init_key), - decremental_offer_curves = PSY.make_market_bid_ts_curve(decr_key, decr_init_key), - ) - PSY.set_operation_cost!(comp, ts_mbc) - return sys, comp -end - -""" -Load c_sys5_uc, add a Source with static ImportExportCost, return (sys, source). -""" -function _make_static_iec_system() - sys = Logging.with_logger(Logging.NullLogger()) do - build_system(PSITestSystems, "c_sys5_uc") - end - bus = first(PSY.get_components(PSY.ACBus, sys)) - source = PSY.Source(; - name = "test_source", - available = true, - bus = bus, - active_power = 0.0, - reactive_power = 0.0, - active_power_limits = (min = -2.0, max = 2.0), - reactive_power_limits = (min = -2.0, max = 2.0), - R_th = 0.01, - X_th = 0.02, - internal_voltage = 1.0, - internal_angle = 0.0, - base_power = 100.0, - ) - import_curve = PSY.make_import_curve( - [0.0, 100.0, 105.0, 120.0, 200.0], [5.0, 10.0, 20.0, 40.0]) - export_curve = PSY.make_export_curve( - [0.0, 100.0, 105.0, 120.0, 200.0], [12.0, 8.0, 4.0, 1.0]) - iec = PSY.ImportExportCost(; - import_offer_curves = import_curve, - export_offer_curves = export_curve, - ) - PSY.set_operation_cost!(source, iec) - PSY.add_component!(sys, source) - return sys, source -end - -""" -Load c_sys5_uc, add a Source with ImportExportTimeSeriesCost backed by real TS, -return (sys, source). -""" -function _make_ts_iec_system() - sys = Logging.with_logger(Logging.NullLogger()) do - build_system(PSITestSystems, "c_sys5_uc") - end - bus = first(PSY.get_components(PSY.ACBus, sys)) - source = PSY.Source(; - name = "test_source", - available = true, - bus = bus, - active_power = 0.0, - reactive_power = 0.0, - active_power_limits = (min = -2.0, max = 2.0), - reactive_power_limits = (min = -2.0, max = 2.0), - R_th = 0.01, - X_th = 0.02, - internal_voltage = 1.0, - internal_angle = 0.0, - base_power = 100.0, - ) - PSY.add_component!(sys, source) - - im_ts = _make_pwl_ts( - sys, "variable_cost_import", - [0.0, 100.0, 105.0, 120.0, 200.0], [5.0, 10.0, 20.0, 40.0]) - im_key = PSY.add_time_series!(sys, source, im_ts) - ex_ts = _make_pwl_ts( - sys, "variable_cost_export", - [0.0, 100.0, 105.0, 120.0, 200.0], [12.0, 8.0, 4.0, 1.0]) - ex_key = PSY.add_time_series!(sys, source, ex_ts) - - ts_iec = PSY.ImportExportTimeSeriesCost(; - import_offer_curves = PSY.make_import_export_ts_curve(im_key), - export_offer_curves = PSY.make_import_export_ts_curve(ex_key), - ) - PSY.set_operation_cost!(source, ts_iec) - return sys, source -end - -# ─── tests ──────────────────────────────────────────────────────────────────── - -@testset "Offer Curve Cost: is_time_variant with new types" begin - _, comp_mbc = _make_static_mbc_system() - mbc = PSY.get_operation_cost(comp_mbc) - _, comp_ts = _make_ts_mbc_system() - ts_mbc = PSY.get_operation_cost(comp_ts) - _, source_iec = _make_static_iec_system() - iec = PSY.get_operation_cost(source_iec) - _, source_ts = _make_ts_iec_system() - ts_iec = PSY.get_operation_cost(source_ts) - - # Static → not time variant - @test !IOM.is_time_variant(PSY.get_incremental_offer_curves(mbc)) - @test !IOM.is_time_variant(PSY.get_import_offer_curves(iec)) - @test !IOM.is_time_variant(PSY.get_shut_down(mbc)) - @test !IOM.is_time_variant(PSY.get_start_up(mbc)) - - # TS → time variant - @test IOM.is_time_variant(PSY.get_incremental_offer_curves(ts_mbc)) - @test IOM.is_time_variant(PSY.get_import_offer_curves(ts_iec)) - @test IOM.is_time_variant(PSY.get_shut_down(ts_mbc)) - @test IOM.is_time_variant(PSY.get_start_up(ts_mbc)) -end - -@testset "Offer Curve Cost: _shutdown_cost_value" begin - @test IOM._shutdown_cost_value(42.0) == 42.0 - @test IOM._shutdown_cost_value(PSY.LinearCurve(99.0)) ≈ 99.0 - @test IOM._shutdown_cost_value(PSY.LinearCurve(0.0)) ≈ 0.0 -end - -@testset "Offer Curve Cost: Detection predicates on devices" begin - _, comp_mbc = _make_static_mbc_system() - @test IOM._has_market_bid_cost(comp_mbc) - @test !IOM._has_import_export_cost(comp_mbc) - @test IOM._has_offer_curve_cost(comp_mbc) - - _, comp_ts = _make_ts_mbc_system() - @test IOM._has_market_bid_cost(comp_ts) - @test !IOM._has_import_export_cost(comp_ts) - @test IOM._has_offer_curve_cost(comp_ts) - - _, source_iec = _make_static_iec_system() - @test !IOM._has_market_bid_cost(source_iec) - @test IOM._has_import_export_cost(source_iec) - @test IOM._has_offer_curve_cost(source_iec) - - _, source_ts = _make_ts_iec_system() - @test !IOM._has_market_bid_cost(source_ts) - @test IOM._has_import_export_cost(source_ts) - @test IOM._has_offer_curve_cost(source_ts) -end - -@testset "Offer Curve Cost: _has_parameter_time_series on devices" begin - _, comp_static = _make_static_mbc_system() - _, comp_ts = _make_ts_mbc_system() - _, source_static = _make_static_iec_system() - _, source_ts = _make_ts_iec_system() - - param = IOM.IncrementalPiecewiseLinearSlopeParameter - @test !IOM._has_parameter_time_series(comp_static) - @test IOM._has_parameter_time_series(comp_ts) - @test !IOM._has_parameter_time_series(source_static) - @test IOM._has_parameter_time_series(source_ts) - - startup_param = IOM.StartupCostParameter - @test !IOM._has_parameter_time_series(comp_static) - @test IOM._has_parameter_time_series(comp_ts) -end - -@testset "Offer Curve Cost: get_initial_input on devices" begin - _, comp = _make_static_mbc_system(; initial_input = 7.5) - @test IOM.get_initial_input(IOM.IncrementalOffer(), comp) ≈ 7.5 - @test IOM.get_initial_input(IOM.DecrementalOffer(), comp) ≈ 7.5 - - _, comp_ts = _make_ts_mbc_system(; initial_input = 12.0) - # TS path: initial_input is a TimeSeriesKey, not a Float64 - @test IOM.get_initial_input(IOM.IncrementalOffer(), comp_ts) isa IS.TimeSeriesKey - @test IOM.get_initial_input(IOM.DecrementalOffer(), comp_ts) isa IS.TimeSeriesKey -end - -@testset "Offer Curve Cost: validate_occ_breakpoints_slopes on devices" begin - # Static MBC: convex incremental curve should validate without error - _, comp = _make_static_mbc_system(; slopes = [25.0, 30.0, 35.0], - breakpoints = [0.0, 50.0, 80.0, 100.0]) - IOM.validate_occ_breakpoints_slopes(comp, IOM.IncrementalOffer()) - - # Static MBC: concave decremental curve should validate without error - IOM.validate_occ_breakpoints_slopes(comp, IOM.DecrementalOffer()) - - # TS MBC: should return immediately (no validation for TS) - _, comp_ts = _make_ts_mbc_system() - IOM.validate_occ_breakpoints_slopes(comp_ts, IOM.IncrementalOffer()) - IOM.validate_occ_breakpoints_slopes(comp_ts, IOM.DecrementalOffer()) -end - -@testset "Offer Curve Cost: validate_occ_component on devices" begin - _, comp = _make_static_mbc_system() - # Startup: static StartUpStages, should warn about multistart but not error - @test_logs (:warn, r"Multi-start") IOM.validate_occ_component( - IOM.StartupCostParameter, comp) - # Shutdown: LinearCurve is valid - IOM.validate_occ_component(IOM.ShutdownCostParameter, comp) - - # CostAtMin: should not error (simplified to no-op) - IOM.validate_occ_component(IOM.IncrementalCostAtMinParameter, comp) - IOM.validate_occ_component(IOM.DecrementalCostAtMinParameter, comp) - - # Breakpoints: validates the static curve - IOM.validate_occ_component( - IOM.IncrementalPiecewiseLinearBreakpointParameter, comp) - IOM.validate_occ_component( - IOM.DecrementalPiecewiseLinearBreakpointParameter, comp) - - # TS MBC: startup validation should return immediately (skip TS) - _, comp_ts = _make_ts_mbc_system() - IOM.validate_occ_component(IOM.StartupCostParameter, comp_ts) - IOM.validate_occ_component(IOM.ShutdownCostParameter, comp_ts) -end - -@testset "Offer Curve Cost: Validation errors (static IEC)" begin - _, source = _make_static_iec_system() - iec = PSY.get_operation_cost(source) - - # Valid IEC should not error - IOM._validate_occ_subtype(iec, IOM.IncrementalOffer(), - PSY.get_import_offer_curves(iec), "test") - - # IEC with non-zero VOM: error - bad_import = PSY.CostCurve( - PSY.PiecewiseIncrementalCurve( - PSY.PiecewiseStepData([0.0, 100.0], [10.0]), 0.0, nothing), - PSY.UnitSystem.NATURAL_UNITS, - PSY.LinearCurve(5.0), - ) - bad_iec = PSY.ImportExportCost(; import_offer_curves = bad_import, - export_offer_curves = PSY.make_export_curve([0.0, 100.0], [10.0])) - @test_throws ArgumentError IOM._validate_occ_subtype( - bad_iec, IOM.IncrementalOffer(), bad_import, "test") - - # IEC with non-zero first breakpoint: error - bad_import2 = PSY.CostCurve( - PSY.PiecewiseIncrementalCurve( - PSY.PiecewiseStepData([10.0, 100.0], [10.0]), 0.0, nothing)) - bad_iec2 = PSY.ImportExportCost(; import_offer_curves = bad_import2, - export_offer_curves = PSY.make_export_curve([0.0, 100.0], [10.0])) - @test_throws ArgumentError IOM._validate_occ_subtype( - bad_iec2, IOM.IncrementalOffer(), bad_import2, "test") -end - -@testset "Offer Curve Cost: TS curve properties (MBC)" begin - _, comp = _make_ts_mbc_system() - ts_mbc = PSY.get_operation_cost(comp) - incr = PSY.get_incremental_offer_curves(ts_mbc) - decr = PSY.get_decremental_offer_curves(ts_mbc) - - @test IS.is_time_series_backed(incr) - @test IS.is_time_series_backed(decr) - @test IOM.is_time_variant(incr) - @test IOM.is_time_variant(decr) - - vc = PSY.get_value_curve(incr) - @test IS.get_time_series_key(vc) isa IS.TimeSeriesKey - @test IS.get_initial_input(vc) isa IS.TimeSeriesKey -end - -@testset "Offer Curve Cost: TS curve properties (IEC)" begin - _, source = _make_ts_iec_system() - ts_iec = PSY.get_operation_cost(source) - im = PSY.get_import_offer_curves(ts_iec) - ex = PSY.get_export_offer_curves(ts_iec) - - @test IS.is_time_series_backed(im) - @test IS.is_time_series_backed(ex) - - # IEC curves have no initial_input - @test IS.get_initial_input(PSY.get_value_curve(im)) === nothing - @test IS.get_initial_input(PSY.get_value_curve(ex)) === nothing -end diff --git a/test/test_utils/mock_operation_models.jl b/test/test_utils/mock_operation_models.jl deleted file mode 100644 index 5b5863eb..00000000 --- a/test/test_utils/mock_operation_models.jl +++ /dev/null @@ -1,167 +0,0 @@ -# NOTE: None of the models and function in this file are functional. All of these are used for testing purposes and do not represent valid examples either to develop custom -# models. Please refer to the documentation. - -struct MockOperationProblem <: IOM.DefaultDecisionProblem end -struct MockEmulationProblem <: IOM.DefaultEmulationProblem end - -function PSI.DecisionModel( - ::Type{MockOperationProblem}, - ::Type{T}, - sys::PSY.System; - name = nothing, - kwargs..., -) where {T <: AbstractPowerModel} - settings = PSI.Settings(sys; kwargs...) - available_resolutions = PSY.get_time_series_resolutions(sys) - if length(available_resolutions) == 1 - PSI.set_resolution!(settings, first(available_resolutions)) - else - error("System has multiple resolutions MockOperationProblem won't work") - end - return DecisionModel{MockOperationProblem}( - ProblemTemplate(T), - sys, - settings, - nothing; - name = name, - ) -end - -function make_mock_forecast( - horizon::Dates.TimePeriod, - resolution::Dates.TimePeriod, - interval::Dates.TimePeriod, - steps, -) - init_time = DateTime("2024-01-01") - timeseries_data = Dict{Dates.DateTime, Vector{Float64}}() - horizon_count = horizon ÷ resolution - for i in 1:steps - forecast_timestamps = init_time + interval * i - timeseries_data[forecast_timestamps] = rand(horizon_count) - end - return Deterministic(; - name = "mock_forecast", - data = timeseries_data, - resolution = resolution, - ) -end - -function make_mock_singletimeseries(horizon, resolution) - init_time = DateTime("2024-01-01") - horizon_count = horizon ÷ resolution - tstamps = collect(range(init_time; length = horizon_count, step = resolution)) - timeseries_data = TimeArray(tstamps, rand(horizon_count)) - return SingleTimeSeries(; name = "mock_timeseries", data = timeseries_data) -end - -function PSI.DecisionModel(::Type{MockOperationProblem}; name = nothing, kwargs...) - sys = System(100.0) - add_component!(sys, ACBus(nothing)) - l = PowerLoad(nothing) - gen = ThermalStandard(nothing) - set_bus!(l, get_component(Bus, sys, "init")) - set_bus!(gen, get_component(Bus, sys, "init")) - add_component!(sys, l) - add_component!(sys, gen) - forecast = make_mock_forecast( - get(kwargs, :horizon, Hour(24)), - get(kwargs, :resolution, Hour(1)), - get(kwargs, :interval, Hour(1)), - get(kwargs, :steps, 2), - ) - add_time_series!(sys, l, forecast) - settings = PSI.Settings(sys; - horizon = get(kwargs, :horizon, Hour(24)), - resolution = get(kwargs, :resolution, Hour(1))) - return DecisionModel{MockOperationProblem}( - ProblemTemplate(CopperPlatePowerModel), - sys, - settings, - nothing; - name = name, - ) -end - -function PSI.EmulationModel(::Type{MockEmulationProblem}; name = nothing, kwargs...) - sys = System(100.0) - add_component!(sys, ACBus(nothing)) - l = PowerLoad(nothing) - gen = ThermalStandard(nothing) - set_bus!(l, get_component(Bus, sys, "init")) - set_bus!(gen, get_component(Bus, sys, "init")) - add_component!(sys, l) - add_component!(sys, gen) - single_ts = make_mock_singletimeseries( - get(kwargs, :horizon, Hour(24)), - get(kwargs, :resolution, Hour(1)), - ) - add_time_series!(sys, l, single_ts) - - settings = PSI.Settings(sys; - horizon = get(kwargs, :resolution, Hour(1)), - resolution = get(kwargs, :resolution, Hour(1))) - return EmulationModel{MockEmulationProblem}( - ProblemTemplate(CopperPlatePowerModel), - sys, - settings, - nothing; - name = name, - ) -end - -function mock_uc_ed_simulation_problems(uc_horizon, ed_horizon) - return SimulationModels([ - DecisionModel(MockOperationProblem; horizon = uc_horizon, name = "UC"), - DecisionModel( - MockOperationProblem; - horizon = ed_horizon, - resolution = Minute(5), - name = "ED", - ), - ]) -end - -function create_simulation_build_test_problems( - template_uc = get_template_standard_uc_simulation(), - template_ed = get_template_nomin_ed_simulation(), - sys_uc = PSB.build_system(PSITestSystems, "c_sys5_uc"), - sys_ed = PSB.build_system(PSITestSystems, "c_sys5_ed"), -) - return SimulationModels(; - decision_models = [ - DecisionModel(template_uc, sys_uc; name = "UC", optimizer = HiGHS_optimizer), - DecisionModel(template_ed, sys_ed; name = "ED", optimizer = HiGHS_optimizer), - ], - ) -end - -struct MockStagesStruct - stages::Dict{Int, Int} -end - -function Base.show(io::IO, struct_stages::MockStagesStruct) - println(io, "mock problem") - return -end - -function setup_ic_model_container!(model::DecisionModel) - # This function is only for testing purposes. - if !PSI.isempty(model) - PSI.reset!(model) - end - - PSI.init_optimization_container!( - PSI.get_optimization_container(model), - PSI.get_network_model(PSI.get_template(model)), - PSI.get_system(model), - ) - - PSI.init_model_store_params!(model) - - @info "Make Initial Conditions Model" - PSI.set_output_dir!(model, mktempdir(; cleanup = true)) - PSI.build_initial_conditions!(model) - PSI.initialize!(model) - return -end diff --git a/test/test_utils/model_checks.jl b/test/test_utils/model_checks.jl deleted file mode 100644 index a4d97312..00000000 --- a/test/test_utils/model_checks.jl +++ /dev/null @@ -1,550 +0,0 @@ -const GAEVF = JuMP.GenericAffExpr{Float64, VariableRef} -const GQEVF = JuMP.GenericQuadExpr{Float64, VariableRef} - -function moi_tests( - model::DecisionModel, - vars::Int, - interval::Int, - lessthan::Int, - greaterthan::Int, - equalto::Int, - binary::Bool, - lessthan_quadratic::Union{Int, Nothing} = nothing, -) - JuMPmodel = PSI.get_jump_model(model) - @test JuMP.num_variables(JuMPmodel) == vars - @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.Interval{Float64}) == interval - @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.LessThan{Float64}) == lessthan - @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.GreaterThan{Float64}) == greaterthan - @test JuMP.num_constraints(JuMPmodel, GAEVF, MOI.EqualTo{Float64}) == equalto - @test ((JuMP.VariableRef, MOI.ZeroOne) in JuMP.list_of_constraint_types(JuMPmodel)) == - binary - !isnothing(lessthan_quadratic) && - @test JuMP.num_constraints(JuMPmodel, GQEVF, MOI.LessThan{Float64}) == - lessthan_quadratic - return -end - -function psi_constraint_test( - model::DecisionModel, - constraint_keys::Vector{<:PSI.ConstraintKey}, -) - constraints = PSI.get_constraints(model) - for con in constraint_keys - if get(constraints, con, nothing) !== nothing - # Ensure constraint container does not have undefined entries: - if typeof(constraints[con]) == DenseAxisArray - @test all(x -> isassigned(constraints[con], x), eachindex(constraints[con])) - else - @test true - end - else - @error con - @test false - end - end - return -end - -function psi_aux_variable_test( - model::DecisionModel, - constraint_keys::Vector{<:PSI.AuxVarKey}, -) - op_container = PSI.get_optimization_container(model) - vars = PSI.get_aux_variables(op_container) - for key in constraint_keys - @test get(vars, key, nothing) !== nothing - end - return -end - -function psi_checkbinvar_test( - model::DecisionModel, - bin_variable_keys::Vector{<:PSI.VariableKey}, -) - container = PSI.get_optimization_container(model) - for variable in bin_variable_keys - for v in PSI.get_variable(container, variable) - @test JuMP.is_binary(v) - end - end - return -end - -function psi_checkobjfun_test(model::DecisionModel, exp_type) - model = PSI.get_jump_model(model) - @test JuMP.objective_function_type(model) == exp_type - return -end - -function moi_lbvalue_test( - model::DecisionModel, - con_key::PSI.ConstraintKey, - value::Number, -) - for con in PSI.get_constraints(model)[con_key] - @test JuMP.constraint_object(con).set.lower == value - end - return -end - -function psi_checksolve_test(model::DecisionModel, status) - model = PSI.get_jump_model(model) - JuMP.optimize!(model) - @test termination_status(model) in status -end - -function psi_checksolve_test(model::DecisionModel, status, expected_output, tol = 0.0) - res = solve!(model) - model = PSI.get_jump_model(model) - @test termination_status(model) in status - obj_value = JuMP.objective_value(model) - @test isapprox(obj_value, expected_output, atol = tol) -end - -function psi_ptdf_lmps(res::OptimizationProblemOutputs, ptdf) - cp_duals = - read_dual(res, PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System)) - λ = Matrix{Float64}(cp_duals[:, propertynames(cp_duals) .!= :DateTime]) - - flow_duals = read_dual(res, PSI.ConstraintKey(NetworkFlowConstraint, PSY.Line)) - μ = Matrix{Float64}(flow_duals[:, PNM.get_branch_ax(ptdf)]) - - buses = get_components(Bus, get_system(res)) - lmps = OrderedDict() - for bus in buses - lmps[get_name(bus)] = μ * ptdf[:, get_number(bus)] - end - lmp = λ .+ DataFrames.DataFrame(lmps) - return lmp[!, sort(propertynames(lmp))] -end - -function check_variable_unbounded( - model::DecisionModel, - ::Type{T}, - ::Type{U}, -) where {T <: PSI.VariableType, U <: PSY.Component} - return check_variable_unbounded(model::DecisionModel, PSI.VariableKey(T, U)) -end - -function check_variable_unbounded(model::DecisionModel, var_key::PSI.VariableKey) - psi_cont = PSI.get_optimization_container(model) - variable = PSI.get_variable(psi_cont, var_key) - for var in variable - if JuMP.has_lower_bound(var) || JuMP.has_upper_bound(var) - return false - end - end - return true -end - -function check_variable_bounded( - model::DecisionModel, - ::Type{T}, - ::Type{U}, -) where {T <: PSI.VariableType, U <: PSY.Component} - return check_variable_bounded(model, PSI.VariableKey(T, U)) -end - -function check_variable_bounded(model::DecisionModel, var_key::PSI.VariableKey) - psi_cont = PSI.get_optimization_container(model) - variable = PSI.get_variable(psi_cont, var_key) - for var in variable - if !JuMP.has_lower_bound(var) || !JuMP.has_upper_bound(var) - return false - end - end - return true -end - -function check_flow_variable_values( - model::DecisionModel, - ::Type{T}, - ::Type{U}, - device_name::String, - limit::Float64, -) where {T <: PSI.VariableType, U <: PSY.Component} - psi_cont = PSI.get_optimization_container(model) - variable = PSI.get_variable(psi_cont, T, U) - for var in variable[device_name, :] - if !(PSI.jump_value(var) <= (limit + 1e-2)) - @error "$device_name out of bounds $(PSI.jump_value(var))" - return false - end - end - return true -end - -function check_flow_variable_values( - model::DecisionModel, - ::Type{T}, - ::Type{U}, - device_name::String, - limit::Float64, -) where {T <: PSI.FlowActivePowerVariable, U <: PSY.Component} - psi_cont = PSI.get_optimization_container(model) - template = model.template - device_model = PSI.get_model(template, U) - dev_formulation = PSI.get_formulation(device_model) - net_formulation = PSI.get_network_formulation(template) - if dev_formulation <: Union{PSI.StaticBranch, PSI.StaticBranchUnbounded} && - net_formulation <: PSI.PTDFPowerModel - variable = PSI.get_expression(psi_cont, PSI.PTDFBranchFlow, U) - else - variable = PSI.get_variable(psi_cont, T, U) - end - for var in variable[device_name, :] - if !(PSI.jump_value(var) <= (limit + 1e-2)) - @error "$device_name out of bounds $(PSI.jump_value(var))" - return false - end - end - return true -end - -function check_flow_variable_values( - model::DecisionModel, - ::Type{T}, - ::Type{U}, - device_name::String, - limit_min::Float64, - limit_max::Float64, -) where {T <: PSI.VariableType, U <: PSY.Component} - psi_cont = PSI.get_optimization_container(model) - variable = PSI.get_variable(psi_cont, T, U) - for var in variable[device_name, :] - if !(PSI.jump_value(var) <= (limit_max + 1e-2)) || - !(PSI.jump_value(var) >= (limit_min - 1e-2)) - return false - end - end - return true -end - -function check_flow_variable_values( - model::DecisionModel, - ::Type{T}, - ::Type{U}, - device_name::String, - limit_min::Float64, - limit_max::Float64, -) where {T <: PSI.FlowActivePowerVariable, U <: PSY.Component} - psi_cont = PSI.get_optimization_container(model) - template = model.template - device_model = PSI.get_model(template, U) - dev_formulation = PSI.get_formulation(device_model) - net_formulation = PSI.get_network_formulation(template) - if dev_formulation <: Union{PSI.StaticBranch, PSI.StaticBranchUnbounded} && - net_formulation <: PSI.PTDFPowerModel - variable = PSI.get_expression(psi_cont, PSI.PTDFBranchFlow, U) - else - variable = PSI.get_variable(psi_cont, T, U) - end - for var in variable[device_name, :] - if !(PSI.jump_value(var) <= (limit_max + 1e-2)) || - !(PSI.jump_value(var) >= (limit_min - 1e-2)) - return false - end - end - return true -end - -function check_flow_variable_values( - model::DecisionModel, - ::Type{T}, - ::Type{U}, - ::Type{V}, - device_name::String, - limit_min::Float64, - limit_max::Float64, -) where {T <: PSI.VariableType, U <: PSI.VariableType, V <: PSY.Component} - psi_cont = PSI.get_optimization_container(model) - time_steps = PSI.get_time_steps(psi_cont) - pvariable = PSI.get_variable(psi_cont, T, V) - qvariable = PSI.get_variable(psi_cont, U, V) - for t in time_steps - fp = PSI.jump_value(pvariable[device_name, t]) - fq = PSI.jump_value(qvariable[device_name, t]) - flow = sqrt((fp)^2 + (fq)^2) - if !(flow <= (limit_max + 1e-2)^2) || !(flow >= (limit_min - 1e-2)^2) - return false - end - end - return true -end - -function check_flow_variable_values( - model::DecisionModel, - ::Type{T}, - ::Type{U}, - ::Type{V}, - device_name::String, - limit::Float64, -) where {T <: PSI.VariableType, U <: PSI.VariableType, V <: PSY.Component} - psi_cont = PSI.get_optimization_container(model) - time_steps = PSI.get_time_steps(psi_cont) - pvariable = PSI.get_variable(psi_cont, T, V) - qvariable = PSI.get_variable(psi_cont, U, V) - for t in time_steps - fp = PSI.jump_value(pvariable[device_name, t]) - fq = PSI.jump_value(qvariable[device_name, t]) - flow = sqrt((fp)^2 + (fq)^2) - if !(flow <= (limit + 1e-2)^2) - return false - end - end - return true -end - -function PSI.jump_value(int::Int) - @warn("This is for testing purposes only.") - return int -end - -function _check_constraint_bounds(bounds::PSI.ConstraintBounds, valid_bounds::NamedTuple) - @test bounds.coefficient.min == valid_bounds.coefficient.min - @test bounds.coefficient.max == valid_bounds.coefficient.max - @test bounds.rhs.min == valid_bounds.rhs.min - @test bounds.rhs.max == valid_bounds.rhs.max -end - -function _check_variable_bounds(bounds::PSI.VariableBounds, valid_bounds::NamedTuple) - @test bounds.bounds.min == valid_bounds.min - @test bounds.bounds.max == valid_bounds.max -end - -function check_duration_on_initial_conditions_values( - model, - ::Type{T}, -) where {T <: PSY.Component} - initial_conditions_data = - PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) - duration_on_data = PSI.get_initial_condition( - PSI.get_optimization_container(model), - InitialTimeDurationOn(), - T, - ) - for ic in duration_on_data - name = PSY.get_name(ic.component) - on_var = PSI.get_initial_condition_value(initial_conditions_data, OnVariable, T)[ - name, - 1, - ] - duration_on = PSI.jump_value(PSI.get_value(ic)) - if on_var == 1.0 && PSY.get_status(ic.component) - @test duration_on == PSY.get_time_at_status(ic.component) - elseif on_var == 1.0 && !PSY.get_status(ic.component) - @test duration_on == 0.0 - end - end -end - -function check_duration_off_initial_conditions_values( - model, - ::Type{T}, -) where {T <: PSY.Component} - initial_conditions_data = - PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) - duration_off_data = PSI.get_initial_condition( - PSI.get_optimization_container(model), - InitialTimeDurationOff(), - T, - ) - for ic in duration_off_data - name = PSY.get_name(ic.component) - on_var = PSI.get_initial_condition_value(initial_conditions_data, OnVariable, T)[ - name, - 1, - ] - duration_off = PSI.jump_value(PSI.get_value(ic)) - if on_var == 0.0 && !PSY.get_status(ic.component) - @test duration_off == PSY.get_time_at_status(ic.component) - elseif on_var == 0.0 && PSY.get_status(ic.component) - @test duration_off == 0.0 - end - end -end - -function check_status_initial_conditions_values(model, ::Type{T}) where {T <: PSY.Component} - initial_conditions = - PSI.get_initial_condition(PSI.get_optimization_container(model), DeviceStatus(), T) - initial_conditions_data = - PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) - for ic in initial_conditions - name = PSY.get_name(ic.component) - status = PSI.get_initial_condition_value(initial_conditions_data, OnVariable, T)[ - name, - 1, - ] - @test PSI.jump_value(PSI.get_value(ic)) == status - end -end - -function check_active_power_initial_condition_values( - model, - ::Type{T}, -) where {T <: PSY.Component} - initial_conditions = - PSI.get_initial_condition(PSI.get_optimization_container(model), DevicePower(), T) - initial_conditions_data = - PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) - for ic in initial_conditions - name = PSY.get_name(ic.component) - power = PSI.get_initial_condition_value( - initial_conditions_data, - ActivePowerVariable, - T, - )[ - name, - 1, - ] - @test PSI.jump_value(PSI.get_value(ic)) == power - end -end - -function check_active_power_abovemin_initial_condition_values( - model, - ::Type{T}, -) where {T <: PSY.Component} - initial_conditions = PSI.get_initial_condition( - PSI.get_optimization_container(model), - PSI.DeviceAboveMinPower(), - T, - ) - initial_conditions_data = - PSI.get_initial_conditions_data(PSI.get_optimization_container(model)) - for ic in initial_conditions - name = PSY.get_name(ic.component) - power = PSI.get_initial_condition_value( - initial_conditions_data, - PSI.PowerAboveMinimumVariable, - T, - )[ - name, - 1, - ] - @test PSI.jump_value(PSI.get_value(ic)) == power - end -end - -function check_initialization_variable_count( - model, - ::S, - ::Type{T}, -) where {S <: PSI.VariableType, T <: PSY.Component} - container = PSI.get_optimization_container(model) - initial_conditions_data = PSI.get_initial_conditions_data(container) - no_component = length(PSY.get_components(PSY.get_available, T, model.sys)) - variable = PSI.get_initial_condition_value(initial_conditions_data, S(), T) - rows, cols = size(variable) - @test rows * cols == no_component * PSI.INITIALIZATION_PROBLEM_HORIZON_COUNT -end - -function check_variable_count( - model, - ::S, - ::Type{T}, -) where {S <: PSI.VariableType, T <: PSY.Component} - no_component = length(PSY.get_components(PSY.get_available, T, model.sys)) - time_steps = PSI.get_time_steps(PSI.get_optimization_container(model))[end] - variable = PSI.get_variable(PSI.get_optimization_container(model), S(), T) - @test length(variable) == no_component * time_steps -end - -function check_initialization_constraint_count( - model, - ::S, - ::Type{T}; - filter_func = PSY.get_available, - meta = PSI.ISOPT.CONTAINER_KEY_EMPTY_META, -) where {S <: PSI.ConstraintType, T <: PSY.Component} - container = - ISOPT.get_initial_conditions_model_container(PSI.get_internal(model)) - no_component = length(PSY.get_components(filter_func, T, model.sys)) - time_steps = PSI.get_time_steps(container)[end] - constraint = PSI.get_constraint(container, S, T, meta) - @test length(constraint) == no_component * time_steps -end - -function check_constraint_count( - model, - ::S, - ::Type{T}; - filter_func = PSY.get_available, - meta = PSI.ISOPT.CONTAINER_KEY_EMPTY_META, -) where {S <: PSI.ConstraintType, T <: PSY.Component} - no_component = length(PSY.get_components(filter_func, T, model.sys)) - time_steps = PSI.get_time_steps(PSI.get_optimization_container(model))[end] - constraint = PSI.get_constraint(PSI.get_optimization_container(model), S(), T, meta) - @test length(constraint) == no_component * time_steps -end - -function check_constraint_count( - model, - ::PSI.RampConstraint, - ::Type{T}, -) where {T <: PSY.Component} - container = PSI.get_optimization_container(model) - device_name_set = - PSY.get_name.( - PSI._get_ramp_constraint_devices( - container, - get_components(PSY.get_available, T, model.sys), - ), - ) - check_constraint_count( - model, - PSI.RampConstraint, - T; - meta = "up", - filter_func = x -> x.name in device_name_set, - ) - check_constraint_count( - model, - PSI.RampConstraint, - T; - meta = "dn", - filter_func = x -> x.name in device_name_set, - ) - return -end - -function check_constraint_count( - model, - ::PSI.DurationConstraint, - ::Type{T}, -) where {T <: PSY.Component} - container = PSI.get_optimization_container(model) - resolution = PSI.get_resolution(container) - steps_per_hour = 60 / Dates.value(Dates.Minute(resolution)) - fraction_of_hour = 1 / steps_per_hour - duration_devices = filter!( - x -> !( - PSY.get_time_limits(x).up <= fraction_of_hour && - PSY.get_time_limits(x).down <= fraction_of_hour - ), - collect(get_components(PSY.get_available, T, model.sys)), - ) - device_name_set = PSY.get_name.(duration_devices) - check_constraint_count( - model, - PSI.DurationConstraint, - T; - meta = "up", - filter_func = x -> x.name in device_name_set, - ) - return check_constraint_count( - model, - PSI.DurationConstraint, - T; - meta = "dn", - filter_func = x -> x.name in device_name_set, - ) -end - -""" -Return a DataFrame from a CSV file. -""" -function read_dataframe(filename::AbstractString) - return CSV.read(filename, DataFrames.DataFrame) -end From 8464f7f3dce76fc453ea5ca396df28aa5b2146fb Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 20 Apr 2026 13:33:48 -0600 Subject: [PATCH 15/19] POM testing: call on sys.data, adjust mocks. format too --- src/InfrastructureOptimizationModels.jl | 2 +- src/common_models/add_constraint_dual.jl | 8 ++- src/common_models/add_param_container.jl | 6 +- src/common_models/duration_constraints.jl | 6 +- src/core/network_model.jl | 3 +- src/core/network_reductions.jl | 4 +- src/core/optimization_container.jl | 13 +++- src/core/optimization_problem_outputs.jl | 4 +- src/core/parameter_container.jl | 5 +- src/core/service_model.jl | 10 ++- src/core/settings.jl | 3 +- src/core/standard_variables_expressions.jl | 5 +- src/operation/decision_model.jl | 11 ++-- src/operation/emulation_model.jl | 7 +- src/operation/problem_outputs.jl | 4 +- src/quadratic_approximations/incremental.jl | 2 + src/utils/component_utils.jl | 3 +- src/utils/time_series_utils.jl | 10 ++- test/mocks/mock_system.jl | 71 +++++++++++++++------ 19 files changed, 127 insertions(+), 50 deletions(-) diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index d671be70..160ee8e3 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -338,7 +338,7 @@ export search_for_reduced_branch_parameter! export search_for_reduced_branch_argument! export get_branch_argument_parameter_axes export get_parameter_dict -export get_device_with_time_series +export get_branch_with_time_series # Container/variable helpers export add_variable_container!, add_constraint_dual! export add_to_objective_invariant_expression!, lazy_container_addition! diff --git a/src/common_models/add_constraint_dual.jl b/src/common_models/add_constraint_dual.jl index b30fc823..8770f377 100644 --- a/src/common_models/add_constraint_dual.jl +++ b/src/common_models/add_constraint_dual.jl @@ -69,7 +69,9 @@ function assign_dual_variable!( constraint_type::Type{<:ConstraintType}, devices::U, ::Type{<:AbstractDeviceFormulation}, -) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: IS.InfrastructureSystemsComponent} +) where { + U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, +} where {D <: IS.InfrastructureSystemsComponent} @assert !isempty(devices) time_steps = get_time_steps(container) add_dual_container!( @@ -88,7 +90,9 @@ function assign_dual_variable!( constraint_type::Type{<:ConstraintType}, devices::U, ::NetworkModel{<:AbstractPowerModel}, -) where {U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}} where {D <: IS.InfrastructureSystemsComponent} +) where { + U <: Union{Vector{D}, IS.FlattenIteratorWrapper{D}}, +} where {D <: IS.InfrastructureSystemsComponent} @assert !isempty(devices) time_steps = get_time_steps(container) add_dual_container!( diff --git a/src/common_models/add_param_container.jl b/src/common_models/add_param_container.jl index 37cfa052..f7522fe4 100644 --- a/src/common_models/add_param_container.jl +++ b/src/common_models/add_param_container.jl @@ -90,7 +90,11 @@ function add_param_container!( axs...; sparse = false, meta = CONTAINER_KEY_EMPTY_META, -) where {T <: EventParameter, U <: IS.InfrastructureSystemsComponent, V <: IS.InfrastructureSystemsComponent} +) where { + T <: EventParameter, + U <: IS.InfrastructureSystemsComponent, + V <: IS.InfrastructureSystemsComponent, +} param_key = ParameterKey(T, U, meta) attributes = EventParametersAttributes(V) return add_param_container_shared_axes!( diff --git a/src/common_models/duration_constraints.jl b/src/common_models/duration_constraints.jl index 9336b2c8..95259fd9 100644 --- a/src/common_models/duration_constraints.jl +++ b/src/common_models/duration_constraints.jl @@ -145,7 +145,11 @@ function device_duration_look_ahead!( ::Type{C_up}, ::Type{C_dn}, ::Type{T}, -) where {C_up <: ConstraintType, C_dn <: ConstraintType, T <: IS.InfrastructureSystemsComponent} +) where { + C_up <: ConstraintType, + C_dn <: ConstraintType, + T <: IS.InfrastructureSystemsComponent, +} time_steps = get_time_steps(container) varon = get_variable(container, OnVariable, T) varstart = get_variable(container, StartVariable, T) diff --git a/src/core/network_model.jl b/src/core/network_model.jl index 9e33d10c..928fa857 100644 --- a/src/core/network_model.jl +++ b/src/core/network_model.jl @@ -1,4 +1,5 @@ -const DeviceModelForBranches = DeviceModel{<:IS.InfrastructureSystemsComponent, <:AbstractDeviceFormulation} +const DeviceModelForBranches = + DeviceModel{<:IS.InfrastructureSystemsComponent, <:AbstractDeviceFormulation} const BranchModelContainer = Dict{Symbol, DeviceModelForBranches} function _check_pm_formulation(::Type{T}) where {T <: AbstractPowerModel} diff --git a/src/core/network_reductions.jl b/src/core/network_reductions.jl index 84432313..3cf2bede 100644 --- a/src/core/network_reductions.jl +++ b/src/core/network_reductions.jl @@ -150,7 +150,7 @@ Find the first device within a reduction entry that has the given time series. Delegates to PNM, which handles BranchesParallel, BranchesSeries, ThreeWindingTransformerWinding, and plain ACTransmission entries. """ -function get_device_with_time_series( +function get_branch_with_time_series( branch::IS.InfrastructureSystemsComponent, ::Type{V}, ts_name::String, @@ -171,7 +171,7 @@ function get_branch_argument_parameter_axes( for (name, (arc, reduction)) in arc_map reduction_entry = net_reduction_data.all_branch_maps_by_type[reduction][T][arc] device_with_time_series = - get_device_with_time_series(reduction_entry, V, ts_name) + get_branch_with_time_series(reduction_entry, V, ts_name) if device_with_time_series !== nothing push!(name_axis, name) push!( diff --git a/src/core/optimization_container.jl b/src/core/optimization_container.jl index 4bb352ac..5214d4e9 100644 --- a/src/core/optimization_container.jl +++ b/src/core/optimization_container.jl @@ -325,9 +325,13 @@ function init_optimization_container!( # NOTE: Simplified to avoid referencing concrete network model types (CopperPlatePowerModel, AreaBalancePowerModel) # PowerSimulations can implement more specific logic based on concrete types total_number_of_devices = - length(get_available_components(network_model, IS.InfrastructureSystemsComponent, sys)) + length( + get_available_components(network_model, IS.InfrastructureSystemsComponent, sys), + ) total_number_of_devices += - length(get_available_components(network_model, IS.InfrastructureSystemsComponent, sys)) + length( + get_available_components(network_model, IS.InfrastructureSystemsComponent, sys), + ) # The 10e6 limit is based on the sizes of the lp benchmark problems http://plato.asu.edu/ftp/lpcom.html # The maximum numbers of constraints and variables in the benchmark problems is 1,918,399 and 1,259,121, @@ -1288,7 +1292,10 @@ function _calculate_dual_variable_value!( container::OptimizationContainer, key::ConstraintKey{T, D}, ::IS.InfrastructureSystemsContainer, -) where {T <: ConstraintType, D <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} +) where { + T <: ConstraintType, + D <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}, +} constraint_duals = jump_value.(get_constraint(container, key)) dual_variable_container = get_duals(container)[key] diff --git a/src/core/optimization_problem_outputs.jl b/src/core/optimization_problem_outputs.jl index a4a93a8f..da4261ec 100644 --- a/src/core/optimization_problem_outputs.jl +++ b/src/core/optimization_problem_outputs.jl @@ -104,7 +104,9 @@ get_optimizer_stats(res::OptimizationProblemOutputs) = res.optimizer_stats get_parameter_values(res::OptimizationProblemOutputs) = res.parameter_values get_source_data(res::OptimizationProblemOutputs) = res.source_data -make_system_filename(sys::IS.InfrastructureSystemsContainer) = make_system_filename(IS.get_uuid(sys)) +# FIXME get_uuid declare as stub +make_system_filename(sys::IS.InfrastructureSystemsContainer) = + make_system_filename(IS.get_uuid(sys.data.internal)) make_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json" """ diff --git a/src/core/parameter_container.jl b/src/core/parameter_container.jl index d0a0ea39..d362c468 100644 --- a/src/core/parameter_container.jl +++ b/src/core/parameter_container.jl @@ -80,7 +80,10 @@ get_sos_status(attr::CostFunctionAttributes) = attr.sos_status get_variable_types(attr::CostFunctionAttributes) = attr.variable_types get_uses_compact_power(attr::CostFunctionAttributes) = attr.uses_compact_power -struct EventParametersAttributes{T <: IS.InfrastructureSystemsComponent, U <: ParameterType} <: ParameterAttributes +struct EventParametersAttributes{ + T <: IS.InfrastructureSystemsComponent, + U <: ParameterType, +} <: ParameterAttributes affected_devices::Vector{<:IS.InfrastructureSystemsComponent} end diff --git a/src/core/service_model.jl b/src/core/service_model.jl index 316eabb4..b122cf30 100644 --- a/src/core/service_model.jl +++ b/src/core/service_model.jl @@ -35,7 +35,10 @@ mutable struct ServiceModel{D <: IS.InfrastructureSystemsComponent, B} duals::Vector{DataType} time_series_names::Dict{Type{<:TimeSeriesParameter}, String} attributes::Dict{String, Any} - contributing_devices_map::Dict{Type{<:IS.InfrastructureSystemsComponent}, Vector{<:IS.InfrastructureSystemsComponent}} + contributing_devices_map::Dict{ + Type{<:IS.InfrastructureSystemsComponent}, + Vector{<:IS.InfrastructureSystemsComponent}, + } subsystem::Union{Nothing, String} function ServiceModel( ::Type{D}, @@ -46,7 +49,10 @@ mutable struct ServiceModel{D <: IS.InfrastructureSystemsComponent, B} duals = Vector{DataType}(), time_series_names = get_default_time_series_names(D, B), attributes = Dict{String, Any}(), - contributing_devices_map = Dict{Type{<:IS.InfrastructureSystemsComponent}, Vector{<:IS.InfrastructureSystemsComponent}}(), + contributing_devices_map = Dict{ + Type{<:IS.InfrastructureSystemsComponent}, + Vector{<:IS.InfrastructureSystemsComponent}, + }(), ) where {D <: IS.InfrastructureSystemsComponent, B} attributes_for_model = get_default_attributes(D, B) for (k, v) in attributes diff --git a/src/core/settings.jl b/src/core/settings.jl index d6a410cd..15e20c35 100644 --- a/src/core/settings.jl +++ b/src/core/settings.jl @@ -57,7 +57,8 @@ function Settings( store_variable_names = false, ext = Dict{String, Any}(), ) - if time_series_cache_size > 0 && stores_time_series_in_memory(sys) + # alternatively, declare as stub and implement in POM. + if time_series_cache_size > 0 && IS.stores_time_series_in_memory(sys.data) @info "Overriding time_series_cache_size because time series is stored in memory" time_series_cache_size = 0 end diff --git a/src/core/standard_variables_expressions.jl b/src/core/standard_variables_expressions.jl index f4f6d25a..23aa9536 100644 --- a/src/core/standard_variables_expressions.jl +++ b/src/core/standard_variables_expressions.jl @@ -84,7 +84,10 @@ function add_expression_container!( axs...; sparse = false, meta = CONTAINER_KEY_EMPTY_META, -) where {T <: ProductionCostExpression, U <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}} +) where { + T <: ProductionCostExpression, + U <: Union{IS.InfrastructureSystemsComponent, IS.InfrastructureSystemsContainer}, +} expr_container = _add_container!(container, T, U, JuMP.QuadExpr, sparse, axs...; meta = meta) remove_undef!(expr_container) diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 45325af6..7ba7d453 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -233,10 +233,11 @@ function init_model_store_params!(model::DecisionModel) num_executions = get_executions(model) horizon = get_horizon(model) system = get_system(model) - interval = IS.get_forecast_interval(system) + interval = IS.get_forecast_interval(system.data) resolution = get_resolution(model) base_power = get_base_power(system) - sys_uuid = IS.get_uuid(system) + # FIXME declare as stub + sys_uuid = IS.get_uuid(system.data.internal) store_params = ModelStoreParams( num_executions, horizon, @@ -253,7 +254,7 @@ end function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem}) sys = get_system(model) settings = get_settings(model) - available_resolutions = IS.get_time_series_resolutions(sys) + available_resolutions = IS.get_time_series_resolutions(sys.data) if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 throw( @@ -274,10 +275,10 @@ function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem}) end if get_horizon(settings) == UNSET_HORIZON - set_horizon!(settings, IS.get_forecast_horizon(sys)) + set_horizon!(settings, IS.get_forecast_horizon(sys.data)) end - counts = IS.get_time_series_counts(sys) + counts = IS.get_time_series_counts(sys.data) if counts.forecast_count < 1 error( "The system does not contain forecast data. A DecisionModel can't be built.", diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl index 5045fe3b..33b04284 100644 --- a/src/operation/emulation_model.jl +++ b/src/operation/emulation_model.jl @@ -211,7 +211,7 @@ end function validate_time_series!(model::EmulationModel{<:DefaultEmulationProblem}) sys = get_system(model) settings = get_settings(model) - available_resolutions = IS.get_time_series_resolutions(sys) + available_resolutions = IS.get_time_series_resolutions(sys.data) if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 throw( @@ -236,7 +236,7 @@ function validate_time_series!(model::EmulationModel{<:DefaultEmulationProblem}) set_horizon!(settings, get_resolution(settings)) end - counts = IS.get_time_series_counts(sys) + counts = IS.get_time_series_counts(sys.data) if counts.static_time_series_count < 1 error( "The system does not contain Static Time Series data. A EmulationModel can't be built.", @@ -258,7 +258,8 @@ function init_model_store_params!(model::EmulationModel) settings = get_settings(model) horizon = interval = resolution = get_resolution(settings) base_power = get_base_power(system) - sys_uuid = IS.get_uuid(system) + # FIXME declare as stub + sys_uuid = IS.get_uuid(system.data.internal) set_store_params!( get_internal(model), ModelStoreParams( diff --git a/src/operation/problem_outputs.jl b/src/operation/problem_outputs.jl index 81d2dd62..c4a70d35 100644 --- a/src/operation/problem_outputs.jl +++ b/src/operation/problem_outputs.jl @@ -30,7 +30,7 @@ function OptimizationProblemOutputs(model::DecisionModel) get_problem_base_power(model), timestamps, sys, - IS.get_uuid(sys), + get_uuid(sys), aux_variable_values, variable_values, dual_values, @@ -73,7 +73,7 @@ function OptimizationProblemOutputs(model::EmulationModel) get_problem_base_power(model), StepRange(initial_time, get_resolution(model), initial_time), sys, - IS.get_uuid(sys), + get_uuid(sys), aux_variables, variables, duals, diff --git a/src/quadratic_approximations/incremental.jl b/src/quadratic_approximations/incremental.jl index 913cba9d..a8fec875 100644 --- a/src/quadratic_approximations/incremental.jl +++ b/src/quadratic_approximations/incremental.jl @@ -166,6 +166,8 @@ function _add_generic_incremental_interpolation_constraint!( # Retrieve all required variables from the optimization container # Retrieve original variable for DCVoltage from the Bus x_var = if (R <: DCVoltage) + # FIXME component type cannot be abstract. + # why is this branch needed in the first place? Why not just pass get_variable(container, R, IS.InfrastructureSystemsComponent) # Original variable (domain of function) else get_variable(container, R, W) # Original variable (domain of function) diff --git a/src/utils/component_utils.jl b/src/utils/component_utils.jl index 6a4bd77d..db218276 100644 --- a/src/utils/component_utils.jl +++ b/src/utils/component_utils.jl @@ -50,9 +50,10 @@ function get_available_components( sys::IS.InfrastructureSystemsContainer, ) where {T <: IS.InfrastructureSystemsComponent} subsystem = get_subsystem(model) + # FIXME have to patch thru to sys.data here return IS.get_components( T, - sys; + sys.data; subsystem_name = subsystem, ) end diff --git a/src/utils/time_series_utils.jl b/src/utils/time_series_utils.jl index 7ebea046..05e26cbd 100644 --- a/src/utils/time_series_utils.jl +++ b/src/utils/time_series_utils.jl @@ -29,7 +29,8 @@ apply_maybe_across_time_series( apply_maybe_across_time_series(fn, component, IS.get_time_series_key(tts)) # case where the element isn't a time series -apply_maybe_across_time_series(fn::Function, ::IS.InfrastructureSystemsComponent, elem) = fn(elem) +apply_maybe_across_time_series(fn::Function, ::IS.InfrastructureSystemsComponent, elem) = + fn(elem) # success case _validate_eltype_helper(::Type{T}, element::T) where {T} = true @@ -57,7 +58,12 @@ _validate_eltype( ) end -function _validate_eltype(::Type{T}, component::IS.InfrastructureSystemsComponent, element, msg = "") where {T} +function _validate_eltype( + ::Type{T}, + component::IS.InfrastructureSystemsComponent, + element, + msg = "", +) where {T} component_name = get_name(component) output = _validate_eltype_helper(T, element) output || throw( diff --git a/test/mocks/mock_system.jl b/test/mocks/mock_system.jl index 74b34a88..2e375e8e 100644 --- a/test/mocks/mock_system.jl +++ b/test/mocks/mock_system.jl @@ -3,39 +3,70 @@ Minimal mock for PSY.System. Implements only the interface required by OptimizationContainer and models. """ +#= +function stores_time_series_in_memory end +function get_time_series_counts_by_type end +function get_time_series_counts end +function get_forecast_interval end +function get_time_series_resolutions end +function get_forecast_horizon end +function get_uuid end +=# + using InfrastructureSystems const IS = InfrastructureSystems -mutable struct MockSystem <: IS.InfrastructureSystemsContainer +mutable struct MockSystemData base_power::Float64 components::Dict{DataType, Vector{Any}} time_series::Dict{Any, Any} stores_in_memory::Bool + internal::IS.InfrastructureSystemsInternal end -# Convenience constructors -MockSystem() = MockSystem(100.0, Dict{DataType, Vector{Any}}(), Dict{Any, Any}(), false) -MockSystem(base_power::Float64) = - MockSystem(base_power, Dict{DataType, Vector{Any}}(), Dict{Any, Any}(), false) +mutable struct MockSystem <: IS.InfrastructureSystemsContainer + data::MockSystemData +end + +MockSystem() = MockSystem(MockSystemData()) +MockSystem(base_power::Float64) = MockSystem(MockSystemData(base_power)) MockSystem(base_power::Float64, stores_in_memory::Bool) = - MockSystem( - base_power, - Dict{DataType, Vector{Any}}(), - Dict{Any, Any}(), - stores_in_memory, - ) + MockSystem(MockSystemData(base_power, stores_in_memory)) + +# Convenience constructors +MockSystemData() = MockSystemData( + 100.0, + Dict{DataType, Vector{Any}}(), + Dict{Any, Any}(), + false, + IS.InfrastructureSystemsInternal(), +) +MockSystemData(base_power::Float64) = MockSystemData( + base_power, + Dict{DataType, Vector{Any}}(), + Dict{Any, Any}(), + false, + IS.InfrastructureSystemsInternal(), +) +MockSystemData(base_power::Float64, stores_in_memory::Bool) = MockSystemData( + base_power, + Dict{DataType, Vector{Any}}(), + Dict{Any, Any}(), + stores_in_memory, + IS.InfrastructureSystemsInternal(), +) # Required interface methods - extend InfrastructureOptimizationModels functions for duck-typing -InfrastructureOptimizationModels.get_base_power(sys::MockSystem) = sys.base_power -InfrastructureOptimizationModels.stores_time_series_in_memory(sys::MockSystem) = - sys.stores_in_memory +IOM.get_base_power(sys::MockSystem) = sys.data.base_power +IOM.stores_time_series_in_memory(sys::MockSystem) = sys.data.stores_in_memory +IOM.stores_time_series_in_memory(data::MockSystemData) = data.stores_in_memory function IOM.get_available_components(::NetworkModel, ::Type{T}, sys::MockSystem) where {T} return get_components(T, sys) end function get_components(::Type{T}, sys::MockSystem) where {T} - return get(sys.components, T, T[]) + return get(sys.data.components, T, T[]) end function get_component(::Type{T}, sys::MockSystem, name::String) where {T} @@ -49,10 +80,10 @@ end function add_component!(sys::MockSystem, component) comp_type = typeof(component) - if !haskey(sys.components, comp_type) - sys.components[comp_type] = [] + if !haskey(sys.data.components, comp_type) + sys.data.components[comp_type] = [] end - push!(sys.components[comp_type], component) + push!(sys.data.components[comp_type], component) return end @@ -63,10 +94,10 @@ function get_time_series( args...; kwargs..., ) where {T} - return get(sys.time_series, (T, component), nothing) + return get(sys.data.time_series, (T, component), nothing) end function add_time_series!(sys::MockSystem, component, ts) - sys.time_series[(typeof(ts), component)] = ts + sys.data.time_series[(typeof(ts), component)] = ts return end From 83e523b6f9d68641c396bb172f3aa21026bbedff Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 20 Apr 2026 15:33:20 -0600 Subject: [PATCH 16/19] `is_time_variant_proportional` rename, fix --- docs/src/explanation/time_varying_objective_functions.md | 2 +- src/common_models/interfaces.jl | 2 +- src/objective_function/proportional.jl | 4 ++-- src/utils/component_utils.jl | 9 ++------- test/test_proportional.jl | 4 ++-- 5 files changed, 8 insertions(+), 13 deletions(-) diff --git a/docs/src/explanation/time_varying_objective_functions.md b/docs/src/explanation/time_varying_objective_functions.md index f53a99e8..29b1c732 100644 --- a/docs/src/explanation/time_varying_objective_functions.md +++ b/docs/src/explanation/time_varying_objective_functions.md @@ -270,7 +270,7 @@ implements: | Validate device-specific constraints | Generic validation | Device-specific overloads (e.g., multi-start units) | | Populate parameter containers | — | `add_parameters!` implementations | | Read parameters during objective build | `add_pwl_term!`, `add_cost_term_variant!` | — | -| Determine if cost is time-varying | — | `is_time_variant_term` implementations | +| Determine if cost is time-varying | — | `is_time_variant_proportional` implementations | | Extract proportional cost per step | — | `proportional_cost` implementations | ```mermaid diff --git a/src/common_models/interfaces.jl b/src/common_models/interfaces.jl index 8e5c9ef5..749a43a6 100644 --- a/src/common_models/interfaces.jl +++ b/src/common_models/interfaces.jl @@ -135,7 +135,7 @@ end Extension point: Check if proportional cost term is time-variant. Returns true if the cost should be added to the variant objective expression. """ -is_time_variant_term(::IS.DeviceParameter) = false +is_time_variant_proportional(::IS.DeviceParameter) = false # corresponds to get_must_run for thermals, but avoiding device specific code here. """ diff --git a/src/objective_function/proportional.jl b/src/objective_function/proportional.jl index 8ed45497..0860cebf 100644 --- a/src/objective_function/proportional.jl +++ b/src/objective_function/proportional.jl @@ -61,8 +61,8 @@ function add_proportional_cost_maybe_time_variant!( for d in devices op_cost_data = get_operation_cost(d) name = get_name(d) - # is_time_variant_term depends only on typeof(op_cost_data); hoist out of the time loop. - add_as_time_variant = is_time_variant_term(op_cost_data) + # is_time_variant_proportional depends only on op_cost_data; hoist out of the time loop. + add_as_time_variant = is_time_variant_proportional(op_cost_data) skip = skip_proportional_cost(d) for t in get_time_steps(container) cost_term = proportional_cost(container, op_cost_data, U, d, V, t) diff --git a/src/utils/component_utils.jl b/src/utils/component_utils.jl index db218276..c5098973 100644 --- a/src/utils/component_utils.jl +++ b/src/utils/component_utils.jl @@ -286,10 +286,5 @@ function _get_piecewise_curve_per_system_unit( return x_coords_normalized, y_coords_normalized end -is_time_variant(::IS.TimeSeriesKey) = true -is_time_variant(::IS.ValueCurve{<:IS.TimeSeriesFunctionData}) = true -is_time_variant( - ::IS.ProductionVariableCostCurve{<:IS.ValueCurve{<:IS.TimeSeriesFunctionData}}, -) = true -is_time_variant(::IS.TupleTimeSeries) = true -is_time_variant(::Any) = false +# Alias for IS.is_time_series_backed — kept as IOM-level name for historical call sites. +is_time_variant(x) = IS.is_time_series_backed(x) diff --git a/test/test_proportional.jl b/test/test_proportional.jl index 93cb9cc2..51e7de50 100644 --- a/test/test_proportional.jl +++ b/test/test_proportional.jl @@ -36,8 +36,8 @@ InfrastructureOptimizationModels.proportional_cost( ::Int, ) = op_cost.proportional_term -# is_time_variant_term: return the is_time_variant flag from MockOperationCost -InfrastructureOptimizationModels.is_time_variant_term(op_cost::MockOperationCost) = +# is_time_variant_proportional: return the is_time_variant flag from MockOperationCost +InfrastructureOptimizationModels.is_time_variant_proportional(op_cost::MockOperationCost) = op_cost.is_time_variant # Helper to set up container with variables for devices From a187d24a8293c59a563251cd30c968226064c9af Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Mon, 20 Apr 2026 16:01:28 -0600 Subject: [PATCH 17/19] remove special case, still fails --- src/quadratic_approximations/incremental.jl | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/src/quadratic_approximations/incremental.jl b/src/quadratic_approximations/incremental.jl index a8fec875..b98a2b1f 100644 --- a/src/quadratic_approximations/incremental.jl +++ b/src/quadratic_approximations/incremental.jl @@ -165,13 +165,7 @@ function _add_generic_incremental_interpolation_constraint!( # Retrieve all required variables from the optimization container # Retrieve original variable for DCVoltage from the Bus - x_var = if (R <: DCVoltage) - # FIXME component type cannot be abstract. - # why is this branch needed in the first place? Why not just pass - get_variable(container, R, IS.InfrastructureSystemsComponent) # Original variable (domain of function) - else - get_variable(container, R, W) # Original variable (domain of function) - end # Original variable (domain of function) + x_var = get_variable(container, R, W) # Original variable (domain of function) y_var = get_variable(container, S, W) # Approximated variable (range of function) δ_var = get_variable(container, T, W) # Interpolation variables (weights for segments) z_var = get_variable(container, U, W) # Binary variables (ordering constraints) From 2f3741d4f46e644749457a30a417cfd6a0206ca0 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Wed, 22 Apr 2026 00:19:03 -0700 Subject: [PATCH 18/19] be smarted about the settings --- src/operation/decision_model.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 066c2702..e4de6b0c 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -303,10 +303,11 @@ function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem}) ) end end - interval_kwarg = - model_interval == UNSET_INTERVAL ? (;) : (; interval = model_interval) if get_horizon(settings) == UNSET_HORIZON - set_horizon!(settings, IS.get_forecast_horizon(sys.data; interval_kwarg...)) + set_horizon!( + settings, + IS.get_forecast_horizon(sys.data; interval = _to_is_interval(model_interval)), + ) end counts = IS.get_time_series_counts(sys.data) From c67dddff5b41371c4eadd7f2d251020da2b1e8a0 Mon Sep 17 00:00:00 2001 From: Luke Kiernan Date: Wed, 22 Apr 2026 10:23:25 -0600 Subject: [PATCH 19/19] CoPilot code review --- src/core/definitions.jl | 1 + src/core/optimization_container.jl | 6 +----- src/operation/decision_model.jl | 3 ++- src/quadratic_approximations/incremental.jl | 1 + 4 files changed, 5 insertions(+), 6 deletions(-) diff --git a/src/core/definitions.jl b/src/core/definitions.jl index 416beb23..fe33e6d0 100644 --- a/src/core/definitions.jl +++ b/src/core/definitions.jl @@ -92,6 +92,7 @@ const UNSET_HORIZON = Dates.Millisecond(0) const UNSET_RESOLUTION = Dates.Millisecond(0) const UNSET_INTERVAL = Dates.Millisecond(0) const UNSET_INI_TIME = Dates.DateTime(0) +const UNSET_FORECAST_INI_TIME = Dates.DateTime(1970) # Tolerance of comparisons # MIP gap tolerances in most solvers are set to 1e-4 diff --git a/src/core/optimization_container.jl b/src/core/optimization_container.jl index c4531797..7dec468b 100644 --- a/src/core/optimization_container.jl +++ b/src/core/optimization_container.jl @@ -292,7 +292,7 @@ end # implementations (e.g. PSY.System in POM) should add their own methods. temp_set_units_base_system!(::IS.InfrastructureSystemsContainer, ::String) = nothing temp_get_forecast_initial_timestamp(::IS.InfrastructureSystemsContainer) = - Dates.DateTime(1970) + UNSET_FORECAST_INI_TIME function init_optimization_container!( container::OptimizationContainer, @@ -328,10 +328,6 @@ function init_optimization_container!( length( get_available_components(network_model, IS.InfrastructureSystemsComponent, sys), ) - total_number_of_devices += - length( - get_available_components(network_model, IS.InfrastructureSystemsComponent, sys), - ) # The 10e6 limit is based on the sizes of the lp benchmark problems http://plato.asu.edu/ftp/lpcom.html # The maximum numbers of constraints and variables in the benchmark problems is 1,918,399 and 1,259,121, diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index e4de6b0c..854a5f8a 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -1,7 +1,8 @@ function get_deterministic_time_series_type(sys::IS.InfrastructureSystemsContainer) time_series_types = IS.get_time_series_counts_by_type(sys.data) existing_types = Set(d["type"] for d in time_series_types) - if Set(["Deterministic", "DeterministicSingleTimeSeries"]) ∈ existing_types + if ("Deterministic" in existing_types) && + ("DeterministicSingleTimeSeries" in existing_types) error( "The System contains a combination of forecast data and transformed time series data. Currently this is not supported.", ) diff --git a/src/quadratic_approximations/incremental.jl b/src/quadratic_approximations/incremental.jl index b98a2b1f..01f31e0a 100644 --- a/src/quadratic_approximations/incremental.jl +++ b/src/quadratic_approximations/incremental.jl @@ -165,6 +165,7 @@ function _add_generic_incremental_interpolation_constraint!( # Retrieve all required variables from the optimization container # Retrieve original variable for DCVoltage from the Bus + # FIXME edge case: DCVoltage. W is InterconnectingConverter but key says DCBus. x_var = get_variable(container, R, W) # Original variable (domain of function) y_var = get_variable(container, S, W) # Approximated variable (range of function) δ_var = get_variable(container, T, W) # Interpolation variables (weights for segments)