Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Battery storage cost constant version 2 #348

Open
wants to merge 25 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from 15 commits
Commits
Show all changes
25 commits
Select commit Hold shift + click to select a range
53f7638
Added battery storage cost constant
toddleif Feb 6, 2024
4b85f29
Fixed bugs in code and added tests
toddleif Feb 11, 2024
284ec0f
Updates to test code and ran tests locally
toddleif Feb 12, 2024
321e353
Updates to runtests.jl to run without battery cost constants
toddleif Feb 13, 2024
7cb7460
Update to runtests.jl
toddleif Feb 13, 2024
334f7cf
Updates to runtests.jl
toddleif Feb 14, 2024
845e09a
Debugged financial result computation
toddleif Feb 16, 2024
42732c6
update to tests with Xpress
toddleif Feb 29, 2024
eb60c3c
Committing again to run tests
toddleif Mar 1, 2024
97173fb
Committing again to run tests
toddleif Mar 4, 2024
711ce88
Merge branch 'develop' into storage-cost-constraints-version2
toddleif Jun 3, 2024
f5192e3
Reformulated the equations to remove the indicator constraints. Updat…
toddleif Jun 4, 2024
60374c2
Added multinode capability to the battery cost constant
toddleif Jun 4, 2024
cb91a7e
Merge branch 'develop' into storage-cost-constraints-version2
adfarth Jul 3, 2024
8e5943d
Added note for the new inputs that are not used in the battery degrad…
toddleif Jul 3, 2024
1c7b658
Merge branch 'develop' into storage-cost-constraints-version2
adfarth Jul 3, 2024
c8c247d
Merge branch 'storage-cost-constraints-version2' of https://github.co…
toddleif Jul 3, 2024
4a20641
Revert "Merge branch 'storage-cost-constraints-version2' of https://g…
toddleif Jul 3, 2024
a887e78
Corrected the version number
toddleif Jul 3, 2024
88e25c4
Updates to the ElectricStorage cost constant code
toddleif Nov 3, 2024
6594016
Merge branch 'develop' into storage-cost-constraints-version2
toddleif Nov 3, 2024
b6fe81d
Update CHANGELOG.md
adfarth Nov 4, 2024
4138157
Edits based on code review
toddleif Nov 7, 2024
7c0acf0
Merge branch 'develop' into storage-cost-constraints-version2
adfarth Nov 7, 2024
ba610a6
Update electric_storage.jl
adfarth Nov 7, 2024
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 35 additions & 1 deletion src/core/energy_storage/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,10 @@ worth factor is used in two different ways, depending on the `maintenance_strate
- `replace_cost_per_kwh`
- `inverter_replacement_year`
- `battery_replacement_year`
- `replace_cost_per_kw`
adfarth marked this conversation as resolved.
Show resolved Hide resolved
- `replace_cost_per_kwh`
- `inverter_replacement_year`
- `battery_replacement_year`
The are replaced by the `maintenance_cost_per_kwh` vector.

!!! note
Expand Down Expand Up @@ -149,10 +153,13 @@ end
can_grid_charge::Bool = off_grid_flag ? false : true
installed_cost_per_kw::Real = 910.0
installed_cost_per_kwh::Real = 455.0
installed_cost_constant::Real = 0.0
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@toddleif could you please add a brief description of this input to this markdown area?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

That's a great idea. I just added a description.

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@toddleif I just modified the description to be in-line with the inputs and to just give a short high-level description of what that input is. Could you review?

replace_cost_per_kw::Real = 715.0
replace_cost_per_kwh::Real = 318.0
replace_cost_constant::Real = 0.0
inverter_replacement_year::Int = 10
battery_replacement_year::Int = 10
cost_constant_replacement_year::Int = 10
macrs_option_years::Int = 7
macrs_bonus_fraction::Float64 = 0.6
macrs_itc_reduction::Float64 = 0.5
Expand Down Expand Up @@ -182,10 +189,13 @@ Base.@kwdef struct ElectricStorageDefaults
can_grid_charge::Bool = off_grid_flag ? false : true
installed_cost_per_kw::Real = 910.0
installed_cost_per_kwh::Real = 455.0
installed_cost_constant::Real = 0.0
replace_cost_per_kw::Real = 715.0
replace_cost_per_kwh::Real = 318.0
replace_cost_constant::Real = 0.0
inverter_replacement_year::Int = 10
battery_replacement_year::Int = 10
cost_constant_replacement_year::Int = 10
macrs_option_years::Int = 7
macrs_bonus_fraction::Float64 = 0.6
macrs_itc_reduction::Float64 = 0.5
Expand Down Expand Up @@ -221,10 +231,13 @@ struct ElectricStorage <: AbstractElectricStorage
can_grid_charge::Bool
installed_cost_per_kw::Real
installed_cost_per_kwh::Real
installed_cost_constant::Real
replace_cost_per_kw::Real
replace_cost_per_kwh::Real
replace_cost_constant::Real
inverter_replacement_year::Int
battery_replacement_year::Int
cost_constant_replacement_year::Int
macrs_option_years::Int
macrs_bonus_fraction::Float64
macrs_itc_reduction::Float64
Expand All @@ -236,6 +249,7 @@ struct ElectricStorage <: AbstractElectricStorage
grid_charge_efficiency::Float64
net_present_cost_per_kw::Real
net_present_cost_per_kwh::Real
net_present_cost_cost_constant::Real
model_degradation::Bool
degradation::Degradation
minimum_avg_soc_fraction::Float64
Expand Down Expand Up @@ -277,6 +291,19 @@ struct ElectricStorage <: AbstractElectricStorage

net_present_cost_per_kwh -= s.total_rebate_per_kwh

net_present_cost_cost_constant = effective_cost(;
itc_basis = s.installed_cost_constant,
replacement_cost = s.cost_constant_replacement_year >= f.analysis_years ? 0.0 : s.replace_cost_constant,
replacement_year = s.cost_constant_replacement_year,
discount_rate = f.owner_discount_rate_fraction,
tax_rate = f.owner_tax_rate_fraction,
itc = s.total_itc_fraction,
macrs_schedule = s.macrs_option_years == 7 ? f.macrs_seven_year : f.macrs_five_year,
macrs_bonus_fraction = s.macrs_bonus_fraction,
macrs_itc_reduction = s.macrs_itc_reduction

)

if haskey(d, :degradation)
degr = Degradation(;dictkeys_tosymbols(d[:degradation])...)
else
Expand All @@ -286,13 +313,16 @@ struct ElectricStorage <: AbstractElectricStorage
# 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
replace_cost_constant = s.replace_cost_constant
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
haskey(d, :replace_cost_per_kwh) && d[:replace_cost_per_kwh] != 0.0 ||
haskey(d, :replace_cost_constant) && d[:replace_cost_constant] != 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
replace_cost_constant = 0.0
end

return new(
Expand All @@ -309,10 +339,13 @@ struct ElectricStorage <: AbstractElectricStorage
s.can_grid_charge,
s.installed_cost_per_kw,
s.installed_cost_per_kwh,
s.installed_cost_constant,
replace_cost_per_kw,
replace_cost_per_kwh,
replace_cost_constant,
s.inverter_replacement_year,
s.battery_replacement_year,
s.cost_constant_replacement_year,
s.macrs_option_years,
s.macrs_bonus_fraction,
s.macrs_itc_reduction,
Expand All @@ -324,6 +357,7 @@ struct ElectricStorage <: AbstractElectricStorage
s.grid_charge_efficiency,
net_present_cost_per_kw,
net_present_cost_per_kwh,
net_present_cost_cost_constant,
s.model_degradation,
degr,
s.minimum_avg_soc_fraction
Expand Down
13 changes: 12 additions & 1 deletion src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -387,9 +387,19 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
end
end

# Include the electric storage cost constants only if the installed_cost_constant or the replace_cost_constant is not zero
for b in p.s.storage.types.elec
adfarth marked this conversation as resolved.
Show resolved Hide resolved
adfarth marked this conversation as resolved.
Show resolved Hide resolved
if p.s.storage.attr[b].installed_cost_constant != 0 || p.s.storage.attr[b].replace_cost_constant != 0
@constraint(m, [b in p.s.storage.types.elec], m[:dvStorageEnergy][b] <= p.s.storage.attr[b].max_kwh * m[:dvBatteryIncluded][b]) # if the dvBatteryIncluded binary is 1, then the storage energy capacity can be greater than 0, but then the battery cost constant is also included in the costs
else
m[:dvBatteryIncluded][b] == 0
end
end

@expression(m, TotalStorageCapCosts, p.third_party_factor * (
sum( p.s.storage.attr[b].net_present_cost_per_kw * m[:dvStoragePower][b] for b in p.s.storage.types.elec) +
adfarth marked this conversation as resolved.
Show resolved Hide resolved
sum( p.s.storage.attr[b].net_present_cost_per_kwh * m[:dvStorageEnergy][b] for b in p.s.storage.types.all )
sum( p.s.storage.attr[b].net_present_cost_per_kwh * m[:dvStorageEnergy][b] for b in p.s.storage.types.all ) +
sum( p.s.storage.attr[b].net_present_cost_cost_constant * m[:dvBatteryIncluded][b] for b in p.s.storage.types.elec)
))

@expression(m, TotalPerUnitSizeOMCosts, p.third_party_factor * p.pwf_om *
Expand Down Expand Up @@ -594,6 +604,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs)
dvPeakDemandMonth[p.months, 1:p.s.electric_tariff.n_monthly_demand_tiers] >= 0 # Peak electrical power demand during month m [kW]
MinChargeAdder >= 0
binGHP[p.ghp_options], Bin # Can be <= 1 if require_ghp_purchase=0, and is ==1 if require_ghp_purchase=1
dvBatteryIncluded[p.s.storage.types.elec], Bin # 0 if no battery is included, 1 if battery is included
end

if !isempty(p.techs.gen) # Problem becomes a MILP
Expand Down
11 changes: 11 additions & 0 deletions src/core/reopt_multinode.jl
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,7 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}
dvs_idx_on_storagetypes = String[
"dvStoragePower",
"dvStorageEnergy",
"dvBatteryIncluded"
]
dvs_idx_on_storagetypes_time_steps = String[
"dvDischargeFromStorage"
Expand Down Expand Up @@ -66,6 +67,15 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}
m[Symbol(dv)] = @variable(m, [p.techs.elec, p.s.electric_tariff.export_bins, p.time_steps], base_name=dv, lower_bound=0)
end

# Include the electric storage cost constants only if the installed_cost_constant or the replace_cost_constant is not zero
for b in p.s.storage.types.elec
if p.s.storage.attr[b].installed_cost_constant != 0 || p.s.storage.attr[b].replace_cost_constant != 0
@constraint(m, [b in p.s.storage.types.elec], m[Symbol("dvStorageEnergy"*_n)][b] <= p.s.storage.attr[b].max_kwh * m[Symbol("dvBatteryIncluded"*_n)][b]) # if the dvBatteryIncluded binary is 1, then the storage energy capacity can be greater than 0, but then the battery cost constant is also included in the costs
else
m[Symbol("dvBatteryIncluded"*_n)][b] == 0
end
end

ex_name = "TotalTechCapCosts"*_n
m[Symbol(ex_name)] = @expression(m, p.third_party_factor *
sum( p.cap_cost_slope[t] * m[Symbol("dvPurchaseSize"*_n)][t] for t in p.techs.all )
Expand All @@ -75,6 +85,7 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}
m[Symbol(ex_name)] = @expression(m, p.third_party_factor *
sum(p.s.storage.attr[b].net_present_cost_per_kw * m[Symbol("dvStoragePower"*_n)][b] for b in p.s.storage.types.elec)
+ sum(p.s.storage.attr[b].net_present_cost_per_kwh * m[Symbol("dvStorageEnergy"*_n)][b] for b in p.s.storage.types.all)
+ sum(p.s.storage.attr[b].net_present_cost_cost_constant * m[Symbol("dvBatteryIncluded"*_n)][b] for b in p.s.storage.types.elec)
)

ex_name = "TotalPerUnitSizeOMCosts"*_n
Expand Down
3 changes: 2 additions & 1 deletion src/results/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,8 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d::
r["storage_to_load_series_kw"] = round.(value.(discharge), digits=3)

r["initial_capital_cost"] = r["size_kwh"] * p.s.storage.attr[b].installed_cost_per_kwh +
r["size_kw"] * p.s.storage.attr[b].installed_cost_per_kw
r["size_kw"] * p.s.storage.attr[b].installed_cost_per_kw +
p.s.storage.attr[b].installed_cost_constant
adfarth marked this conversation as resolved.
Show resolved Hide resolved

if p.s.storage.attr[b].model_degradation
r["state_of_health"] = value.(m[:SOH]).data / value.(m[:dvStorageEnergy])["ElectricStorage"];
Expand Down
13 changes: 11 additions & 2 deletions src/results/financial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -146,7 +146,8 @@ function initial_capex(m::JuMP.AbstractModel, p::REoptInputs; _n="")
for b in p.s.storage.types.elec
if p.s.storage.attr[b].max_kw > 0
initial_capex += p.s.storage.attr[b].installed_cost_per_kw * value.(m[Symbol("dvStoragePower"*_n)])[b] +
p.s.storage.attr[b].installed_cost_per_kwh * value.(m[Symbol("dvStorageEnergy"*_n)])[b]
p.s.storage.attr[b].installed_cost_per_kwh * value.(m[Symbol("dvStorageEnergy"*_n)])[b] +
(p.s.storage.attr[b].installed_cost_constant * ceil(value.(m[Symbol("dvStorageEnergy"*_n)])[b]/(value.(m[Symbol("dvStorageEnergy"*_n)])[b] + 1)))
adfarth marked this conversation as resolved.
Show resolved Hide resolved
end
end

Expand Down Expand Up @@ -238,12 +239,20 @@ function replacement_costs_future_and_present(m::JuMP.AbstractModel, p::REoptInp
else
future_cost_storage = p.s.storage.attr[b].replace_cost_per_kwh * value.(m[Symbol("dvStorageEnergy"*_n)])[b]
end
future_cost += future_cost_inverter + future_cost_storage
if p.s.storage.attr[b].cost_constant_replacement_year >= p.s.financial.analysis_years
future_cost_cost_constant = 0
else
future_cost_cost_constant = p.s.storage.attr[b].replace_cost_constant * value.(m[Symbol("dvBatteryIncluded"*_n)])[b]
end

future_cost += future_cost_inverter + future_cost_storage + future_cost_cost_constant

present_cost += future_cost_inverter * (1 - p.s.financial.owner_tax_rate_fraction) /
((1 + p.s.financial.owner_discount_rate_fraction)^p.s.storage.attr[b].inverter_replacement_year)
present_cost += future_cost_storage * (1 - p.s.financial.owner_tax_rate_fraction) /
((1 + p.s.financial.owner_discount_rate_fraction)^p.s.storage.attr[b].battery_replacement_year)
present_cost += future_cost_cost_constant * (1 - p.s.financial.owner_tax_rate_fraction) /
((1 + p.s.financial.owner_discount_rate_fraction)^p.s.storage.attr[b].cost_constant_replacement_year)
end

if !isempty(p.techs.gen) # Generator replacement
Expand Down
60 changes: 60 additions & 0 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,66 @@ else # run HiGHS tests
@test r["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1
end

@testset "Solar and ElectricStorage w/ zero ElectricStorage cost constants" begin
adfarth marked this conversation as resolved.
Show resolved Hide resolved
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["ElectricStorage"]["installed_cost_constant"] = 0
d["ElectricStorage"]["replace_cost_constant"] = 0
d["ElectricStorage"]["cost_constant_replacement_year"] = 10

d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false)
adfarth marked this conversation as resolved.
Show resolved Hide resolved
s = Scenario(d)
inputs = REoptInputs(s)
results = run_reopt([m1,m2], inputs)

UpfrontCosts_NoIncentive = (results["ElectricStorage"]["size_kw"]*d["ElectricStorage"]["installed_cost_per_kw"] ) +
(results["ElectricStorage"]["size_kwh"]*d["ElectricStorage"]["installed_cost_per_kwh"]) +
d["ElectricStorage"]["installed_cost_constant"] +
(results["PV"]["size_kw"]*d["PV"]["installed_cost_per_kw"])

@test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01
adfarth marked this conversation as resolved.
Show resolved Hide resolved
@test results["PV"]["lcoe_per_kwh"] ≈ 0.0469 atol = 0.001
@test results["Financial"]["lcc"] ≈ 1.23917861648e7 rtol=1e-5
@test results["Financial"]["lcc_bau"] ≈ 1.27663970441e7 rtol=1e-5
@test results["ElectricStorage"]["size_kw"] ≈ 49.05 atol=0.1
@test results["ElectricStorage"]["size_kwh"] ≈ 83.32 atol=0.1
@test results["Financial"]["initial_capital_costs"] ≈ UpfrontCosts_NoIncentive rtol=1e-5
@test results["Financial"]["lifecycle_storage_capital_costs"] ≈ 66248.8454 rtol=1e-5

end

@testset "Solar and ElectricStorage w/ non-zero ElectricStorage cost constants" 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["ElectricStorage"]["installed_cost_constant"] = 7500
d["ElectricStorage"]["replace_cost_constant"] = 5025
d["ElectricStorage"]["cost_constant_replacement_year"] = 10

d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false)
s = Scenario(d)
inputs = REoptInputs(s)
results = run_reopt([m1,m2], inputs)

UpfrontCosts_NoIncentive = (results["ElectricStorage"]["size_kw"]*d["ElectricStorage"]["installed_cost_per_kw"] ) +
(results["ElectricStorage"]["size_kwh"]*d["ElectricStorage"]["installed_cost_per_kwh"]) +
d["ElectricStorage"]["installed_cost_constant"] +
(results["PV"]["size_kw"]*d["PV"]["installed_cost_per_kw"])

@test results["PV"]["size_kw"] ≈ 216.667 atol=0.01
@test results["PV"]["lcoe_per_kwh"] ≈ 0.0469 atol = 0.001
@test results["Financial"]["lcc"] ≈ 1.23981029171e7 rtol=1e-5
@test results["Financial"]["lcc_bau"] ≈ 1.27663970441e7 rtol=1e-5
@test results["ElectricStorage"]["size_kw"] ≈ 49.05 atol=0.1
@test results["ElectricStorage"]["size_kwh"] ≈ 83.32 atol=0.1
@test results["Financial"]["initial_capital_costs"] ≈ UpfrontCosts_NoIncentive rtol=1e-5
@test results["Financial"]["lifecycle_storage_capital_costs"] ≈ 72565.5977 rtol=1e-5

end

@testset "Outage with Generator" begin
adfarth marked this conversation as resolved.
Show resolved Hide resolved
model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false))
results = run_reopt(model, "./scenarios/generator.json")
Expand Down
31 changes: 31 additions & 0 deletions test/test_with_xpress.jl
Original file line number Diff line number Diff line change
Expand Up @@ -439,6 +439,37 @@ end
@test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199
end

@testset "Solar and ElectricStorage w/ ElectricStorage cost constants" begin
adfarth marked this conversation as resolved.
Show resolved Hide resolved
m1 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
d = JSON.parsefile("./scenarios/pv_storage.json");

d["ElectricStorage"]["installed_cost_constant"] = 10000
d["ElectricStorage"]["replace_cost_constant"] = 5000
d["ElectricStorage"]["cost_constant_replacement_year"] = 10

d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false)
s = Scenario(d)
inputs = REoptInputs(s)
results = run_reopt([m1,m2], inputs)

UpfrontCosts_NoIncentive = (results["ElectricStorage"]["size_kw"]*d["ElectricStorage"]["installed_cost_per_kw"] ) +
(results["ElectricStorage"]["size_kwh"]*d["ElectricStorage"]["installed_cost_per_kwh"]) +
d["ElectricStorage"]["installed_cost_constant"] +
(results["PV"]["size_kw"]*d["PV"]["installed_cost_per_kw"])

@test results["PV"]["size_kw"] ≈ 216.667 atol=0.01
@test results["PV"]["lcoe_per_kwh"] ≈ 0.0469 atol = 0.001
@test results["Financial"]["lcc"] ≈ 1.23997e7 rtol=1e-5
@test results["Financial"]["lcc_bau"] ≈ 1.27664e7 rtol=1e-5
@test results["ElectricStorage"]["size_kw"] ≈ 49.05 atol=0.1
@test results["ElectricStorage"]["size_kwh"] ≈ 83.32 atol=0.1
@test results["Financial"]["initial_capital_costs"] ≈ UpfrontCosts_NoIncentive rtol=1e-5
@test results["Financial"]["lifecycle_storage_capital_costs"] ≈ 74203.0768 rtol=1e-5

end


@testset "Outage with Generator, outage simulator, BAU critical load outputs" begin
m1 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
Expand Down
Loading