Skip to content

Commit

Permalink
Merge branch 'develop' into degradation-cleanup
Browse files Browse the repository at this point in the history
  • Loading branch information
rathod-b committed Oct 15, 2024
2 parents a4690b7 + bac9593 commit 2c300f7
Show file tree
Hide file tree
Showing 25 changed files with 208 additions and 105 deletions.
22 changes: 21 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 @@ -32,6 +34,24 @@ Classify the change according to the following categories:
### Removed
- 80% scaling of battery maintenance costs when using augmentation strategy

## 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.
- Enforce minimum allowable sizes for ASHP technologies by introducing improved big-M values for segmented size constraints.
- Removed default values from ASHP functions that calculate minimum allowable size and performance.

## v0.48.0
### Added
- Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly
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 @@ -50,21 +50,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 @@ -104,16 +118,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 @@ -122,14 +144,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 @@ -140,36 +162,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
10 changes: 5 additions & 5 deletions src/core/ashp.jl
Original file line number Diff line number Diff line change
Expand Up @@ -444,14 +444,14 @@ function get_ashp_performance(cop_reference,
cf_reference,
reference_temps,
ambient_temp_degF,
back_up_temp_threshold_degF = 10.0
back_up_temp_threshold_degF
)
"""
function get_ashp_performance(cop_reference,
cf_reference,
reference_temps,
ambient_temp_degF,
back_up_temp_threshold_degF = 10.0
back_up_temp_threshold_degF
)
num_timesteps = length(ambient_temp_degF)
cop = zeros(num_timesteps)
Expand Down Expand Up @@ -493,9 +493,9 @@ Obtains the default minimum allowable size for ASHP system. This is calculated
"""
function get_ashp_default_min_allowable_size(heating_load::Array{<:Real,1},
heating_cf::Array{<:Real,1},
cooling_load::Array{<:Real,1} = Real[],
cooling_cf::Array{<:Real,1} = Real[],
peak_load_thermal_factor::Real = 0.5
cooling_load::Array{<:Real,1},
cooling_cf::Array{<:Real,1},
peak_load_thermal_factor::Real
)

if isempty(cooling_cf)
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
Loading

0 comments on commit 2c300f7

Please sign in to comment.