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

LDES #406

Open
wants to merge 113 commits into
base: develop
Choose a base branch
from
Open

LDES #406

Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
113 commits
Select commit Hold shift + click to select a range
5b04b56
Add per kW and per kWh installed capacity based battery O&M
lixiangk1 Jan 23, 2024
8627a8d
Add O&M financials to ElectricStorage results
lixiangk1 Jan 24, 2024
564365f
Add battery self discharge, defaulted to zero
lixiangk1 Feb 29, 2024
ae61517
Merge branch 'develop' into battery_om
lixiangk1 Feb 29, 2024
3e1a3d6
Merge branch 'develop' into battery_om
lixiangk1 Mar 19, 2024
5542a5d
Merge branch 'develop' into battery_om
lixiangk1 Mar 28, 2024
c2f3c5d
Add battery self-discharge to MPC model
lixiangk1 Mar 28, 2024
37e3083
Enable BESS O&M for multiple sites
lixiangk1 Mar 29, 2024
9ee741e
Add code comments and tests for battery O&M and self-discharge; updat…
lixiangk1 Apr 2, 2024
e2a0cac
Update code comments
lixiangk1 Apr 2, 2024
66e848a
Merge branch 'develop' into battery_om
adfarth Apr 5, 2024
53014bc
Merge branch 'develop' into battery_om
lixiangk1 Apr 19, 2024
14f3ced
Update storage self-discharge input from daily to per timestep
lixiangk1 Apr 19, 2024
b67bdcc
Add duration constraint
lixiangk1 Apr 19, 2024
7c339a1
Fix ldes constraint, add duration input
lixiangk1 Apr 20, 2024
ac253e5
Allow battery wholesale export, preliminary implementation
toddleif Apr 28, 2024
d66297f
Added optional constraint for requiring start and end battery charge …
toddleif May 14, 2024
cdc4da8
Set initial SOC = final SOC for BESS and allow REopt to decide value,…
lixiangk1 May 20, 2024
0fbdb98
rename self discharge input for consistency
hdunham May 23, 2024
58b6769
Update CHANGELOG.md
hdunham May 23, 2024
0e739ab
simplify WHL_benefit expression
hdunham May 23, 2024
25f310f
correct self discharge term
hdunham May 23, 2024
4f987ac
simplify soc expressions
hdunham May 23, 2024
1f4ebe9
picking nits
hdunham May 23, 2024
e6f7cc4
only use 1 bin to prevent simultaneous charge/discharge
hdunham May 23, 2024
8f6ff4f
change binBattCharging definition too
hdunham May 23, 2024
515a684
dont need 0 index for binBattCharging
hdunham May 23, 2024
fbd6d46
rm unnecessary loop
hdunham May 30, 2024
9954293
rm manifest in test directory
hdunham May 30, 2024
019b6bc
fix extra ] typo
hdunham May 30, 2024
371a76b
fix variable def syntax
hdunham May 30, 2024
b84e917
make storage om test more comprehensive
hdunham May 30, 2024
5ce036a
consolidate storage om and self discharge tests
hdunham May 30, 2024
ad46c89
more concise
hdunham May 30, 2024
ce5977a
uncomment tests
hdunham May 30, 2024
51a7ff9
rm self discharge test now dupe
hdunham May 30, 2024
1d3181f
Update CHANGELOG.md
hdunham May 30, 2024
d6b9b6e
comment out battery charge/discharge binary
hdunham May 31, 2024
2b07689
comment out added battery power constraint
hdunham May 31, 2024
c6ff686
comment out dvStorageToGrid
hdunham May 31, 2024
0b1618b
Revert "comment out dvStorageToGrid"
hdunham May 31, 2024
eb6ed96
comment out dvStorageToGrid in WHL_benefit expression
hdunham May 31, 2024
c273972
Revert "comment out added battery power constraint"
hdunham May 31, 2024
3a8834a
Revert "comment out battery charge/discharge binary"
hdunham May 31, 2024
134adb6
add battery max_kw to bigM_hourly_load
hdunham Jun 1, 2024
5db75ae
change to bigM_hourly_load_plus_battery_power where used
hdunham Jun 4, 2024
0f13d77
add b index to dvStorageToGrid
hdunham Jun 4, 2024
7d2694e
add dvStorageToGrid to mpc model
hdunham Jun 4, 2024
31e912b
is_ldes and duration -> fixed_duration
hdunham Jun 4, 2024
0d37b7c
account for fixed_duration not in MPCElectricStorage
hdunham Jun 4, 2024
84293bd
account for require_start_and_end_charge_to_be_equal not in MPCElectr…
hdunham Jun 4, 2024
3bd6e24
add allow_simultaneous_charge_discharge input to ElectricStorage
hdunham Jun 5, 2024
d0b4671
move def of binBattCharging to be with rest of variable defs
hdunham Jun 5, 2024
f34f44c
missing comma
hdunham Jun 5, 2024
0336a2d
only prevent simultaneous battery charge/discharge when that input is…
hdunham Jun 5, 2024
300c95e
add allow_simultaneous_charge_discharge to MPCElectricStorage
hdunham Jun 5, 2024
9f4df44
rm dvStorageToGrid from general storage discharge power constraint
hdunham Jun 5, 2024
5f64284
make dvStorageToGrid multinode compat
hdunham Jun 5, 2024
4cf25f3
Merge branch 'develop' into ldes-analysis-fixed-duration
lixiangk1 Jun 7, 2024
6212406
add more export incentive and om tax/period combos to results
hdunham Jun 20, 2024
8ad69fd
add can_export_to_grid ElectricStorage input
hdunham Aug 6, 2024
88cc28e
Revert "add can_export_to_grid ElectricStorage input"
hdunham Aug 6, 2024
f04b60d
add can_export_to_grid ElectricStorage input
hdunham Aug 6, 2024
c069122
fix storage export to zero if can_export_to_grid=false
hdunham Aug 6, 2024
1c85716
rename require_start_and_end_charge_to_be_equal to optimize_soc_init_…
hdunham Aug 6, 2024
0f2f246
still fix init and final soc for non elec storage and elec storage wh…
hdunham Aug 6, 2024
5c512b5
remove final soc constraint
hdunham Aug 7, 2024
9bde839
add can_export_to_grid to test inputs
hdunham Aug 7, 2024
bb90447
improve log msg
hdunham Aug 8, 2024
81927a2
use _n where missing in wind results processing
hdunham Aug 8, 2024
7edb8a3
round initial_capital_cost in wind and pv results
hdunham Aug 8, 2024
a885836
add initial_capital_cost output to steam turbine
hdunham Aug 8, 2024
80e7f6c
add initial_capital_cost output to generator
hdunham Aug 8, 2024
1428684
add initial_capital_cost output to electric heater
hdunham Aug 8, 2024
e8032d1
add initial_capital_cost output to chp
hdunham Aug 8, 2024
d13ff0b
add initial_capital_cost output to boiler
hdunham Aug 8, 2024
586b4c5
correct some initial_capital_cost calcs
hdunham Aug 8, 2024
8f42464
add initial_capital_cost output to thermal storage
hdunham Aug 8, 2024
f642462
Update CHANGELOG.md
hdunham Aug 8, 2024
b0a24c5
add initial_capital_cost output to absorption chiller
hdunham Aug 12, 2024
305075d
remove initial_capital_cost from chp results for now
hdunham Aug 12, 2024
dd13655
make b arg required in degradation functions
hdunham Aug 29, 2024
2960b23
add name field to ElectricStorage
hdunham Aug 29, 2024
c9115a5
loop elec stor, don't hardcode "ElectricStorage" (bigM_hourly_load_pl…
hdunham Aug 29, 2024
5f2ceb6
use b arg don't hardcode "ElectricStorage"
hdunham Aug 29, 2024
710f2f5
handle ElectricStorage being a list of dicts
hdunham Aug 29, 2024
1d3bce0
loop elec stor, don't hardcode "ElectricStorage" (steam turbine results)
hdunham Aug 29, 2024
b69edbf
(gen results) loop elec stor, don't hardcode "ElectricStorage"
hdunham Aug 29, 2024
949bcb8
(utility results) loop elec stor, don't hardcode "ElectricStorage"
hdunham Aug 29, 2024
293fcd7
(chp results) loop elec stor, don't hardcode "ElectricStorage"
hdunham Aug 29, 2024
1de9ca6
(elec stor results) use b arg don't hardcode "ElectricStorage"
hdunham Aug 29, 2024
793e8d7
(mpc/model.jl) don't hardcode "ElectricStorage"
hdunham Aug 29, 2024
e3a7656
correct error msg
hdunham Aug 29, 2024
3ce3e32
error in ERP if multiple elec stor
hdunham Aug 29, 2024
dcda12a
in ERP use d["ElectricStorage"]["name"] not "ElectricStorage" hardcoded
hdunham Aug 29, 2024
970bd2b
(outagesim) handle elec stor name and error if multiple
hdunham Aug 29, 2024
febc367
Revert "(outagesim) handle elec stor name and error if multiple"
hdunham Aug 30, 2024
8fb61ee
Revert "in ERP use d["ElectricStorage"]["name"] not "ElectricStorage"…
hdunham Aug 30, 2024
b1b8dd7
Revert "error in ERP if multiple elec stor"
hdunham Aug 30, 2024
1e06aac
Revert "correct error msg"
hdunham Aug 30, 2024
9d0c564
Revert "(mpc/model.jl) don't hardcode "ElectricStorage""
hdunham Aug 30, 2024
12889a5
Revert "(elec stor results) use b arg don't hardcode "ElectricStorage""
hdunham Aug 30, 2024
63ab4eb
Revert "(chp results) loop elec stor, don't hardcode "ElectricStorage""
hdunham Aug 30, 2024
c4f138a
Revert "(utility results) loop elec stor, don't hardcode "ElectricSto…
hdunham Aug 30, 2024
10d3264
Revert "(gen results) loop elec stor, don't hardcode "ElectricStorage""
hdunham Aug 30, 2024
34179ac
Revert "loop elec stor, don't hardcode "ElectricStorage" (steam turbi…
hdunham Aug 30, 2024
fc44e06
Revert "handle ElectricStorage being a list of dicts"
hdunham Aug 30, 2024
7161f6e
Revert "use b arg don't hardcode "ElectricStorage""
hdunham Aug 30, 2024
6139428
Revert "loop elec stor, don't hardcode "ElectricStorage" (bigM_hourly…
hdunham Aug 30, 2024
ac5a954
Revert "add name field to ElectricStorage"
hdunham Aug 30, 2024
29657c0
Revert "make b arg required in degradation functions"
hdunham Aug 30, 2024
a8f12a3
Merge branch 'develop' into ldes-analysis-fixed-duration
lixiangk1 Sep 7, 2024
7d53be8
correct changelog merge
hdunham Sep 10, 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
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,20 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## Develop LDES
### Added
- Added inputs **om_cost_per_kw** and **om_cost_per_kwh** to `ElectricStorage` for modeling capacity-based O&M
- Added outputs **lifecycle_om_cost_after_tax** and **year_one_om_cost_before_tax** to `ElectricStorage`
- Added new input **self_discharge_fraction_per_timestep** to `ElectricStorage` for modeling battery self-discharge
- Added input option **can_export_to_grid** (defaults to _false_) to `ElectricStorage` and decision variable **dvStorageToGrid**
- Added input **allow_simultaneous_charge_discharge** (defaults to _true_) to `ElectricStorage` to give option to disallow battery from simultaneously charging and discharging, which adds binary decision variables often unnecessarily
- Added input **fixed_duration** to `ElectricStorage`, which fixes the ratio between **size_kw** and **size_kwh** in the optimized solution if provided
- Added input option **optimize_soc_init_fraction** (defaults to _false_) to `ElectricStorage`, which makes the optimization choose the inital SOC (equal to final SOC) instead of using **soc_init_fraction**. The initial SOC is also constrained to equal the final SOC, which eliminates the "free energy" issue. We currently do not fix SOC when **soc_init_fraction** is used because this has caused infeasibility.
- Added output **initial_capital_cost** to all techs
### Fixed
- Added missing outputs **lifecycle_export_benefit_before_tax** and **year_one_export_benefit_after_tax** to `ElectricTariff`
- Add missing output **year_one_om_cost_before_tax** to `PV`

