Skip to content

Commit

Permalink
Merge branch 'develop' into limit_battery_energy_cap
Browse files Browse the repository at this point in the history
  • Loading branch information
adfarth committed Oct 11, 2024
2 parents 2266097 + 8999b3c commit c06cd28
Show file tree
Hide file tree
Showing 23 changed files with 183 additions and 95 deletions.
20 changes: 19 additions & 1 deletion CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,9 @@ The format is based on [Keep a Changelog](https://keepachangelog.com/en/1.0.0/),
and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0.html).

## Guidelines
- When making a Pull Request into `develop` start a new double-hash header for "Develop - YYYY-MM-DD"
- When working in feature branch, start a new double-hash header with the name of the branch and record changes under that
- When merging `develop` into a feature branch, keep the feature branch section and the "Develop" section separate to simplify merge conflicts
- When making a Pull Request into `develop`, merge the feature branch section into the "Develop" section (if it exists), else rename the feature branch header to "Develop"
- When making a Pull Request into `master` change "Develop" to the next version number

### Formatting
Expand All @@ -26,6 +28,22 @@ Classify the change according to the following categories:
## Develop - 2024-10-11
### Added
- Add new **ElectricStorage** parameters **max_duration_hours** and **min_duration_hours** to bound the energy duration of battery storage

## Develop
### Changed
- Replace all `1/p.s.settings.time_steps_per_hour` with `p.hours_per_time_step` for simplicity/consistency
- Rename function `add_storage_sum_constraints` to `add_storage_sum_grid_constraints` for clarity
### Added
- Constraints to prevent simultaneous charge/discharge of storage
- Specify in docstrings that **PV** **max_kw** and **size_kw** are kW-DC
- Add the Logging package to `test/Project.toml` because it is used in `runtests.jl`
### Fixed
- Force **ElectricLoad** **critical_load_kw** to be _nothing_ when **off_grid_flag** is _true_ (**critical_load_fraction** was already being forced to 1, but the user was still able to get around this by providing **critical_load_kw**)
- Removed looping over storage name in functions `add_hot_thermal_storage_dispatch_constraints` and `add_cold_thermal_storage_dispatch_constraints` because this loop is already done when calling these functions and storage name is passed in as argument `b`
- Remove extraneous line of code in `results/wind.jl`
- Change type of **value_of_lost_load** in **FinancialInputs** struct to fix convert error when user provides an _Int_
- Change international location in "Solar Dataset" test set from Cameroon to Oulu because the locations in the NSRDB have been expanded significantly so there is now an NSRDB point at Cameroon
- Handle edge case where the values of **outage_start_time_steps** and **outage_durations** makes an outage extend beyond the end of the year. The outage will now wrap around to the beginning of the year.

