From 897516e94ba0edc1916148a171c586871a97e7bb Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:33:17 -0700 Subject: [PATCH 1/6] Accommodate PSY.get_time_series_counts API change --- src/operation/decision_model.jl | 4 ++-- src/operation/emulation_model.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/operation/decision_model.jl b/src/operation/decision_model.jl index e5d61a2fea..f416865bde 100644 --- a/src/operation/decision_model.jl +++ b/src/operation/decision_model.jl @@ -268,8 +268,8 @@ end function validate_time_series(model::DecisionModel{<:DefaultDecisionProblem}) sys = get_system(model) - _, _, forecast_count = PSY.get_time_series_counts(sys) - if forecast_count < 1 + 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.", ) diff --git a/src/operation/emulation_model.jl b/src/operation/emulation_model.jl index 0f27b8ddc4..d946aa6331 100644 --- a/src/operation/emulation_model.jl +++ b/src/operation/emulation_model.jl @@ -247,8 +247,8 @@ end function validate_time_series(model::EmulationModel{<:DefaultEmulationProblem}) sys = get_system(model) - _, ts_count, _ = PSY.get_time_series_counts(sys) - if ts_count < 1 + 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.", ) From c24559da27dacf8d876d6103e1bb796bc42fee25 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Sun, 25 Feb 2024 23:35:44 -0700 Subject: [PATCH 2/6] FunctionData refactor intermediate step The refactor is not done yet and tests are not passing, but to proceed with the refactor I need to make a decision that I'd like to be able to easily revert if it ends up being the wrong one. Done so far: - Update documentation given FunctionData - Update all VariableCosts to refer to FunctionData - Update most cost-looking tuples to FunctionData Not done: - `moi_tests` still fail - There is still a conflict in the typing of the data stored in parameter containers - Probably more issues I haven't gotten to in the tests yet --- docs/src/formulation_library/General.md | 41 +++-- docs/src/formulation_library/Load.md | 4 +- docs/src/formulation_library/RenewableGen.md | 4 +- .../devices/common/objective_functions.jl | 143 ++++++++---------- .../devices/thermal_generation.jl | 16 +- src/parameters/update_parameters.jl | 8 +- src/utils/powersystems_utils.jl | 9 +- 7 files changed, 113 insertions(+), 112 deletions(-) diff --git a/docs/src/formulation_library/General.md b/docs/src/formulation_library/General.md index 71e3e552e6..1f7f20891f 100644 --- a/docs/src/formulation_library/General.md +++ b/docs/src/formulation_library/General.md @@ -56,38 +56,53 @@ No constraints are created for `DeviceModel(<:DeviceType, FixedOutput)` --- -## `VariableCost` Options +## `FunctionData` Options PowerSimulations can represent variable costs using a variety of different methods depending on the data available in each device. The following describes the objective function terms that are populated for each variable cost option. -### Scalar `VariableCost` +### `LinearFunctionData` -`variable_cost <: Float64`: creates a fixed marginal cost term in the objective function +`variable_cost = LinearFunctionData(c)`: creates a fixed marginal cost term in the objective function ```math \begin{aligned} -& \text{min} \sum_{t} C * G_t +& \text{min} \sum_{t} c * G_t \end{aligned} ``` -### Polynomial `VariableCost` +### `QuadraticFunctionData` and `PolynomialFunctionData` -`variable_cost <: Tuple{Float64, Float64}`: creates a polynomial cost term in the objective function where - -- ``C_g``=`variable_cost[1]` -- ``C_g^\prime``=`variable_cost[2]` +`variable_cost::QuadraticFunctionData` and `variable_cost::PolynomialFunctionData`: create a polynomial cost term in the objective function ```math \begin{aligned} -& \text{min} \sum_{t} C * G_t + C^\prime * G_t^2 +& \text{min} \sum_{t} \sum_{n} C_n * G_t^n \end{aligned} ``` -### Piecewise Linear `VariableCost` +where + +- For `QuadraticFunctionData`: + - ``C_0`` = `get_constant_term(variable_cost)` + - ``C_1`` = `get_proportional_term(variable_cost)` + - ``C_2`` = `get_quadratic_term(variable_cost)` +- For `PolynomialFunctionData`: + - ``C_n`` = `get_coefficients(variable_cost)[n]` + +### `PiecewiseLinearPointData` and `PiecewiseLinearSlopeData` + +`variable_cost::PiecewiseLinearPointData` and `variable_cost::PiecewiseLinearSlopeData`: create a piecewise linear cost term in the objective function + +```math +\begin{aligned} +& \text{min} \sum_{t} f(G_t) +\end{aligned} +``` -`variable_cost <: Vector{Tuple{Float64, Float64}}`: creates a piecewise linear cost term in the objective function +where -TODO: add formulation +- For `variable_cost::PiecewiseLinearPointData`, ``f(x)`` is the piecewise linear function obtained by connecting the `(x, y)` points `get_points(variable_cost)` in order. +- For `variable_cost = PiecewiseLinearSlopeData([x0, x1, x2, ...], y0, [s0, s1, s2, ...])`, ``f(x)`` is the piecewise linear function obtained by starting at `(x0, y0)`, drawing a segment at slope `s0` to `x=x1`, drawing a segment at slope `s1` to `x=x2`, etc. ___ diff --git a/docs/src/formulation_library/Load.md b/docs/src/formulation_library/Load.md index f3fd31cb1a..c3bcbabb3c 100644 --- a/docs/src/formulation_library/Load.md +++ b/docs/src/formulation_library/Load.md @@ -89,7 +89,7 @@ mdtable(combo_table, latex = false) **Objective:** -Creates an objective function term based on the [`VariableCost` Options](@ref) where the quantity term is defined as ``Pg``. +Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``Pg``. **Expressions:** @@ -143,7 +143,7 @@ mdtable(combo_table, latex = false) **Objective:** -Creates an objective function term based on the [`VariableCost` Options](@ref) where the quantity term is defined as ``Pg``. +Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``Pg``. **Expressions:** diff --git a/docs/src/formulation_library/RenewableGen.md b/docs/src/formulation_library/RenewableGen.md index 9a9a65f8d1..dd2de3122d 100644 --- a/docs/src/formulation_library/RenewableGen.md +++ b/docs/src/formulation_library/RenewableGen.md @@ -57,7 +57,7 @@ mdtable(combo_table, latex = false) **Objective:** -Creates an objective function term based on the [`VariableCost` Options](@ref) where the quantity term is defined as ``- Pg_t`` to incentivize generation from `RenewableGen` devices. +Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``- Pg_t`` to incentivize generation from `RenewableGen` devices. **Expressions:** @@ -113,7 +113,7 @@ mdtable(combo_table, latex = false) **Objective:** -Creates an objective function term based on the [`VariableCost` Options](@ref) where the quantity term is defined as ``- Pg_t`` to incentivize generation from `RenewableGen` devices. +Creates an objective function term based on the [`FunctionData` Options](@ref) where the quantity term is defined as ``- Pg_t`` to incentivize generation from `RenewableGen` devices. **Expressions:** diff --git a/src/devices_models/devices/common/objective_functions.jl b/src/devices_models/devices/common/objective_functions.jl index c5b5655136..c6d8ec4d8f 100644 --- a/src/devices_models/devices/common/objective_functions.jl +++ b/src/devices_models/devices/common/objective_functions.jl @@ -167,7 +167,7 @@ function _add_variable_cost_to_objective!( set_parameter!( parameter_container, jump_model, - PSY.get_cost(variable_cost_forecast_values[t]), + Tuple.(PSY.get_points(variable_cost_forecast_values[t])), component_name, t, ) @@ -201,7 +201,6 @@ function _add_variable_cost_to_objective!( time_steps = get_time_steps(container) variable_cost_forecast = get_time_series(container, component, "variable_cost") variable_cost_forecast_values = TimeSeries.values(variable_cost_forecast) - variable_cost_forecast_values = map(PSY.VariableCost, variable_cost_forecast_values) parameter_container = _get_cost_function_parameter_container( container, CostFunctionParameter(), @@ -225,7 +224,7 @@ function _add_variable_cost_to_objective!( set_parameter!( parameter_container, jump_model, - PSY.get_cost(variable_cost_forecast_values[t]), + Tuple.(PSY.get_points(variable_cost_forecast_values[t])), component_name, t, ) @@ -286,15 +285,13 @@ function _add_start_up_cost_to_objective!( return end -_get_cost_function_data_type(::Type{PSY.VariableCost{T}}) where {T} = T - function _get_cost_function_parameter_container( container::OptimizationContainer, ::S, component::T, ::U, ::V, - cost_type::DataType, + cost_type::Type{<:PSY.FunctionData}, ) where { S <: ObjectiveFunctionParameter, T <: PSY.Component, @@ -317,7 +314,7 @@ function _get_cost_function_parameter_container( U, sos_val, uses_compact_power(component, V()), - _get_cost_function_data_type(cost_type), + cost_type, container_axes..., ) end @@ -372,18 +369,18 @@ Adds to the cost function cost terms for sum of variables with common factor to - container::OptimizationContainer : the optimization_container model built in PowerSimulations - var_key::VariableKey: The variable name - component_name::String: The component_name of the variable container - - cost_component::PSY.VariableCost{Float64} : container for cost to be associated with variable + - cost_component::PSY.LinearFunctionData : container for cost to be associated with variable """ function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, - cost_component::PSY.VariableCost{Float64}, + cost_component::PSY.LinearFunctionData, ::U, ) where {T <: VariableType, U <: AbstractDeviceFormulation} multiplier = objective_function_multiplier(T(), U()) base_power = get_base_power(container) - cost_data = PSY.get_cost(cost_component) + cost_data = PSY.get_proportional_term(cost_component) resolution = get_resolution(container) dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR for time_period in get_time_steps(container) @@ -424,27 +421,31 @@ linear cost term `sum(variable)*cost_data[2]` * container::OptimizationContainer : the optimization_container model built in PowerSimulations * var_key::VariableKey: The variable name * component_name::String: The component_name of the variable container -* cost_component::PSY.VariableCost{NTuple{2, Float64}} : container for quadratic and linear factors +* cost_component::PSY.QuadraticFunctionData : container for quadratic factors """ function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, - cost_component::PSY.VariableCost{NTuple{2, Float64}}, + cost_component::PSY.QuadraticFunctionData, ::U, ) where {T <: VariableType, U <: AbstractDeviceFormulation} multiplier = objective_function_multiplier(T(), U()) base_power = get_base_power(container) - cost_data = PSY.get_cost(cost_component) + quadratic_term = PSY.get_quadratic_term(cost_component) + proportional_term = PSY.get_proportional_term(cost_component) + constant_term = PSY.get_constant_term(cost_component) + (constant_term == 0) || + throw(ArgumentError("Not yet implemented for nonzero constant term")) resolution = get_resolution(container) dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR for time_period in get_time_steps(container) - if cost_data[1] >= eps() + if quadratic_term >= eps() cost_term = _add_quadratic_term!( container, T(), component, - cost_data, + (quadratic_term, proportional_term), base_power, multiplier * dt, time_period, @@ -454,7 +455,7 @@ function _add_variable_cost_to_objective!( container, T(), component, - cost_data[2] * multiplier * base_power * dt, + proportional_term * multiplier * base_power * dt, time_period, ) end @@ -477,25 +478,24 @@ Creates piecewise linear cost function using a sum of variables and expression w - container::OptimizationContainer : the optimization_container model built in PowerSimulations - var_key::VariableKey: The variable name - component_name::String: The component_name of the variable container - - cost_component::PSY.VariableCost{Vector{NTuple{2, Float64}}} + - cost_component::PSY.PiecewiseLinearPointData: container for piecewise linear cost """ function _add_variable_cost_to_objective!( container::OptimizationContainer, ::T, component::PSY.Component, - cost_component::PSY.VariableCost{Vector{NTuple{2, Float64}}}, + cost_component::PSY.PiecewiseLinearPointData, ::U, ) where {T <: VariableType, U <: AbstractDeviceFormulation} component_name = PSY.get_name(component) @debug "PWL Variable Cost" _group = LOG_GROUP_COST_FUNCTIONS component_name # If array is full of tuples with zeros return 0.0 - cost_data = PSY.get_cost(cost_component) - if all(iszero.(last.(cost_data))) + if all(iszero.((point -> point.y).(PSY.get_points(cost_component)))) # TODO I think this should have been first. before? @debug "All cost terms for component $(component_name) are 0.0" _group = LOG_GROUP_COST_FUNCTIONS return end - pwl_cost_expressions = _add_pwl_term!(container, component, cost_data, T(), U()) + pwl_cost_expressions = _add_pwl_term!(container, component, cost_component, T(), U()) for t in get_time_steps(container) add_to_expression!( container, @@ -509,20 +509,6 @@ function _add_variable_cost_to_objective!( return end -""" -Returns True/False depending on compatibility of the cost data with the convex implementation method -""" -function _slope_convexity_check(slopes::Vector{Float64}) - flag = true - for ix in 1:(length(slopes) - 1) - if slopes[ix] > slopes[ix + 1] - @debug slopes _group = LOG_GROUP_COST_FUNCTIONS - return flag = false - end - end - return flag -end - function _get_sos_value( container::OptimizationContainer, ::Type{V}, @@ -547,7 +533,7 @@ end function _add_pwl_term!( container::OptimizationContainer, component::T, - cost_data::Vector{PSY.VariableCost{Float64}}, + cost_data::AbstractVector{PSY.LinearFunctionData}, ::U, ::V, ) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation} @@ -559,9 +545,9 @@ function _add_pwl_term!( time_steps = get_time_steps(container) cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end]) for t in time_steps - proportial_value = PSY.get_cost(cost_data[t]) * multiplier * base_power * dt + proportional_value = PSY.get_proportional_term(cost_data[t]) * multiplier * base_power * dt cost_expressions[t] = - _add_proportional_term!(container, U(), component, proportial_value, t) + _add_proportional_term!(container, U(), component, proportional_value, t) end return cost_expressions end @@ -572,7 +558,7 @@ Add PWL cost terms for data coming from the MarketBidCost function _add_pwl_term!( container::OptimizationContainer, component::T, - cost_data::Vector{PSY.VariableCost{Vector{Tuple{Float64, Float64}}}}, + cost_data::AbstractVector{PSY.PiecewiseLinearPointData}, ::U, ::V, ) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation} @@ -587,7 +573,7 @@ function _add_pwl_term!( sos_val = _get_sos_value(container, V, component) for t in time_steps # Run checks in every time step because each time step has a PWL cost function - data = PSY.get_cost(cost_data[t]) + data = cost_data[t] compact_status = validate_compact_pwl_data(component, data, base_power) if !uses_compact_power(component, V()) && compact_status == COMPACT_PWL_STATUS.VALID error( @@ -597,20 +583,17 @@ function _add_pwl_term!( elseif uses_compact_power(component, V()) && compact_status != COMPACT_PWL_STATUS.VALID @warn( - "The cost data provided is not in compact form. Will atempt to convert. Errors may occur." + "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 - slopes = PSY.get_slopes(data) - # First element of the return is the average cost at P_min. - # Shouldn't be passed for convexity check - is_convex = _slope_convexity_check(slopes[2:end]) - break_points = map(x -> last(x), data) ./ base_power + cost_is_convex = PSY.is_convex(data) + break_points = PSY.get_x_coords(data) ./ base_power # TODO should this be get_x_lengths? _add_pwl_variables!(container, T, name, t, data) _add_pwl_constraint!(container, component, U(), break_points, sos_val, t) - if !is_convex + if !cost_is_convex _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t) end pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt) @@ -622,7 +605,7 @@ end function _add_pwl_term!( container::OptimizationContainer, component::T, - cost_data::Vector{PSY.VariableCost{Vector{Tuple{Float64, Float64}}}}, + cost_data::AbstractVector{PSY.PiecewiseLinearPointData}, ::U, ::V, ) where {T <: PSY.Component, U <: VariableType, V <: AbstractServiceFormulation} @@ -636,15 +619,11 @@ function _add_pwl_term!( pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end]) sos_val = _get_sos_value(container, V, component) for t in time_steps - data = PSY.get_cost(cost_data[t]) - # Shouldn't be passed for convexity check - is_convex = false - break_points = map(x -> last(x), data) ./ base_power + data = cost_data[t] + break_points = PSY.get_x_coords(data) ./ base_power _add_pwl_variables!(container, T, name, t, data) _add_pwl_constraint!(container, component, U(), break_points, sos_val, t) - if !is_convex - _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t) - end + _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t) pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt) pwl_cost_expressions[t] = pwl_cost end @@ -657,7 +636,7 @@ Add PWL cost terms for data coming from a constant PWL cost function function _add_pwl_term!( container::OptimizationContainer, component::T, - data::Vector{NTuple{2, Float64}}, + data::PSY.PiecewiseLinearPointData, ::U, ::V, ) where {T <: PSY.Component, U <: VariableType, V <: AbstractDeviceFormulation} @@ -676,25 +655,22 @@ function _add_pwl_term!( # 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 atempt to convert. Errors may occur." + "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 - slopes = PSY.get_slopes(data) - # First element of the return is the average cost at P_min. - # Shouldn't be passed for convexity check - is_convex = _slope_convexity_check(slopes[2:end]) + cost_is_convex = PSY.is_convex(data) + break_points = PSY.get_x_coords(data) ./ base_power time_steps = get_time_steps(container) pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end]) - break_points = map(x -> last(x), data) ./ base_power sos_val = _get_sos_value(container, V, component) for t in time_steps _add_pwl_variables!(container, T, name, t, data) _add_pwl_constraint!(container, component, U(), break_points, sos_val, t) - if !is_convex + if !cost_is_convex _add_pwl_sos_constraint!(container, component, U(), break_points, sos_val, t) end pwl_cost = _get_pwl_cost_expression(container, component, t, data, multiplier * dt) @@ -706,7 +682,7 @@ end function _add_pwl_term!( container::OptimizationContainer, component::T, - data::Vector{NTuple{2, Float64}}, + data::PSY.PiecewiseLinearPointData, ::U, ::V, ) where {T <: PSY.ThermalGen, U <: VariableType, V <: ThermalDispatchNoMin} @@ -717,7 +693,7 @@ function _add_pwl_term!( @debug "PWL cost function detected for device $(component_name) using $V" base_power = get_base_power(container) slopes = PSY.get_slopes(data) - if any(slopes .< 0) || !_slope_convexity_check(slopes[2:end]) + if any(slopes .< 0) || !PSY.is_convex(data) throw( IS.InvalidValue( "The PWL cost data provided for generator $(component_name) is not compatible with $U.", @@ -732,16 +708,15 @@ function _add_pwl_term!( if slopes[1] != 0.0 @debug "PWL has no 0.0 intercept for generator $(component_name)" - # adds a first intercept a x = 0.0 and Y below the intercept of the first tuple to make convex equivalent - first_pair = data[1] - intercept_point = (0.0, first_pair[2] - COST_EPSILON) - data = vcat(intercept_point, data) - @assert _slope_convexity_check(slopes) + # adds a first intercept a x = 0.0 and y below the intercept of the first tuple to make convex equivalent + intercept_point = (x = 0.0, y = first(data).y - COST_EPSILON) + data = PSY.PiecewiseLinearPointData(vcat(intercept_point, get_points(data))) + @assert PSY.is_convex(slopes) end time_steps = get_time_steps(container) pwl_cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end]) - break_points = map(x -> last(x), data) ./ base_power + break_points = PSY.get_x_coords(data) ./ base_power sos_val = _get_sos_value(container, V, component) for t in time_steps _add_pwl_variables!(container, T, component_name, t, data) @@ -757,11 +732,12 @@ function _add_pwl_variables!( ::Type{T}, component_name::String, time_period::Int, - cost_data::Vector{NTuple{2, Float64}}, + cost_data::PSY.PiecewiseLinearPointData, ) where {T <: PSY.Component} var_container = lazy_container_addition!(container, PieceWiseLinearCostVariable(), T) - pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data)) - for i in 1:length(cost_data) + # length(PiecewiseLinearPointData) gets number of segments, here we want number of points + pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data)+1) + for i in 1:length(cost_data)+1 pwlvars[i] = var_container[(component_name, i, time_period)] = JuMP.@variable( get_jump_model(container), @@ -850,16 +826,17 @@ function _get_pwl_cost_expression( container::OptimizationContainer, component::T, time_period::Int, - cost_data::Vector{NTuple{2, Float64}}, + cost_data::PSY.PiecewiseLinearPointData, multiplier::Float64, ) where {T <: PSY.Component} name = PSY.get_name(component) pwl_var_container = get_variable(container, PieceWiseLinearCostVariable(), T) gen_cost = JuMP.AffExpr(0.0) + cost_data = PSY.get_points(cost_data) for i in 1:length(cost_data) JuMP.add_to_expression!( gen_cost, - cost_data[i][1] * multiplier * pwl_var_container[(name, i, time_period)], + cost_data[i].y * multiplier * pwl_var_container[(name, i, time_period)], ) end return gen_cost @@ -874,16 +851,18 @@ function _get_no_load_cost( end function _convert_to_compact_variable_cost( - var_cost::Vector{NTuple{2, Float64}}, - no_load_cost::Float64, + var_cost::PSY.PiecewiseLinearPointData, p_min::Float64, + no_load_cost::Float64, ) - return [(c - no_load_cost, pp - p_min) for (c, pp) in var_cost] + points = PSY.get_points(var_cost) + new_points = [(pp - p_min, c - no_load_cost) for (pp, c) in points] + return PSY.PiecewiseLinearPointData(new_points) end -function _convert_to_compact_variable_cost(var_cost::Vector{NTuple{2, Float64}}) - no_load_cost, p_min = var_cost[1] - return _convert_to_compact_variable_cost(var_cost, no_load_cost, p_min) +function _convert_to_compact_variable_cost(var_cost::PSY.PiecewiseLinearPointData) + 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 _add_proportional_term!( diff --git a/src/devices_models/devices/thermal_generation.jl b/src/devices_models/devices/thermal_generation.jl index 79ace79bed..f86602ebfd 100644 --- a/src/devices_models/devices/thermal_generation.jl +++ b/src/devices_models/devices/thermal_generation.jl @@ -115,11 +115,17 @@ function no_load_cost(cost::Union{PSY.ThreePartCost, PSY.TwoPartCost}, S::OnVari return no_load_cost(PSY.get_variable(cost), S, T, U) end -no_load_cost(cost::PSY.VariableCost{Vector{NTuple{2, Float64}}}, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = first(PSY.get_cost(cost))[1] -no_load_cost(cost::PSY.VariableCost{Float64}, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_cost(cost) * PSY.get_active_power_limits(d).min * PSY.get_base_power(d) - -function no_load_cost(cost::PSY.VariableCost{Tuple{Float64, Float64}}, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) - return (PSY.get_cost(cost)[1] * (PSY.get_active_power_limits(d).min)^2 + PSY.get_cost(cost)[2] * PSY.get_active_power_limits(d).min)* PSY.get_base_power(d) +# TODO given the old implementations, these functions seem to get the cost at *minimum* load, not *zero* load. Is that correct? +no_load_cost(cost::PSY.PiecewiseLinearPointData, ::OnVariable, ::PSY.ThermalGen, ::AbstractThermalFormulation) = last(first(PSY.get_points(cost))) +no_load_cost(cost::PSY.LinearFunctionData, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) = PSY.get_proportional_term(cost) * PSY.get_active_power_limits(d).min * PSY.get_base_power(d) + +function no_load_cost(cost::PSY.QuadraticFunctionData, ::OnVariable, d::PSY.ThermalGen, ::AbstractThermalFormulation) + min_power = PSY.get_active_power_limits(d).min + evaluated = LinearAlgebra.dot( + [PSY.get_quadratic_term(cost), PSY.get_proportional_term(cost), PSY.get_constant_term(cost)], + [min_power^2, min_power, 1] + ) + return evaluated * PSY.get_base_power(d) end #! format: on diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl index bc81e0259c..9ffcdbcb29 100644 --- a/src/parameters/update_parameters.jl +++ b/src/parameters/update_parameters.jl @@ -571,14 +571,14 @@ function _update_pwl_cost_expression( ::Type{T}, component_name::String, time_period::Int, - cost_data::Vector{NTuple{2, Float64}}, + cost_data::PSY.PiecewiseLinearPointData, ) where {T <: PSY.Component} pwl_var_container = get_variable(container, PieceWiseLinearCostVariable(), T) resolution = get_resolution(container) dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR gen_cost = JuMP.AffExpr(0.0) slopes = PSY.get_slopes(cost_data) - upb = PSY.get_breakpoint_upperbounds(cost_data) + upb = PSY.get_x_lengths(cost_data) for i in 1:length(cost_data) JuMP.add_to_expression!( gen_cost, @@ -614,9 +614,9 @@ end function update_variable_cost!( container::OptimizationContainer, - parameter_array::DenseAxisArray{Vector{NTuple{2, Float64}}}, + parameter_array::DenseAxisArray{PSY.PiecewiseLinearPointData}, parameter_multiplier::JuMPFloatArray, - ::CostFunctionAttributes{Vector{NTuple{2, Float64}}}, + ::CostFunctionAttributes{PSY.PiecewiseLinearPointData}, component::T, time_period::Int, ) where {T <: PSY.Component} diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index b50f676fbe..3f238164be 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -85,10 +85,11 @@ end function _validate_compact_pwl_data( min::Float64, max::Float64, - data::Vector{Tuple{Float64, Float64}}, + data::PSY.PiecewiseLinearPointData, base_power::Float64, ) - if isapprox(max - min, data[end][2] / base_power) && iszero(data[1][2]) + data = PSY.get_points(data) + if isapprox(max - min, last(data).x / base_power) && iszero(first(data).x) return COMPACT_PWL_STATUS.VALID else return COMPACT_PWL_STATUS.INVALID @@ -97,7 +98,7 @@ end function validate_compact_pwl_data( d::PSY.ThermalGen, - data::Vector{Tuple{Float64, Float64}}, + data::PSY.PiecewiseLinearPointData, base_power::Float64, ) min = PSY.get_active_power_limits(d).min @@ -107,7 +108,7 @@ end function validate_compact_pwl_data( d::PSY.Component, - ::Vector{Tuple{Float64, Float64}}, + ::PSY.PiecewiseLinearPointData, ::Float64, ) @warn "Validation of compact pwl data is not implemented for $(typeof(d))." From 106e77b5e43752a3a57e4b0e5d32c414a11a82c7 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Mon, 26 Feb 2024 02:42:36 -0700 Subject: [PATCH 3/6] Finish accommodating FunctionData --- .../devices/common/objective_functions.jl | 15 ++++++------- src/parameters/update_parameters.jl | 21 ++++++++++++------- 2 files changed, 21 insertions(+), 15 deletions(-) diff --git a/src/devices_models/devices/common/objective_functions.jl b/src/devices_models/devices/common/objective_functions.jl index c6d8ec4d8f..a320cf551d 100644 --- a/src/devices_models/devices/common/objective_functions.jl +++ b/src/devices_models/devices/common/objective_functions.jl @@ -167,7 +167,7 @@ function _add_variable_cost_to_objective!( set_parameter!( parameter_container, jump_model, - Tuple.(PSY.get_points(variable_cost_forecast_values[t])), + PSY.get_raw_data(variable_cost_forecast_values[t]), component_name, t, ) @@ -224,7 +224,7 @@ function _add_variable_cost_to_objective!( set_parameter!( parameter_container, jump_model, - Tuple.(PSY.get_points(variable_cost_forecast_values[t])), + PSY.get_raw_data(variable_cost_forecast_values[t]), component_name, t, ) @@ -314,7 +314,7 @@ function _get_cost_function_parameter_container( U, sos_val, uses_compact_power(component, V()), - cost_type, + PSY.get_raw_data_type(cost_type), container_axes..., ) end @@ -335,7 +335,7 @@ function _add_service_bid_cost!( start_time = initial_time, len = length(time_steps), ) - forecast_data_values = PSY.get_cost.(TimeSeries.values(forecast_data)) .* base_power + forecast_data_values = PSY.get_raw_data.(TimeSeries.values(forecast_data)) .* base_power reserve_variable = get_variable(container, U(), T, PSY.get_name(service)) component_name = PSY.get_name(component) for t in time_steps @@ -545,7 +545,8 @@ function _add_pwl_term!( time_steps = get_time_steps(container) cost_expressions = Vector{JuMP.AffExpr}(undef, time_steps[end]) for t in time_steps - proportional_value = PSY.get_proportional_term(cost_data[t]) * multiplier * base_power * dt + proportional_value = + PSY.get_proportional_term(cost_data[t]) * multiplier * base_power * dt cost_expressions[t] = _add_proportional_term!(container, U(), component, proportional_value, t) end @@ -736,8 +737,8 @@ function _add_pwl_variables!( ) where {T <: PSY.Component} var_container = lazy_container_addition!(container, PieceWiseLinearCostVariable(), T) # length(PiecewiseLinearPointData) gets number of segments, here we want number of points - pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data)+1) - for i in 1:length(cost_data)+1 + pwlvars = Array{JuMP.VariableRef}(undef, length(cost_data) + 1) + for i in 1:(length(cost_data) + 1) pwlvars[i] = var_container[(component_name, i, time_period)] = JuMP.@variable( get_jump_model(container), diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl index 9ffcdbcb29..09109e8575 100644 --- a/src/parameters/update_parameters.jl +++ b/src/parameters/update_parameters.jl @@ -543,10 +543,8 @@ function _update_parameter_values!( ) variable_cost_forecast_values = TimeSeries.values(ts_vector) for (t, value) in enumerate(variable_cost_forecast_values) - if attributes.uses_compact_power - value, _ = _convert_variable_cost(value) - end - _set_param_value!(parameter_array, PSY.get_cost(value), name, t) + # TODO removed an apparently unused block of code here? + _set_param_value!(parameter_array, PSY.get_raw_data(value), name, t) update_variable_cost!( container, parameter_array, @@ -600,7 +598,8 @@ function update_variable_cost!( dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR base_power = get_base_power(container) component_name = PSY.get_name(component) - cost_data = parameter_array[component_name, time_period] + cost_data = parameter_array[component_name, time_period] # TODO is this a new-style cost? + println(typeof(cost_data)) # TODO REMOVE if iszero(cost_data) return end @@ -614,9 +613,9 @@ end function update_variable_cost!( container::OptimizationContainer, - parameter_array::DenseAxisArray{PSY.PiecewiseLinearPointData}, + parameter_array::DenseAxisArray{Vector{NTuple{2, Float64}}}, parameter_multiplier::JuMPFloatArray, - ::CostFunctionAttributes{PSY.PiecewiseLinearPointData}, + ::CostFunctionAttributes{Vector{NTuple{2, Float64}}}, component::T, time_period::Int, ) where {T <: PSY.Component} @@ -627,7 +626,13 @@ function update_variable_cost!( end mult_ = parameter_multiplier[component_name, time_period] gen_cost = - _update_pwl_cost_expression(container, T, component_name, time_period, cost_data) + _update_pwl_cost_expression( + container, + T, + component_name, + time_period, + PSY.PiecewiseLinearPointData(cost_data), + ) add_to_objective_variant_expression!(container, mult_ * gen_cost) set_expression!(container, ProductionCostExpression, gen_cost, component, time_period) return From 1185950fd76896a8b1d59960a75b8224c0ca87b4 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Mon, 26 Feb 2024 10:10:29 -0700 Subject: [PATCH 4/6] Remove debug print --- src/parameters/update_parameters.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl index 09109e8575..d347d8050c 100644 --- a/src/parameters/update_parameters.jl +++ b/src/parameters/update_parameters.jl @@ -599,7 +599,6 @@ function update_variable_cost!( base_power = get_base_power(container) component_name = PSY.get_name(component) cost_data = parameter_array[component_name, time_period] # TODO is this a new-style cost? - println(typeof(cost_data)) # TODO REMOVE if iszero(cost_data) return end From 9376f342173c41f5ebe056a5be59a62fe6dfff03 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:40:15 -0700 Subject: [PATCH 5/6] Add alias `get_breakpoint_upper_bounds` for `PSY.get_x_lengths` --- src/devices_models/devices/common/objective_functions.jl | 2 +- src/parameters/update_parameters.jl | 2 +- src/utils/powersystems_utils.jl | 2 ++ 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/src/devices_models/devices/common/objective_functions.jl b/src/devices_models/devices/common/objective_functions.jl index a320cf551d..9dd9aade79 100644 --- a/src/devices_models/devices/common/objective_functions.jl +++ b/src/devices_models/devices/common/objective_functions.jl @@ -591,7 +591,7 @@ function _add_pwl_term!( @debug uses_compact_power(component, V()) compact_status name T V end cost_is_convex = PSY.is_convex(data) - break_points = PSY.get_x_coords(data) ./ base_power # TODO should this be get_x_lengths? + break_points = PSY.get_x_coords(data) ./ base_power # TODO should this be get_x_lengths/get_breakpoint_upper_bounds? _add_pwl_variables!(container, T, name, t, data) _add_pwl_constraint!(container, component, U(), break_points, sos_val, t) if !cost_is_convex diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl index d347d8050c..d2ecfe04ef 100644 --- a/src/parameters/update_parameters.jl +++ b/src/parameters/update_parameters.jl @@ -576,7 +576,7 @@ function _update_pwl_cost_expression( dt = Dates.value(Dates.Second(resolution)) / SECONDS_IN_HOUR gen_cost = JuMP.AffExpr(0.0) slopes = PSY.get_slopes(cost_data) - upb = PSY.get_x_lengths(cost_data) + upb = get_breakpoint_upper_bounds(cost_data) for i in 1:length(cost_data) JuMP.add_to_expression!( gen_cost, diff --git a/src/utils/powersystems_utils.jl b/src/utils/powersystems_utils.jl index 3f238164be..7b0bc5befc 100644 --- a/src/utils/powersystems_utils.jl +++ b/src/utils/powersystems_utils.jl @@ -114,3 +114,5 @@ function validate_compact_pwl_data( @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 8126fa1d44b30d2bde2602585b4e796498b2e952 Mon Sep 17 00:00:00 2001 From: GabrielKS <23368820+GabrielKS@users.noreply.github.com> Date: Mon, 26 Feb 2024 15:40:47 -0700 Subject: [PATCH 6/6] Reinstate untested code --- src/parameters/update_parameters.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/parameters/update_parameters.jl b/src/parameters/update_parameters.jl index d2ecfe04ef..05b36167e4 100644 --- a/src/parameters/update_parameters.jl +++ b/src/parameters/update_parameters.jl @@ -543,6 +543,10 @@ function _update_parameter_values!( ) variable_cost_forecast_values = TimeSeries.values(ts_vector) for (t, value) in enumerate(variable_cost_forecast_values) + if attributes.uses_compact_power + # TODO implement this + value, _ = _convert_variable_cost(value) + end # TODO removed an apparently unused block of code here? _set_param_value!(parameter_array, PSY.get_raw_data(value), name, t) update_variable_cost!(