## Develop 08-09-2024
### Changed
- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage
Expand Down
16 changes: 11 additions & 5 deletions src/constraints/electric_utility_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -129,8 +129,14 @@ function add_export_constraints(m, p; _n="")
if typeof(binNEM) <: Real # no need for wholesale binary
binWHL = 1
WHL_benefit = @expression(m, p.pwf_e * p.hours_per_time_step *
sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts]
for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps)
#sum( sum(p.s.electric_tariff.export_rates[:WHL][ts] * m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts]
# for t in p.techs_by_exportbin[:WHL]) for ts in p.time_steps)
sum(p.s.electric_tariff.export_rates[:WHL][ts] *
(
sum(m[Symbol("dvStorageToGrid"*_n)][b, ts] for b in p.s.storage.types.elec) +
sum(m[Symbol("dvProductionToGrid"*_n)][t, :WHL, ts] for t in p.techs_by_exportbin[:WHL])
) for ts in p.time_steps
)
)
else
binWHL = @variable(m, binary = true)
Expand Down Expand Up @@ -273,13 +279,13 @@ function add_simultaneous_export_import_constraint(m, p; _n="")
}
)
else
bigM_hourly_load = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal)
bigM_hourly_load_plus_battery_power = maximum(p.s.electric_load.loads_kw)+maximum(p.s.space_heating_load.loads_kw)+maximum(p.s.process_heat_load.loads_kw)+maximum(p.s.dhw_load.loads_kw)+maximum(p.s.cooling_load.loads_kw_thermal)+p.s.storage.attr["ElectricStorage"].max_kw
@constraint(m, NoGridPurchasesBinary[ts in p.time_steps],
sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) +
sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) <= bigM_hourly_load*(1-m[Symbol("binNoGridPurchases"*_n)][ts])
sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) <= bigM_hourly_load_plus_battery_power*(1-m[Symbol("binNoGridPurchases"*_n)][ts])
)
@constraint(m, ExportOnlyAfterSiteLoadMetCon[ts in p.time_steps],
sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= bigM_hourly_load * m[Symbol("binNoGridPurchases"*_n)][ts]
sum(m[Symbol("dvProductionToGrid"*_n)][t,u,ts] for t in p.techs.elec, u in p.export_bins_by_tech[t]) <= bigM_hourly_load_plus_battery_power * m[Symbol("binNoGridPurchases"*_n)][ts]
)
end
end
Expand Down
62 changes: 53 additions & 9 deletions src/constraints/storage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,10 +25,21 @@ end