## v0.48.0
### Added
Expand Down
18 changes: 13 additions & 5 deletions src/constraints/outage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -2,8 +2,8 @@
function add_dv_UnservedLoad_constraints(m,p)
# Effective load balance
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
m[:dvUnservedLoad][s, tz, ts] == p.s.electric_load.critical_loads_kw[tz+ts-1]
- sum( m[:dvMGRatedProduction][t, s, tz, ts] * (p.production_factor[t, tz+ts-1] + p.unavailability[t][tz+ts-1]) * p.levelization_factor[t]
m[:dvUnservedLoad][s, tz, ts] == p.s.electric_load.critical_loads_kw[time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]
- sum( m[:dvMGRatedProduction][t, s, tz, ts] * (p.production_factor[t, time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] + p.unavailability[t][time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]) * p.levelization_factor[t]
- m[:dvMGProductionToStorage][t, s, tz, ts] - m[:dvMGCurtail][t, s, tz, ts]
for t in p.techs.elec
)
Expand All @@ -22,7 +22,7 @@ end

function add_outage_cost_constraints(m,p)
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps],
m[:dvMaxOutageCost][s] >= p.pwf_e * sum(p.value_of_lost_load_per_kwh[tz+ts-1] * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
m[:dvMaxOutageCost][s] >= p.pwf_e * sum(p.value_of_lost_load_per_kwh[time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] * m[:dvUnservedLoad][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
)

@expression(m, ExpectedOutageCost,
Expand Down Expand Up @@ -116,7 +116,7 @@ function add_MG_production_constraints(m,p)
# Electrical production sent to storage or export must be less than technology's rated production
@constraint(m, [t in p.techs.elec, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
m[:dvMGProductionToStorage][t, s, tz, ts] + m[:dvMGCurtail][t, s, tz, ts] <=
(p.production_factor[t, tz+ts-1] + p.unavailability[t][tz+ts-1]) * p.levelization_factor[t] * m[:dvMGRatedProduction][t, s, tz, ts]
(p.production_factor[t, time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] + p.unavailability[t][time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]) * p.levelization_factor[t] * m[:dvMGRatedProduction][t, s, tz, ts]
)

@constraint(m, [t in p.techs.elec, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
Expand All @@ -138,7 +138,7 @@ function add_MG_Gen_fuel_burn_constraints(m,p)
# Define dvMGFuelUsed by summing over outage time_steps.
@constraint(m, [t in p.techs.gen, s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps],
m[:dvMGFuelUsed][t, s, tz] == fuel_slope_gal_per_kwhe * p.hours_per_time_step * p.levelization_factor[t] *
sum( (p.production_factor[t, tz+ts-1] + p.unavailability[t][tz+ts-1]) * m[:dvMGRatedProduction][t, s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
sum( (p.production_factor[t, time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)] + p.unavailability[t][time_step_wrap_around(tz+ts-1, time_steps_per_hour=p.s.settings.time_steps_per_hour)]) * m[:dvMGRatedProduction][t, s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
+ fuel_intercept_gal_per_hr * p.hours_per_time_step *
sum( m[:binMGGenIsOnInTS][s, tz, ts] for ts in 1:p.s.electric_utility.outage_durations[s])
)
Expand Down Expand Up @@ -287,6 +287,14 @@ function add_MG_storage_dispatch_constraints(m,p)
)
)

# Prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
@constraint(m, [ts in p.time_steps_without_grid],
m[:dvStorageEnergy]["ElectricStorage"] >= m[:dvMGStoredEnergy][s, tz, ts-1] + p.hours_per_time_step * (
p.s.storage.attr["ElectricStorage"].charge_efficiency * sum(m[:dvMGProductionToStorage][t, s, tz, ts] for t in p.techs.elec)
)
)

# Min SOC
if p.s.storage.attr["ElectricStorage"].soc_min_applies_during_outages
# Minimum state of charge
@constraint(m, [s in p.s.electric_utility.scenarios, tz in p.s.electric_utility.outage_start_time_steps, ts in p.s.electric_utility.outage_time_steps],
Expand Down
70 changes: 50 additions & 20 deletions src/constraints/storage_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -61,21 +61,35 @@ end

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

# Constraint (4g): state-of-charge for electrical storage - with grid
# Constraint (4g)-1: 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 * (
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
)
)

# Constraint (4h): state-of-charge for electrical storage - no grid
# Constraint (4g)-2: 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 * (
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
)
)

# Constraint (4h): prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
# (4h)-1: with grid
@constraint(m, [ts in p.time_steps_with_grid],
m[Symbol("dvStorageEnergy"*_n)][b] >= 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]
)
)
# (4h)-2: no grid
@constraint(m, [ts in p.time_steps_without_grid],
m[Symbol("dvStorageEnergy"*_n)][b] >= 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)
)
)

# Constraint (4i)-1: Dispatch to electrical storage is no greater than power capacity
Expand Down Expand Up @@ -115,16 +129,24 @@ end
function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4j)-1: Reconcile state-of-charge for (hot) thermal storage
@constraint(m, [b in p.s.storage.types.hot, ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + (1/p.s.settings.time_steps_per_hour) * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp), q in p.heating_loads) -
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads) / p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp), q in p.heating_loads) -
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads) / p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

# Prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStorageEnergy"*_n)][b] >= m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp), q in p.heating_loads)
- p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

#Constraint (4n)-1: Dispatch to and from thermal storage is no greater than power capacity
@constraint(m, [b in p.s.storage.types.hot, ts in p.time_steps],
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoragePower"*_n)][b] >=
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] +
sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for t in union(p.techs.heating, p.techs.chp))
Expand All @@ -133,14 +155,14 @@ function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="")
# TODO missing thermal storage constraints from API ???

