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 24 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
7 changes: 6 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,11 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## Develop - 2024-11-03
### Added
- Add new **ElectricStorage** input fields **installed_cost_constant**, **replace_cost_constant** (both default to 0), and **cost_constant_replacement_year** (defaults to year 10).
- Added new binary variable **binIncludeStorageCostConstant** which is indexed on `p.s.storage.types.elec`


## Develop
### Added
Expand Down Expand Up @@ -106,7 +111,7 @@ Classify the change according to the following categories:
- Refactored various functions to ensure **ProcessHeatLoad** is processed correctly in line with other heating loads.
- When the URDB response `energyratestructure` has a "unit" value that is not "kWh", throw an error instead of averaging rates in each energy tier.
- Refactored heating flow constraints to be in ./src/constraints/thermal_tech_constraints.jl instead of its previous separate locations in the storage and turbine constraints.
- Changed default Financial **owner_tax_rate_fraction** and **offtaker_tax_rate_fraction** from 0.257 to 0.26 to align with API and user manual defaults.
- Changed default Financial **owner_tax_rate_fraction** and **offtaker_tax_rate_fraction** from 0.257 to 0.26 to align with API and user manual defaults.
### Fixed
- Updated the PV result **lifecycle_om_cost_after_tax** to account for the third-party factor for third-party ownership analyses.
- Convert `max_electric_load_kw` to _Float64_ before passing to function `get_chp_defaults_prime_mover_size_class`
Expand Down
3 changes: 2 additions & 1 deletion src/constraints/outage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -77,7 +77,8 @@ function add_outage_cost_constraints(m,p)
m[:dvMGStorageUpgradeCost] >= p.s.financial.microgrid_upgrade_cost_fraction * m[:TotalStorageCapCosts] - (
p.s.financial.microgrid_upgrade_cost_fraction * p.third_party_factor * (
sum( p.s.storage.attr[b].net_present_cost_per_kw * p.s.storage.attr[b].max_kw for b in p.s.storage.types.elec) +
sum( p.s.storage.attr[b].net_present_cost_per_kwh * p.s.storage.attr[b].max_kwh for b in p.s.storage.types.all )
sum( p.s.storage.attr[b].net_present_cost_per_kwh * p.s.storage.attr[b].max_kwh for b in p.s.storage.types.all ) +
sum(p.storage.attr[b].net_present_cost_cost_constant for b in p.storage.types.elec)
) * (1-m[:binMGStorageUsed]) # Big-M is capital cost of battery with max size kw and kwh
)
)
Expand Down
12 changes: 11 additions & 1 deletion src/constraints/storage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -111,7 +111,7 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] +
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
)
# Remove grid-to-storage as an option if option to grid charge is turned off
if !(p.s.storage.attr[b].can_grid_charge)
for ts in p.time_steps_with_grid
Expand All @@ -128,6 +128,16 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
end
end

function add_elec_storage_cost_constant_constraints(m, p, b; _n="")
Copy link
Collaborator

Choose a reason for hiding this comment

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

@toddleif If I'm understanding correctly, I think the following could work for avoiding the increased run time even when a cost constant isn't included:

  • Move the if statementif p.s.storage.attr[b].installed_cost_constant != 0 || p.s.storage.attr[b].replace_cost_constant != 0 to reopt.jl. Within that statement, add a warning about binary vars and then call "add_elec_storage_cost_constant_constraints".
  • Define the binary vars within "add_elec_storage_cost_constant_constraints" here. (see add_prod_incent_vars_and_constraints as an example)

# Include the electric storage cost constants only if the installed_cost_constant or the replace_cost_constant is not zero
if p.s.storage.attr[b].installed_cost_constant != 0 || p.s.storage.attr[b].replace_cost_constant != 0
# If there is a battery, then the binIncludeStorageCostConstant binary must be 1
@constraint(m, m[Symbol("dvStorageEnergy"*_n)][b] <= p.s.storage.attr[b].max_kwh * m[Symbol("binIncludeStorageCostConstant"*_n)][b])
else
m[Symbol("binIncludeStorageCostConstant"*_n)][b] == 0
end
end