function add_general_storage_dispatch_constraints(m, p, b; _n="")
# Constraint (4a): initial state of charge
@constraint(m,
m[Symbol("dvStoredEnergy"*_n)][b, 0] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
# Constraint (4a): initial and final state of charge
if hasproperty(p.s.storage.attr[b], :optimize_soc_init_fraction) && p.s.storage.attr[b].optimize_soc_init_fraction
# @constraint(m, m[:dvStoredEnergy]["ElectricStorage",maximum(p.time_steps)] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b] )
print("\nOptimizing "*b*" inital SOC and constraining initial SOC = final SOC\n")
@constraint(m,
m[Symbol("dvStoredEnergy"*_n)][b, 0] == m[:dvStoredEnergy]["ElectricStorage", maximum(p.time_steps)]
)
else
@constraint(m,
m[Symbol("dvStoredEnergy"*_n)][b, 0] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
# @constraint(m,
# m[Symbol("dvStoredEnergy"*_n)][b, maximum(p.time_steps)] == p.s.storage.attr[b].soc_init_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
# )
end

#Constraint (4n): State of charge upper bound is storage system size
@constraint(m, [ts in p.time_steps],
Expand All @@ -52,16 +63,18 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4g): state-of-charge for electrical storage - with grid
@constraint(m, [ts in p.time_steps_with_grid],
m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
m[Symbol("dvStoredEnergy"*_n)][b, ts] == (1-p.s.storage.attr[b].self_discharge_fraction_per_timestep) * m[Symbol("dvStoredEnergy"*_n)][b, ts-1] +
p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec)
+ p.s.storage.attr[b].grid_charge_efficiency * m[Symbol("dvGridToStorage"*_n)][b, ts]
- m[Symbol("dvDischargeFromStorage"*_n)][b,ts] / p.s.storage.attr[b].discharge_efficiency
- ((m[Symbol("dvDischargeFromStorage"*_n)][b,ts]+m[Symbol("dvStorageToGrid"*_n)][b, ts]) / p.s.storage.attr[b].discharge_efficiency)
)
)

