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/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/operation/decision_model.jl b/src/operation/decision_model.jl index 091e9833c5..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,42 +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 - else - 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/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) 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/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 7cf36f874c..9ef409ff6f 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -335,77 +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 ################ -################################################## - -# 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, -) - data = PSY.get_x_coords(cost_data) - if isapprox(max - min, last(data) / base_power) && 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 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),