From 7f627ece8d4024b089377df2b11218a3756062ff Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Thu, 19 Sep 2024 10:40:55 -0400 Subject: [PATCH 01/15] degradation model cleanup Moving old degradation branch changes to this "cleaned up" branch --- src/constraints/battery_degradation.jl | 129 +++++++++++--------- src/core/electric_utility.jl | 6 +- src/core/energy_storage/electric_storage.jl | 80 +++++++----- src/core/generator.jl | 8 +- src/core/reopt.jl | 26 ++-- src/results/electric_storage.jl | 5 +- 6 files changed, 149 insertions(+), 105 deletions(-) diff --git a/src/constraints/battery_degradation.jl b/src/constraints/battery_degradation.jl index be7c78cc2..d5ca399c9 100644 --- a/src/constraints/battery_degradation.jl +++ b/src/constraints/battery_degradation.jl @@ -7,6 +7,7 @@ function add_degradation_variables(m, p) @variable(m, Eplus_sum[days] >= 0) @variable(m, Eminus_sum[days] >= 0) @variable(m, EFC[days] >= 0) + @variable(m, SOH[days]) end @@ -47,33 +48,35 @@ NOTE the average SOC and EFC variables are in absolute units. For example, the S at the battery capacity in kWh. """ function add_degradation(m, p; b="ElectricStorage") + + # Indices days = 1:365*p.s.financial.analysis_years + months = 1:p.s.financial.analysis_years*12 + strategy = p.s.storage.attr[b].degradation.maintenance_strategy if isempty(p.s.storage.attr[b].degradation.maintenance_cost_per_kwh) - function pwf(day::Int) + # Correctly account for discount rate and install cost declination rate for days over analysis period + function pwf_bess_replacements(day::Int) (1-p.s.storage.attr[b].degradation.installed_cost_per_kwh_declination_rate)^(day/365) / (1+p.s.financial.owner_discount_rate_fraction)^(day/365) end - # for the augmentation strategy the maintenance cost curve (function of time) starts at - # 80% of the installed cost since we are not replacing the entire battery - f = strategy == "augmentation" ? 0.8 : 1.0 - p.s.storage.attr[b].degradation.maintenance_cost_per_kwh = [ f * - p.s.storage.attr[b].installed_cost_per_kwh * pwf(d) for d in days[1:end-1] + p.s.storage.attr[b].degradation.maintenance_cost_per_kwh = [ + p.s.storage.attr[b].installed_cost_per_kwh * pwf_bess_replacements(d) for d in days[1:end-1] ] end - @assert(length(p.s.storage.attr[b].degradation.maintenance_cost_per_kwh) == length(days) - 1, - "The degradation maintenance_cost_per_kwh must have a length of $(length(days)-1)." - ) - - @variable(m, SOH[days]) + # Under augmentation scenario, each day's battery augmentation cost is calculated using day-1 value from maintenance_cost_per_kwh vector + # Therefore, on last day, day-1's maintenance cost is utilized. + if length(p.s.storage.attr[b].degradation.maintenance_cost_per_kwh) != length(days) - 1 + throw(@error("The degradation maintenance_cost_per_kwh must have a length of $(length(days)-1).")) + end add_degradation_variables(m, p) constrain_degradation_variables(m, p, b=b) @constraint(m, [d in 2:days[end]], - SOH[d] == SOH[d-1] - p.hours_per_time_step * ( + m[:SOH][d] == m[:SOH][d-1] - p.hours_per_time_step * ( p.s.storage.attr[b].degradation.calendar_fade_coefficient * p.s.storage.attr[b].degradation.time_exponent * m[:Eavg][d-1] * d^(p.s.storage.attr[b].degradation.time_exponent-1) + @@ -82,7 +85,7 @@ function add_degradation(m, p; b="ElectricStorage") ) # NOTE SOH can be negative - @constraint(m, SOH[1] == m[:dvStorageEnergy][b]) + @constraint(m, m[:SOH][1] == m[:dvStorageEnergy][b]) # NOTE SOH is _not_ normalized, and has units of kWh if strategy == "replacement" @@ -100,29 +103,9 @@ function add_degradation(m, p; b="ElectricStorage") The first month that the battery is replaced is determined by d_0p8, which is the integer number of days that the SOH is at least 80% of the purchased capacity. We define a binary for each month and only allow one month to be chosen. - =# - - # define d_0p8 - @warn "Adding binary and indicator constraints for - ElectricStorage.degradation.maintenance_strategy = \"replacement\". - Not all solvers support indicators and some are slow with integers." - # TODO import the latest battery degradation model in the degradation branch - @variable(m, soh_indicator[days], Bin) - @constraint(m, [d in days], - soh_indicator[d] => {SOH[d] >= 0.8*m[:dvStorageEnergy][b]} - ) - @expression(m, d_0p8, sum(soh_indicator[d] for d in days)) - # define binaries for the finding the month that battery must be replaced - months = 1:p.s.financial.analysis_years*12 - @variable(m, bmth[months], Bin) - # can only pick one month (or no month if SOH is >= 80% in last day) - @constraint(m, sum(bmth[mth] for mth in months) == 1-soh_indicator[length(days)]) - # the month picked is at most the month in which the SOH hits 80% - @constraint(m, sum(mth*bmth[mth] for mth in months) <= d_0p8 / 30.42) - # 30.42 is the average number of days in a month - - #= + # maintenance_cost_per_kwh must have length == length(days) - 1, i.e. starts on day 2 + number of replacments as function of d_0p8 ^ | @@ -139,41 +122,77 @@ function add_degradation(m, p; b="ElectricStorage") The above curve is multiplied by the maintenance_cost_per_kwh to create the cost coefficients =# - c = zeros(length(months)) # initialize cost coefficients - N = 365*p.s.financial.analysis_years + + @warn "Adding binary decision variables for + ElectricStorage.degradation.maintenance_strategy = \"replacement\". + Some solvers are slow with integers." + + @variable(m, binSOHIndicator[months], Bin) # track SOH levels, should be 1 if SOH >= 80%, 0 otherwise + @variable(m, binSOHIndicatorChange[months], Bin) # track which month SOH indicator drops to < 80% + @variable(m, 0 <= dvSOHChangeTimesEnergy[months]) # track the kwh to be replaced in a replacement month + + # the big M + if p.s.storage.attr[b].max_kwh == 1.0e6 || p.s.storage.attr[b].max_kwh == 0 + # Under default max_kwh (i.e. not modeling large batteries) or max_kwh = 0 + bigM_StorageEnergy = 24*maximum(p.s.electric_load.loads_kw) + else + # Select the larger value of maximum electric load or provided max_kwh size. + bigM_StorageEnergy = max(24*maximum(p.s.electric_load.loads_kw), p.s.storage.attr[b].max_kwh) + end + + # HEALTHY: if binSOHIndicator is 1, then SOH >= 80%. If binSOHIndicator is 0 and SOH >= very negative number + @constraint(m, [mth in months], m[:SOH][Int(round(30.4167*mth))] >= 0.8*m[:dvStorageEnergy][b] - bigM_StorageEnergy * (1-binSOHIndicator[mth])) + + # UNHEALTHY: if binSOHIndicator is 1, then SOH <= large number. If binSOHIndicator is 0 and SOH <= 80% + @constraint(m, [mth in months], m[:SOH][Int(round(30.4167*mth))] <= 0.8*m[:dvStorageEnergy][b] + bigM_StorageEnergy * (binSOHIndicator[mth])) + + # binSOHIndicatorChange[mth] = binSOHIndicator[mth-1] - binSOHIndicator[mth]. + # If replacement month is x, then binSOHIndicatorChange[x] = 1. All other binSOHIndicatorChange values will be 0s (either 1-1 or 0-0) + @constraint(m, m[:binSOHIndicatorChange][1] == 1 - m[:binSOHIndicator][1]) + @constraint(m, [mth in 2:months[end]], m[:binSOHIndicatorChange][mth] == m[:binSOHIndicator][mth-1] - m[:binSOHIndicator][mth]) + + @expression(m, months_to_first_replacement, sum(m[:binSOHIndicator][mth] for mth in months)) + + # -> linearize the product of binSOHIndicatorChange & m[:dvStorageEnergy][b] + @constraint(m, [mth in months], m[:dvSOHChangeTimesEnergy][mth] >= m[:dvStorageEnergy][b] - bigM_StorageEnergy * (1 - m[:binSOHIndicatorChange][mth])) + @constraint(m, [mth in months], m[:dvSOHChangeTimesEnergy][mth] <= m[:dvStorageEnergy][b] + bigM_StorageEnergy * (1 - m[:binSOHIndicatorChange][mth])) + @constraint(m, [mth in months], m[:dvSOHChangeTimesEnergy][mth] <= bigM_StorageEnergy * m[:binSOHIndicatorChange][mth]) + + replacement_costs = zeros(length(months)) # initialize cost coefficients + residual_values = zeros(length(months)) # initialize cost coefficients for residual_value + N = 365*p.s.financial.analysis_years # number of days + for mth in months - day = Int(round((mth-1)*30.42 + 15, digits=0)) - c[mth] = p.s.storage.attr[b].degradation.maintenance_cost_per_kwh[day] * - ceil(N/day - 1) + day = Int(round((mth-1)*30.4167 + 15, digits=0)) + batt_replace_count = Int(ceil(N/day - 1)) # number of battery replacements in analysis period if they periodically happened on "day" + maint_cost = sum(p.s.storage.attr[b].degradation.maintenance_cost_per_kwh[day*i] for i in 1:batt_replace_count) + replacement_costs[mth] = maint_cost + + residual_factor = 1 - (p.s.financial.analysis_years*12/mth - floor(p.s.financial.analysis_years*12/mth)) + residual_value = p.s.storage.attr[b].degradation.maintenance_cost_per_kwh[end]*residual_factor + residual_values[mth] = residual_value end - # linearize the product of bmth & m[:dvStorageEnergy][b] - M = p.s.storage.attr[b].max_kwh # the big M - @variable(m, 0 <= bmth_BkWh[months]) - @constraint(m, [mth in months], bmth_BkWh[mth] <= m[:dvStorageEnergy][b]) - @constraint(m, [mth in months], bmth_BkWh[mth] <= M * bmth[mth]) - @constraint(m, [mth in months], bmth_BkWh[mth] >= m[:dvStorageEnergy][b] - M*(1-bmth[mth])) + # create replacement cost expression for objective + @expression(m, degr_cost, sum(replacement_costs[mth] * m[:dvSOHChangeTimesEnergy][mth] for mth in months)) - # add replacment cost to objective - @expression(m, degr_cost, - sum(c[mth] * bmth_BkWh[mth] for mth in months) - ) + # create residual value expression for objective + @expression(m, residual_value, sum(residual_values[mth] * m[:dvSOHChangeTimesEnergy][mth] for mth in months)) elseif strategy == "augmentation" @expression(m, degr_cost, sum( - p.s.storage.attr[b].degradation.maintenance_cost_per_kwh[d-1] * (SOH[d-1] - SOH[d]) + p.s.storage.attr[b].degradation.maintenance_cost_per_kwh[d-1] * (m[:SOH][d-1] - m[:SOH][d]) for d in days[2:end] ) ) - # add augmentation cost to objective - # maintenance_cost_per_kwh must have length == length(days) - 1, i.e. starts on day 2 + + # No lifetime based residual value assigned to battery under the augmentation strategy + @expression(m, residual_value, 0.0) else throw(@error("Battery maintenance strategy $strategy is not supported. Choose from augmentation and replacement.")) end - - @objective(m, Min, m[:Costs] + m[:degr_cost]) # NOTE adding to Costs expression does not modify the objective function end diff --git a/src/core/electric_utility.jl b/src/core/electric_utility.jl index aaa45e77f..1fa2c8404 100644 --- a/src/core/electric_utility.jl +++ b/src/core/electric_utility.jl @@ -8,7 +8,7 @@ # Single Outage Modeling Inputs (Outage Modeling Option 1) outage_start_time_step::Int=0, # for modeling a single outage, with critical load spliced into the baseline load ... - outage_end_time_step::Int=0, # ... utiltity production_factor = 0 during the outage + outage_end_time_step::Int=0, # ... utility production_factor = 0 during the outage # Multiple Outage Modeling Inputs (Outage Modeling Option 2): minimax the expected outage cost, # with max taken over outage start time, expectation taken over outage duration @@ -115,7 +115,7 @@ struct ElectricUtility emissions_factor_SO2_decrease_fraction::Real emissions_factor_PM25_decrease_fraction::Real outage_start_time_step::Int # for modeling a single outage, with critical load spliced into the baseline load ... - outage_end_time_step::Int # ... utiltity production_factor = 0 during the outage + outage_end_time_step::Int # ... utility production_factor = 0 during the outage allow_simultaneous_export_import::Bool # if true the site has two meters (in effect) # next 5 variables below used for minimax the expected outage cost, # with max taken over outage start time, expectation taken over outage duration @@ -147,7 +147,7 @@ struct ElectricUtility net_metering_limit_kw::Real = 0, # Upper limit on the total capacity of technologies that can participate in net metering agreement. interconnection_limit_kw::Real = 1.0e9, outage_start_time_step::Int=0, # for modeling a single outage, with critical load spliced into the baseline load ... - outage_end_time_step::Int=0, # ... utiltity production_factor = 0 during the outage + outage_end_time_step::Int=0, # ... utility production_factor = 0 during the outage allow_simultaneous_export_import::Bool=true, # if true the site has two meters (in effect) # next 5 variables below used for minimax the expected outage cost, # with max taken over outage start time, expectation taken over outage duration diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 626e2993a..19ebbf7b1 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -15,10 +15,7 @@ end ``` None of the above values are required. If `ElectricStorage.model_degradation` is `true` then the -defaults above are used. -If the `maintenance_cost_per_kwh` is not provided then it is determined using the `ElectricStorage.installed_cost_per_kwh` -and the `installed_cost_per_kwh_declination_rate` along with a present worth factor ``f`` to account for the present cost -of buying a battery in the future. The present worth factor for each day is: +defaults above are used. If the `maintenance_cost_per_kwh` is not provided then it is determined using the `ElectricStorage.installed_cost_per_kwh` and the `installed_cost_per_kwh_declination_rate` along with a present worth factor ``f`` to account for the present cost of buying a battery in the future. The present worth factor for each day is: `` f(day) = \\frac{ (1-r_g)^\\frac{day}{365} } { (1+r_d)^\\frac{day}{365} } @@ -28,18 +25,17 @@ where ``r_g`` = `installed_cost_per_kwh_declination_rate` and ``r_d`` = `p.s.fin Note this day-specific calculation of the present-worth factor accumulates differently from the annually updated discount rate for other net-present value calculations in REopt, and has a higher effective discount rate as a result. The present -worth factor is used in two different ways, depending on the `maintenance_strategy`, which is described below. +worth factor is used in the same manner irrespective of the `maintenance_strategy`. !!! warn When modeling degradation the following ElectricStorage inputs are not used: - - `replace_cost_per_kw` - `replace_cost_per_kwh` - - `inverter_replacement_year` - `battery_replacement_year` The are replaced by the `maintenance_cost_per_kwh` vector. + Inverter replacement costs and inverter replacement year should still be used to model scheduled replacement of inverter. !!! note - When providing the `maintenance_cost_per_kwh` it must have a length equal to `Financial.analysis_years*365`. + When providing the `maintenance_cost_per_kwh` it must have a length equal to `Financial.analysis_years*365`-1. # Battery State Of Health @@ -61,16 +57,22 @@ where: The `SOH` is used to determine the maintence cost of the storage system, which depends on the `maintenance_strategy`. +!!! note + Battery degradation parameters are from based on laboratory aging data, and are expected to be reasonable only within + the range of conditions tested. Battery lifetime can vary widely from these estimates based on battery use and system design. + Battery cost estimates are based on domain expertise and published guidelines and are not to be taken as an indicator of real + system costs. + # Augmentation Maintenance Strategy The augmentation maintenance strategy assumes that the battery energy capacity is maintained by replacing degraded cells daily in terms of cost. Using the definition of the `SOH` above the maintenance cost is: `` -C_{\\text{aug}} = \\sum_{d \\in \\{2\\dots D\\}} 0.8 C_{\\text{install}} f(day) \\left( SOH[d-1] - SOH[d] \\right) +C_{\\text{aug}} = \\sum_{d \\in \\{2\\dots D\\}} C_{\\text{install}} f(day) \\left( SOH[d-1] - SOH[d] \\right) `` where -- the ``0.8`` factor accounts for sunk costs that do not need to be paid; +- ``f(day)`` is the present worth factor of battery degradation costs as described above; - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`; and - ``SOH[d-1] - SOH[d]`` is the incremental amount of battery capacity lost in a day. @@ -79,13 +81,13 @@ The ``C_{\\text{aug}}`` is added to the objective function to be minimized with # Replacement Maintenance Strategy Modeling the replacement maintenance strategy is more complex than the augmentation strategy. -Effectively the replacement strategy says that the battery has to be replaced once the `SOH` hits 80% -of the optimal, purchased capacity. It is possible that multiple replacements could be required under +Effectively the replacement strategy says that the battery has to be replaced once the `SOH` drops below 80% +of the optimal, purchased capacity. It is possible that multiple replacements (at same replacement frequency) could be required under this strategy. !!! warn - The "replacement" maintenance strategy requires integer variables and indicator constraints. - Not all solvers support indicator constraints and some solvers are slow with integer variables. + The "replacement" maintenance strategy requires integer decision variables. + Some solvers are slow with integer decision variables. The replacement strategy cost is: @@ -96,7 +98,22 @@ C_{\\text{repl}} = B_{\\text{kWh}} N_{\\text{repl}} f(d_{80}) C_{\\text{install} where: - ``B_{\\text{kWh}}`` is the optimal battery capacity (`ElectricStorage.size_kwh` in the results dictionary); - ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` reaches 80% of original capacity); -- ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month that the `SOH` reaches 80% of original capacity. +- ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month that the `SOH` crosses 80% of original capacity; +- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. +The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. +## Battery residual value +Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. +For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). +The last battery which was placed in service during month 290 only serves for 10 months (i.e. 6.89% of its expected life assuming 145 month replacement frequecy). +In this case, the battery has 93.1% of residual life remaining as useful life left after analysis period ends. +A residual value cost vector is created to hold this value for all months. Residual value is calculated as: +`` +C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} +`` +where: +- ``R`` is the `residual_factor` which determines portion of battery life remaining at end of analysis period; +- ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of last month in analysis period; +- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. @@ -123,9 +140,9 @@ The following shows how one would use the degradation model in REopt via the [Sc Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. """ Base.@kwdef mutable struct Degradation - calendar_fade_coefficient::Real = 2.46E-03 - cycle_fade_coefficient::Real = 7.82E-05 - time_exponent::Real = 0.5 + calendar_fade_coefficient::Real = 2.55E-03 + cycle_fade_coefficient::Real = 9.83E-05 + time_exponent::Real = 0.42 installed_cost_per_kwh_declination_rate::Real = 0.05 maintenance_strategy::String = "augmentation" # one of ["augmentation", "replacement"] maintenance_cost_per_kwh::Vector{<:Real} = Real[] @@ -251,9 +268,20 @@ struct ElectricStorage <: AbstractElectricStorage @warn "Battery replacement costs (per_kwh) will not be considered because battery_replacement_year is greater than or equal to analysis_years." end + # copy the replace_costs in case we need to change them + replace_cost_per_kw = s.replace_cost_per_kw + replace_cost_per_kwh = s.replace_cost_per_kwh + if s.model_degradation + if haskey(d, :replace_cost_per_kwh) && d[:replace_cost_per_kwh] != 0.0 + @warn "Setting ElectricStorage replacement costs to zero. \nUsing degradation.maintenance_cost_per_kwh instead." + end + replace_cost_per_kwh = 0.0 # Always modeled using maintenance_cost_vector in degradation model. + # replace_cost_per_kw is unchanged here. + end + net_present_cost_per_kw = effective_cost(; itc_basis = s.installed_cost_per_kw, - replacement_cost = s.inverter_replacement_year >= f.analysis_years ? 0.0 : s.replace_cost_per_kw, + replacement_cost = s.inverter_replacement_year >= f.analysis_years ? 0.0 : replace_cost_per_kw, replacement_year = s.inverter_replacement_year, discount_rate = f.owner_discount_rate_fraction, tax_rate = f.owner_tax_rate_fraction, @@ -265,7 +293,7 @@ struct ElectricStorage <: AbstractElectricStorage ) net_present_cost_per_kwh = effective_cost(; itc_basis = s.installed_cost_per_kwh, - replacement_cost = s.battery_replacement_year >= f.analysis_years ? 0.0 : s.replace_cost_per_kwh, + replacement_cost = s.battery_replacement_year >= f.analysis_years ? 0.0 : replace_cost_per_kwh, replacement_year = s.battery_replacement_year, discount_rate = f.owner_discount_rate_fraction, tax_rate = f.owner_tax_rate_fraction, @@ -282,18 +310,6 @@ struct ElectricStorage <: AbstractElectricStorage else degr = Degradation() end - - # copy the replace_costs in case we need to change them - replace_cost_per_kw = s.replace_cost_per_kw - replace_cost_per_kwh = s.replace_cost_per_kwh - if s.model_degradation - if haskey(d, :replace_cost_per_kw) && d[:replace_cost_per_kw] != 0.0 || - haskey(d, :replace_cost_per_kwh) && d[:replace_cost_per_kwh] != 0.0 - @warn "Setting ElectricStorage replacement costs to zero. Using degradation.maintenance_cost_per_kwh instead." - end - replace_cost_per_kw = 0.0 - replace_cost_per_kwh = 0.0 - end return new( s.min_kw, diff --git a/src/core/generator.jl b/src/core/generator.jl index 691e39bcb..e65c08b58 100644 --- a/src/core/generator.jl +++ b/src/core/generator.jl @@ -42,8 +42,8 @@ emissions_factor_lb_NOx_per_gal::Real = 0.0775544, emissions_factor_lb_SO2_per_gal::Real = 0.040020476, emissions_factor_lb_PM25_per_gal::Real = 0.0, - replacement_year::Int = off_grid_flag ? 10 : analysis_years, - replace_cost_per_kw::Real = off_grid_flag ? installed_cost_per_kw : 0.0 + replacement_year::Int = off_grid_flag ? 10 : analysis_years, # Project year in which generator capacity will be replaced at a cost of replace_cost_per_kw. + replace_cost_per_kw::Real = off_grid_flag ? installed_cost_per_kw : 0.0 # Per kW replacement cost for generator capacity. Replacement costs are considered tax deductible. ``` !!! note "Replacement costs" @@ -137,8 +137,8 @@ struct Generator <: AbstractGenerator emissions_factor_lb_NOx_per_gal::Real = 0.0775544, emissions_factor_lb_SO2_per_gal::Real = 0.040020476, emissions_factor_lb_PM25_per_gal::Real = 0.0, - replacement_year::Int = off_grid_flag ? 10 : analysis_years, - replace_cost_per_kw::Real = off_grid_flag ? installed_cost_per_kw : 0.0 + replacement_year::Int = off_grid_flag ? 10 : analysis_years, # Project year in which generator capacity will be replaced at a cost of replace_cost_per_kw. + replace_cost_per_kw::Real = off_grid_flag ? installed_cost_per_kw : 0.0 # Per kW replacement cost for generator capacity. Replacement costs are considered tax deductible. ) if (replacement_year >= analysis_years) && !(replace_cost_per_kw == 0.0) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index e9344b703..d887837ff 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -450,6 +450,15 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:OffgridOtherCapexAfterDepr] = p.s.financial.offgrid_other_capital_costs - offgrid_other_capex_depr_savings end + for b in p.s.storage.types.elec + if p.s.storage.attr[b].model_degradation + add_degradation(m, p; b=b) + if p.s.settings.add_soc_incentive # this warning should be tied to IF condition where SOC incentive is added + @warn "Settings.add_soc_incentive is set to true and it will incentivize BESS energy levels to be kept high. It could conflict with the battery degradation model and should be disabled." + end + end + end + ################################# Objective Function ######################################## @expression(m, Costs, # Capital Costs @@ -494,6 +503,14 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) if p.s.settings.include_health_in_objective add_to_expression!(Costs, m[:Lifecycle_Emissions_Cost_Health]) end + + has_degr = false + for b in p.s.storage.types.elec + if p.s.storage.attr[b].model_degradation + has_degr = true + add_to_expression!(Costs, m[:degr_cost] - m[:residual_value]) # maximize residual value + end + end ## Modify objective with incentives that are not part of the LCC # 1. Comfort limit violation costs @@ -512,15 +529,6 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) # Set model objective @objective(m, Min, m[:Costs] + m[:ObjectivePenalties] ) - - for b in p.s.storage.types.elec - if p.s.storage.attr[b].model_degradation - add_degradation(m, p; b=b) - if p.s.settings.add_soc_incentive - @warn "Settings.add_soc_incentive is set to true but no incentive will be added because it conflicts with the battery degradation model." - end - end - end nothing end diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index 4c2b1483c..ef07eb48c 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -38,10 +38,11 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: r["state_of_health"] = value.(m[:SOH]).data / value.(m[:dvStorageEnergy])["ElectricStorage"]; r["maintenance_cost"] = value(m[:degr_cost]) if p.s.storage.attr[b].degradation.maintenance_strategy == "replacement" - r["replacement_month"] = Int(value( - sum(mth * m[:bmth][mth] for mth in 1:p.s.financial.analysis_years*12) + r["replacement_month"] = round(Int, value( + sum(mth * m[:binSOHIndicatorChange][mth] for mth in 1:p.s.financial.analysis_years*12) )) end + r["residual_value"] = value(m[:residual_value]) end else r["soc_series_fraction"] = [] From 6b20ef478f19a2bb08a081aa5bccafc86eadc2a9 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Fri, 20 Sep 2024 14:39:48 -0400 Subject: [PATCH 02/15] Update tests Remove "replacement" strategy test because it cannot be run using open source solvers. --- test/runtests.jl | 75 +++++++++++++++++++++++----- test/scenarios/batt_degradation.json | 18 +++++++ 2 files changed, 81 insertions(+), 12 deletions(-) create mode 100644 test/scenarios/batt_degradation.json diff --git a/test/runtests.jl b/test/runtests.jl index 4aee1d809..78e2038d6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -947,10 +947,60 @@ else # run HiGHS tests if results["PV"]["electric_to_grid_series_kw"][i] > 0) end + #= + Battery degradation replacement strategy test can be validated against solvers like Xpress. + Commented out of this testset due to solve time constraints using open-source solvers. + This test has been validated via local testing. + =# + @testset "Battery degradation replacement strategy" begin + # Replacement + nothing + # d = JSON.parsefile("scenarios/batt_degradation.json"); + + # d["ElectricStorage"]["macrs_option_years"] = 0 + # d["ElectricStorage"]["macrs_bonus_fraction"] = 0.0 + # d["ElectricStorage"]["macrs_itc_reduction"] = 0.0 + # d["ElectricStorage"]["total_itc_fraction"] = 0.0 + # d["ElectricStorage"]["replace_cost_per_kwh"] = 0.0 + # d["ElectricStorage"]["replace_cost_per_kw"] = 0.0 + # d["Financial"] = Dict( + # "offtaker_tax_rate_fraction" => 0.0, + # "owner_tax_rate_fraction" => 0.0 + # ) + # d["ElectricStorage"]["degradation"]["installed_cost_per_kwh_declination_rate"] = 0.2 + + # d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) + + # s = Scenario(d) + # p = REoptInputs(s) + # for t in 1:4380 + # p.s.electric_tariff.energy_rates[2*t-1] = 0 + # p.s.electric_tariff.energy_rates[2*t] = 10.0 + # end + # m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + # results = run_reopt(m, p) + + # @test results["ElectricStorage"]["size_kw"] ≈ 11.13 atol=0.05 + # @test results["ElectricStorage"]["size_kwh"] ≈ 14.07 atol=0.05 + # @test results["ElectricStorage"]["replacement_month"] == 8 + # @test results["ElectricStorage"]["maintenance_cost"] ≈ 32820.9 atol=1 + # @test results["ElectricStorage"]["state_of_health"][8760] ≈ -6.8239 atol=0.001 + # @test results["ElectricStorage"]["residual_value"] ≈ 2.61 atol=0.1 + # @test sum(results["ElectricStorage"]["storage_to_load_series_kw"]) ≈ 43800 atol=1.0 #battery should serve all load, every other period + + + # # Validate model decision variables make sense. + # replace_month = Int(value.(m[:months_to_first_replacement]))+1 + # @test replace_month ≈ results["ElectricStorage"]["replacement_month"] + # @test sum(value.(m[:binSOHIndicator])[replace_month:end]) ≈ 0.0 + # @test sum(value.(m[:binSOHIndicatorChange])) ≈ value.(m[:binSOHIndicatorChange])[replace_month] ≈ 1.0 + # @test value.(m[:binSOHIndicator])[end] ≈ 0.0 + end + @testset "Solar and ElectricStorage w/BAU and degradation" begin m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - d = JSON.parsefile("scenarios/pv_storage.json"); + d = JSON.parsefile("pv_storage.json"); d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) results = run_reopt([m1,m2], d) @@ -967,33 +1017,34 @@ else # run HiGHS tests # compare avg soc with and without degradation, # using default augmentation battery maintenance strategy avg_soc_no_degr = sum(results["ElectricStorage"]["soc_series_fraction"]) / 8760 + + d = JSON.parsefile("pv_storage.json"); d["ElectricStorage"]["model_degradation"] = true m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) r_degr = run_reopt(m, d) avg_soc_degr = sum(r_degr["ElectricStorage"]["soc_series_fraction"]) / 8760 @test avg_soc_no_degr > avg_soc_degr - # test the replacement strategy - d["ElectricStorage"]["degradation"] = Dict("maintenance_strategy" => "replacement") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - set_optimizer_attribute(m, "mip_rel_gap", 0.01) - r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + # test the replacement strategy ## Cannot test with open source solvers. + # d["ElectricStorage"]["degradation"] = Dict("maintenance_strategy" => "replacement") + # m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + # set_optimizer_attribute(m, "mip_rel_gap", 0.01) + # r = run_reopt(m, d) + # @test occursin("not supported by the solver", string(r["Messages"]["errors"])) # #optimal SOH at end of horizon is 80\% to prevent any replacement - # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 + # @test sum(value.(m[:dvSOHChangeTimesEnergy])) ≈ 68.48 atol=0.01 # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 # # the maintenance_cost comes out to 3004.39 on Actions, so we test the LCC since it should match # @test r["Financial"]["lcc"] ≈ 1.240096e7 rtol=0.01 - # @test last(value.(m[:SOH])) ≈ 66.633 rtol=0.01 - # @test r["ElectricStorage"]["size_kwh"] ≈ 83.29 rtol=0.01 + # @test last(value.(m[:SOH])) ≈ 42.95 rtol=0.01 + # @test r["ElectricStorage"]["size_kwh"] ≈ 68.48 rtol=0.01 # test minimum_avg_soc_fraction d["ElectricStorage"]["minimum_avg_soc_fraction"] = 0.72 m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) set_optimizer_attribute(m, "mip_rel_gap", 0.01) r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) - # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 + @test round(sum(r["ElectricStorage"]["soc_series_fraction"])/8760, digits=2) >= 0.72 end @testset "Outage with Generator, outage simulator, BAU critical load outputs" begin diff --git a/test/scenarios/batt_degradation.json b/test/scenarios/batt_degradation.json new file mode 100644 index 000000000..b3d9d1265 --- /dev/null +++ b/test/scenarios/batt_degradation.json @@ -0,0 +1,18 @@ +{ + "ElectricStorage":{ + "model_degradation":true, + "degradation":{"maintenance_strategy":"replacement"} + }, + "ElectricTariff":{ + "monthly_energy_rates": [0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1], + "monthly_demand_rates": [0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0] + }, + "Site":{ + "latitude":34.5794343, + "longitude":-118.1164613 + }, + "ElectricLoad":{ + "doe_reference_name":"FlatLoad", + "annual_kwh": 87600 + } +} \ No newline at end of file From 82617aa13bd9cdd2b467f367449e2cd7c6c6a575 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Fri, 20 Sep 2024 17:11:54 -0400 Subject: [PATCH 03/15] Update runtests.jl this should fix failing/erroring tests --- test/runtests.jl | 12 ++++++------ 1 file changed, 6 insertions(+), 6 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 78e2038d6..0f4719676 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -42,26 +42,26 @@ else # run HiGHS tests latitude, longitude = 32.775212075983646, -96.78105623767185 radius = 0 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "nsrdb" + @test dataset == "nsrdb" # 2. Merefa, Ukraine latitude, longitude = 49.80670544975866, 36.05418033509974 radius = 0 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "nsrdb" + @test dataset == "nsrdb" # 3. Younde, Cameroon latitude, longitude = 3.8603988398663125, 11.528880303663136 radius = 0 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "intl" + @test dataset == "nsrdb" # 4. Fairbanks, AK site = "Fairbanks" latitude, longitude = 64.84112047064114, -147.71570239058084 radius = 20 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "tmy3" + @test dataset == "tmy3" end @testset "January Export Rates" begin @@ -1000,7 +1000,7 @@ else # run HiGHS tests @testset "Solar and ElectricStorage w/BAU and degradation" begin m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - d = JSON.parsefile("pv_storage.json"); + d = JSON.parsefile("scenarios/pv_storage.json"); d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) results = run_reopt([m1,m2], d) @@ -1018,7 +1018,7 @@ else # run HiGHS tests # using default augmentation battery maintenance strategy avg_soc_no_degr = sum(results["ElectricStorage"]["soc_series_fraction"]) / 8760 - d = JSON.parsefile("pv_storage.json"); + d = JSON.parsefile("scenarios/pv_storage.json"); d["ElectricStorage"]["model_degradation"] = true m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) r_degr = run_reopt(m, d) From 270ae96738df99e3d7b6ceb54289c5bc98fd5b2d Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 7 Oct 2024 12:04:51 -0600 Subject: [PATCH 04/15] minor docstrings updates --- src/core/energy_storage/electric_storage.jl | 11 ++++++----- 1 file changed, 6 insertions(+), 5 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 19ebbf7b1..b392a5f84 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -97,10 +97,11 @@ C_{\\text{repl}} = B_{\\text{kWh}} N_{\\text{repl}} f(d_{80}) C_{\\text{install} where: - ``B_{\\text{kWh}}`` is the optimal battery capacity (`ElectricStorage.size_kwh` in the results dictionary); -- ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` reaches 80% of original capacity); -- ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month that the `SOH` crosses 80% of original capacity; +- ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` falls below 80% of original capacity); +- ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month in which the `SOH` falls below 80% of original capacity; - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. + ## Battery residual value Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). @@ -111,11 +112,11 @@ A residual value cost vector is created to hold this value for all months. Resid C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} `` where: -- ``R`` is the `residual_factor` which determines portion of battery life remaining at end of analysis period; -- ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of last month in analysis period; +- ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; +- ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. -The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. +The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. # Example of inputs The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: From 8563bb3a30d92c06aa07ce4ae3509d8388048b2f Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 7 Oct 2024 12:05:15 -0600 Subject: [PATCH 05/15] refactor constrain_degradation_variables() --- src/constraints/battery_degradation.jl | 42 ++++++++++++++------------ 1 file changed, 22 insertions(+), 20 deletions(-) diff --git a/src/constraints/battery_degradation.jl b/src/constraints/battery_degradation.jl index d5ca399c9..4b31e5297 100644 --- a/src/constraints/battery_degradation.jl +++ b/src/constraints/battery_degradation.jl @@ -15,29 +15,31 @@ function constrain_degradation_variables(m, p; b="ElectricStorage") days = 1:365*p.s.financial.analysis_years ts_per_day = 24 / p.hours_per_time_step ts_per_year = ts_per_day * 365 + ts0 = Dict() + tsF = Dict() for d in days - ts0 = Int((ts_per_day * (d - 1) + 1) % ts_per_year) - tsF = Int(ts_per_day * d % ts_per_year) - if tsF == 0 - tsF = Int(ts_per_day * 365) + ts0[d] = Int((ts_per_day * (d - 1) + 1) % ts_per_year) + tsF[d] = Int(ts_per_day * d % ts_per_year) + if tsF[d] == 0 + tsF[d] = Int(ts_per_day * 365) end - @constraint(m, - m[:Eavg][d] == 1/ts_per_day * sum(m[:dvStoredEnergy][b, ts] for ts in ts0:tsF) - ) - @constraint(m, - m[:Eplus_sum][d] == - p.hours_per_time_step * ( - sum(m[:dvProductionToStorage][b, t, ts] for t in p.techs.elec, ts in ts0:tsF) - + sum(m[:dvGridToStorage][b, ts] for ts in ts0:tsF) - ) - ) - @constraint(m, - m[:Eminus_sum][d] == p.hours_per_time_step * sum(m[:dvDischargeFromStorage][b, ts] for ts in ts0:tsF) - ) - @constraint(m, - m[:EFC][d] == (m[:Eplus_sum][d] + m[:Eminus_sum][d]) / 2 - ) end + @constraint(m, [d in days], + m[:Eavg][d] == 1/ts_per_day * sum(m[:dvStoredEnergy][b, ts] for ts in ts0[d]:tsF[d]) + ) + @constraint(m, [d in days], + m[:Eplus_sum][d] == + p.hours_per_time_step * ( + sum(m[:dvProductionToStorage][b, t, ts] for t in p.techs.elec, ts in ts0[d]:tsF[d]) + + sum(m[:dvGridToStorage][b, ts] for ts in ts0[d]:tsF[d]) + ) + ) + @constraint(m, [d in days], + m[:Eminus_sum][d] == p.hours_per_time_step * sum(m[:dvDischargeFromStorage][b, ts] for ts in ts0[d]:tsF[d]) + ) + @constraint(m, [d in days], + m[:EFC][d] == (m[:Eplus_sum][d] + m[:Eminus_sum][d]) / 2 + ) end From dfaf25879c0af26c14cb4a2bbde3748a1f74c602 Mon Sep 17 00:00:00 2001 From: Alex Zolan Date: Mon, 7 Oct 2024 16:24:10 -0600 Subject: [PATCH 06/15] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 6f1fc300c..a44dfd73b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## Develop degradation-cleanup +### Changed +- Revised the battery degradation model, refactoring some methods to increase model-building efficiency and reformulating indicator constraints as big-M constraints with smaller big-M's to reduce solve time. +- Edited several documentation entries and docstrings for clarity. + ## v0.48.0 ### Added - Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly From 24d0036b900d468fda7877597923004198e375ff Mon Sep 17 00:00:00 2001 From: Bhavesh Rathod <83797218+rathod-b@users.noreply.github.com> Date: Tue, 8 Oct 2024 12:48:33 -0400 Subject: [PATCH 07/15] Update CHANGELOG.md --- CHANGELOG.md | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index a44dfd73b..7e8a5f1d8 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,9 +24,13 @@ Classify the change according to the following categories: ### Removed ## Develop degradation-cleanup +### Added +- Battery residual value if choosing replacement strategy for degradation ### Changed - Revised the battery degradation model, refactoring some methods to increase model-building efficiency and reformulating indicator constraints as big-M constraints with smaller big-M's to reduce solve time. - Edited several documentation entries and docstrings for clarity. +### Removed +- 80% scaling of battery maintenance costs when using augmentation strategy ## v0.48.0 ### Added From 8f385ffca6036fc1cc0d2b9d0f9224e6769782a8 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Tue, 8 Oct 2024 13:04:48 -0400 Subject: [PATCH 08/15] Update Manifest.toml --- Manifest.toml | 120 ++++++++++++++++++++++++++++++++++++++++---------- 1 file changed, 97 insertions(+), 23 deletions(-) diff --git a/Manifest.toml b/Manifest.toml index 70d4fc8bb..8485d5d89 100644 --- a/Manifest.toml +++ b/Manifest.toml @@ -1,20 +1,29 @@ # This file is machine-generated - editing it directly is not advised -julia_version = "1.8.3" +julia_version = "1.9.0" manifest_format = "2.0" -project_hash = "2e3f73051e60a5c2ffca2d998f6043d51c7f6c2a" +project_hash = "6787ce6711da7a7bf012a2defdfff479e23ec7c6" [[deps.AbstractFFTs]] -deps = ["ChainRulesCore", "LinearAlgebra", "Test"] +deps = ["LinearAlgebra"] git-tree-sha1 = "d92ad398961a3ed262d8bf04a1a2b8340f915fef" uuid = "621f4979-c628-5d54-868e-fcf4e3e8185c" version = "1.5.0" +weakdeps = ["ChainRulesCore", "Test"] + + [deps.AbstractFFTs.extensions] + AbstractFFTsChainRulesCoreExt = "ChainRulesCore" + AbstractFFTsTestExt = "Test" [[deps.Adapt]] deps = ["LinearAlgebra", "Requires"] git-tree-sha1 = "76289dc51920fdc6e0013c872ba9551d54961c24" uuid = "79e6a3ab-5dfb-504d-930d-738a2a938a0e" version = "3.6.2" +weakdeps = ["StaticArrays"] + + [deps.Adapt.extensions] + AdaptStaticArraysExt = "StaticArrays" [[deps.ArchGDAL]] deps = ["CEnum", "ColorTypes", "Dates", "DiskArrays", "Extents", "GDAL", "GDAL_jll", "GeoFormatTypes", "GeoInterface", "GeoInterfaceRecipes", "ImageCore", "Tables"] @@ -79,10 +88,14 @@ uuid = "d360d2e6-b24c-11e9-a2a3-2a2ae2dbcce4" version = "1.16.0" [[deps.ChangesOfVariables]] -deps = ["InverseFunctions", "LinearAlgebra", "Test"] +deps = ["LinearAlgebra", "Test"] git-tree-sha1 = "2fba81a302a7be671aefe194f0525ef231104e7f" uuid = "9e997f8a-9a97-42d5-a9f1-ce6bfc15e2c0" version = "0.1.8" +weakdeps = ["InverseFunctions"] + + [deps.ChangesOfVariables.extensions] + ChangesOfVariablesInverseFunctionsExt = "InverseFunctions" [[deps.CodecBzip2]] deps = ["Bzip2_jll", "Libdl", "TranscodingStreams"] @@ -126,15 +139,19 @@ uuid = "bbf7d656-a473-5ed7-a52c-81e309532950" version = "0.3.0" [[deps.Compat]] -deps = ["Dates", "LinearAlgebra", "UUIDs"] +deps = ["UUIDs"] git-tree-sha1 = "8a62af3e248a8c4bad6b32cbbe663ae02275e32c" uuid = "34da2185-b29b-5c13-b0c7-acf172513d20" version = "4.10.0" +weakdeps = ["Dates", "LinearAlgebra"] + + [deps.Compat.extensions] + CompatLinearAlgebraExt = "LinearAlgebra" [[deps.CompilerSupportLibraries_jll]] deps = ["Artifacts", "Libdl"] uuid = "e66e0078-7015-5450-92f7-15fbd957f2ae" -version = "0.5.2+0" +version = "1.0.2+0" [[deps.ConcurrentUtilities]] deps = ["Serialization", "Sockets"] @@ -148,6 +165,14 @@ git-tree-sha1 = "c53fc348ca4d40d7b371e71fd52251839080cbc9" uuid = "187b0558-2788-49d3-abe0-74a17ed4e7c9" version = "1.5.4" + [deps.ConstructionBase.extensions] + ConstructionBaseIntervalSetsExt = "IntervalSets" + ConstructionBaseStaticArraysExt = "StaticArrays" + + [deps.ConstructionBase.weakdeps] + IntervalSets = "8197267c-284f-5f27-9208-e0e47529a953" + StaticArrays = "90137ffa-7385-5640-81b9-e52037218182" + [[deps.CoolProp]] deps = ["CoolProp_jll", "Markdown", "Unitful"] git-tree-sha1 = "94062163b5656b1351f7f7a784341b8fe13c1ca1" @@ -193,7 +218,9 @@ uuid = "ade2ca70-3891-5945-98fb-dc099432e06a" [[deps.DelimitedFiles]] deps = ["Mmap"] +git-tree-sha1 = "9e2f36d3c96a820c678f2f1f1782582fcf685bae" uuid = "8bb1440f-4735-579b-a4ab-409b98df4dab" +version = "1.9.1" [[deps.DiffResults]] deps = ["StaticArraysCore"] @@ -273,10 +300,14 @@ uuid = "53c48c17-4a7d-5ca2-90c5-79b7896eea93" version = "0.8.4" [[deps.ForwardDiff]] -deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions", "StaticArrays"] +deps = ["CommonSubexpressions", "DiffResults", "DiffRules", "LinearAlgebra", "LogExpFunctions", "NaNMath", "Preferences", "Printf", "Random", "SpecialFunctions"] git-tree-sha1 = "cf0fe81336da9fb90944683b8c41984b08793dad" uuid = "f6369f11-7733-5829-9624-2563aa707210" version = "0.10.36" +weakdeps = ["StaticArrays"] + + [deps.ForwardDiff.extensions] + ForwardDiffStaticArraysExt = "StaticArrays" [[deps.Future]] deps = ["Random"] @@ -425,6 +456,12 @@ git-tree-sha1 = "3700a700bc80856fe673b355123ae4574f2d5dfe" uuid = "4076af6c-e467-56ae-b986-b466b2749572" version = "1.15.1" + [deps.JuMP.extensions] + JuMPDimensionalDataExt = "DimensionalData" + + [deps.JuMP.weakdeps] + DimensionalData = "0703355e-b756-11e9-17c0-8b28908087d0" + [[deps.LERC_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Pkg"] git-tree-sha1 = "bf36f528eec6634efc60d7ec062008f171071434" @@ -481,7 +518,7 @@ uuid = "bf674bac-ffe4-48d3-9f32-72124ffa9ede" version = "0.2.0" [[deps.LinearAlgebra]] -deps = ["Libdl", "libblastrampoline_jll"] +deps = ["Libdl", "OpenBLAS_jll", "libblastrampoline_jll"] uuid = "37e2e46d-f89d-539d-b4ee-838fcccc9c8e" [[deps.LittleCMS_jll]] @@ -491,10 +528,16 @@ uuid = "d3a379c0-f9a3-5b72-a4c0-6bf4d2e8af0f" version = "2.12.0+0" [[deps.LogExpFunctions]] -deps = ["ChainRulesCore", "ChangesOfVariables", "DocStringExtensions", "InverseFunctions", "IrrationalConstants", "LinearAlgebra"] +deps = ["DocStringExtensions", "IrrationalConstants", "LinearAlgebra"] git-tree-sha1 = "7d6dd4e9212aebaeed356de34ccf262a3cd415aa" uuid = "2ab3a3ac-af41-5b50-aa03-7779005ae688" version = "0.3.26" +weakdeps = ["ChainRulesCore", "ChangesOfVariables", "InverseFunctions"] + + [deps.LogExpFunctions.extensions] + LogExpFunctionsChainRulesCoreExt = "ChainRulesCore" + LogExpFunctionsChangesOfVariablesExt = "ChangesOfVariables" + LogExpFunctionsInverseFunctionsExt = "InverseFunctions" [[deps.Logging]] uuid = "56ddb016-857b-54e1-b83d-db4d58db5568" @@ -559,7 +602,7 @@ version = "1.1.7" [[deps.MbedTLS_jll]] deps = ["Artifacts", "Libdl"] uuid = "c8ffd9c3-330d-5841-b78e-0817d7145fa1" -version = "2.28.0+0" +version = "2.28.2+0" [[deps.Memento]] deps = ["Dates", "Distributed", "Requires", "Serialization", "Sockets", "Test", "UUIDs"] @@ -590,7 +633,7 @@ version = "0.3.4" [[deps.MozillaCACerts_jll]] uuid = "14a3606d-f60d-562e-9121-12d972cd8159" -version = "2022.2.1" +version = "2022.10.11" [[deps.MutableArithmetics]] deps = ["LinearAlgebra", "SparseArrays", "Test"] @@ -617,7 +660,7 @@ version = "1.12.10" [[deps.OpenBLAS_jll]] deps = ["Artifacts", "CompilerSupportLibraries_jll", "Libdl"] uuid = "4536629a-c528-5b80-bd46-f80d51c5b363" -version = "0.3.20+0" +version = "0.3.21+4" [[deps.OpenJpeg_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl", "Libtiff_jll", "LittleCMS_jll", "Pkg", "libpng_jll"] @@ -678,9 +721,9 @@ uuid = "69de0a69-1ddd-5017-9359-2bf0b02dc9f0" version = "2.7.2" [[deps.Pkg]] -deps = ["Artifacts", "Dates", "Downloads", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] +deps = ["Artifacts", "Dates", "Downloads", "FileWatching", "LibGit2", "Libdl", "Logging", "Markdown", "Printf", "REPL", "Random", "SHA", "Serialization", "TOML", "Tar", "UUIDs", "p7zip_jll"] uuid = "44cfe95a-1eb2-52ea-b672-e2afdf69b78f" -version = "1.8.0" +version = "1.9.0" [[deps.PolyhedralRelaxations]] deps = ["DataStructures", "ForwardDiff", "JuMP", "Logging", "LoggingExtras"] @@ -757,6 +800,18 @@ git-tree-sha1 = "06b5ac80ff1b88bd82df92c1c1875eea3954cd6e" uuid = "f2b01f46-fcfa-551c-844a-d8ac1e96c665" version = "2.0.20" + [deps.Roots.extensions] + RootsForwardDiffExt = "ForwardDiff" + RootsIntervalRootFindingExt = "IntervalRootFinding" + RootsSymPyExt = "SymPy" + RootsSymPyPythonCallExt = "SymPyPythonCall" + + [deps.Roots.weakdeps] + ForwardDiff = "f6369f11-7733-5829-9624-2563aa707210" + IntervalRootFinding = "d2bf35a9-74e0-55ec-b149-d360ff49b807" + SymPy = "24249f21-da20-56a4-8eb1-6a02cf4ae2e6" + SymPyPythonCall = "bc8888f7-b21e-4b7c-a06a-5d9c9496438c" + [[deps.SHA]] uuid = "ea8e919c-243c-51af-8825-aaa63cd721ce" version = "0.7.0" @@ -803,14 +858,18 @@ uuid = "a2af1166-a08f-5f64-846c-94a0d3cef48c" version = "1.1.1" [[deps.SparseArrays]] -deps = ["LinearAlgebra", "Random"] +deps = ["Libdl", "LinearAlgebra", "Random", "Serialization", "SuiteSparse_jll"] uuid = "2f01184e-e22b-5df5-ae63-d93ebab69eaf" [[deps.SpecialFunctions]] -deps = ["ChainRulesCore", "IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] +deps = ["IrrationalConstants", "LogExpFunctions", "OpenLibm_jll", "OpenSpecFun_jll"] git-tree-sha1 = "e2cfc4012a19088254b3950b85c3c1d8882d864d" uuid = "276daf66-3868-5448-9aa4-cd146d93841b" version = "2.3.1" +weakdeps = ["ChainRulesCore"] + + [deps.SpecialFunctions.extensions] + SpecialFunctionsChainRulesCoreExt = "ChainRulesCore" [[deps.StackViews]] deps = ["OffsetArrays"] @@ -819,10 +878,14 @@ uuid = "cae243ae-269e-4f55-b966-ac2d0dc13c15" version = "0.1.1" [[deps.StaticArrays]] -deps = ["LinearAlgebra", "Random", "StaticArraysCore", "Statistics"] +deps = ["LinearAlgebra", "Random", "StaticArraysCore"] git-tree-sha1 = "0adf069a2a490c47273727e029371b31d44b72b2" uuid = "90137ffa-7385-5640-81b9-e52037218182" version = "1.6.5" +weakdeps = ["Statistics"] + + [deps.StaticArrays.extensions] + StaticArraysStatisticsExt = "Statistics" [[deps.StaticArraysCore]] git-tree-sha1 = "36b3d696ce6366023a0ea192b4cd442268995a0d" @@ -832,6 +895,7 @@ version = "1.4.2" [[deps.Statistics]] deps = ["LinearAlgebra", "SparseArrays"] uuid = "10745b16-79ce-11e8-11f9-7d13ad32a3b2" +version = "1.9.0" [[deps.StringManipulation]] deps = ["PrecompileTools"] @@ -839,10 +903,15 @@ git-tree-sha1 = "a04cabe79c5f01f4d723cc6704070ada0b9d46d5" uuid = "892a3eda-7b42-436c-8928-eab12a02cf0e" version = "0.3.4" +[[deps.SuiteSparse_jll]] +deps = ["Artifacts", "Libdl", "Pkg", "libblastrampoline_jll"] +uuid = "bea87d4a-7f5b-5778-9afe-8cc45184846c" +version = "5.10.1+6" + [[deps.TOML]] deps = ["Dates"] uuid = "fa267f1f-6049-4f14-aa54-33bafae1ed76" -version = "1.0.0" +version = "1.0.3" [[deps.TableTraits]] deps = ["IteratorInterfaceExtensions"] @@ -859,7 +928,7 @@ version = "1.11.0" [[deps.Tar]] deps = ["ArgTools", "SHA"] uuid = "a4e569a6-e804-4fa4-b0f3-eef7a1d5b13e" -version = "1.10.1" +version = "1.10.0" [[deps.TensorCore]] deps = ["LinearAlgebra"] @@ -896,10 +965,15 @@ uuid = "cf7118a7-6976-5b1a-9a39-7adc72f591a4" uuid = "4ec0a83e-493e-50e2-b9ac-8f72acf5a8f5" [[deps.Unitful]] -deps = ["ConstructionBase", "Dates", "InverseFunctions", "LinearAlgebra", "Random"] +deps = ["Dates", "LinearAlgebra", "Random"] git-tree-sha1 = "a72d22c7e13fe2de562feda8645aa134712a87ee" uuid = "1986cc42-f94f-5a68-af5c-568840ba703d" version = "1.17.0" +weakdeps = ["ConstructionBase", "InverseFunctions"] + + [deps.Unitful.extensions] + ConstructionBaseUnitfulExt = "ConstructionBase" + InverseFunctionsUnitfulExt = "InverseFunctions" [[deps.WeakRefStrings]] deps = ["DataAPI", "InlineStrings", "Parsers"] @@ -915,7 +989,7 @@ version = "1.6.1" [[deps.Zlib_jll]] deps = ["Libdl"] uuid = "83775a58-1f1d-513f-b197-d71354ab007a" -version = "1.2.12+3" +version = "1.2.13+0" [[deps.Zstd_jll]] deps = ["Artifacts", "JLLWrappers", "Libdl"] @@ -930,9 +1004,9 @@ uuid = "477f73a3-ac25-53e9-8cc3-50b2fa2566f0" version = "1.0.6+1" [[deps.libblastrampoline_jll]] -deps = ["Artifacts", "Libdl", "OpenBLAS_jll"] +deps = ["Artifacts", "Libdl"] uuid = "8e850b90-86db-534c-a0d3-1478176c7d93" -version = "5.1.1+0" +version = "5.7.0+0" [[deps.libgeotiff_jll]] deps = ["Artifacts", "JLLWrappers", "LibCURL_jll", "Libdl", "Libtiff_jll", "PROJ_jll", "Pkg"] From ae0e87c7608a7fa3ddc42f46594fe2856db38798 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 14 Oct 2024 11:25:38 -0400 Subject: [PATCH 09/15] Update electric_storage.jl rm documentation to check failing test --- src/core/energy_storage/electric_storage.jl | 281 ++++++++++---------- 1 file changed, 141 insertions(+), 140 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index b392a5f84..5c4e5436a 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -1,145 +1,146 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. -""" - Degradation - -Inputs used when `ElectricStorage.model_degradation` is `true`: -```julia -Base.@kwdef mutable struct Degradation - calendar_fade_coefficient::Real = 2.46E-03 - cycle_fade_coefficient::Real = 7.82E-05 - time_exponent::Real = 0.5 - installed_cost_per_kwh_declination_rate::Float64 = 0.05 - maintenance_strategy::String = "augmentation" # one of ["augmentation", "replacement"] - maintenance_cost_per_kwh::Vector{<:Real} = Real[] -end -``` +# """ +# Degradation + +# Inputs used when `ElectricStorage.model_degradation` is `true`: +# ```julia +# Base.@kwdef mutable struct Degradation +# calendar_fade_coefficient::Real = 2.46E-03 +# cycle_fade_coefficient::Real = 7.82E-05 +# time_exponent::Real = 0.5 +# installed_cost_per_kwh_declination_rate::Float64 = 0.05 +# maintenance_strategy::String = "augmentation" # one of ["augmentation", "replacement"] +# maintenance_cost_per_kwh::Vector{<:Real} = Real[] +# end +# ``` + +# None of the above values are required. If `ElectricStorage.model_degradation` is `true` then the +# defaults above are used. If the `maintenance_cost_per_kwh` is not provided then it is determined using the `ElectricStorage.installed_cost_per_kwh` and the `installed_cost_per_kwh_declination_rate` along with a present worth factor ``f`` to account for the present cost of buying a battery in the future. The present worth factor for each day is: + +# `` +# f(day) = \\frac{ (1-r_g)^\\frac{day}{365} } { (1+r_d)^\\frac{day}{365} } +# `` + +# where ``r_g`` = `installed_cost_per_kwh_declination_rate` and ``r_d`` = `p.s.financial.owner_discount_rate_fraction`. + +# Note this day-specific calculation of the present-worth factor accumulates differently from the annually updated discount +# rate for other net-present value calculations in REopt, and has a higher effective discount rate as a result. The present +# worth factor is used in the same manner irrespective of the `maintenance_strategy`. + +# !!! warn +# When modeling degradation the following ElectricStorage inputs are not used: +# - `replace_cost_per_kwh` +# - `battery_replacement_year` +# The are replaced by the `maintenance_cost_per_kwh` vector. +# Inverter replacement costs and inverter replacement year should still be used to model scheduled replacement of inverter. + +# !!! note +# When providing the `maintenance_cost_per_kwh` it must have a length equal to `Financial.analysis_years*365`-1. + + +# # Battery State Of Health +# The state of health [`SOH`] is a linear function of the daily average state of charge [`Eavg`] and +# the daily equivalent full cycles [`EFC`]. The initial `SOH` is set to the optimal battery energy capacity +# (in kWh). The evolution of the `SOH` beyond the first day is: + +# `` +# SOH[d] = SOH[d-1] - h\\left( +# \\frac{1}{2} k_{cal} Eavg[d-1] / \\sqrt{d} + k_{cyc} EFC[d-1] \\quad \\forall d \\in \\{2\\dots D\\} +# \\right) +# `` + +# where: +# - ``k_{cal}`` is the `calendar_fade_coefficient` +# - ``k_{cyc}`` is the `cycle_fade_coefficient` +# - ``h`` is the hours per time step +# - ``D`` is the total number of days, 365 * `analysis_years` + +# The `SOH` is used to determine the maintence cost of the storage system, which depends on the `maintenance_strategy`. + +# !!! note +# Battery degradation parameters are from based on laboratory aging data, and are expected to be reasonable only within +# the range of conditions tested. Battery lifetime can vary widely from these estimates based on battery use and system design. +# Battery cost estimates are based on domain expertise and published guidelines and are not to be taken as an indicator of real +# system costs. + +# # Augmentation Maintenance Strategy +# The augmentation maintenance strategy assumes that the battery energy capacity is maintained by replacing +# degraded cells daily in terms of cost. Using the definition of the `SOH` above the maintenance cost is: + +# `` +# C_{\\text{aug}} = \\sum_{d \\in \\{2\\dots D\\}} C_{\\text{install}} f(day) \\left( SOH[d-1] - SOH[d] \\right) +# `` + +# where +# - ``f(day)`` is the present worth factor of battery degradation costs as described above; +# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`; and +# - ``SOH[d-1] - SOH[d]`` is the incremental amount of battery capacity lost in a day. + + +# The ``C_{\\text{aug}}`` is added to the objective function to be minimized with all other costs. + +# # Replacement Maintenance Strategy +# Modeling the replacement maintenance strategy is more complex than the augmentation strategy. +# Effectively the replacement strategy says that the battery has to be replaced once the `SOH` drops below 80% +# of the optimal, purchased capacity. It is possible that multiple replacements (at same replacement frequency) could be required under +# this strategy. + +# !!! warn +# The "replacement" maintenance strategy requires integer decision variables. +# Some solvers are slow with integer decision variables. + +# The replacement strategy cost is: + +# `` +# C_{\\text{repl}} = B_{\\text{kWh}} N_{\\text{repl}} f(d_{80}) C_{\\text{install}} +# `` + +# where: +# - ``B_{\\text{kWh}}`` is the optimal battery capacity (`ElectricStorage.size_kwh` in the results dictionary); +# - ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` falls below 80% of original capacity); +# - ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month in which the `SOH` falls below 80% of original capacity; +# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. +# The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. + +# ## Battery residual value +# Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. +# For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). +# The last battery which was placed in service during month 290 only serves for 10 months (i.e. 6.89% of its expected life assuming 145 month replacement frequecy). +# In this case, the battery has 93.1% of residual life remaining as useful life left after analysis period ends. +# A residual value cost vector is created to hold this value for all months. Residual value is calculated as: +# `` +# C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} +# `` +# where: +# - ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; +# - ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; +# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. + +# The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. + +# # Example of inputs +# The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: +# ```javascript +# { +# ... +# "ElectricStorage": { +# "installed_cost_per_kwh": 390, +# ... +# "model_degradation": true, +# "degradation": { +# "calendar_fade_coefficient": 2.86E-03, +# "cycle_fade_coefficient": 6.22E-05, +# "installed_cost_per_kwh_declination_rate": 0.06, +# "maintenance_strategy": "replacement", +# ... +# } +# }, +# ... +# } +# ``` +# Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. +# """ -None of the above values are required. If `ElectricStorage.model_degradation` is `true` then the -defaults above are used. If the `maintenance_cost_per_kwh` is not provided then it is determined using the `ElectricStorage.installed_cost_per_kwh` and the `installed_cost_per_kwh_declination_rate` along with a present worth factor ``f`` to account for the present cost of buying a battery in the future. The present worth factor for each day is: - -`` -f(day) = \\frac{ (1-r_g)^\\frac{day}{365} } { (1+r_d)^\\frac{day}{365} } -`` - -where ``r_g`` = `installed_cost_per_kwh_declination_rate` and ``r_d`` = `p.s.financial.owner_discount_rate_fraction`. - -Note this day-specific calculation of the present-worth factor accumulates differently from the annually updated discount -rate for other net-present value calculations in REopt, and has a higher effective discount rate as a result. The present -worth factor is used in the same manner irrespective of the `maintenance_strategy`. - -!!! warn - When modeling degradation the following ElectricStorage inputs are not used: - - `replace_cost_per_kwh` - - `battery_replacement_year` - The are replaced by the `maintenance_cost_per_kwh` vector. - Inverter replacement costs and inverter replacement year should still be used to model scheduled replacement of inverter. - -!!! note - When providing the `maintenance_cost_per_kwh` it must have a length equal to `Financial.analysis_years*365`-1. - - -# Battery State Of Health -The state of health [`SOH`] is a linear function of the daily average state of charge [`Eavg`] and -the daily equivalent full cycles [`EFC`]. The initial `SOH` is set to the optimal battery energy capacity -(in kWh). The evolution of the `SOH` beyond the first day is: - -`` -SOH[d] = SOH[d-1] - h\\left( - \\frac{1}{2} k_{cal} Eavg[d-1] / \\sqrt{d} + k_{cyc} EFC[d-1] \\quad \\forall d \\in \\{2\\dots D\\} -\\right) -`` - -where: -- ``k_{cal}`` is the `calendar_fade_coefficient` -- ``k_{cyc}`` is the `cycle_fade_coefficient` -- ``h`` is the hours per time step -- ``D`` is the total number of days, 365 * `analysis_years` - -The `SOH` is used to determine the maintence cost of the storage system, which depends on the `maintenance_strategy`. - -!!! note - Battery degradation parameters are from based on laboratory aging data, and are expected to be reasonable only within - the range of conditions tested. Battery lifetime can vary widely from these estimates based on battery use and system design. - Battery cost estimates are based on domain expertise and published guidelines and are not to be taken as an indicator of real - system costs. - -# Augmentation Maintenance Strategy -The augmentation maintenance strategy assumes that the battery energy capacity is maintained by replacing -degraded cells daily in terms of cost. Using the definition of the `SOH` above the maintenance cost is: - -`` -C_{\\text{aug}} = \\sum_{d \\in \\{2\\dots D\\}} C_{\\text{install}} f(day) \\left( SOH[d-1] - SOH[d] \\right) -`` - -where -- ``f(day)`` is the present worth factor of battery degradation costs as described above; -- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`; and -- ``SOH[d-1] - SOH[d]`` is the incremental amount of battery capacity lost in a day. - - -The ``C_{\\text{aug}}`` is added to the objective function to be minimized with all other costs. - -# Replacement Maintenance Strategy -Modeling the replacement maintenance strategy is more complex than the augmentation strategy. -Effectively the replacement strategy says that the battery has to be replaced once the `SOH` drops below 80% -of the optimal, purchased capacity. It is possible that multiple replacements (at same replacement frequency) could be required under -this strategy. - -!!! warn - The "replacement" maintenance strategy requires integer decision variables. - Some solvers are slow with integer decision variables. - -The replacement strategy cost is: - -`` -C_{\\text{repl}} = B_{\\text{kWh}} N_{\\text{repl}} f(d_{80}) C_{\\text{install}} -`` - -where: -- ``B_{\\text{kWh}}`` is the optimal battery capacity (`ElectricStorage.size_kwh` in the results dictionary); -- ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` falls below 80% of original capacity); -- ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month in which the `SOH` falls below 80% of original capacity; -- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. -The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. - -## Battery residual value -Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. -For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). -The last battery which was placed in service during month 290 only serves for 10 months (i.e. 6.89% of its expected life assuming 145 month replacement frequecy). -In this case, the battery has 93.1% of residual life remaining as useful life left after analysis period ends. -A residual value cost vector is created to hold this value for all months. Residual value is calculated as: -`` -C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} -`` -where: -- ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; -- ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; -- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. - -The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. - -# Example of inputs -The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: -```javascript -{ - ... - "ElectricStorage": { - "installed_cost_per_kwh": 390, - ... - "model_degradation": true, - "degradation": { - "calendar_fade_coefficient": 2.86E-03, - "cycle_fade_coefficient": 6.22E-05, - "installed_cost_per_kwh_declination_rate": 0.06, - "maintenance_strategy": "replacement", - ... - } - }, - ... -} -``` -Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. -""" Base.@kwdef mutable struct Degradation calendar_fade_coefficient::Real = 2.55E-03 cycle_fade_coefficient::Real = 9.83E-05 From 6f5eea9c66187d2db92aef941f3b23b12dccc014 Mon Sep 17 00:00:00 2001 From: Bhavesh Rathod <83797218+rathod-b@users.noreply.github.com> Date: Mon, 14 Oct 2024 11:40:51 -0400 Subject: [PATCH 10/15] Update CHANGELOG.md From fcd419254032cc78e91224d6c4eff90268360452 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 14 Oct 2024 11:50:59 -0400 Subject: [PATCH 11/15] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 126 ++++++++++---------- 1 file changed, 63 insertions(+), 63 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 5c4e5436a..c9565aa18 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -1,84 +1,84 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. -# """ -# Degradation - -# Inputs used when `ElectricStorage.model_degradation` is `true`: -# ```julia -# Base.@kwdef mutable struct Degradation -# calendar_fade_coefficient::Real = 2.46E-03 -# cycle_fade_coefficient::Real = 7.82E-05 -# time_exponent::Real = 0.5 -# installed_cost_per_kwh_declination_rate::Float64 = 0.05 -# maintenance_strategy::String = "augmentation" # one of ["augmentation", "replacement"] -# maintenance_cost_per_kwh::Vector{<:Real} = Real[] -# end -# ``` +""" + Degradation -# None of the above values are required. If `ElectricStorage.model_degradation` is `true` then the -# defaults above are used. If the `maintenance_cost_per_kwh` is not provided then it is determined using the `ElectricStorage.installed_cost_per_kwh` and the `installed_cost_per_kwh_declination_rate` along with a present worth factor ``f`` to account for the present cost of buying a battery in the future. The present worth factor for each day is: +Inputs used when `ElectricStorage.model_degradation` is `true`: +```julia +Base.@kwdef mutable struct Degradation + calendar_fade_coefficient::Real = 2.46E-03 + cycle_fade_coefficient::Real = 7.82E-05 + time_exponent::Real = 0.5 + installed_cost_per_kwh_declination_rate::Float64 = 0.05 + maintenance_strategy::String = "augmentation" # one of ["augmentation", "replacement"] + maintenance_cost_per_kwh::Vector{<:Real} = Real[] +end +``` -# `` -# f(day) = \\frac{ (1-r_g)^\\frac{day}{365} } { (1+r_d)^\\frac{day}{365} } -# `` +None of the above values are required. If `ElectricStorage.model_degradation` is `true` then the +defaults above are used. If the `maintenance_cost_per_kwh` is not provided then it is determined using the `ElectricStorage.installed_cost_per_kwh` and the `installed_cost_per_kwh_declination_rate` along with a present worth factor ``f`` to account for the present cost of buying a battery in the future. The present worth factor for each day is: -# where ``r_g`` = `installed_cost_per_kwh_declination_rate` and ``r_d`` = `p.s.financial.owner_discount_rate_fraction`. +`` +f(day) = \\frac{ (1-r_g)^\\frac{day}{365} } { (1+r_d)^\\frac{day}{365} } +`` -# Note this day-specific calculation of the present-worth factor accumulates differently from the annually updated discount -# rate for other net-present value calculations in REopt, and has a higher effective discount rate as a result. The present -# worth factor is used in the same manner irrespective of the `maintenance_strategy`. +where ``r_g`` = `installed_cost_per_kwh_declination_rate` and ``r_d`` = `p.s.financial.owner_discount_rate_fraction`. -# !!! warn -# When modeling degradation the following ElectricStorage inputs are not used: -# - `replace_cost_per_kwh` -# - `battery_replacement_year` -# The are replaced by the `maintenance_cost_per_kwh` vector. -# Inverter replacement costs and inverter replacement year should still be used to model scheduled replacement of inverter. +Note this day-specific calculation of the present-worth factor accumulates differently from the annually updated discount +rate for other net-present value calculations in REopt, and has a higher effective discount rate as a result. The present +worth factor is used in the same manner irrespective of the `maintenance_strategy`. -# !!! note -# When providing the `maintenance_cost_per_kwh` it must have a length equal to `Financial.analysis_years*365`-1. +!!! warn + When modeling degradation the following ElectricStorage inputs are not used: + - `replace_cost_per_kwh` + - `battery_replacement_year` + The are replaced by the `maintenance_cost_per_kwh` vector. + Inverter replacement costs and inverter replacement year should still be used to model scheduled replacement of inverter. +!!! note + When providing the `maintenance_cost_per_kwh` it must have a length equal to `Financial.analysis_years*365`-1. -# # Battery State Of Health -# The state of health [`SOH`] is a linear function of the daily average state of charge [`Eavg`] and -# the daily equivalent full cycles [`EFC`]. The initial `SOH` is set to the optimal battery energy capacity -# (in kWh). The evolution of the `SOH` beyond the first day is: -# `` -# SOH[d] = SOH[d-1] - h\\left( -# \\frac{1}{2} k_{cal} Eavg[d-1] / \\sqrt{d} + k_{cyc} EFC[d-1] \\quad \\forall d \\in \\{2\\dots D\\} -# \\right) -# `` +# Battery State Of Health +The state of health [`SOH`] is a linear function of the daily average state of charge [`Eavg`] and +the daily equivalent full cycles [`EFC`]. The initial `SOH` is set to the optimal battery energy capacity +(in kWh). The evolution of the `SOH` beyond the first day is: -# where: -# - ``k_{cal}`` is the `calendar_fade_coefficient` -# - ``k_{cyc}`` is the `cycle_fade_coefficient` -# - ``h`` is the hours per time step -# - ``D`` is the total number of days, 365 * `analysis_years` +`` +SOH[d] = SOH[d-1] - h\\left( + \\frac{1}{2} k_{cal} Eavg[d-1] / \\sqrt{d} + k_{cyc} EFC[d-1] \\quad \\forall d \\in \\{2\\dots D\\} +\\right) +`` -# The `SOH` is used to determine the maintence cost of the storage system, which depends on the `maintenance_strategy`. +where: +- ``k_{cal}`` is the `calendar_fade_coefficient` +- ``k_{cyc}`` is the `cycle_fade_coefficient` +- ``h`` is the hours per time step +- ``D`` is the total number of days, 365 * `analysis_years` -# !!! note -# Battery degradation parameters are from based on laboratory aging data, and are expected to be reasonable only within -# the range of conditions tested. Battery lifetime can vary widely from these estimates based on battery use and system design. -# Battery cost estimates are based on domain expertise and published guidelines and are not to be taken as an indicator of real -# system costs. +The `SOH` is used to determine the maintence cost of the storage system, which depends on the `maintenance_strategy`. -# # Augmentation Maintenance Strategy -# The augmentation maintenance strategy assumes that the battery energy capacity is maintained by replacing -# degraded cells daily in terms of cost. Using the definition of the `SOH` above the maintenance cost is: +!!! note + Battery degradation parameters are from based on laboratory aging data, and are expected to be reasonable only within + the range of conditions tested. Battery lifetime can vary widely from these estimates based on battery use and system design. + Battery cost estimates are based on domain expertise and published guidelines and are not to be taken as an indicator of real + system costs. -# `` -# C_{\\text{aug}} = \\sum_{d \\in \\{2\\dots D\\}} C_{\\text{install}} f(day) \\left( SOH[d-1] - SOH[d] \\right) -# `` +# Augmentation Maintenance Strategy +The augmentation maintenance strategy assumes that the battery energy capacity is maintained by replacing +degraded cells daily in terms of cost. Using the definition of the `SOH` above the maintenance cost is: -# where -# - ``f(day)`` is the present worth factor of battery degradation costs as described above; -# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`; and -# - ``SOH[d-1] - SOH[d]`` is the incremental amount of battery capacity lost in a day. +`` +C_{\\text{aug}} = \\sum_{d \\in \\{2\\dots D\\}} C_{\\text{install}} f(day) \\left( SOH[d-1] - SOH[d] \\right) +`` +where +- ``f(day)`` is the present worth factor of battery degradation costs as described above; +- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`; and +- ``SOH[d-1] - SOH[d]`` is the incremental amount of battery capacity lost in a day. -# The ``C_{\\text{aug}}`` is added to the objective function to be minimized with all other costs. +The ``C_{\\text{aug}}`` is added to the objective function to be minimized with all other costs. +""" # # Replacement Maintenance Strategy # Modeling the replacement maintenance strategy is more complex than the augmentation strategy. # Effectively the replacement strategy says that the battery has to be replaced once the `SOH` drops below 80% @@ -139,7 +139,7 @@ # } # ``` # Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. -# """ + Base.@kwdef mutable struct Degradation calendar_fade_coefficient::Real = 2.55E-03 From bcccb4eedaca9267fe1624f7465c93c60e8f5c3f Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 14 Oct 2024 12:45:50 -0400 Subject: [PATCH 12/15] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 39 ++++++++++----------- 1 file changed, 19 insertions(+), 20 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index c9565aa18..6ccaf4389 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -78,30 +78,30 @@ where The ``C_{\\text{aug}}`` is added to the objective function to be minimized with all other costs. -""" -# # Replacement Maintenance Strategy -# Modeling the replacement maintenance strategy is more complex than the augmentation strategy. -# Effectively the replacement strategy says that the battery has to be replaced once the `SOH` drops below 80% -# of the optimal, purchased capacity. It is possible that multiple replacements (at same replacement frequency) could be required under -# this strategy. -# !!! warn -# The "replacement" maintenance strategy requires integer decision variables. -# Some solvers are slow with integer decision variables. +# Replacement Maintenance Strategy +Modeling the replacement maintenance strategy is more complex than the augmentation strategy. +Effectively the replacement strategy says that the battery has to be replaced once the `SOH` drops below 80% +of the optimal, purchased capacity. It is possible that multiple replacements (at same replacement frequency) could be required under +this strategy. -# The replacement strategy cost is: +!!! warn + The "replacement" maintenance strategy requires integer decision variables. + Some solvers are slow with integer decision variables. -# `` -# C_{\\text{repl}} = B_{\\text{kWh}} N_{\\text{repl}} f(d_{80}) C_{\\text{install}} -# `` +The replacement strategy cost is: -# where: -# - ``B_{\\text{kWh}}`` is the optimal battery capacity (`ElectricStorage.size_kwh` in the results dictionary); -# - ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` falls below 80% of original capacity); -# - ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month in which the `SOH` falls below 80% of original capacity; -# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. -# The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. +`` +C_{\\text{repl}} = B_{\\text{kWh}} N_{\\text{repl}} f(d_{80}) C_{\\text{install}} +`` +where: +- ``B_{\\text{kWh}}`` is the optimal battery capacity (`ElectricStorage.size_kwh` in the results dictionary); +- ``N_{\\text{repl}}`` is the number of battery replacments required (a function of the month in which the `SOH` falls below 80% of original capacity); +- ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month in which the `SOH` falls below 80% of original capacity; +- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. +The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. +""" # ## Battery residual value # Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. # For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). @@ -140,7 +140,6 @@ The ``C_{\\text{aug}}`` is added to the objective function to be minimized with # ``` # Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. - Base.@kwdef mutable struct Degradation calendar_fade_coefficient::Real = 2.55E-03 cycle_fade_coefficient::Real = 9.83E-05 From 6658c68aba2dd3a8a53901020168ad8fd38857af Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 14 Oct 2024 15:33:11 -0400 Subject: [PATCH 13/15] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 44 +++++++++++---------- 1 file changed, 23 insertions(+), 21 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 6ccaf4389..1329080be 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -118,27 +118,29 @@ The ``C_{\\text{repl}}`` is added to the objective function to be minimized with # The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. -# # Example of inputs -# The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: -# ```javascript -# { -# ... -# "ElectricStorage": { -# "installed_cost_per_kwh": 390, -# ... -# "model_degradation": true, -# "degradation": { -# "calendar_fade_coefficient": 2.86E-03, -# "cycle_fade_coefficient": 6.22E-05, -# "installed_cost_per_kwh_declination_rate": 0.06, -# "maintenance_strategy": "replacement", -# ... -# } -# }, -# ... -# } -# ``` -# Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. +""" +# Example of inputs +The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: +```javascript +{ + ... + "ElectricStorage": { + "installed_cost_per_kwh": 390, + ... + "model_degradation": true, + "degradation": { + "calendar_fade_coefficient": 2.86E-03, + "cycle_fade_coefficient": 6.22E-05, + "installed_cost_per_kwh_declination_rate": 0.06, + "maintenance_strategy": "replacement", + ... + } + }, + ... +} +``` +Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. +""" Base.@kwdef mutable struct Degradation calendar_fade_coefficient::Real = 2.55E-03 From 8e74c7f85eb729de6dd3303609dc5c7d3a904efa Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 14 Oct 2024 15:44:08 -0400 Subject: [PATCH 14/15] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 32 ++++++++++----------- 1 file changed, 16 insertions(+), 16 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 1329080be..5cc6f6526 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -101,24 +101,14 @@ where: - ``f(d_{80})`` is the present worth factor at approximately the 15th day of the month in which the `SOH` falls below 80% of original capacity; - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. The ``C_{\\text{repl}}`` is added to the objective function to be minimized with all other costs. -""" -# ## Battery residual value -# Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. -# For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). -# The last battery which was placed in service during month 290 only serves for 10 months (i.e. 6.89% of its expected life assuming 145 month replacement frequecy). -# In this case, the battery has 93.1% of residual life remaining as useful life left after analysis period ends. -# A residual value cost vector is created to hold this value for all months. Residual value is calculated as: -# `` -# C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} -# `` -# where: -# - ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; -# - ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; -# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. -# The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. +## Battery residual value +Since the battery can be replaced one-to-many times under this strategy, battery residual value captures the \$ value of remaining battery life at end of analysis period. +For example if replacement happens in month 145, then assuming 25 year analysis period there will be 2 replacements (months 145 and 290). +The last battery which was placed in service during month 290 only serves for 10 months (i.e. 6.89% of its expected life assuming 145 month replacement frequecy). +In this case, the battery has 93.1% of residual life remaining as useful life left after analysis period ends. +A residual value cost vector is created to hold this value for all months. Residual value is calculated as: -""" # Example of inputs The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: ```javascript @@ -142,6 +132,16 @@ The following shows how one would use the degradation model in REopt via the [Sc Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. """ +# `` +# C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} +# `` +# where: +# - ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; +# - ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; +# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. + +# The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. + Base.@kwdef mutable struct Degradation calendar_fade_coefficient::Real = 2.55E-03 cycle_fade_coefficient::Real = 9.83E-05 From a4690b799564fcffcefbcd26a608c6bdfd146690 Mon Sep 17 00:00:00 2001 From: "Rathod, Bhavesh" Date: Mon, 14 Oct 2024 16:50:15 -0400 Subject: [PATCH 15/15] Update electric_storage.jl --- src/core/energy_storage/electric_storage.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/energy_storage/electric_storage.jl b/src/core/energy_storage/electric_storage.jl index 5cc6f6526..8f99fef77 100644 --- a/src/core/energy_storage/electric_storage.jl +++ b/src/core/energy_storage/electric_storage.jl @@ -109,6 +109,16 @@ The last battery which was placed in service during month 290 only serves for 10 In this case, the battery has 93.1% of residual life remaining as useful life left after analysis period ends. A residual value cost vector is created to hold this value for all months. Residual value is calculated as: +`` +C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} +`` +where: +- ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; +- ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; +- ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. + +The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. + # Example of inputs The following shows how one would use the degradation model in REopt via the [Scenario](@ref) inputs: ```javascript @@ -132,16 +142,6 @@ The following shows how one would use the degradation model in REopt via the [Sc Note that not all of the above inputs are necessary. When not providing `calendar_fade_coefficient` for example the default value will be used. """ -# `` -# C_{\\text{residual}} = R f(d_{\\text{last}}) C_{\\text{install}} -# `` -# where: -# - ``R`` is the `residual_factor` which determines portion of battery life remaining at the end of the analysis period; -# - ``f(d_{\\text{last}})`` is the present worth factor at approximately the 15th day of the last month in the analysis period; -# - ``C_{\\text{install}}`` is the `ElectricStorage.installed_cost_per_kwh`. - -# The ``C_{\\text{residual}}`` is added to the objective function to be minimized with all other costs. - Base.@kwdef mutable struct Degradation calendar_fade_coefficient::Real = 2.55E-03 cycle_fade_coefficient::Real = 9.83E-05