diff --git a/src/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index 4e11e34b..1fa5f860 100644 --- a/src/InfrastructureOptimizationModels.jl +++ b/src/InfrastructureOptimizationModels.jl @@ -348,7 +348,7 @@ export add_sparse_pwl_interpolation_variables! export JuMPOrFloat # Constraint helpers export add_range_constraints!, add_parameterized_upper_bound_range_constraints -export add_reserve_bound_range_constraints! +export add_reserve_bound_range_constraints!, add_commitment_bound_range_constraints! export add_semicontinuous_range_constraints!, add_semicontinuous_ramp_constraints! # Cost helpers export add_shut_down_cost!, add_start_up_cost! diff --git a/src/common_models/add_constraint_dual.jl b/src/common_models/add_constraint_dual.jl index 643d9e58..fa56fc3e 100644 --- a/src/common_models/add_constraint_dual.jl +++ b/src/common_models/add_constraint_dual.jl @@ -20,7 +20,8 @@ function add_constraint_dual!( model::NetworkModel{T}, ) where {T <: AbstractPowerModel} if !isempty(get_duals(model)) - devices = get_available_components(model, IS.InfrastructureSystemsComponent, sys) + # component is ACBus, but we don't have PSY as a dependency. + devices = get_available_components(model, component_for_network_dual(nothing), sys) for constraint_type in get_duals(model) assign_dual_variable!(container, constraint_type, devices, model) end diff --git a/src/common_models/interfaces.jl b/src/common_models/interfaces.jl index 749a43a6..1fa1ad88 100644 --- a/src/common_models/interfaces.jl +++ b/src/common_models/interfaces.jl @@ -145,6 +145,40 @@ For thermals, equivalent to `get_must_run`, but that implementation belongs in P """ skip_proportional_cost(d::IS.InfrastructureSystemsComponent) = false +############################### +#### System query stubs ####### +############################### +# Extension points for querying a system object. POM provides methods for +# PSY.System; tests provide methods for MockSystem. IOM itself never accesses +# sys.data. + +"Extension point: time-series resolutions available on the system." +function get_time_series_resolutions end + +"Extension point: counts summary of time series on the system." +function get_time_series_counts end + +"Extension point: counts by component type of time series on the system." +function get_time_series_counts_by_type end + +"Extension point: forecast interval configured on the system." +function get_forecast_interval end + +"Extension point: forecast horizon configured on the system." +function get_forecast_horizon end + +"Extension point: summary table of forecasts on the system." +function get_forecast_summary_table end + +"Extension point: transform single time series into deterministic forecasts on the system." +function transform_single_time_series! end + +"Extension point: stable UUID for the system (used as a filename identifier)." +function get_system_uuid end + +"Extension point: get components of type `T` in a subsystem of the system." +function get_subsystem_components end + ############################### ###### Start-up Cost ########## ############################### @@ -245,3 +279,15 @@ Only called in `emulation_model.jl`: that file's contents and this function shou likely be moved to POM or PSI. """ function update_container_parameter_values! end + +""" +Component type associated with network duals. Returns ACBus when passed in `nothing`. +Only called in a single spot in `add_constraint_dual!` for the network model. +""" +function component_for_network_dual end + +""" +Component type associated with hvdc interpolation constraints. Returns DCBus when passed in +`nothing`. Only called in a single spot in `incremental.jl`. +""" +function component_for_hvdc_interpolation end diff --git a/src/common_models/range_constraint.jl b/src/common_models/range_constraint.jl index 2b10439a..8a141acf 100644 --- a/src/common_models/range_constraint.jl +++ b/src/common_models/range_constraint.jl @@ -195,14 +195,18 @@ function add_semicontinuous_range_constraints!( return end -# Generic component version - always uses binary variable +# Generic component version - always uses binary variable. +# `meta_suffix` is appended to the default constraint meta so callers can stack a second +# OnVariable-keyed bound alongside another bound constraint (e.g. a reservation-keyed +# one) on the same `(T, V)` without a meta collision — see `add_commitment_bound_range_constraints!`. function _add_semicontinuous_bound_range_constraints_impl!( container::OptimizationContainer, ::Type{T}, dir::BoundDirection, array, devices::Union{Vector{V}, IS.FlattenIteratorWrapper{V}}, - ::DeviceModel{V, W}, + ::DeviceModel{V, W}; + meta_suffix::String = "", ) where { T <: ConstraintType, V <: IS.InfrastructureSystemsComponent, @@ -212,7 +216,7 @@ function _add_semicontinuous_bound_range_constraints_impl!( 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)) + container, T, V, names, time_steps; meta = constraint_meta(dir) * meta_suffix) varbin = get_variable(container, OnVariable, V) for device in devices, t in time_steps @@ -225,6 +229,21 @@ function _add_semicontinuous_bound_range_constraints_impl!( return end +# Exported wrapper: use this from downstream packages to add an OnVariable-keyed bound +# alongside another bound constraint on the same `(T, V)` key — pass `meta_suffix = "_aux"` +# (or similar) to avoid colliding with the default "lb"/"ub" meta. +add_commitment_bound_range_constraints!( + container::OptimizationContainer, + ::Type{T}, + dir::BoundDirection, + array, + devices, + model::DeviceModel; + meta_suffix::String = "", +) where {T <: ConstraintType} = + _add_semicontinuous_bound_range_constraints_impl!( + container, T, dir, array, devices, model; meta_suffix) + # Unified reserve range constraints impl # invert_binary: true for InputActivePower (uses 1-varbin), false for others (uses varbin) function add_reserve_bound_range_constraints!( diff --git a/src/core/optimization_container.jl b/src/core/optimization_container.jl index 7dec468b..36f1979b 100644 --- a/src/core/optimization_container.jl +++ b/src/core/optimization_container.jl @@ -309,7 +309,7 @@ function init_optimization_container!( # 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) <: IS.SingleTimeSeries - ini_time, _ = IS.check_time_series_consistency(sys, IS.SingleTimeSeries) + ini_time, _ = IS.check_time_series_consistency(sys.data, IS.SingleTimeSeries) set_initial_time!(settings, ini_time) end end diff --git a/src/core/optimization_problem_outputs.jl b/src/core/optimization_problem_outputs.jl index da4261ec..2db94f1d 100644 --- a/src/core/optimization_problem_outputs.jl +++ b/src/core/optimization_problem_outputs.jl @@ -104,9 +104,8 @@ get_optimizer_stats(res::OptimizationProblemOutputs) = res.optimizer_stats get_parameter_values(res::OptimizationProblemOutputs) = res.parameter_values get_source_data(res::OptimizationProblemOutputs) = res.source_data -# FIXME get_uuid declare as stub make_system_filename(sys::IS.InfrastructureSystemsContainer) = - make_system_filename(IS.get_uuid(sys.data.internal)) + make_system_filename(get_system_uuid(sys)) make_system_filename(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json" """ diff --git a/src/core/settings.jl b/src/core/settings.jl index 37f9be9a..58c38c15 100644 --- a/src/core/settings.jl +++ b/src/core/settings.jl @@ -59,8 +59,7 @@ function Settings( store_variable_names = false, ext = Dict{String, Any}(), ) - # alternatively, declare as stub and implement in POM. - if time_series_cache_size > 0 && IS.stores_time_series_in_memory(sys.data) + if time_series_cache_size > 0 && stores_time_series_in_memory(sys) @info "Overriding time_series_cache_size because time series is stored in memory" time_series_cache_size = 0 end diff --git a/src/objective_function/proportional.jl b/src/objective_function/proportional.jl index 0860cebf..0abb7cae 100644 --- a/src/objective_function/proportional.jl +++ b/src/objective_function/proportional.jl @@ -5,8 +5,9 @@ # this is only used for ControllableLoads with non-PowerLoadInterruptible formulations. # The rest go through a thin wrapper around the maybe-variant version. """ -Default implementation for proportional cost, where the cost term is not time variant. Anything -time-varying should implement its own method. +Default implementation for proportional cost, where the cost term is not time variant. +See also: `add_proportional_cost_maybe_time_variant!` for a common basis for devices that +might have time-variant proportional costs. """ function add_proportional_cost!( container::OptimizationContainer, @@ -18,7 +19,6 @@ function add_proportional_cost!( U <: VariableType, V <: AbstractDeviceFormulation, } - # NOTE: anything time-varying should implement its own method. multiplier = objective_function_multiplier(U, V) for d in devices op_cost_data = get_operation_cost(d) @@ -26,17 +26,19 @@ function add_proportional_cost!( iszero(cost_term) && continue name = get_name(d) rate = cost_term * multiplier + skip = skip_proportional_cost(d) 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, - ) + if skip + # must-run etc.: bookkeep in ProductionCostExpression but not in objective + add_cost_to_expression!( + container, ProductionCostExpression, rate, T, name, t) + else + variable = get_variable(container, U, T)[name, t] + add_cost_term_invariant!( + container, variable, rate, + ProductionCostExpression, T, name, t, + ) + end end end return diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 854a5f8a..5585c422 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -1,5 +1,5 @@ function get_deterministic_time_series_type(sys::IS.InfrastructureSystemsContainer) - time_series_types = IS.get_time_series_counts_by_type(sys.data) + time_series_types = get_time_series_counts_by_type(sys) existing_types = Set(d["type"] for d in time_series_types) if ("Deterministic" in existing_types) && ("DeterministicSingleTimeSeries" in existing_types) @@ -242,12 +242,11 @@ function init_model_store_params!(model::DecisionModel) if model_interval != UNSET_INTERVAL interval = model_interval else - interval = IS.get_forecast_interval(system.data) + interval = get_forecast_interval(system) end resolution = get_resolution(model) base_power = get_base_power(system) - # FIXME declare as stub - sys_uuid = IS.get_uuid(system.data.internal) + sys_uuid = get_system_uuid(system) store_params = ModelStoreParams( num_executions, horizon, @@ -264,7 +263,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.data) + available_resolutions = get_time_series_resolutions(sys) if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 throw( @@ -307,11 +306,11 @@ function validate_time_series!(model::DecisionModel{<:DefaultDecisionProblem}) if get_horizon(settings) == UNSET_HORIZON set_horizon!( settings, - IS.get_forecast_horizon(sys.data; interval = _to_is_interval(model_interval)), + get_forecast_horizon(sys; interval = _to_is_interval(model_interval)), ) end - counts = IS.get_time_series_counts(sys.data) + counts = 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 33b04284..1898e5b0 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.data) + available_resolutions = 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 = IS.get_time_series_counts(sys.data) + counts = 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.", @@ -258,8 +258,7 @@ function init_model_store_params!(model::EmulationModel) settings = get_settings(model) horizon = interval = resolution = get_resolution(settings) base_power = get_base_power(system) - # FIXME declare as stub - sys_uuid = IS.get_uuid(system.data.internal) + sys_uuid = get_system_uuid(system) set_store_params!( get_internal(model), ModelStoreParams( diff --git a/src/quadratic_approximations/incremental.jl b/src/quadratic_approximations/incremental.jl index 01f31e0a..527256ca 100644 --- a/src/quadratic_approximations/incremental.jl +++ b/src/quadratic_approximations/incremental.jl @@ -165,8 +165,12 @@ 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) + if R <: DCVoltage + # workaround for the fact that we can't write PSY.DCBus. + x_var = get_variable(container, R, component_for_hvdc_interpolation(nothing)) + else + x_var = get_variable(container, R, W) # Original variable (domain of function) + end 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) diff --git a/src/utils/component_utils.jl b/src/utils/component_utils.jl index 1d240e6b..6dba8400 100644 --- a/src/utils/component_utils.jl +++ b/src/utils/component_utils.jl @@ -66,12 +66,7 @@ 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.data; - subsystem_name = subsystem, - ) + return get_subsystem_components(T, sys; subsystem_name = subsystem) end ################################################## @@ -305,7 +300,7 @@ end is_time_variant(x) = IS.is_time_series_backed(x) function get_forecast_intervals(sys::IS.InfrastructureSystemsContainer) - table = IS.get_forecast_summary_table(sys.data) + table = get_forecast_summary_table(sys) return Set(row.interval for row in eachrow(table) if row.interval !== nothing) end @@ -319,7 +314,7 @@ function auto_transform_time_series!( return end - counts = IS.get_time_series_counts(sys.data) + counts = get_time_series_counts(sys) if counts.static_time_series_count < 1 return end @@ -333,9 +328,8 @@ function auto_transform_time_series!( @info "Auto-transforming SingleTimeSeries to DeterministicSingleTimeSeries" horizon = Dates.canonicalize(model_horizon) interval = Dates.canonicalize(model_interval) - IS.transform_single_time_series!( - sys.data, - IS.DeterministicSingleTimeSeries, + transform_single_time_series!( + sys, model_horizon, model_interval; delete_existing = false, diff --git a/test/mocks/mock_system.jl b/test/mocks/mock_system.jl index 2e375e8e..74b34a88 100644 --- a/test/mocks/mock_system.jl +++ b/test/mocks/mock_system.jl @@ -3,70 +3,39 @@ 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 MockSystemData +mutable struct MockSystem <: IS.InfrastructureSystemsContainer base_power::Float64 components::Dict{DataType, Vector{Any}} time_series::Dict{Any, Any} stores_in_memory::Bool - internal::IS.InfrastructureSystemsInternal -end - -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(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(), -) +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) +MockSystem(base_power::Float64, stores_in_memory::Bool) = + MockSystem( + base_power, + Dict{DataType, Vector{Any}}(), + Dict{Any, Any}(), + stores_in_memory, + ) # Required interface methods - extend InfrastructureOptimizationModels functions for duck-typing -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 +InfrastructureOptimizationModels.get_base_power(sys::MockSystem) = sys.base_power +InfrastructureOptimizationModels.stores_time_series_in_memory(sys::MockSystem) = + sys.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.data.components, T, T[]) + return get(sys.components, T, T[]) end function get_component(::Type{T}, sys::MockSystem, name::String) where {T} @@ -80,10 +49,10 @@ end function add_component!(sys::MockSystem, component) comp_type = typeof(component) - if !haskey(sys.data.components, comp_type) - sys.data.components[comp_type] = [] + if !haskey(sys.components, comp_type) + sys.components[comp_type] = [] end - push!(sys.data.components[comp_type], component) + push!(sys.components[comp_type], component) return end @@ -94,10 +63,10 @@ function get_time_series( args...; kwargs..., ) where {T} - return get(sys.data.time_series, (T, component), nothing) + return get(sys.time_series, (T, component), nothing) end function add_time_series!(sys::MockSystem, component, ts) - sys.data.time_series[(typeof(ts), component)] = ts + sys.time_series[(typeof(ts), component)] = ts return end