# Constraint (4h): state-of-charge for electrical storage - no grid
@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStoredEnergy"*_n)][b, ts] == m[Symbol("dvStoredEnergy"*_n)][b, ts-1] + p.hours_per_time_step * (
m[Symbol("dvStoredEnergy"*_n)][b, ts] == (1-p.s.storage.attr[b].self_discharge_fraction_per_timestep) * m[Symbol("dvStoredEnergy"*_n)][b, ts-1] +
p.hours_per_time_step * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.elec)
- m[Symbol("dvDischargeFromStorage"*_n)][b, ts] / p.s.storage.attr[b].discharge_efficiency
)
Expand All @@ -77,28 +90,59 @@ function add_elec_storage_dispatch_constraints(m, p, b; _n="")
@constraint(m, [ts in p.time_steps_with_grid],
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) + m[Symbol("dvGridToStorage"*_n)][b, ts]
+ m[Symbol("dvStorageToGrid"*_n)][b, ts]
)

#Dispatch from electrical storage is no greater than power capacity
Copy link
Collaborator Author

@hdunham hdunham May 23, 2024

Choose a reason for hiding this comment

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

why was this constraint added? doesn't seem to do anything beyond what (4l)-alt already accomplishes since it's over time steps without grid

@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] + m[Symbol("dvStorageToGrid"*_n)][b, ts])