function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4j)-1: Reconcile state-of-charge for (hot) thermal storage
Expand Down
56 changes: 55 additions & 1 deletion src/core/energy_storage/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,10 @@ worth factor is used in the same manner irrespective of the `maintenance_strateg
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.
- `installed_cost_constant`
- `replace_cost_constant`
- `cost_constant_replacement_year`
They 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
Expand Down Expand Up @@ -140,6 +143,15 @@ 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.


ElectricStorage Cost Constant

The ElectricStorage cost constant is considered in the model if the `ElectricStorage.installed_cost_constant` or `ElectricStorage.replace_cost_constant` are non-zero.
The REopt model includes the cost constant in the installation costs only if the ElectricStorage size is non-zero.
The REopt model includes the cost constant in the replacement costs only if the ElectricStorage size is non-zero and the replacement year is less than the number of analysis years.
The ElectricStorage cost constant is not considered when modeling electric storage degradation.

"""

Base.@kwdef mutable struct Degradation
Expand Down Expand Up @@ -169,10 +181,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 @@ -204,10 +219,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 @@ -245,10 +263,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 @@ -260,6 +281,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 @@ -318,11 +340,39 @@ 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
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
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_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(
s.min_kw,
Expand All @@ -338,10 +388,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 @@ -353,6 +406,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
6 changes: 5 additions & 1 deletion src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -209,6 +209,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
@constraint(m, m[:dvStorageEnergy][b] == 0)
@constraint(m, [ts in p.time_steps], m[:dvDischargeFromStorage][b, ts] == 0)
if b in p.s.storage.types.elec
@constraint(m, m[:binIncludeStorageCostConstant][b] == 0)
@constraint(m, m[:dvStoragePower][b] == 0)
@constraint(m, [ts in p.time_steps], m[:dvGridToStorage][b, ts] == 0)
@constraint(m, [t in p.techs.elec, ts in p.time_steps_with_grid],
Expand Down Expand Up @@ -236,6 +237,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
add_general_storage_dispatch_constraints(m, p, b)
if b in p.s.storage.types.elec
add_elec_storage_dispatch_constraints(m, p, b)
add_elec_storage_cost_constant_constraints(m, p, b)
elseif b in p.s.storage.types.hot
add_hot_thermal_storage_dispatch_constraints(m, p, b)
elseif b in p.s.storage.types.cold
Expand Down Expand Up @@ -407,7 +409,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)

@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[:binIncludeStorageCostConstant][b] for b in p.s.storage.types.elec)
))

@expression(m, TotalPerUnitSizeOMCosts, p.third_party_factor * p.pwf_om *
Expand Down Expand Up @@ -623,6 +626,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
binIncludeStorageCostConstant[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
6 changes: 6 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",
"binIncludeStorageCostConstant"
]
dvs_idx_on_storagetypes_time_steps = String[
"dvDischargeFromStorage"
Expand Down Expand Up @@ -75,6 +76,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("binIncludeStorageCostConstant"*_n)][b] for b in p.s.storage.types.elec)
)

ex_name = "TotalPerUnitSizeOMCosts"*_n
Expand Down Expand Up @@ -120,11 +122,15 @@ function build_reopt!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}})
m[Symbol("dvProductionToStorage"*_n)][b, t, ts] == 0)
@constraint(m, [ts in p.time_steps], m[Symbol("dvDischargeFromStorage"*_n)][b, ts] == 0)
@constraint(m, [ts in p.time_steps], m[Symbol("dvGridToStorage"*_n)][b, ts] == 0)
if b in p.s.storage.types.elec
@constraint(m, m[Symbol("binIncludeStorageCostConstant"*_n)][b] == 0)
end
else
add_storage_size_constraints(m, p, b; _n=_n)
add_general_storage_dispatch_constraints(m, p, b; _n=_n)
if b in p.s.storage.types.elec
add_elec_storage_dispatch_constraints(m, p, b; _n=_n)
add_elec_storage_cost_constant_constraints(m, p, b; _n=_n)
elseif b in p.s.storage.types.hot
add_hot_thermal_storage_dispatch_constraints(m, p, b; _n=_n)
elseif b in p.s.storage.types.cold
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 * value.(m[Symbol("binIncludeStorageCostConstant"*_n)])[b])
end
end

Expand Down Expand Up @@ -233,12 +234,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("binIncludeStorageCostConstant"*_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
7 changes: 4 additions & 3 deletions src/results/proforma.jl
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,11 @@ function proforma_results(p::REoptInputs, d::Dict)
storage = p.s.storage.attr["ElectricStorage"]
total_kw = d["ElectricStorage"]["size_kw"]
total_kwh = d["ElectricStorage"]["size_kwh"]
capital_cost = total_kw * storage.installed_cost_per_kw + total_kwh * storage.installed_cost_per_kwh
capital_cost = total_kw * storage.installed_cost_per_kw + total_kwh * storage.installed_cost_per_kwh + storage.installed_cost_constant
battery_replacement_year = storage.battery_replacement_year
battery_replacement_cost = -1 * ((total_kw * storage.replace_cost_per_kw) + (
total_kwh * storage.replace_cost_per_kwh))
battery_replacement_cost = -1 * ((total_kw * storage.replace_cost_per_kw) +
(total_kwh * storage.replace_cost_per_kwh) +
storage.replace_cost_constant)
m.om_series += [yr != battery_replacement_year ? 0 : battery_replacement_cost for yr in 1:years]

# storage only has cbi in the API
Expand Down
Loading
Loading