diff --git a/Project.toml b/Project.toml index 3757555..b7f1c9c 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "StorageSystemsSimulations" uuid = "e2f1a126-19d0-4674-9252-42b2384f8e3c" authors = ["Jose Daniel Lara, Rodrigo Henriquez-Auba, Sourabh Dalvi"] -version = "0.9.0" +version = "0.10.0" [deps] DataStructures = "864edb3b-99cc-5e75-8d2d-829cb0a9cfe8" @@ -18,10 +18,10 @@ PowerSystems = "bcd98974-b02a-5e2f-9ee0-a103f5c450dd" Dates = "1" DataStructures = "~0.18" DocStringExtensions = "~0.8, ~0.9" -InfrastructureSystems = "1" +InfrastructureSystems = "2" JuMP = "1" LinearAlgebra = "1" MathOptInterface = "1" -PowerSimulations = "^0.27" -PowerSystems = "3" +PowerSimulations = "^0.28" +PowerSystems = "4" julia = "^1.6" diff --git a/docs/Project.toml b/docs/Project.toml index f6b0acf..3094652 100644 --- a/docs/Project.toml +++ b/docs/Project.toml @@ -13,4 +13,4 @@ StorageSystemsSimulations = "e2f1a126-19d0-4674-9252-42b2384f8e3c" [compat] Documenter = "1" -InfrastructureSystems = "1" +InfrastructureSystems = "2" diff --git a/docs/src/formulation_library/StorageDispatchWithReserves.md b/docs/src/formulation_library/StorageDispatchWithReserves.md index 942ba91..fa6b319 100644 --- a/docs/src/formulation_library/StorageDispatchWithReserves.md +++ b/docs/src/formulation_library/StorageDispatchWithReserves.md @@ -6,17 +6,17 @@ StorageDispatchWithReserves ## Attributes - - `"reservation"`: Forces the battery to operate exclusively on charge or discharge mode through the entire operation interval. We recommend setting this to false for models with relatively large resolutions (e.g., 1-Hr) since the storage can take simultaneous charge or discharge positions on average over the period. - - `"cycling_limits"`: This limits the battery's energy cycling. The calculation uses the total energy charge/discharge and the number of cycles. Currently, the formulation only supports a fixed value per operation period. Additional variables for [`StorageChargeCyclingSlackVariable`](@ref) and [`StorageDischargeCyclingSlackVariable`](@ref) are included in the model if `use_slacks` is set to `true`. - - `"energy_target"`: Set a target at the end of the model horizon for the state of charge. Currently, the formulation only supports a fixed value per operation period. Additional variables for [`StorageEnergyShortageVariable`](@ref) and [`StorageEnergySurplusVariable`](@ref) are included in the model if `use_slacks` is set to `true`. + - `"reservation"`: Forces the storage to operate exclusively on charge or discharge mode through the entire operation interval. We recommend setting this to false for models with relatively longer time resolutions (e.g., 1-Hr) since the storage can take simultaneous charge or discharge positions on average over the period. + - `"cycling_limits"`: This limits the storage's energy cycling. A single charging (discharging) cycle is fully charging (discharging) the storage once. The calculation uses the total energy charge/discharge and the number of cycles. Currently, the formulation only supports a fixed value per operation period. Additional variables for [`StorageChargeCyclingSlackVariable`](@ref) and [`StorageDischargeCyclingSlackVariable`](@ref) are included in the model if `use_slacks` is set to `true`. + - `"energy_target"`: Set a target at the end of the model horizon for the storage's state of charge. Currently, the formulation only supports a fixed value per operation period. Additional variables for [`StorageEnergyShortageVariable`](@ref) and [`StorageEnergySurplusVariable`](@ref) are included in the model if `use_slacks` is set to `true`. !!! warning - Combining the cycle limits and energy target attributes is not recommended. Both - attributes impose constraints on the energy; there is no guarantee that the constraints can be satisfied simultaneously. + Combining cycle limits and energy target attributes is not recommended. Both + attributes impose constraints on energy. There is no guarantee that the constraints can be satisfied simultaneously. - `"complete_coverage"`: This attribute implements constraints that require the battery to cover the sum of all the ancillary services it participates in simultaneously. It is equivalent to holding energy in case all the services get deployed simultaneously. This constraint is added to the constraints that cover each service independently and corresponds to a more conservative operation regime. - - `"regularization"`: This attribute smooths the charge/discharge profiles to avoid bang-bang solutions via a penalty on the absolute value of the intra-temporal variations of the charge and discharge power. The model can stall in models with large amounts of curtailment or long periods with negative or zero prices due to numerical degeneracy. The regularization term is scaled by the power limits to normalize the term and avoid additional penalties to larger storage units. + - `"regularization"`: This attribute smooths the charge/discharge profiles to avoid bang-bang solutions via a penalty on the absolute value of the intra-temporal variations of the charge and discharge power. Solving for optimal storage dispatch can stall in models with large amounts of curtailment or long periods with negative or zero prices due to numerical degeneracy. The regularization term is scaled by the storage device's power limits to normalize the term and avoid additional penalties to larger storage units. !!! danger diff --git a/docs/src/index.md b/docs/src/index.md index 4dbaa6f..047c6e2 100644 --- a/docs/src/index.md +++ b/docs/src/index.md @@ -7,16 +7,16 @@ CurrentModule = StorageSystemsSimulations ## Overview `StorageSimulations.jl` is a `PowerSimulations.jl` extension to support formulations and models -related to energ storage. +related to energy storage. -Operational Storage Model can have multiple combinations of different restrictions. For instance, +An Operational Storage Model can have multiple combinations of different restrictions. For instance, it might be relevant to a study to consider cycling limits or employ energy targets coming from a planning model. To manage all these variations `StorageSimulations.jl` heavily uses the `DeviceModel` attributes feature. For example, the formulation `StorageDispatchWithReserves` can be parametrized as follows: ```julia DeviceModel( - StorageType, # E.g. BatteryEMS or GenericStorage + StorageType, # E.g. EnergyReservoirStorage StorageDispatchWithReserves; attributes=Dict( "reservation" => true, diff --git a/docs/src/tutorials/single_stage_model.md b/docs/src/tutorials/single_stage_model.md index 2c03c41..641bf6e 100644 --- a/docs/src/tutorials/single_stage_model.md +++ b/docs/src/tutorials/single_stage_model.md @@ -33,7 +33,7 @@ set_available!(orcd, false) ``` ```@example op_problem -batt = get_component(BatteryEMS, c_sys5_bat, "Bat2") +batt = get_component(EnergyReservoirStorage, c_sys5_bat, "Bat2") operation_cost = get_operation_cost(batt) ``` @@ -48,7 +48,7 @@ set_device_model!(template_uc, Line, StaticBranch) ```@example op_problem storage_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict( "reservation" => true, diff --git a/src/StorageSystemsSimulations.jl b/src/StorageSystemsSimulations.jl index e882b20..2c054b2 100644 --- a/src/StorageSystemsSimulations.jl +++ b/src/StorageSystemsSimulations.jl @@ -55,6 +55,7 @@ const PSI = PowerSimulations const PSY = PowerSystems const PM = PSI.PM const IS = InfrastructureSystems +const ISOPT = InfrastructureSystems.Optimization using DocStringExtensions @template (FUNCTIONS, METHODS) = """ diff --git a/src/core/feedforward.jl b/src/core/feedforward.jl index 6a87110..9a73b2d 100644 --- a/src/core/feedforward.jl +++ b/src/core/feedforward.jl @@ -12,7 +12,7 @@ struct EnergyTargetFeedforward <: PSI.AbstractAffectFeedforward affected_values::Vector{DataType}, target_period::Int, penalty_cost::Float64, - meta=PSI.CONTAINER_KEY_EMPTY_META, + meta=ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T} values_vector = Vector{PSI.VariableKey}(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) @@ -135,7 +135,7 @@ struct EnergyLimitFeedforward <: PSI.AbstractAffectFeedforward source::Type{T}, affected_values::Vector{DataType}, number_of_periods::Int, - meta=PSI.CONTAINER_KEY_EMPTY_META, + meta=ISOPT.CONTAINER_KEY_EMPTY_META, ) where {T} values_vector = Vector{PSI.VariableKey}(undef, length(affected_values)) for (ix, v) in enumerate(affected_values) diff --git a/src/core/formulations.jl b/src/core/formulations.jl index 9670a9a..d47d772 100644 --- a/src/core/formulations.jl +++ b/src/core/formulations.jl @@ -10,7 +10,7 @@ The formulation supports the following attributes. See Documentation for more de ```julia DeviceModel( - StorageType, # E.g. BatteryEMS or GenericStorage + StorageType, # E.g. EnergyReservoirStorage or GenericStorage StorageDispatchWithReserves; attributes=Dict( "reservation" => true, diff --git a/src/storage_constructor.jl b/src/storage_constructor.jl index 7d1b7dc..5c9f4b1 100644 --- a/src/storage_constructor.jl +++ b/src/storage_constructor.jl @@ -219,7 +219,7 @@ function PSI.construct_device!( model::PSI.DeviceModel{St, D}, network_model::PSI.NetworkModel{S}, ) where {St <: PSY.Storage, D <: StorageDispatchWithReserves, S <: PM.AbstractPowerModel} - devices = PSI.get_available_components(St, sys) + devices = PSI.get_available_components(model, sys) _active_power_variables_and_expressions(container, devices, model, network_model) PSI.add_variables!(container, PSI.ReactivePowerVariable, devices, D()) @@ -252,7 +252,7 @@ function PSI.construct_device!( model::PSI.DeviceModel{St, D}, network_model::PSI.NetworkModel{S}, ) where {St <: PSY.Storage, D <: StorageDispatchWithReserves, S <: PM.AbstractPowerModel} - devices = PSI.get_available_components(St, sys) + devices = PSI.get_available_components(model, sys) _active_power_and_energy_bounds(container, devices, model, network_model) PSI.add_constraints!( @@ -318,7 +318,7 @@ function PSI.construct_device!( D <: StorageDispatchWithReserves, S <: PM.AbstractActivePowerModel, } - devices = PSI.get_available_components(St, sys) + devices = PSI.get_available_components(model, sys) _active_power_variables_and_expressions(container, devices, model, network_model) if PSI.get_attribute(model, "regularization") @@ -345,7 +345,7 @@ function PSI.construct_device!( D <: StorageDispatchWithReserves, S <: PM.AbstractActivePowerModel, } - devices = PSI.get_available_components(St, sys) + devices = PSI.get_available_components(model, sys) _active_power_and_energy_bounds(container, devices, model, network_model) # Energy Balanace limits diff --git a/src/storage_models.jl b/src/storage_models.jl index d4b3a4e..15da470 100644 --- a/src/storage_models.jl +++ b/src/storage_models.jl @@ -22,9 +22,9 @@ PSI.get_variable_multiplier(::PSI.ReactivePowerVariable, d::Type{<:PSY.Storage}, ############## EnergyVariable, Storage #################### PSI.get_variable_binary(::PSI.EnergyVariable, ::Type{<:PSY.Storage}, ::AbstractStorageFormulation) = false -PSI.get_variable_upper_bound(::PSI.EnergyVariable, d::PSY.Storage, ::AbstractStorageFormulation) = PSY.get_state_of_charge_limits(d).max -PSI.get_variable_lower_bound(::PSI.EnergyVariable, d::PSY.Storage, ::AbstractStorageFormulation) = PSY.get_state_of_charge_limits(d).min -PSI.get_variable_warm_start_value(::PSI.EnergyVariable, d::PSY.Storage, ::AbstractStorageFormulation) = PSY.get_initial_energy(d) +PSI.get_variable_upper_bound(::PSI.EnergyVariable, d::PSY.Storage, ::AbstractStorageFormulation) = PSY.get_storage_level_limits(d).max * PSY.get_storage_capacity(d) * PSY.get_conversion_factor(d) +PSI.get_variable_lower_bound(::PSI.EnergyVariable, d::PSY.Storage, ::AbstractStorageFormulation) = PSY.get_storage_level_limits(d).min * PSY.get_storage_capacity(d) * PSY.get_conversion_factor(d) +PSI.get_variable_warm_start_value(::PSI.EnergyVariable, d::PSY.Storage, ::AbstractStorageFormulation) = PSY.get_initial_storage_capacity_level(d) * PSY.get_storage_capacity(d) * PSY.get_conversion_factor(d) ############## ReservationVariable, Storage #################### PSI.get_variable_binary(::PSI.ReservationVariable, ::Type{<:PSY.Storage}, ::AbstractStorageFormulation) = true @@ -63,14 +63,14 @@ PSI.objective_function_multiplier(::PSI.VariableType, ::AbstractStorageFormulati PSI.objective_function_multiplier(::StorageEnergySurplusVariable, ::AbstractStorageFormulation)=PSI.OBJECTIVE_FUNCTION_POSITIVE PSI.objective_function_multiplier(::StorageEnergyShortageVariable, ::AbstractStorageFormulation)=PSI.OBJECTIVE_FUNCTION_POSITIVE -PSI.proportional_cost(cost::PSY.StorageManagementCost, ::StorageEnergySurplusVariable, ::PSY.BatteryEMS, ::AbstractStorageFormulation)=PSY.get_energy_surplus_cost(cost) -PSI.proportional_cost(cost::PSY.StorageManagementCost, ::StorageEnergyShortageVariable, ::PSY.BatteryEMS, ::AbstractStorageFormulation)=PSY.get_energy_shortage_cost(cost) -PSI.proportional_cost(::PSY.StorageManagementCost, ::StorageChargeCyclingSlackVariable, ::PSY.BatteryEMS, ::AbstractStorageFormulation)=CYCLE_VIOLATION_COST -PSI.proportional_cost(::PSY.StorageManagementCost, ::StorageDischargeCyclingSlackVariable, ::PSY.BatteryEMS, ::AbstractStorageFormulation)=CYCLE_VIOLATION_COST +PSI.proportional_cost(cost::PSY.StorageCost, ::StorageEnergySurplusVariable, ::PSY.EnergyReservoirStorage, ::AbstractStorageFormulation)=PSY.get_energy_surplus_cost(cost) +PSI.proportional_cost(cost::PSY.StorageCost, ::StorageEnergyShortageVariable, ::PSY.EnergyReservoirStorage, ::AbstractStorageFormulation)=PSY.get_energy_shortage_cost(cost) +PSI.proportional_cost(::PSY.StorageCost, ::StorageChargeCyclingSlackVariable, ::PSY.EnergyReservoirStorage, ::AbstractStorageFormulation)=CYCLE_VIOLATION_COST +PSI.proportional_cost(::PSY.StorageCost, ::StorageDischargeCyclingSlackVariable, ::PSY.EnergyReservoirStorage, ::AbstractStorageFormulation)=CYCLE_VIOLATION_COST -PSI.variable_cost(cost::PSY.StorageManagementCost, ::PSI.ActivePowerOutVariable, ::PSY.Storage, ::AbstractStorageFormulation)=PSY.get_variable(cost) -PSI.variable_cost(cost::PSY.StorageManagementCost, ::PSI.ActivePowerInVariable, ::PSY.Storage, ::AbstractStorageFormulation)=PSY.get_variable(cost) +PSI.variable_cost(cost::PSY.StorageCost, ::PSI.ActivePowerOutVariable, ::PSY.Storage, ::AbstractStorageFormulation)=PSY.get_discharge_variable_cost(cost) +PSI.variable_cost(cost::PSY.StorageCost, ::PSI.ActivePowerInVariable, ::PSY.Storage, ::AbstractStorageFormulation)=PSY.get_charge_variable_cost(cost) ######################## Parameters ################################################## @@ -85,13 +85,12 @@ PSI.get_variable_lower_bound(::StorageRegularizationVariable, d::PSY.Storage, :: #! format: on function PSI.variable_cost( - cost::PSY.StorageManagementCost, + cost::PSY.StorageCost, ::StorageRegularizationVariable, ::PSY.Storage, ::AbstractStorageFormulation, ) - max_val = max(REG_COST, cost.variable.cost[2] * REG_COST) - return PSY.VariableCost(max_val) + return PSY.CostCurve(PSY.LinearCurve(REG_COST), PSY.UnitSystem.SYSTEM_BASE) end function PSI.get_default_time_series_names( @@ -102,20 +101,7 @@ function PSI.get_default_time_series_names( end function PSI.get_default_attributes( - ::Type{PSY.GenericBattery}, - ::Type{T}, -) where {T <: AbstractStorageFormulation} - return Dict{String, Any}( - "reservation" => true, - "cycling_limits" => false, - "energy_target" => false, - "complete_coverage" => false, - "regularization" => false, - ) -end - -function PSI.get_default_attributes( - ::Type{PSY.BatteryEMS}, + ::Type{PSY.EnergyReservoirStorage}, ::Type{T}, ) where {T <: AbstractStorageFormulation} return Dict{String, Any}( @@ -137,7 +123,10 @@ PSI.initial_condition_default( ::PSI.InitialEnergyLevel, d::PSY.Storage, ::AbstractStorageFormulation, -) = PSY.get_initial_energy(d) +) = + PSY.get_initial_storage_capacity_level(d) * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) PSI.initial_condition_variable( ::PSI.InitialEnergyLevel, d::PSY.Storage, @@ -300,7 +289,15 @@ function PSI.get_min_max_limits( ::Type{StateofChargeLimitsConstraint}, ::Type{<:AbstractStorageFormulation}, ) - return PSY.get_state_of_charge_limits(d) + min_max_limits = ( + min=PSY.get_storage_level_limits(d).min * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d), + max=PSY.get_storage_level_limits(d).max * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d), + ) + return min_max_limits end function PSI.add_constraints!( @@ -950,7 +947,14 @@ function PSI.add_constraints!( ci_name = PSY.get_name(storage) inv_efficiency = 1.0 / PSY.get_efficiency(storage).out eff_in = PSY.get_efficiency(storage).in - soc_limits = PSY.get_state_of_charge_limits(storage) + soc_limits = ( + min=PSY.get_storage_level_limits(storage).min * + PSY.get_storage_capacity(storage) * + PSY.get_conversion_factor(storage), + max=PSY.get_storage_level_limits(storage).max * + PSY.get_storage_capacity(storage) * + PSY.get_conversion_factor(storage), + ) for service in PSY.get_services(storage) sustained_time = PSY.get_sustained_time(service) num_periods = sustained_time / Dates.value(Dates.Second(resolution)) @@ -1092,7 +1096,14 @@ function PSI.add_constraints!( ci_name = PSY.get_name(storage) inv_efficiency = 1.0 / PSY.get_efficiency(storage).out eff_in = PSY.get_efficiency(storage).in - soc_limits = PSY.get_state_of_charge_limits(storage) + soc_limits = ( + min=PSY.get_storage_level_limits(storage).min * + PSY.get_storage_capacity(storage) * + PSY.get_conversion_factor(storage), + max=PSY.get_storage_level_limits(storage).max * + PSY.get_storage_capacity(storage) * + PSY.get_conversion_factor(storage), + ) expr_up_discharge = Set() expr_dn_charge = Set() for service in PSY.get_services(storage) @@ -1235,37 +1246,13 @@ function PSI.add_constraints!( return end -function PSI.add_constraints!( - ::PSI.OptimizationContainer, - ::Type{StateofChargeTargetConstraint}, - devices::IS.FlattenIteratorWrapper{V}, - model::PSI.DeviceModel{V, StorageDispatchWithReserves}, - network_model::PSI.NetworkModel{X}, -) where {V <: PSY.GenericBattery, X <: PM.AbstractPowerModel} - error("$V is not supported for $(PSY.GenericBattery). \ - Set the attribute energy_target to false in the device model") - return -end - -function PSI.add_constraints!( - ::PSI.OptimizationContainer, - ::Type{<:Union{StorageCyclingCharge, StorageCyclingDischarge}}, - devices::IS.FlattenIteratorWrapper{V}, - model::PSI.DeviceModel{V, StorageDispatchWithReserves}, - network_model::PSI.NetworkModel{X}, -) where {V <: PSY.GenericBattery, X <: PM.AbstractPowerModel} - error("$V is not supported for $(PSY.GenericBattery). \ - Set the attribute energy_target to false in the device model") - return -end - function PSI.add_constraints!( container::PSI.OptimizationContainer, ::Type{StateofChargeTargetConstraint}, devices::IS.FlattenIteratorWrapper{V}, model::PSI.DeviceModel{V, StorageDispatchWithReserves}, network_model::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} energy_var = PSI.get_variable(container, PSI.EnergyVariable(), V) surplus_var = PSI.get_variable(container, StorageEnergySurplusVariable(), V) shortfall_var = PSI.get_variable(container, StorageEnergyShortageVariable(), V) @@ -1295,7 +1282,7 @@ function add_cycling_charge_without_reserves!( devices::IS.FlattenIteratorWrapper{V}, ::PSI.DeviceModel{V, StorageDispatchWithReserves}, ::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} time_steps = PSI.get_time_steps(container) resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR @@ -1308,7 +1295,10 @@ function add_cycling_charge_without_reserves!( for d in devices name = PSY.get_name(d) - e_max = PSY.get_state_of_charge_limits(d).max + e_max = + PSY.get_storage_level_limits(d).max * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) constraint[name] = JuMP.@constraint( @@ -1326,7 +1316,7 @@ function add_cycling_charge_with_reserves!( devices::IS.FlattenIteratorWrapper{V}, ::PSI.DeviceModel{V, StorageDispatchWithReserves}, ::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} time_steps = PSI.get_time_steps(container) resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR @@ -1340,7 +1330,10 @@ function add_cycling_charge_with_reserves!( for d in devices name = PSY.get_name(d) - e_max = PSY.get_state_of_charge_limits(d).max + e_max = + PSY.get_storage_level_limits(d).max * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) constraint[name] = JuMP.@constraint( @@ -1361,7 +1354,7 @@ function PSI.add_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::PSI.DeviceModel{V, StorageDispatchWithReserves}, network_model::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} if PSI.has_service_model(model) add_cycling_charge_with_reserves!(container, devices, model, network_model) else @@ -1375,7 +1368,7 @@ function add_cycling_discharge_without_reserves!( devices::IS.FlattenIteratorWrapper{V}, ::PSI.DeviceModel{V, StorageDispatchWithReserves}, ::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} time_steps = PSI.get_time_steps(container) resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR @@ -1388,7 +1381,10 @@ function add_cycling_discharge_without_reserves!( for d in devices name = PSY.get_name(d) - e_max = PSY.get_state_of_charge_limits(d).max + e_max = + PSY.get_storage_level_limits(d).max * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) constraint[name] = JuMP.@constraint( @@ -1407,7 +1403,7 @@ function add_cycling_discharge_with_reserves!( devices::IS.FlattenIteratorWrapper{V}, ::PSI.DeviceModel{V, StorageDispatchWithReserves}, ::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} time_steps = PSI.get_time_steps(container) resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR @@ -1421,7 +1417,10 @@ function add_cycling_discharge_with_reserves!( for d in devices name = PSY.get_name(d) - e_max = PSY.get_state_of_charge_limits(d).max + e_max = + PSY.get_storage_level_limits(d).max * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) cycle_count = PSY.get_cycle_limits(d) efficiency = PSY.get_efficiency(d) constraint[name] = JuMP.@constraint( @@ -1441,7 +1440,7 @@ function PSI.add_constraints!( devices::IS.FlattenIteratorWrapper{V}, model::PSI.DeviceModel{V, StorageDispatchWithReserves}, network_model::PSI.NetworkModel{X}, -) where {V <: PSY.BatteryEMS, X <: PM.AbstractPowerModel} +) where {V <: PSY.EnergyReservoirStorage, X <: PM.AbstractPowerModel} if PSI.has_service_model(model) add_cycling_discharge_with_reserves!(container, devices, model, network_model) else @@ -1630,8 +1629,8 @@ end function PSI.objective_function!( container::PSI.OptimizationContainer, - devices::IS.FlattenIteratorWrapper{PSY.BatteryEMS}, - model::PSI.DeviceModel{PSY.BatteryEMS, T}, + devices::IS.FlattenIteratorWrapper{PSY.EnergyReservoirStorage}, + model::PSI.DeviceModel{PSY.EnergyReservoirStorage, T}, ::Type{V}, ) where {T <: AbstractStorageFormulation, V <: PM.AbstractPowerModel} PSI.add_variable_cost!(container, PSI.ActivePowerOutVariable(), devices, T()) @@ -1678,7 +1677,7 @@ function PSI.add_proportional_cost!( formulation::AbstractStorageFormulation, ) where { T <: Union{StorageChargeCyclingSlackVariable, StorageDischargeCyclingSlackVariable}, - U <: PSY.BatteryEMS, + U <: PSY.EnergyReservoirStorage, } variable = PSI.get_variable(container, T(), U) for d in devices @@ -1696,7 +1695,7 @@ function PSI.add_proportional_cost!( formulation::AbstractStorageFormulation, ) where { T <: Union{StorageEnergyShortageVariable, StorageEnergySurplusVariable}, - U <: PSY.BatteryEMS, + U <: PSY.EnergyReservoirStorage, } variable = PSI.get_variable(container, T(), U) for d in devices @@ -1754,14 +1753,13 @@ function PSI.calculate_aux_variable_value!( ::PSI.AuxVarKey{StorageEnergyOutput, T}, system::PSY.System, ) where {T <: PSY.Storage} - devices = PSI.get_available_components(T, system) time_steps = PSI.get_time_steps(container) resolution = PSI.get_resolution(container) fraction_of_hour = Dates.value(Dates.Minute(resolution)) / PSI.MINUTES_IN_HOUR p_variable_results = PSI.get_variable(container, PSI.ActivePowerOutVariable(), T) aux_variable_container = PSI.get_aux_variable(container, StorageEnergyOutput(), T) - for d in devices, t in time_steps - name = PSY.get_name(d) + device_names = axes(aux_variable_container, 1) + for name in device_names, t in time_steps aux_variable_container[name, t] = PSI.jump_value(p_variable_results[name, t]) * fraction_of_hour end diff --git a/test/test_storage_device_models.jl b/test/test_storage_device_models.jl index d9964dd..a92098d 100644 --- a/test/test_storage_device_models.jl +++ b/test/test_storage_device_models.jl @@ -1,6 +1,6 @@ @testset "Storage Basic Storage With DC - PF" begin device_model = DeviceModel( - GenericBattery, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => false, @@ -19,7 +19,7 @@ end @testset "Storage Basic Storage With AC - PF" begin device_model = DeviceModel( - GenericBattery, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => false, @@ -37,7 +37,7 @@ end end @testset "Storage with Reservation & DC - PF" begin - device_model = DeviceModel(GenericBattery, StorageDispatchWithReserves) + device_model = DeviceModel(EnergyReservoirStorage, StorageDispatchWithReserves) c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat") model = DecisionModel(MockOperationProblem, DCPPowerModel, c_sys5_bat) mock_construct_device!(model, device_model) @@ -46,7 +46,7 @@ end end @testset "Storage with Reservation & AC - PF" begin - device_model = DeviceModel(GenericBattery, StorageDispatchWithReserves) + device_model = DeviceModel(EnergyReservoirStorage, StorageDispatchWithReserves) c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat") model = DecisionModel(MockOperationProblem, ACPPowerModel, c_sys5_bat) mock_construct_device!(model, device_model) @@ -54,9 +54,9 @@ end psi_checkobjfun_test(model, GAEVF) end -@testset "BatteryEMS with EnergyTarget with DC - PF" begin +@testset "EnergyReservoirStorage with EnergyTarget with DC - PF" begin device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -73,7 +73,7 @@ end psi_checkobjfun_test(model, GAEVF) device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => false, @@ -89,9 +89,9 @@ end psi_checkobjfun_test(model, GAEVF) end -@testset "BatteryEMS with EnergyTarget With AC - PF" begin +@testset "EnergyReservoirStorage with EnergyTarget With AC - PF" begin device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -108,7 +108,7 @@ end psi_checkobjfun_test(model, GAEVF) device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => false, @@ -127,9 +127,9 @@ end ### Feedforward Test ### # TODO: Feedforward debugging -@testset "Test EnergyTargetFeedforward to GenericBattery with BookKeeping model" begin +@testset "Test EnergyTargetFeedforward to EnergyReservoirStorage with StorageDispatch model" begin device_model = DeviceModel( - GenericBattery, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -141,7 +141,7 @@ end ) ff_et = EnergyTargetFeedforward(; - component_type=GenericBattery, + component_type=EnergyReservoirStorage, source=EnergyVariable, affected_values=[EnergyVariable], target_period=12, @@ -155,9 +155,9 @@ end moi_tests(model, 122, 0, 72, 73, 24, true) end -@testset "Test EnergyTargetFeedforward to BatteryEMS with BookKeeping model" begin +@testset "Test EnergyTargetFeedforward to EnergyReservoirStorage with BookKeeping model" begin device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -169,7 +169,7 @@ end ) ff_et = EnergyTargetFeedforward(; - component_type=BatteryEMS, + component_type=EnergyReservoirStorage, source=EnergyVariable, affected_values=[EnergyVariable], target_period=12, @@ -184,11 +184,11 @@ end end #= -@testset "Test EnergyLimitFeedforward to GenericBattery with BookKeeping model" begin - device_model = DeviceModel(GenericBattery, BookKeeping) +@testset "Test EnergyLimitFeedforward to EnergyReservoirStorage with BookKeeping model" begin + device_model = DeviceModel(EnergyReservoirStorage, BookKeeping) ff_il = EnergyLimitFeedforward(; - component_type=GenericBattery, + component_type=EnergyReservoirStorage, source=ActivePowerOutVariable, affected_values=[ActivePowerOutVariable], number_of_periods=12, @@ -201,11 +201,11 @@ end moi_tests(model, 121, 0, 74, 72, 24, true) end -@testset "Test EnergyLimitFeedforward to GenericBattery with BatteryAncillaryServices model" begin - device_model = DeviceModel(GenericBattery, BatteryAncillaryServices) +@testset "Test EnergyLimitFeedforward to EnergyReservoirStorage with BatteryAncillaryServices model" begin + device_model = DeviceModel(EnergyReservoirStorage, BatteryAncillaryServices) ff_il = EnergyLimitFeedforward(; - component_type=GenericBattery, + component_type=EnergyReservoirStorage, source=ActivePowerOutVariable, affected_values=[ActivePowerOutVariable], number_of_periods=12, @@ -221,7 +221,7 @@ end # To Fix @testset "Test Reserves from Storage" begin template = get_thermal_dispatch_template_network(CopperPlatePowerModel) - set_device_model!(template, DeviceModel(GenericBattery, BatteryAncillaryServices)) + set_device_model!(template, DeviceModel(EnergyReservoirStorage, BatteryAncillaryServices)) set_device_model!(template, RenewableDispatch, FixedOutput) set_service_model!( template, diff --git a/test/test_storage_simulation.jl b/test/test_storage_simulation.jl index d4aab48..7c7b6a4 100644 --- a/test/test_storage_simulation.jl +++ b/test/test_storage_simulation.jl @@ -2,17 +2,17 @@ ######## Test with BookKeeping ######## template = get_thermal_dispatch_template_network() c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat"; force_build=true) - set_device_model!(template, GenericBattery, StorageDispatchWithReserves) + set_device_model!(template, EnergyReservoirStorage, StorageDispatchWithReserves) model = DecisionModel(template, c_sys5_bat; optimizer=HiGHS_optimizer) - @test build!(model; output_dir=mktempdir(; cleanup=true)) == BuildStatus.BUILT - check_energy_initial_conditions_values(model, GenericBattery) - @test solve!(model) == RunStatus.SUCCESSFUL + @test build!(model; output_dir=mktempdir(; cleanup=true)) == PSI.ModelBuildStatus.BUILT + check_energy_initial_conditions_values(model, EnergyReservoirStorage) + @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with EnergyTarget ######## template = get_thermal_dispatch_template_network() c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat_ems"; force_build=true) device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -24,9 +24,9 @@ ) set_device_model!(template, device_model) model = DecisionModel(template, c_sys5_bat; optimizer=HiGHS_optimizer) - @test build!(model; output_dir=mktempdir(; cleanup=true)) == BuildStatus.BUILT - check_energy_initial_conditions_values(model, BatteryEMS) - @test solve!(model) == RunStatus.SUCCESSFUL + @test build!(model; output_dir=mktempdir(; cleanup=true)) == PSI.ModelBuildStatus.BUILT + check_energy_initial_conditions_values(model, EnergyReservoirStorage) + @test solve!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end @testset "Emulation Model initial_conditions test for Storage" begin @@ -38,21 +38,24 @@ end add_single_time_series=true, force_build=true, ) - set_device_model!(template, GenericBattery, StorageDispatchWithReserves) + set_device_model!(template, EnergyReservoirStorage, StorageDispatchWithReserves) model = EmulationModel(template, c_sys5_bat; optimizer=HiGHS_optimizer) @test build!(model; executions=10, output_dir=mktempdir(; cleanup=true)) == - BuildStatus.BUILT + PSI.ModelBuildStatus.BUILT ic_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialEnergyLevel(), - GenericBattery, + EnergyReservoirStorage, ) for ic in ic_data - name = PSY.get_name(ic.component) + d = ic.component + name = PSY.get_name(d) e_var = PSI.jump_value(PSI.get_value(ic)) - @test PSY.get_initial_energy(ic.component) == e_var + @test PSY.get_initial_storage_capacity_level(d) * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) == e_var end - @test run!(model) == RunStatus.SUCCESSFUL + @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with BatteryAncillaryServices ######## template = get_thermal_dispatch_template_network() @@ -62,21 +65,24 @@ end add_single_time_series=true, force_build=true, ) - set_device_model!(template, GenericBattery, StorageDispatchWithReserves) + set_device_model!(template, EnergyReservoirStorage, StorageDispatchWithReserves) model = EmulationModel(template, c_sys5_bat; optimizer=HiGHS_optimizer) @test build!(model; executions=10, output_dir=mktempdir(; cleanup=true)) == - BuildStatus.BUILT + PSI.ModelBuildStatus.BUILT ic_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialEnergyLevel(), - GenericBattery, + EnergyReservoirStorage, ) for ic in ic_data - name = PSY.get_name(ic.component) + d = ic.component + name = PSY.get_name(d) e_var = PSI.jump_value(PSI.get_value(ic)) - @test PSY.get_initial_energy(ic.component) == e_var + @test PSY.get_initial_storage_capacity_level(d) * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) == e_var end - @test run!(model) == RunStatus.SUCCESSFUL + @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED ######## Test with EnergyTarget ######## template = get_thermal_dispatch_template_network() @@ -87,7 +93,7 @@ end force_build=true, ) device_model = DeviceModel( - BatteryEMS, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -100,21 +106,24 @@ end set_device_model!(template, device_model) model = EmulationModel(template, c_sys5_bat; optimizer=HiGHS_optimizer) @test build!(model; executions=10, output_dir=mktempdir(; cleanup=true)) == - BuildStatus.BUILT + PSI.ModelBuildStatus.BUILT ic_data = PSI.get_initial_condition( PSI.get_optimization_container(model), InitialEnergyLevel(), - BatteryEMS, + EnergyReservoirStorage, ) for ic in ic_data - name = PSY.get_name(ic.component) + d = ic.component + name = PSY.get_name(d) e_var = PSI.jump_value(PSI.get_value(ic)) - @test PSY.get_initial_energy(ic.component) == e_var + @test PSY.get_initial_storage_capacity_level(d) * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) == e_var end - @test run!(model) == RunStatus.SUCCESSFUL + @test run!(model) == PSI.RunStatus.SUCCESSFULLY_FINALIZED end -@testset "Simulation with 2-Stages EnergyLimitFeedforward with GenericBattery" begin +@testset "Simulation with 2-Stages EnergyLimitFeedforward with EnergyReservoirStorage" begin sys_uc = build_system(PSITestSystems, "c_sys5_bat") sys_ed = build_system(PSITestSystems, "c_sys5_bat") @@ -150,7 +159,7 @@ end affected_values=[ActivePowerVariable], ), EnergyLimitFeedforward(; - component_type=GenericBattery, + component_type=EnergyReservoirStorage, source=ActivePowerOutVariable, affected_values=[ActivePowerOutVariable], number_of_periods=12, @@ -169,18 +178,39 @@ end ) build_out = build!(sim_cache) - @test build_out == PSI.BuildStatus.BUILT + @test build_out == PSI.SimulationBuildStatus.BUILT execute_out = execute!(sim_cache) - @test execute_out == PSI.RunStatus.SUCCESSFUL + @test execute_out == PSI.RunStatus.SUCCESSFULLY_FINALIZED # Test UC Vars are equal to ED params res = SimulationResults(sim_cache) res_ed = res.decision_problem_results["ED"] - param_ed = read_realized_parameter(res_ed, "EnergyLimitParameter__GenericBattery") + param_ed = + read_realized_parameter(res_ed, "EnergyLimitParameter__EnergyReservoirStorage") res_uc = res.decision_problem_results["UC"] - p_out_bat = read_realized_variable(res_uc, "ActivePowerOutVariable__GenericBattery") + p_out_bat = + read_realized_variable(res_uc, "ActivePowerOutVariable__EnergyReservoirStorage") @test isapprox(param_ed[!, 2], p_out_bat[!, 2] / 100.0; atol=1e-4) end + +@testset "Test cost handling" begin + c_sys5_bat = PSB.build_system(PSITestSystems, "c_sys5_bat"; force_build=true) + template = get_thermal_dispatch_template_network() + storage_model = DeviceModel( + EnergyReservoirStorage, + StorageDispatchWithReserves; + attributes=Dict( + "reservation" => false, + "cycling_limits" => false, + "energy_target" => false, + "complete_coverage" => false, + "regularization" => true, + ), + ) + set_device_model!(template, storage_model) + model = DecisionModel(template, c_sys5_bat; optimizer=HiGHS_optimizer) + @test build!(model; output_dir=mktempdir(; cleanup=true)) == PSI.ModelBuildStatus.BUILT +end diff --git a/test/test_utils/mock_operation_models.jl b/test/test_utils/mock_operation_models.jl index c6b5bc7..063021f 100644 --- a/test/test_utils/mock_operation_models.jl +++ b/test/test_utils/mock_operation_models.jl @@ -103,9 +103,15 @@ function mock_construct_device!( set_device_model!(problem.template, model) template = PSI.get_template(problem) PSI.finalize_template!(template, PSI.get_system(problem)) + settings = PSI.get_settings(problem) + PSI.set_resolution!( + settings, + first(PSY.get_time_series_resolutions(PSI.get_system(problem))), + ) + PSI.set_horizon!(settings, PSY.get_forecast_horizon(PSI.get_system(problem))) PSI.init_optimization_container!( PSI.get_optimization_container(problem), - PSI.get_network_formulation(template), + PSI.get_network_model(template), PSI.get_system(problem), ) PSI.get_network_model(template).subnetworks = @@ -202,7 +208,7 @@ function setup_ic_model_container!(model::DecisionModel) PSI.init_optimization_container!( PSI.get_optimization_container(model), - PSI.get_network_formulation(PSI.get_template(model)), + PSI.get_network_model(PSI.get_template(model)), PSI.get_system(model), ) diff --git a/test/test_utils/model_checks.jl b/test/test_utils/model_checks.jl index 23b2fcc..90adca5 100644 --- a/test/test_utils/model_checks.jl +++ b/test/test_utils/model_checks.jl @@ -90,7 +90,7 @@ function psi_checksolve_test(model::DecisionModel, status, expected_result, tol= @test isapprox(obj_value, expected_result, atol=tol) end -function psi_ptdf_lmps(res::ProblemResults, ptdf) +function psi_ptdf_lmps(res::OptimizationProblemResults, ptdf) cp_duals = read_dual(res, PSI.ConstraintKey(CopperPlateBalanceConstraint, PSY.System)) λ = Matrix{Float64}(cp_duals[:, propertynames(cp_duals) .!= :DateTime]) @@ -304,9 +304,12 @@ function check_energy_initial_conditions_values(model, ::Type{T}) where {T <: PS T, ) for ic in ic_data - name = PSY.get_name(ic.component) + d = ic.component + name = PSY.get_name(d) e_value = PSI.jump_value(PSI.get_value(ic)) - @test PSY.get_initial_energy(ic.component) == e_value + @test PSY.get_initial_storage_capacity_level(d) * + PSY.get_storage_capacity(d) * + PSY.get_conversion_factor(d) == e_value end end @@ -414,7 +417,7 @@ function check_initialization_constraint_count( ::S, ::Type{T}; filter_func=PSY.get_available, - meta=PSI.CONTAINER_KEY_EMPTY_META, + meta=ISOPT.CONTAINER_KEY_EMPTY_META, ) where {S <: PSI.ConstraintType, T <: PSY.Component} container = model.internal.ic_model_container no_component = length(PSY.get_components(filter_func, T, model.sys)) @@ -428,7 +431,7 @@ function check_constraint_count( ::S, ::Type{T}; filter_func=PSY.get_available, - meta=PSI.CONTAINER_KEY_EMPTY_META, + meta=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] diff --git a/test/test_utils/modify_systems.jl b/test/test_utils/modify_systems.jl index 3365403..1581e95 100644 --- a/test/test_utils/modify_systems.jl +++ b/test/test_utils/modify_systems.jl @@ -16,13 +16,15 @@ function _build_battery( efficiency_out, ) name = string(bus.number) * "_BATTERY" - device = BatteryEMS(; + device = EnergyReservoirStorage(; name=name, available=true, bus=bus, prime_mover_type=PSY.PrimeMovers.BA, - initial_energy=0.2, - state_of_charge_limits=(min=energy_capacity * 0.0, max=energy_capacity), + storage_technology_type=PSY.StorageTech.OTHER_CHEM, + storage_capacity=energy_capacity, + storage_level_limits=(min=0.0, max=1.0), + initial_storage_capacity_level=0.2, rating=rating, active_power=rating, cycle_limits=1000.0, @@ -33,7 +35,7 @@ function _build_battery( reactive_power=0.0, reactive_power_limits=nothing, base_power=100.0, - operation_cost=PSY.StorageManagementCost( + operation_cost=PSY.StorageCost( energy_shortage_cost=1000.0, energy_surplus_cost=1000.0, fixed=0.0, diff --git a/test/test_utils/operations_problems_templates.jl b/test/test_utils/operations_problems_templates.jl index 1b53ffd..0487723 100644 --- a/test/test_utils/operations_problems_templates.jl +++ b/test/test_utils/operations_problems_templates.jl @@ -80,7 +80,7 @@ function get_template_basic_uc_storage_simulation() set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) device_model = DeviceModel( - GenericBattery, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true, @@ -100,7 +100,7 @@ function get_template_dispatch_storage_simulation() set_device_model!(template, RenewableDispatch, RenewableFullDispatch) set_device_model!(template, PowerLoad, StaticPowerLoad) device_model = DeviceModel( - GenericBattery, + EnergyReservoirStorage, StorageDispatchWithReserves; attributes=Dict{String, Any}( "reservation" => true,