#Constraint (4l)-alt: Dispatch from electrical storage is no greater than power capacity (no grid connection)
@constraint(m, [ts in p.time_steps_without_grid],
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

#Constraint (4m)-1: 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
fix(m[Symbol("dvGridToStorage"*_n)][b, ts], 0.0, force=true)
end
end

#Constraint (4m)-2: Force storage export to grid to zero if option to grid export is turned off
if !p.s.storage.attr[b].can_export_to_grid
for ts in p.time_steps
fix(m[Symbol("dvStorageToGrid"*_n)][b, ts], 0.0, force=true)
end
end

if p.s.storage.attr[b].minimum_avg_soc_fraction > 0
avg_soc = sum(m[Symbol("dvStoredEnergy"*_n)][b, ts] for ts in p.time_steps) /
(8760. / p.hours_per_time_step)
@constraint(m, avg_soc >= p.s.storage.attr[b].minimum_avg_soc_fraction *
sum(m[Symbol("dvStorageEnergy"*_n)][b])
)
end

if p.s.storage.attr[b] isa ElectricStorage && !isnothing(p.s.storage.attr[b].fixed_duration)
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

I don't think we need these isa ElectricStorage checks because we're already inside the function add_elec_storage_dispatch_constraints(m, p, b; _n="") which is only passed ElectricStorage as b

@constraint(m, m[Symbol("dvStoragePower"*_n)][b] == m[Symbol("dvStorageEnergy"*_n)][b] / p.s.storage.attr[b].fixed_duration)
end

# Prevent charging and discharging of the battery at the same time
hdunham marked this conversation as resolved.
Show resolved Hide resolved
if !(p.s.storage.attr[b].allow_simultaneous_charge_discharge)
#TODO: implement indicator constraint version for solvers that support it
@constraint(m, [ts in p.time_steps],
m[Symbol("dvGridToStorage"*_n)][b, ts] +
sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for t in p.techs.elec) <=
p.s.storage.attr[b].max_kw * m[Symbol("binBattCharging")][ts])
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStorageToGrid"*_n)][b, ts] +
m[Symbol("dvDischargeFromStorage"*_n)][b, ts] <=
p.s.storage.attr[b].max_kw * (1-m[Symbol("binBattCharging")][ts]))
end


end

