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 8b0bd2b2..4419271f 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/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/InfrastructureOptimizationModels.jl b/src/InfrastructureOptimizationModels.jl index cf700fd4..4e11e34b 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 @@ -168,7 +174,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! @@ -225,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 @@ -334,13 +338,12 @@ 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! 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 @@ -362,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 @@ -377,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) @@ -586,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") @@ -644,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 a09bd1ef..643d9e58 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,18 +69,20 @@ 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) metas = _existing_constraint_metas(container, constraint_type, D) if isempty(metas) - device_names = PSY.get_name.(devices) + device_names = IS.get_name.(devices) add_dual_container!(container, constraint_type, D, device_names, time_steps) else # Reuse the existing constraint container's row axis so the dual axis # matches the constraint exactly. Network reductions (radial / # degree-two) drop branches that pass the device-model filter, so the - # constraint axis is a strict subset of PSY.get_name.(devices). Sizing + # constraint axis is a strict subset of IS.get_name.(devices). Sizing # the dual from the device list would leave the dual broadcast in # process_duals incompatible with the constraint matrix. for meta in metas @@ -121,14 +123,16 @@ 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..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 <: 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..95259fd9 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,11 @@ 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 +247,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 +369,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 b7d74940..b7fc6410 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}; interval::Dates.Millisecond = UNSET_INTERVAL, ) where {T <: IS.TimeSeriesData} @@ -20,7 +20,7 @@ function get_time_series( ::Type{P}, meta = CONTAINER_KEY_EMPTY_META; interval::Dates.Millisecond = UNSET_INTERVAL, -) 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, @@ -34,7 +34,7 @@ end # refactor is done. function get_time_series( container::OptimizationContainer, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, forecast_name::String; interval::Dates.Millisecond = UNSET_INTERVAL, ) diff --git a/src/common_models/interfaces.jl b/src/common_models/interfaces.jl index 4cc283dc..749a43a6 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,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, - ::Type{<:VariableType}, - ::Type{<:IS.InfrastructureSystemsComponent}, - ::Type{<:AbstractDeviceFormulation}, - ::Int, -) = false +is_time_variant_proportional(::IS.DeviceParameter) = false # corresponds to get_must_run for thermals, but avoiding device specific code here. """ @@ -210,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}, @@ -231,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 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/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..928fa857 100644 --- a/src/core/network_model.jl +++ b/src/core/network_model.jl @@ -1,4 +1,5 @@ -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 +61,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 +93,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 a688e2a4..47418b0a 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}}, }, } @@ -142,7 +142,7 @@ function get_branch_argument_parameter_axes( ::Type{V}, ts_name::String; interval::Dates.Millisecond = UNSET_INTERVAL, -) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData} +) where {T <: IS.InfrastructureSystemsComponent, V <: IS.TimeSeriesData} return get_branch_argument_parameter_axes( net_reduction_data, T, @@ -157,11 +157,11 @@ 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( - branch::PSY.ACTransmission, +function get_branch_with_time_series( + 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 @@ -171,7 +171,7 @@ function get_branch_argument_parameter_axes( ::Type{V}, ts_name::String; interval::Dates.Millisecond = UNSET_INTERVAL, -) where {T <: PSY.ACTransmission, V <: PSY.TimeSeriesData} +) where {T <: IS.InfrastructureSystemsComponent, V <: IS.TimeSeriesData} is_interval = _to_is_interval(interval) name_axis = Vector{String}() ts_uuid_axis = Vector{String}() @@ -180,7 +180,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!( @@ -202,32 +202,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, @@ -241,7 +233,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] @@ -251,7 +243,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 9bfe44f6..7dec468b 100644 --- a/src/core/optimization_container.jl +++ b/src/core/optimization_container.jl @@ -288,31 +288,28 @@ 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) + UNSET_FORECAST_INI_TIME function init_optimization_container!( container::OptimizationContainer, 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)) - 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,11 @@ 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 +1305,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 +1316,7 @@ end function _calculate_dual_variables_discrete_model!( container::OptimizationContainer, - ::PSY.System, + ::IS.InfrastructureSystemsContainer, ) return process_duals(container, container.settings.optimizer) end @@ -1415,14 +1415,14 @@ end function get_time_series_initial_values!( container::OptimizationContainer, ::Type{T}, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, time_series_name::AbstractString; interval::Dates.Millisecond = UNSET_INTERVAL, resolution::Dates.Millisecond = UNSET_RESOLUTION, ) 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 96db0836..da4261ec 100644 --- a/src/core/optimization_problem_outputs.jl +++ b/src/core/optimization_problem_outputs.jl @@ -104,6 +104,11 @@ 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(sys_uuid::Union{Base.UUID, AbstractString}) = "system-$(sys_uuid).json" + """ Load the system from disk if not already set, and return it. @@ -113,7 +118,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") @@ -512,7 +517,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 @@ -558,7 +563,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 """ @@ -609,7 +614,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 """ @@ -648,7 +653,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 """ @@ -698,7 +703,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 """ @@ -737,7 +742,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 """ @@ -789,7 +794,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 """ @@ -828,7 +833,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 """ @@ -881,7 +886,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 """ @@ -920,7 +925,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..d362c468 100644 --- a/src/core/parameter_container.jl +++ b/src/core/parameter_container.jl @@ -80,13 +80,16 @@ 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..b122cf30 100644 --- a/src/core/service_model.jl +++ b/src/core/service_model.jl @@ -28,14 +28,17 @@ 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 +49,11 @@ 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 +76,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 +104,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 +137,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/settings.jl b/src/core/settings.jl index 58c38c15..37f9be9a 100644 --- a/src/core/settings.jl +++ b/src/core/settings.jl @@ -59,7 +59,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 e980ab01..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{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/proportional.jl b/src/objective_function/proportional.jl index 6b073643..0860cebf 100644 --- a/src/objective_function/proportional.jl +++ b/src/objective_function/proportional.jl @@ -61,12 +61,15 @@ 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_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) 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, @@ -78,8 +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(container, op_cost_data, U, T, V, t) 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 fc523dda..cb988603 100644 --- a/src/objective_function/start_up_shut_down.jl +++ b/src/objective_function/start_up_shut_down.jl @@ -5,6 +5,19 @@ 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) + +# Trait: does this cost type store startup/shutdown in time-series parameters? +# POM adds an override for PSY.MarketBidTimeSeriesCost; mocks can duck-type. +_is_time_series_cost(::IS.DeviceParameter) = false + +################################################################################# +# Shutdown cost +################################################################################# + function add_shut_down_cost!( container::OptimizationContainer, ::Type{U}, @@ -18,32 +31,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 - 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, ::Type{U}, @@ -55,19 +84,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, ::Type{T}, component::C, - op_cost, + op_cost::IS.DeviceParameter, ::Type{U}, ) where { T <: VariableType, @@ -77,44 +105,30 @@ 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) + # 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 + 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, - ::Type{T}, - component::V, - ::Type{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/src/objective_function/value_curve_cost.jl b/src/objective_function/value_curve_cost.jl index da1b828f..d06e855a 100644 --- a/src/objective_function/value_curve_cost.jl +++ b/src/objective_function/value_curve_cost.jl @@ -2,73 +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 ######################### -# 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. -get_output_offer_curves( - component::PSY.Component, - cost::PSY.ImportExportCost; - kwargs..., -) = PSY.get_import_offer_curves(component, cost; kwargs...) -get_output_offer_curves( - component::PSY.Component, - cost::PSY.MarketBidCost; - kwargs..., -) = PSY.get_incremental_offer_curves(component, cost; kwargs...) -get_input_offer_curves( - component::PSY.Component, - cost::PSY.ImportExportCost; - kwargs..., -) = PSY.get_export_offer_curves(component, cost; kwargs...) -get_input_offer_curves( - component::PSY.Component, - cost::PSY.MarketBidCost; - kwargs..., -) = PSY.get_decremental_offer_curves(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) = - PSY.get_decremental_initial_input(PSY.get_operation_cost(device)) -get_initial_input(::IncrementalOffer, device::PSY.StaticInjection) = - PSY.get_incremental_initial_input(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 @@ -91,79 +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) = - PSY.get_incremental_initial_input(op_cost) -_get_parameter_field(::Type{<:DecrementalCostAtMinParameter}, op_cost) = - PSY.get_decremental_initial_input(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) = - PSY.get_operation_cost(device) isa PSY.MarketBidCost - -_has_import_export_cost(device::PSY.Source) = - PSY.get_operation_cost(device) isa PSY.ImportExportCost -_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(::Type{<:StartupCostParameter}, device::PSY.StaticInjection) = - is_time_variant(PSY.get_start_up(PSY.get_operation_cost(device))) - -_has_parameter_time_series(::Type{<:ShutdownCostParameter}, device::PSY.StaticInjection) = - is_time_variant(PSY.get_shut_down(PSY.get_operation_cost(device))) - -_has_parameter_time_series( - ::Type{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( - ::Type{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( - ::Type{T}, - device::PSY.StaticInjection, -) where {T <: AbstractPiecewiseLinearBreakpointParameter} = - _has_offer_curve_cost(device) && - is_time_variant(_get_parameter_field(T, PSY.get_operation_cost(device))) - -################################################################################# -# 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). ################################################################################# @@ -199,487 +159,67 @@ _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 ################################################################################# -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) +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) - 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 -end - -function _validate_occ_subtype( - ::PSY.MarketBidCost, - dir::OfferDirection, - is_ts, - curve::PSY.PiecewiseStepData, - device_name::String, - p1::Union{Nothing, Float64}, -) - @assert is_ts - my_p1 = first(PSY.get_x_coords(curve)) - if isnothing(p1) - p1 = my_p1 - elseif !isapprox(p1, my_p1) - 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.", - ), - ) - end - return p1 -end - -_validate_occ_subtype( - ::PSY.MarketBidCost, - dir::OfferDirection, - is_ts, - ::PSY.CostCurve, - args..., -) = - @assert !is_ts - -function _validate_occ_subtype( - cost::PSY.ImportExportCost, - dir::OfferDirection, - is_ts, - 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))) - 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) - 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" - end - return -end - -function validate_occ_component( - ::Type{<:ShutdownCostParameter}, - device::PSY.StaticInjection, -) - shutdown = PSY.get_shut_down(PSY.get_operation_cost(device)) - _validate_eltype(Float64, device, shutdown, " for shutdown cost") -end - -validate_occ_component( - ::Type{<:IncrementalCostAtMinParameter}, - device::PSY.StaticInjection, -) = validate_initial_input_time_series(device, IncrementalOffer()) - -validate_occ_component( - ::Type{<:DecrementalCostAtMinParameter}, - device::PSY.StaticInjection, -) = validate_initial_input_time_series(device, DecrementalOffer()) - -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(P, 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 = filter(_has_import_export_cost, collect(devices_in)) - - 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 = filter(_has_market_bid_cost, collect(devices_in)) - 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 -end - -################################################################################# -# Section 10: PWL Data Retrieval -################################################################################# - -function _get_pwl_data( - dir::OfferDirection, - container::OptimizationContainer, - component::T, - 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 - - 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 +""" +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 11: PWL Objective Terms + Variable Objective Formulation (generic) -# Load formulation overloads (AbstractControllablePowerLoadFormulation) are in POM. +# Section 5: TimeSeriesValueCurve Objective Formulation (PSY-free) +# Delta PWL objective for CostCurve{TimeSeriesPiecewiseIncrementalCurve}. +# Reads slopes/breakpoints from pre-populated parameter containers. ################################################################################# -""" -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!( +# 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, - 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)) - for t in time_steps - breakpoints, slopes = _get_pwl_data(dir, container, component, t) - 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 - -""" -Generic: incremental offers only (most device formulations). -Decremental-only overload for load formulations is in POM. -""" -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 !isnothing(get_input_offer_curves(cost_function)) - error("Component $(component_name) is not allowed to participate as a demand.") + name::String, + cost_data::IS.CostCurve{IS.TimeSeriesPiecewiseIncrementalCurve}, + time::Int, +) 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) + @assert size(slope_arr) == size(slope_mult) + 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 - 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 + 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) + 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 - _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 + @assert_op length(slope_cost_component) == length(breakpoint_cost_component) - 1 + return breakpoint_cost_component, slope_cost_component, IS.get_power_units(cost_data) 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 7f7ac5c0..854a5f8a 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -1,3 +1,23 @@ +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 ("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.", + ) + end + if "Deterministic" ∈ existing_types + return IS.Deterministic + elseif "DeterministicSingleTimeSeries" ∈ existing_types + return IS.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. @@ -12,7 +32,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 @@ -22,7 +42,7 @@ end """ DecisionModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model}=nothing; kwargs...) where {M<:DecisionProblem} @@ -32,7 +52,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 @@ -64,7 +84,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, @@ -97,7 +117,7 @@ end function DecisionModel{M}( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; name = nothing, optimizer = nothing, @@ -156,7 +176,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 @@ -169,7 +189,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} @@ -178,7 +198,7 @@ end function DecisionModel( template::AbstractProblemTemplate, - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) @@ -186,7 +206,7 @@ function DecisionModel( end function DecisionModel{M}( - sys::PSY.System, + sys::IS.InfrastructureSystemsContainer, jump_model::Union{Nothing, JuMP.Model} = nothing; kwargs..., ) where {M <: DefaultDecisionProblem} @@ -222,11 +242,12 @@ function init_model_store_params!(model::DecisionModel) if model_interval != UNSET_INTERVAL interval = model_interval else - interval = PSY.get_forecast_interval(system) + interval = IS.get_forecast_interval(system.data) end resolution = get_resolution(model) - base_power = PSY.get_base_power(system) - sys_uuid = IS.get_uuid(system) + base_power = get_base_power(system) + # FIXME declare as stub + sys_uuid = IS.get_uuid(system.data.internal) store_params = ModelStoreParams( num_executions, horizon, @@ -243,7 +264,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.data) if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 throw( @@ -283,13 +304,14 @@ 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, PSY.get_forecast_horizon(sys; interval_kwarg...)) + set_horizon!( + settings, + IS.get_forecast_horizon(sys.data; interval = _to_is_interval(model_interval)), + ) end - counts = PSY.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 33e181a1..33b04284 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.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 = PSY.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.", @@ -257,8 +257,9 @@ 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) - sys_uuid = IS.get_uuid(system) + base_power = get_base_power(system) + # FIXME declare as stub + sys_uuid = IS.get_uuid(system.data.internal) set_store_params!( get_internal(model), ModelStoreParams( @@ -333,7 +334,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/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/operation/time_series_interface.jl b/src/operation/time_series_interface.jl index 25363a23..fbc3b51f 100644 --- a/src/operation/time_series_interface.jl +++ b/src/operation/time_series_interface.jl @@ -7,7 +7,7 @@ function get_time_series_values!( horizon::Int; ignore_scaling_factors = true, interval::Dates.Millisecond = UNSET_INTERVAL, -) where {T <: PSY.Forecast} +) where {T <: IS.Forecast} is_interval = _to_is_interval(interval) settings = get_settings(model) resolution = get_resolution(settings) @@ -54,7 +54,7 @@ function get_time_series_values!( len::Int = 1; ignore_scaling_factors = true, resolution::Dates.Millisecond = UNSET_RESOLUTION, -) where {T <: PSY.StaticTimeSeries, U <: PSY.Component} +) where {T <: IS.StaticTimeSeries, U <: IS.InfrastructureSystemsComponent} settings = get_settings(model) key_resolution = resolution == UNSET_RESOLUTION ? get_resolution(settings) : resolution diff --git a/src/quadratic_approximations/incremental.jl b/src/quadratic_approximations/incremental.jl index b6c8f08a..01f31e0a 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 @@ -165,11 +165,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) - get_variable(container, R, PSY.DCBus) # Original variable (domain of function) - else - get_variable(container, R, W) # Original variable (domain of function) - end # Original variable (domain of function) + # 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) z_var = get_variable(container, U, W) # Binary variables (ordering constraints) @@ -199,7 +196,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/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 """ diff --git a/src/utils/powersystems_utils.jl b/src/utils/component_utils.jl similarity index 51% rename from src/utils/powersystems_utils.jl rename to src/utils/component_utils.jl index a9183f56..1d240e6b 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/component_utils.jl @@ -16,20 +16,20 @@ _to_is_resolution(resolution::Dates.Millisecond) = 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, @@ -39,20 +39,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, @@ -60,109 +60,20 @@ 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) - -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( + # FIXME have to patch thru to sys.data here + return IS.get_components( T, - sys; + sys.data; 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 -=# - -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 ############## ################################################## @@ -265,8 +176,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, ) @@ -279,8 +190,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, ) @@ -288,8 +199,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, ) @@ -299,12 +210,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, ) @@ -313,7 +224,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 """ @@ -324,15 +235,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, @@ -343,7 +254,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, ) @@ -359,7 +270,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, ) @@ -369,7 +280,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, ) @@ -382,7 +293,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, ) @@ -391,112 +302,24 @@ function _get_piecewise_curve_per_system_unit( return x_coords_normalized, y_coords_normalized end -is_time_variant(::IS.TimeSeriesKey) = 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 +is_time_variant(x) = IS.is_time_series_backed(x) -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 - -""" -Return the set of distinct forecast intervals present in the system. -""" -function get_forecast_intervals(sys::PSY.System) - table = PSY.get_forecast_summary_table(sys) +function get_forecast_intervals(sys::IS.InfrastructureSystemsContainer) + table = IS.get_forecast_summary_table(sys.data) return Set(row.interval for row in eachrow(table) if row.interval !== nothing) end -""" -Return `(initial_timestamp, length)` for the `SingleTimeSeries` in `sys` whose -resolution matches `resolution`. Throws `IS.InvalidValue` when no match exists -or when matching series disagree on either field. -""" -function get_single_time_series_consistency( - sys::PSY.System, - resolution::Dates.Period, +function auto_transform_time_series!( + sys::IS.InfrastructureSystemsContainer, + settings::Settings, ) - table = PSY.get_static_time_series_summary_table(sys) - target = Dates.canonicalize(Dates.Millisecond(resolution)) - filtered = - [row for row in eachrow(table) if row.resolution == target] - if isempty(filtered) - throw( - IS.InvalidValue( - "No SingleTimeSeries found at resolution $(target)", - ), - ) - end - unique_pairs = - unique((row.initial_timestamp, row.time_step_count) for row in filtered) - if length(unique_pairs) > 1 - throw( - IS.InvalidValue( - "SingleTimeSeries at resolution $(target) have inconsistent " * - "initial times and lengths: $(collect(unique_pairs))", - ), - ) - end - ini_time_str, ts_length = first(unique_pairs) - return (Dates.DateTime(ini_time_str), ts_length) -end - -""" -Automatically transform `SingleTimeSeries` into `DeterministicSingleTimeSeries` for a -given (horizon, interval) when a DecisionModel is built with these settings and the -system contains only static time series. - -Does nothing when: - - The model's `horizon` or `interval` are unset. - - The system has no `SingleTimeSeries` to transform. - - The system has existing forecast data AND the requested interval is already present in those forecasts. -""" -function auto_transform_time_series!(sys::PSY.System, settings::Settings) model_interval = get_interval(settings) model_horizon = get_horizon(settings) if model_interval == UNSET_INTERVAL || model_horizon == UNSET_HORIZON return end - counts = PSY.get_time_series_counts(sys) + counts = IS.get_time_series_counts(sys.data) if counts.static_time_series_count < 1 return end @@ -510,8 +333,9 @@ function auto_transform_time_series!(sys::PSY.System, settings::Settings) @info "Auto-transforming SingleTimeSeries to DeterministicSingleTimeSeries" horizon = Dates.canonicalize(model_horizon) interval = Dates.canonicalize(model_interval) - PSY.transform_single_time_series!( - sys, + IS.transform_single_time_series!( + sys.data, + IS.DeterministicSingleTimeSeries, model_horizon, model_interval; delete_existing = false, 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 3a72840b..05e26cbd 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,13 +16,21 @@ 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::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 @@ -36,7 +44,7 @@ is of the type given """ _validate_eltype( ::Type{T}, - component::PSY.Component, + component::IS.InfrastructureSystemsComponent, ts_key::IS.TimeSeriesKey, msg = "", ) where {T} = @@ -50,7 +58,12 @@ _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 82970458..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,29 +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")) - 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 e6a15492..fed99d0f 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 dfaa6914..00000000 --- a/test/includes.jl +++ /dev/null @@ -1,55 +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") -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 99ce948b..75b990d4 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,24 @@ 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, 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", + ) +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 +73,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 +82,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 @@ -78,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 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 diff --git a/test/test_proportional.jl b/test/test_proportional.jl index fbd7ebca..51e7de50 100644 --- a/test/test_proportional.jl +++ b/test/test_proportional.jl @@ -36,15 +36,9 @@ 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( - ::InfrastructureOptimizationModels.OptimizationContainer, - op_cost::MockOperationCost, - ::Type{TestProportionalVariable}, - ::Type{MockThermalGen}, - ::Type{TestProportionalFormulation}, - ::Int, -) = op_cost.is_time_variant +# 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 function setup_proportional_test_container( diff --git a/test/test_start_up_shut_down.jl b/test/test_start_up_shut_down.jl index 8713c169..31cd7e4d 100644 --- a/test/test_start_up_shut_down.jl +++ b/test/test_start_up_shut_down.jl @@ -30,27 +30,24 @@ IOM.start_up_cost( ::Type{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 @@ -343,8 +340,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, @@ -385,8 +381,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, 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 5a70f9d1..00000000 --- a/test/test_utils/add_market_bid_cost.jl +++ /dev/null @@ -1,326 +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 = 0.0, - start_up = (hot = 0.0, warm = 0.0, cold = 0.0), - shut_down = 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 - -""" -Extend the MarketBidCost objects attached to the selected components such that they're determined by a 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" - # 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) - 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 - - @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", - ), - ) - 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_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)) - # 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) - - 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) - setter_initial(op_cost, initial_key) - setter_curves(op_cost, curve_key) - end - 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 61757eec..00000000 --- a/test/test_utils/iec_simulation_utils.jl +++ /dev/null @@ -1,270 +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) - - set_import_offer_curves!(oc, im_key) - set_export_offer_curves!(oc, ex_key) - - 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/Sienna-Platform/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 ImportExportCost 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 - 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 PSI.is_time_variant(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 561f2d26..00000000 --- a/test/test_utils/mbc_system_utils.jl +++ /dev/null @@ -1,474 +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 MarketBidCost && 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, copying any time series in the process.""" -function transfer_mbc!( - new_comp::PSY.Device, - old_comp::PSY.Device, - new_sys::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 - 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, 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_start_up!(cost, (hot = 0.0, warm = 0.0, cold = 0.0)) - set_shut_down!(cost, 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 - 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" -function no_load_to_initial_input!(comp::Generator) - cost = get_operation_cost(comp)::MarketBidCost - no_load = 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) - return -end - -no_load_to_initial_input!( - sys::PSY.System, - sel = make_selector(x -> get_operation_cost(x) isa MarketBidCost, 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 - 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 - -""" -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`. -""" -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 - startup_ts_1 = make_deterministic_ts( - sys, - "start_up", - (1.0, 1.5, 2.0), - 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) - 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`. -""" -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 - startup_ts_1 = make_deterministic_ts( - sys, - "start_up", - base_startup, - res_incr, - interval_incr, - ) - set_start_up!(sys, unit1, startup_ts_1) - shutdown_ts_1 = - make_deterministic_ts( - sys, - "shut_down", - base_shutdown, - res_incr, - interval_incr, - ) - set_shut_down!(sys, unit1, shutdown_ts_1) - return startup_ts_1, shutdown_ts_1 -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 = 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 = nothing, - start_up = (hot = 300.0, warm = 450.0, cold = 500.0), - shut_down = 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 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