From c7e13834b87eda9e58f0fc01f4e6804020a1c8e8 Mon Sep 17 00:00:00 2001 From: rodrigomha Date: Mon, 20 May 2024 11:00:56 -0700 Subject: [PATCH 1/5] add new compact pwl constraints --- .../objective_function/piecewise_linear.jl | 89 +++++++++++++++---- src/utils/powersystems_utils.jl | 9 +- 2 files changed, 79 insertions(+), 19 deletions(-) diff --git a/src/devices_models/devices/common/objective_function/piecewise_linear.jl b/src/devices_models/devices/common/objective_function/piecewise_linear.jl index 01e152037b..d3407f21ef 100644 --- a/src/devices_models/devices/common/objective_function/piecewise_linear.jl +++ b/src/devices_models/devices/common/objective_function/piecewise_linear.jl @@ -121,6 +121,75 @@ function _add_pwl_constraint!( return end +""" +Implement the constraints for PWL variables for Compact form. That is: + +```math +\\sum_{k\\in\\mathcal{K}} P_k^{max} \\delta_{k,t} = p_t + P_min * u_t \\\\ +\\sum_{k\\in\\mathcal{K}} \\delta_{k,t} = on_t +``` +""" +function _add_pwl_constraint!( + container::OptimizationContainer, + component::T, + ::U, + break_points::Vector{Float64}, + sos_status::SOSStatusVariable, + period::Int, +) where {T <: PSY.Component, U <: PowerAboveMinimumVariable} + variables = get_variable(container, U(), T) + const_container = lazy_container_addition!( + container, + PieceWiseLinearCostConstraint(), + T, + axes(variables)..., + ) + len_cost_data = length(break_points) + jump_model = get_jump_model(container) + pwl_vars = get_variable(container, PieceWiseLinearCostVariable(), T) + name = PSY.get_name(component) + + if sos_status == SOSStatusVariable.NO_VARIABLE + bin = 1.0 + @debug "Using Piecewise Linear cost function but no variable/parameter ref for ON status is passed. Default status will be set to online (1.0)" _group = + LOG_GROUP_COST_FUNCTIONS + + elseif sos_status == SOSStatusVariable.PARAMETER + param = get_default_on_parameter(component) + bin = get_parameter(container, param, T).parameter_array[name, period] + @debug "Using Piecewise Linear cost function with parameter OnStatusParameter, $T" _group = + LOG_GROUP_COST_FUNCTIONS + elseif sos_status == SOSStatusVariable.VARIABLE + var = get_default_on_variable(component) + bin = get_variable(container, var, T)[name, period] + @debug "Using Piecewise Linear cost function with variable OnVariable $T" _group = + LOG_GROUP_COST_FUNCTIONS + else + @assert false + end + P_min = PSY.get_active_power_limits(component).min + + const_container[name, period] = JuMP.@constraint( + jump_model, + bin * P_min + variables[name, period] == + sum(pwl_vars[name, ix, period] * break_points[ix] for ix in 1:len_cost_data) + ) + + const_normalization_container = lazy_container_addition!( + container, + PieceWiseLinearCostConstraint(), + T, + axes(variables)...; + meta = "normalization", + ) + + const_normalization_container[name, period] = JuMP.@constraint( + jump_model, + sum(pwl_vars[name, i, period] for i in 1:len_cost_data) == bin + ) + return +end + """ Implement the SOS for PWL variables. That is: @@ -313,20 +382,7 @@ function _add_pwl_term!( return end - compact_status = validate_compact_pwl_data(component, data, base_power) - if !uses_compact_power(component, V()) && compact_status == COMPACT_PWL_STATUS.VALID - error( - "The data provided is not compatible with formulation $V. Use a formulation compatible with Compact Cost Functions", - ) - # data = _convert_to_full_variable_cost(data, component) - elseif uses_compact_power(component, V()) && compact_status != COMPACT_PWL_STATUS.VALID - @warn( - "The cost data provided is not in compact form. Will attempt to convert. Errors may occur." - ) - data = convert_to_compact_variable_cost(data) - else - @debug uses_compact_power(component, V()) compact_status name T V - end + # Compact PWL data does not exists anymore cost_is_convex = PSY.is_convex(data) break_points = PSY.get_x_coords(data) @@ -383,10 +439,7 @@ function _add_pwl_term!( ) end - if validate_compact_pwl_data(component, data, base_power) == COMPACT_PWL_STATUS.VALID - error("The data provided is not compatible with formulation $V. \\ - Use a formulation compatible with Compact Cost Functions") - end + # Compact PWL data does not exists anymore if slopes[1] != 0.0 @debug "PWL has no 0.0 intercept for generator $(component_name)" diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 7cf36f874c..9cdd64fb0c 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -340,6 +340,9 @@ end ############### Auxiliary Methods ################ ################################################## +# Compact Data does not exists anymore +#= + # These conversions are not properly done for the new models function convert_to_compact_variable_cost( var_cost::PSY.PiecewiseLinearData, @@ -381,8 +384,11 @@ function _validate_compact_pwl_data( cost_data::PSY.PiecewiseStepData, base_power::Float64, ) + @show min + @show max data = PSY.get_x_coords(cost_data) - if isapprox(max - min, last(data) / base_power) && iszero(first(data)) + @show last(data) + if isapprox(max - min, last(data)) && iszero(first(data)) return COMPACT_PWL_STATUS.VALID else return COMPACT_PWL_STATUS.INVALID @@ -409,3 +415,4 @@ function validate_compact_pwl_data( end get_breakpoint_upper_bounds = PSY.get_x_lengths +=# From 8fb4482fb8f23a767b5d0aa4ad9ee494ead999b0 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Tue, 28 May 2024 12:30:30 -0600 Subject: [PATCH 2/5] fix resolution checker --- src/operation/decision_model.jl | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 091e9833c5..4344e1f6d6 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -286,7 +286,7 @@ function validate_time_series(model::DecisionModel{<:DefaultDecisionProblem}) "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)", ), ) - elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) > 1 + elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) >= 1 if get_resolution(settings) ∉ available_resolutions throw( IS.ConflictingInputsError( @@ -294,7 +294,10 @@ function validate_time_series(model::DecisionModel{<:DefaultDecisionProblem}) ), ) end + set_resolution!(settings, first(available_resolutions)) else + IS.@assert_op get_resolution(settings) == UNSET_RESOLUTION + @info "Resolution not set, using $(first(available_resolutions)) from the system data" set_resolution!(settings, first(available_resolutions)) end From 562c4f73174ed221eb2df1a2889da5cb8f154946 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Tue, 28 May 2024 14:56:12 -0600 Subject: [PATCH 3/5] delete stale code --- src/utils/powersystems_utils.jl | 81 --------------------------------- 1 file changed, 81 deletions(-) diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 9cdd64fb0c..9ef409ff6f 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -335,84 +335,3 @@ function _get_piecewise_incrementalcurve_per_system_unit( y_coords_normalized = y_coords .* system_base_power return PSY.PiecewiseStepData(x_coords_normalized, y_coords_normalized) end - -################################################## -############### Auxiliary Methods ################ -################################################## - -# Compact Data does not exists anymore -#= - -# These conversions are not properly done for the new models -function convert_to_compact_variable_cost( - var_cost::PSY.PiecewiseLinearData, - p_min::Float64, - no_load_cost::Float64, -) - points = PSY.get_points(var_cost) - new_points = [(pp - p_min, c - no_load_cost) for (pp, c) in points] - return PSY.PiecewiseLinearData(new_points) -end - -# These conversions are not properly done for the new models -function convert_to_compact_variable_cost( - var_cost::PSY.PiecewiseStepData, - p_min::Float64, - no_load_cost::Float64, -) - x = PSY.get_x_coords(var_cost) - y = vcat(PSY.get_y_coords(var_cost), PSY.get_y_coords(var_cost)[end]) - points = [(x[i], y[i]) for i in length(x)] - new_points = [(x = pp - p_min, y = c - no_load_cost) for (pp, c) in points] - return PSY.PiecewiseLinearData(new_points) -end - -# TODO: This method needs to be corrected to account for actual StepData. The TestData is point wise -function convert_to_compact_variable_cost(var_cost::PSY.PiecewiseStepData) - p_min, no_load_cost = (PSY.get_x_coords(var_cost)[1], PSY.get_y_coords(var_cost)[1]) - return convert_to_compact_variable_cost(var_cost, p_min, no_load_cost) -end - -function convert_to_compact_variable_cost(var_cost::PSY.PiecewiseLinearData) - p_min, no_load_cost = first(PSY.get_points(var_cost)) - return convert_to_compact_variable_cost(var_cost, p_min, no_load_cost) -end - -function _validate_compact_pwl_data( - min::Float64, - max::Float64, - cost_data::PSY.PiecewiseStepData, - base_power::Float64, -) - @show min - @show max - data = PSY.get_x_coords(cost_data) - @show last(data) - if isapprox(max - min, last(data)) && iszero(first(data)) - return COMPACT_PWL_STATUS.VALID - else - return COMPACT_PWL_STATUS.INVALID - end -end - -function validate_compact_pwl_data( - d::PSY.ThermalGen, - data::PSY.PiecewiseStepData, - base_power::Float64, -) - min = PSY.get_active_power_limits(d).min - max = PSY.get_active_power_limits(d).max - return _validate_compact_pwl_data(min, max, data, base_power) -end - -function validate_compact_pwl_data( - d::PSY.Component, - ::PSY.PiecewiseLinearData, - ::Float64, -) - @warn "Validation of compact pwl data is not implemented for $(typeof(d))." - return COMPACT_PWL_STATUS.UNDETERMINED -end - -get_breakpoint_upper_bounds = PSY.get_x_lengths -=# From 4a9b225ce6bd41d6678e8fb11a60c749fae8ef28 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Tue, 28 May 2024 14:56:23 -0600 Subject: [PATCH 4/5] made validate time series a single method --- .../src/tutorials/adding_new_problem_model.md | 2 +- src/operation/decision_model.jl | 42 +---------------- src/operation/emulation_model.jl | 47 +------------------ src/operation/operation_model_interface.jl | 39 +++++++++++++++ src/simulation/simulation_models.jl | 6 +-- test/test_utils/mock_operation_models.jl | 2 +- 6 files changed, 46 insertions(+), 92 deletions(-) diff --git a/docs/src/tutorials/adding_new_problem_model.md b/docs/src/tutorials/adding_new_problem_model.md index bc41792a29..6f420817de 100644 --- a/docs/src/tutorials/adding_new_problem_model.md +++ b/docs/src/tutorials/adding_new_problem_model.md @@ -62,7 +62,7 @@ my_model = DecisionModel{MyCustomDecisionProblem}( These methods can be defined optionally for your problem. By default for problems subtyped from `DecisionProblem` these checks are not executed. If the problems are subtyped from `DefaultDecisionProblem` these checks are always conducted with PowerSimulations defaults and require compliance with those defaults to pass. In any case, these can be overloaded when necessary depending on the problem requirements. 1. `validate_template` -2. `validate_time_series` +2. `validate_time_series!` 3. `reset!` 4. `solve_impl!` diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index 4344e1f6d6..0eddef0f22 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -89,7 +89,7 @@ function DecisionModel{M}( DecisionModelStore(), Dict{String, Any}(), ) - validate_time_series(model) + PSI.validate_time_series!(model) return model end @@ -244,7 +244,6 @@ end get_problem_type(::DecisionModel{M}) where {M <: DecisionProblem} = M validate_template(::DecisionModel{<:DecisionProblem}) = nothing -validate_time_series(::DecisionModel{<:DecisionProblem}) = nothing # Probably could be more efficient by storing the info in the internal function get_current_time(model::DecisionModel) @@ -275,45 +274,6 @@ function init_model_store_params!(model::DecisionModel) return end -function validate_time_series(model::DecisionModel{<:DefaultDecisionProblem}) - sys = get_system(model) - settings = get_settings(model) - available_resolutions = PSY.get_time_series_resolutions(sys) - - if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 - throw( - IS.ConflictingInputsError( - "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)", - ), - ) - elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) >= 1 - if get_resolution(settings) ∉ available_resolutions - throw( - IS.ConflictingInputsError( - "Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)", - ), - ) - end - set_resolution!(settings, first(available_resolutions)) - else - IS.@assert_op get_resolution(settings) == UNSET_RESOLUTION - @info "Resolution not set, using $(first(available_resolutions)) from the system data" - set_resolution!(settings, first(available_resolutions)) - end - - if get_horizon(settings) == UNSET_HORIZON - set_horizon!(settings, PSY.get_forecast_horizon(sys)) - end - - counts = PSY.get_time_series_counts(sys) - if counts.forecast_count < 1 - error( - "The system does not contain forecast data. A DecisionModel can't be built.", - ) - end - return -end - function build_pre_step!(model::DecisionModel{<:DecisionProblem}) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build pre-step" begin validate_template(model) diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl index 22d7d82258..f45e0d6be4 100644 --- a/src/operation/emulation_model.jl +++ b/src/operation/emulation_model.jl @@ -134,7 +134,7 @@ function EmulationModel{M}( resolution = resolution, ) model = EmulationModel{M}(template, sys, settings, jump_model; name = name) - validate_time_series(model) + validate_time_series!(model) return model end @@ -231,7 +231,7 @@ end get_problem_type(::EmulationModel{M}) where {M <: EmulationProblem} = M validate_template(::EmulationModel{<:EmulationProblem}) = nothing -validate_time_series(::EmulationModel{<:EmulationProblem}) = nothing +validate_time_series!(::EmulationModel{<:EmulationProblem}) = nothing function get_current_time(model::EmulationModel) execution_count = get_execution_count(model) @@ -262,49 +262,6 @@ function init_model_store_params!(model::EmulationModel) return end -function validate_time_series(model::EmulationModel{<:DefaultEmulationProblem}) - sys = get_system(model) - counts = PSY.get_time_series_counts(sys) - if counts.static_time_series_count < 1 - error( - "The system does not contain Static TimeSeries data. An Emulation model can't be formulated.", - ) - end - counts = PSY.get_time_series_counts(sys) - - if counts.forecast_count < 1 - error( - "The system does not contain time series data. A EmulationModel can't be built.", - ) - end - - settings = get_settings(model) - available_resolutions = PSY.get_time_series_resolutions(sys) - - if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 - throw( - IS.ConflictingInputsError( - "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)", - ), - ) - elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) > 1 - if get_resolution(settings) ∉ available_resolutions - throw( - IS.ConflictingInputsError( - "Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)", - ), - ) - end - else - set_resolution!(settings, first(available_resolutions)) - end - - if get_horizon(settings) == UNSET_HORIZON - set_horizon!(settings, get_resolution(settings)) - end - return -end - function build_pre_step!(model::EmulationModel) TimerOutputs.@timeit BUILD_PROBLEMS_TIMER "Build pre-step" begin validate_template(model) diff --git a/src/operation/operation_model_interface.jl b/src/operation/operation_model_interface.jl index 1eb0e8ecc3..cae20ccddc 100644 --- a/src/operation/operation_model_interface.jl +++ b/src/operation/operation_model_interface.jl @@ -478,3 +478,42 @@ function serialize_optimization_model(model::OperationModel, save_path::String) ) return end + +function validate_time_series!(model::OperationModel) + sys = get_system(model) + settings = get_settings(model) + available_resolutions = PSY.get_time_series_resolutions(sys) + + if get_resolution(settings) == UNSET_RESOLUTION && length(available_resolutions) != 1 + throw( + IS.ConflictingInputsError( + "Data contains multiple resolutions, the resolution keyword argument must be added to the Model. Time Series Resolutions: $(available_resolutions)", + ), + ) + elseif get_resolution(settings) != UNSET_RESOLUTION && length(available_resolutions) >= 1 + if get_resolution(settings) ∉ available_resolutions + throw( + IS.ConflictingInputsError( + "Resolution $(get_resolution(settings)) is not available in the system data. Time Series Resolutions: $(available_resolutions)", + ), + ) + end + set_resolution!(settings, first(available_resolutions)) + else + IS.@assert_op get_resolution(settings) == UNSET_RESOLUTION + @info "Resolution not set, using $(first(available_resolutions)) from the system data" + set_resolution!(settings, first(available_resolutions)) + end + + if get_horizon(settings) == UNSET_HORIZON + set_horizon!(settings, PSY.get_forecast_horizon(sys)) + end + + counts = PSY.get_time_series_counts(sys) + if counts.forecast_count < 1 + error( + "The system does not contain forecast data. A DecisionModel can't be built.", + ) + end + return +end diff --git a/src/simulation/simulation_models.jl b/src/simulation/simulation_models.jl index 4b40c0444f..525f792df8 100644 --- a/src/simulation/simulation_models.jl +++ b/src/simulation/simulation_models.jl @@ -106,10 +106,8 @@ function determine_horizons!(models::SimulationModels) if horizon == UNSET_HORIZON sys = get_system(model) horizon = PSY.get_forecast_horizon(sys) - # TODO: PSY to return horizon in TimePeriod - resolution = get_resolution(settings) - set_horizon!(settings, horizon * resolution) - horizons[get_name(model)] = horizon * resolution + set_horizon!(settings, horizon) + horizons[get_name(model)] = horizon else horizons[get_name(model)] = horizon end diff --git a/test/test_utils/mock_operation_models.jl b/test/test_utils/mock_operation_models.jl index a7c53f10ea..078bfaa69e 100644 --- a/test/test_utils/mock_operation_models.jl +++ b/test/test_utils/mock_operation_models.jl @@ -119,7 +119,7 @@ function mock_construct_device!( set_device_model!(problem.template, model) template = PSI.get_template(problem) PSI.finalize_template!(template, PSI.get_system(problem)) - PSI.validate_time_series(problem) + PSI.validate_time_series!(problem) PSI.init_optimization_container!( PSI.get_optimization_container(problem), PSI.get_network_model(template), From f7fdf49d1968cee6bd061bff34088a22389d6fe1 Mon Sep 17 00:00:00 2001 From: Jose Daniel Lara Date: Tue, 28 May 2024 16:56:48 -0600 Subject: [PATCH 5/5] fix subtype --- src/parameters/update_parameters.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl index be4ca5cdb0..97188aca39 100644 --- a/src/parameters/update_parameters.jl +++ b/src/parameters/update_parameters.jl @@ -390,7 +390,7 @@ function update_container_parameter_values!( model::OperationModel, key::ParameterKey{T, U}, input::DatasetContainer{InMemoryDataset}, -) where {T <: ObjectiveFunctionParameter, U <: PSY.Service} +) where {T <: ParameterType, U <: PSY.Service} # Note: Do not instantite a new key here because it might not match the param keys in the container # if the keys have strings in the meta fields parameter_array = get_parameter_array(optimization_container, key)