# Constraint (4o): Discharge from storage is equal to sum of heat from storage for all qualities
@constraint(m, HeatDischargeReconciliation[b in p.s.storage.types.hot, ts in p.time_steps],
@constraint(m, HeatDischargeReconciliation[ts in p.time_steps],
m[Symbol("dvDischargeFromStorage"*_n)][b,ts] ==
sum(m[Symbol("dvHeatFromStorage"*_n)][b,q,ts] for q in p.heating_loads)
)

#Do not allow GHP to charge storage
if !isempty(p.techs.ghp)
for b in p.s.storage.types.hot, t in p.techs.ghp, q in p.heating_loads, ts in p.time_steps
for t in p.techs.ghp, q in p.heating_loads, ts in p.time_steps
fix(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts], 0.0, force=true)
end
end
Expand All @@ -151,36 +173,44 @@ function add_cold_thermal_storage_dispatch_constraints(m, p, b; _n="")

# Constraint (4f)-2: (Cold) Thermal production sent to storage or grid must be less than technology's rated production
if !isempty(p.techs.cooling)
@constraint(m, CoolingTechProductionFlowCon[b in p.s.storage.types.cold, t in p.techs.cooling, ts in p.time_steps],
@constraint(m, CoolingTechProductionFlowCon[t in p.techs.cooling, ts in p.time_steps],
m[Symbol("dvProductionToStorage"*_n)][b,t,ts] <=
m[Symbol("dvCoolingProduction"*_n)][t,ts]
)
end

# Constraint (4j)-2: Reconcile state-of-charge for (cold) thermal storage
@constraint(m, ColdTESInventoryCon[b in p.s.storage.types.cold, ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + (1/p.s.settings.time_steps_per_hour) * (
sum(p.s.storage.attr[b].charge_efficiency * m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling) -
m[Symbol("dvDischargeFromStorage"*_n)][b,ts]/p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
@constraint(m, ColdTESInventoryCon[ts in p.time_steps],
m[Symbol("dvStoredEnergy"*_n)][b,ts] == 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.cooling) -
m[Symbol("dvDischargeFromStorage"*_n)][b,ts]/p.s.storage.attr[b].discharge_efficiency -
p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

# Prevent simultaneous charge and discharge by limitting charging alone to not make the SOC exceed 100%
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStorageEnergy"*_n)][b] >= m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + p.hours_per_time_step * (
p.s.storage.attr[b].charge_efficiency * sum(m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling)
- p.s.storage.attr[b].thermal_decay_rate_fraction * m[Symbol("dvStorageEnergy"*_n)][b]
)
)

#Constraint (4n)-2: Dispatch to and from thermal storage is no greater than power capacity
@constraint(m, [b in p.s.storage.types.cold, ts in p.time_steps],
@constraint(m, [ts in p.time_steps],
m[Symbol("dvStoragePower"*_n)][b] >= m[Symbol("dvDischargeFromStorage"*_n)][b,ts] +
sum(m[Symbol("dvProductionToStorage"*_n)][b,t,ts] for t in p.techs.cooling)
)

#Do not allow GHP to charge storage
if !isempty(p.techs.ghp)
for b in p.s.storage.types.cold, t in p.techs.ghp, ts in p.time_steps
for t in p.techs.ghp, ts in p.time_steps
fix(m[Symbol("dvProductionToStorage"*_n)][b,t,ts], 0.0, force=true)
end
end
end

function add_storage_sum_constraints(m, p; _n="")
function add_storage_sum_grid_constraints(m, p; _n="")

##Constraint (8c): Grid-to-storage no greater than grid purchases
@constraint(m, [ts in p.time_steps_with_grid],
Expand Down
2 changes: 1 addition & 1 deletion src/constraints/thermal_tech_constraints.jl
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ function add_boiler_tech_constraints(m, p; _n="")
)
if "Boiler" in p.techs.boiler # ExistingBoiler does not have om_cost_per_kwh
m[:TotalBoilerPerUnitProdOMCosts] = @expression(m, p.third_party_factor * p.pwf_om *
sum(p.s.boiler.om_cost_per_kwh / p.s.settings.time_steps_per_hour *
sum(p.s.boiler.om_cost_per_kwh * p.hours_per_time_step *
m[Symbol("dvHeatingProduction"*_n)]["Boiler",q,ts] for q in p.heating_loads, ts in p.time_steps)
)
else
Expand Down
2 changes: 1 addition & 1 deletion src/core/bau_inputs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -287,7 +287,7 @@ function setup_bau_emissions_inputs(p::REoptInputs, s_bau::BAUScenario, generato
bau_grid_to_load = [max(i,0) for i in bau_grid_to_load]
end

bau_grid_emissions_lb_CO2_per_year = sum(p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh .* bau_grid_to_load) / p.s.settings.time_steps_per_hour
bau_grid_emissions_lb_CO2_per_year = sum(p.s.electric_utility.emissions_factor_series_lb_CO2_per_kwh .* bau_grid_to_load) * p.hours_per_time_step
bau_emissions_lb_CO2_per_year += bau_grid_emissions_lb_CO2_per_year

## Generator emissions (during outages)
Expand Down
12 changes: 9 additions & 3 deletions src/core/electric_load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -105,9 +105,15 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o
min_load_met_annual_fraction::Real = off_grid_flag ? 0.99999 : 1.0 # if off grid, 99.999%, else must be 100%. Applied to each time_step as a % of electric load.
)

if off_grid_flag && !(critical_load_fraction == 1.0)
@warn "ElectricLoad critical_load_fraction must be 1.0 (100%) for off-grid scenarios. Any other value will be overriden when `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction."
critical_load_fraction = 1.0
if off_grid_flag
if !isnothing(critical_loads_kw)
@warn "ElectricLoad critical_loads_kw will be ignored because `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction."
critical_loads_kw = nothing
end
if critical_load_fraction != 1.0
@warn "ElectricLoad critical_load_fraction must be 1.0 (100%) for off-grid scenarios. Any other value will be overriden when `off_grid_flag` is true. If you wish to alter the load profile or load met, adjust the loads_kw or min_load_met_annual_fraction."
critical_load_fraction = 1.0
end
end

if !(off_grid_flag)
Expand Down
2 changes: 1 addition & 1 deletion src/core/financial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -56,7 +56,7 @@ struct Financial
owner_tax_rate_fraction::Float64
owner_discount_rate_fraction::Float64
analysis_years::Int
value_of_lost_load_per_kwh::Union{Array{Float64,1}, Float64}
value_of_lost_load_per_kwh::Union{Array{<:Real,1}, Real}
microgrid_upgrade_cost_fraction::Float64
macrs_five_year::Array{Float64,1}
macrs_seven_year::Array{Float64,1}
Expand Down
2 changes: 1 addition & 1 deletion src/core/pv.jl
Original file line number Diff line number Diff line change
Expand Up @@ -13,7 +13,7 @@
location::String="both", # one of ["roof", "ground", "both"]
existing_kw::Real=0,
min_kw::Real=0,
max_kw::Real=1.0e9, # max new capacity (beyond existing_kw)
max_kw::Real=1.0e9, # max new DC capacity (beyond existing_kw)
installed_cost_per_kw::Real=1790.0,
om_cost_per_kw::Real=18.0,
degradation_fraction::Real=0.005,
Expand Down
4 changes: 2 additions & 2 deletions src/core/reopt.jl
Original file line number Diff line number Diff line change
Expand Up @@ -68,7 +68,7 @@ Solve the model using a `Scenario` or `BAUScenario`.
function run_reopt(m::JuMP.AbstractModel, s::AbstractScenario)

try
if s.site.CO2_emissions_reduction_min_fraction > 0.0 || s.site.CO2_emissions_reduction_max_fraction < 1.0
if (!isnothing(s.site.CO2_emissions_reduction_min_fraction) && s.site.CO2_emissions_reduction_min_fraction > 0.0) || (!isnothing(s.site.CO2_emissions_reduction_max_fraction) && s.site.CO2_emissions_reduction_max_fraction < 1.0)
throw(@error("To constrain CO2 emissions reduction min or max percentages, the optimal and business as usual scenarios must be run in parallel. Use a version of run_reopt() that takes an array of two models."))
end
run_reopt(m, REoptInputs(s))
Expand Down Expand Up @@ -247,7 +247,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs)
end

if any(max_kw->max_kw > 0, (p.s.storage.attr[b].max_kw for b in p.s.storage.types.elec))
add_storage_sum_constraints(m, p)
add_storage_sum_grid_constraints(m, p)
end

add_production_constraints(m, p)
Expand Down
Loading

0 comments on commit c06cd28

Please sign in to comment.