function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="")
Expand Down
32 changes: 30 additions & 2 deletions src/core/energy_storage/electric_storage.jl
Original file line number Diff line number Diff line change
Expand Up @@ -147,10 +147,13 @@ end
soc_min_applies_during_outages::Bool = false
soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5
can_grid_charge::Bool = off_grid_flag ? false : true
can_export_to_grid::Bool = false
installed_cost_per_kw::Real = 910.0
installed_cost_per_kwh::Real = 455.0
replace_cost_per_kw::Real = 715.0
replace_cost_per_kwh::Real = 318.0
om_cost_per_kw::Real = 0.0 # Capacity-based O&M costs in \$/kW-rated/year. When including battery replacement costs, the user should ensure that O&M costs do not account for augmentation/replacement.
om_cost_per_kwh::Real = 0.0 # Capacity-based O&M costs in \$/kWh-rated/year. When including battery replacement costs, the user should ensure that O&M costs do not account for augmentation/replacement.
inverter_replacement_year::Int = 10
battery_replacement_year::Int = 10
macrs_option_years::Int = 7
Expand All @@ -165,7 +168,11 @@ end
model_degradation::Bool = false
degradation::Dict = Dict()
minimum_avg_soc_fraction::Float64 = 0.0
```
self_discharge_fraction_per_timestep::Float64 = 0.0 # Battery self-discharge as a fraction per timestep loss based on kWh stored in each timestep
fixed_duration::Union{Real, Nothing} = nothing
optimize_soc_init_fraction::Bool = false
allow_simultaneous_charge_discharge::Bool = true # Simultaneous charge/discharge is typically suboptimal anyway and allowing this avoids additional binary variables (which can slow solve time)
```
"""
Base.@kwdef struct ElectricStorageDefaults
off_grid_flag::Bool = false
Expand All @@ -180,10 +187,13 @@ Base.@kwdef struct ElectricStorageDefaults
soc_min_applies_during_outages::Bool = false
soc_init_fraction::Float64 = off_grid_flag ? 1.0 : 0.5
can_grid_charge::Bool = off_grid_flag ? false : true
can_export_to_grid::Bool = false
installed_cost_per_kw::Real = 910.0
installed_cost_per_kwh::Real = 455.0
replace_cost_per_kw::Real = 715.0
replace_cost_per_kwh::Real = 318.0
om_cost_per_kw::Real = 0.0
om_cost_per_kwh::Real = 0.0
inverter_replacement_year::Int = 10
battery_replacement_year::Int = 10
macrs_option_years::Int = 7
Expand All @@ -198,6 +208,10 @@ Base.@kwdef struct ElectricStorageDefaults
model_degradation::Bool = false
degradation::Dict = Dict()
minimum_avg_soc_fraction::Float64 = 0.0
self_discharge_fraction_per_timestep::Float64 = 0.0
fixed_duration::Union{Real, Nothing} = nothing
optimize_soc_init_fraction::Bool = false
allow_simultaneous_charge_discharge::Bool = true
end


Expand All @@ -219,10 +233,13 @@ struct ElectricStorage <: AbstractElectricStorage
soc_min_applies_during_outages::Bool
soc_init_fraction::Float64
can_grid_charge::Bool
can_export_to_grid::Bool
installed_cost_per_kw::Real
installed_cost_per_kwh::Real
replace_cost_per_kw::Real
replace_cost_per_kwh::Real
om_cost_per_kw::Real
om_cost_per_kwh::Real
inverter_replacement_year::Int
battery_replacement_year::Int
macrs_option_years::Int
Expand All @@ -239,6 +256,10 @@ struct ElectricStorage <: AbstractElectricStorage
model_degradation::Bool
degradation::Degradation
minimum_avg_soc_fraction::Float64
self_discharge_fraction_per_timestep::Float64
fixed_duration::Union{Real, Nothing}
optimize_soc_init_fraction::Bool
allow_simultaneous_charge_discharge::Bool

function ElectricStorage(d::Dict, f::Financial)
s = ElectricStorageDefaults(;d...)
Expand Down Expand Up @@ -307,10 +328,13 @@ struct ElectricStorage <: AbstractElectricStorage
s.soc_min_applies_during_outages,
s.soc_init_fraction,
s.can_grid_charge,
s.can_export_to_grid,
s.installed_cost_per_kw,
s.installed_cost_per_kwh,
replace_cost_per_kw,
replace_cost_per_kwh,
s.om_cost_per_kw,
s.om_cost_per_kwh,
s.inverter_replacement_year,
s.battery_replacement_year,
s.macrs_option_years,
Expand All @@ -326,7 +350,11 @@ struct ElectricStorage <: AbstractElectricStorage
net_present_cost_per_kwh,
s.model_degradation,
degr,
s.minimum_avg_soc_fraction
s.minimum_avg_soc_fraction,
s.self_discharge_fraction_per_timestep,
s.fixed_duration,
s.optimize_soc_init_fraction,
s.allow_simultaneous_charge_discharge
)
end
end
22 changes: 17 additions & 5 deletions src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -213,6 +213,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
@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],
m[:dvProductionToStorage][b, t, ts] == 0)
@constraint(m, [ts in p.time_steps], m[Symbol("dvStorageToGrid"*_n)][b, ts] == 0) # if there isn't a battery, then the battery can't export power to the grid
elseif b in p.s.storage.types.hot
@constraint(m, [q in q in setdiff(p.heating_loads, p.heating_loads_served_by_tes[b]), ts in p.time_steps], m[:dvHeatFromStorage][b,q,ts] == 0)
if "DomesticHotWater" in p.heating_loads_served_by_tes[b]
Expand Down Expand Up @@ -392,9 +393,11 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
sum( p.s.storage.attr[b].net_present_cost_per_kwh * m[:dvStorageEnergy][b] for b in p.s.storage.types.all )
))

@expression(m, TotalPerUnitSizeOMCosts, p.third_party_factor * p.pwf_om *
sum( p.om_cost_per_kw[t] * m[:dvSize][t] for t in p.techs.all )
)
@expression(m, TotalPerUnitSizeOMCosts, p.third_party_factor * p.pwf_om * (
sum(p.om_cost_per_kw[t] * m[:dvSize][t] for t in p.techs.all) +
sum(p.s.storage.attr[b].om_cost_per_kw * m[:dvStoragePower][b] for b in p.s.storage.types.elec) +
Copy link
Collaborator Author

Choose a reason for hiding this comment

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

why is O&M only being added for electric storage when the inputs for it were added to all storage models?

sum(p.s.storage.attr[b].om_cost_per_kwh * m[:dvStorageEnergy][b] for b in p.s.storage.types.elec)
))

add_elec_utility_expressions(m, p)

Expand Down Expand Up @@ -587,13 +590,22 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs)
dvProductionToStorage[p.s.storage.types.all, union(p.techs.ghp,p.techs.all), p.time_steps] >= 0 # Power from technology t used to charge storage system b [kW]
dvDischargeFromStorage[p.s.storage.types.all, p.time_steps] >= 0 # Power discharged from storage system b [kW]
dvGridToStorage[p.s.storage.types.elec, p.time_steps] >= 0 # Electrical power delivered to storage by the grid [kW]
dvStorageToGrid[p.s.storage.types.elec, p.time_steps] >= 0 # TODO, add: "p.StorageSalesTiers" as well? export of energy from storage to the grid
dvStoredEnergy[p.s.storage.types.all, 0:p.time_steps[end]] >= 0 # State of charge of storage system b
dvStoragePower[p.s.storage.types.all] >= 0 # Power capacity of storage system b [kW]
dvStorageEnergy[p.s.storage.types.all] >= 0 # Energy capacity of storage system b [kWh]
dvPeakDemandTOU[p.ratchets, 1:p.s.electric_tariff.n_tou_demand_tiers] >= 0 # Peak electrical power demand during ratchet r [kW]
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

end

for b in p.s.storage.types.elec
if !(p.s.storage.attr[b].allow_simultaneous_charge_discharge)
@warn "Adding binary variable to prevent simultaneous battery charge/discharge. Some solvers are very slow with integer variables."
@variable(m, binBattCharging[p.time_steps], Bin) # Binary for battery charging (vs discharging)
end
end

if !isempty(p.techs.gen) # Problem becomes a MILP
Expand All @@ -612,7 +624,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs)
end

if !(p.s.electric_utility.allow_simultaneous_export_import) & !isempty(p.s.electric_tariff.export_bins)
@warn "Adding binary variable to prevent simultaneous grid import/export. Some solvers are very slow with integer variables"
@warn "Adding binary variable to prevent simultaneous grid import/export. Some solvers are very slow with integer variables."
@variable(m, binNoGridPurchases[p.time_steps], Bin)
end

Expand Down Expand Up @@ -644,7 +656,7 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs)
end

if !isempty(p.s.electric_utility.outage_durations) # add dvUnserved Load if there is at least one outage
@warn "Adding binary variable to model outages. Some solvers are very slow with integer variables"
@warn "Adding binary variable to model outages. Some solvers are very slow with integer variables."
max_outage_duration = maximum(p.s.electric_utility.outage_durations)
outage_time_steps = p.s.electric_utility.outage_time_steps
tZeros = p.s.electric_utility.outage_start_time_steps
Expand Down
4 changes: 2 additions & 2 deletions src/core/reopt_multinode.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@


function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}}) where T <: AbstractScenario

dvs_idx_on_techs = String[
"dvSize",
"dvPurchaseSize",
Expand All @@ -16,7 +15,8 @@ function add_variables!(m::JuMP.AbstractModel, ps::AbstractVector{REoptInputs{T}
"dvStorageEnergy",
]
dvs_idx_on_storagetypes_time_steps = String[
"dvDischargeFromStorage"
"dvDischargeFromStorage",
"dvStorageToGrid"
]
for p in ps
_n = string("_", p.s.site.node)
Expand Down
Loading
Loading