Skip to content

Commit

Permalink
Merge pull request #199 from NREL/develop
Browse files Browse the repository at this point in the history
CHP class size, financial input output bugs
  • Loading branch information
rathod-b authored Mar 16, 2023
2 parents 1cb8285 + 7d72557 commit bbde91b
Show file tree
Hide file tree
Showing 10 changed files with 128 additions and 61 deletions.
12 changes: 12 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -22,6 +22,18 @@ Classify the change according to the following categories:
### Fixed
### Deprecated
### Removed


## v0.28
### Changed
- Changed Financial **breakeven_cost_of_emissions_reduction_per_tonnes_CO2** to **breakeven_cost_of_emissions_reduction_per_tonne_CO2**
- Changed `CHP.size_class` to start at 0 instead of 1, consistent with the API, and 0 represents the average of all `size_class`s
- Change `CHP.max_kw` to be based on either the heuristic sizing from average heating load (if heating) or peak electric load (if no heating, aka Prime Generator in the UI)
- The "big_number" for `max_kw` was causing the model to take forever to solve and some erroneous behavior; this is also consistent with the API to limit max_kw to a reasonable number
### Added
- Added previously missing Financial BAU outputs: **lifecycle_om_costs_before_tax**, **lifecycle_om_costs_after_tax**, **year_one_om_costs_before_tax**
### Fixed
- Fixed if statement to determing ElectricLoad "year" from && to ||, so that defaults to 2017 if any CRB input is used

## v0.27.0
### Added
Expand Down
2 changes: 1 addition & 1 deletion Project.toml
Original file line number Diff line number Diff line change
@@ -1,7 +1,7 @@
name = "REopt"
uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6"
authors = ["Nick Laws", "Hallie Dunham <[email protected]>", "Bill Becker <[email protected]>", "Bhavesh Rathod <[email protected]>", "Alex Zolan <[email protected]>", "Amanda Farthing <[email protected]>"]
version = "0.27.0"
version = "0.28.0"

[deps]
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
Expand Down
101 changes: 72 additions & 29 deletions src/core/chp.jl

Large diffs are not rendered by default.

4 changes: 2 additions & 2 deletions src/core/electric_load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,7 +35,7 @@
doe_reference_name::String = "",
blended_doe_reference_names::Array{String, 1} = String[],
blended_doe_reference_percents::Array{<:Real,1} = Real[],
year::Int = doe_reference_name ≠ nothing && blended_doe_reference_names ≠ nothing ? 2017 : 2022, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles must use 2017. If providing load data, specify year of data.
year::Int = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] ? 2017 : 2022, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles must use 2017. If providing load data, specify year of data.
city::String = "",
annual_kwh::Union{Real, Nothing} = nothing,
monthly_totals_kwh::Array{<:Real,1} = Real[],
Expand Down Expand Up @@ -117,7 +117,7 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o
doe_reference_name::String = "",
blended_doe_reference_names::Array{String, 1} = String[],
blended_doe_reference_percents::Array{<:Real,1} = Real[],
year::Int = doe_reference_name "" && blended_doe_reference_names String[] ? 2017 : 2022, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles must use 2017. If providing load data, specify year of data.
year::Int = doe_reference_name "" || blended_doe_reference_names String[] ? 2017 : 2022, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles must use 2017. If providing load data, specify year of data.
city::String = "",
annual_kwh::Union{Real, Nothing} = nothing,
monthly_totals_kwh::Array{<:Real,1} = Real[],
Expand Down
8 changes: 6 additions & 2 deletions src/core/scenario.jl
Original file line number Diff line number Diff line change
Expand Up @@ -320,6 +320,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
boiler_inputs[:time_steps_per_hour] = settings.time_steps_per_hour
if haskey(d, "ExistingBoiler")
boiler_inputs = merge(boiler_inputs, dictkeys_tosymbols(d["ExistingBoiler"]))
else
throw(@error("Must include ExistingBoiler input with at least fuel_cost_per_mmbtu if modeling heating load"))
end
existing_boiler = ExistingBoiler(; boiler_inputs...)
end
Expand All @@ -342,9 +344,11 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
avg_boiler_fuel_load_mmbtu_per_hour = sum(total_fuel_heating_load_mmbtu_per_hour) / length(total_fuel_heating_load_mmbtu_per_hour)
chp = CHP(d["CHP"];
avg_boiler_fuel_load_mmbtu_per_hour = avg_boiler_fuel_load_mmbtu_per_hour,
existing_boiler = existing_boiler)
existing_boiler = existing_boiler,
electric_load_series_kw = electric_load.loads_kw)
else # Only if modeling CHP without heating_load and existing_boiler (for electric-only CHP)
chp = CHP(d["CHP"])
chp = CHP(d["CHP"],
electric_load_series_kw = electric_load.loads_kw)
end
chp_prime_mover = chp.prime_mover
end
Expand Down
21 changes: 11 additions & 10 deletions src/core/steam_turbine.jl
Original file line number Diff line number Diff line change
Expand Up @@ -143,7 +143,8 @@ function get_steam_turbine_defaults(size_class::Int, defaults_all::Dict)
steam_turbine_defaults = Dict{String, Any}()

for key in keys(defaults_all)
steam_turbine_defaults[key] = defaults_all[key][size_class]
# size_class is zero-based index so plus-1 for indexing Julia one-based indexed arrays
steam_turbine_defaults[key] = defaults_all[key][size_class+1]
end
defaults_all = nothing

Expand Down Expand Up @@ -227,11 +228,10 @@ function get_steam_turbine_defaults_size_class(;avg_boiler_fuel_load_mmbtu_per_h
class_bounds = [(0.0, 25000.0), (0, 1000.0), (1000.0, 5000.0), (5000.0, 25000.0)]
n_classes = length(class_bounds)
if !isnothing(size_class)
if size_class < 1 || size_class > n_classes
throw(@error("Invalid size_class given for steam_turbine, must be in [1,2,3,4]"))
if size_class < 0 || size_class > (n_classes-1)
throw(@error("Invalid size_class $size_class given for steam_turbine, must be in [0,1,2,3]"))
end
end
if !isnothing(avg_boiler_fuel_load_mmbtu_per_hour)
elseif !isnothing(avg_boiler_fuel_load_mmbtu_per_hour)
if avg_boiler_fuel_load_mmbtu_per_hour <= 0
throw(@error("avg_boiler_fuel_load_mmbtu_per_hour must be > 0.0 MMBtu/hr"))
end
Expand All @@ -241,21 +241,22 @@ function get_steam_turbine_defaults_size_class(;avg_boiler_fuel_load_mmbtu_per_h
# With heuristic size, find the suggested size class
if st_elec_size_heuristic_kw < class_bounds[2][2]
# If smaller than the upper bound of the smallest class, assign the smallest class
size_class = 2
size_class = 1
elseif st_elec_size_heuristic_kw >= class_bounds[n_classes][1]
# If larger than or equal to the lower bound of the largest class, assign the largest class
size_class = n_classes # Size classes are zero-indexed
size_class = n_classes - 1 # Size classes are zero-indexed
else
# For middle size classes
for sc in 2:(n_classes-1)
for sc in 3:(n_classes-1)
if st_elec_size_heuristic_kw >= class_bounds[sc][1] &&
st_elec_size_heuristic_kw < class_bounds[sc][2]
size_class = sc
size_class = sc - 1
break
end
end
end
else
size_class = 1
size_class = 0
st_elec_size_heuristic_kw = nothing
end

Expand Down
4 changes: 2 additions & 2 deletions src/results/financial.jl
Original file line number Diff line number Diff line change
Expand Up @@ -32,7 +32,7 @@
- `lcc` Optimal lifecycle cost
- `lifecycle_generation_tech_capital_costs` LCC component. Net capital costs for all generation technologies, in present value, including replacement costs and incentives. This value does not include offgrid_other_capital_costs.
- `lifecycle_storage_capital_costs` LCC component. Net capital costs for all storage technologies, in present value, including replacement costs and incentives. This value does not include offgrid_other_capital_costs.
- `lifecycle_om_costs_after_tax` LCC component. Present value of all O&M costs, after tax.
- `lifecycle_om_costs_after_tax` LCC component. Present value of all O&M costs, after tax. (does not include fuel costs)
- `lifecycle_fuel_costs_after_tax` LCC component. Present value of all fuel costs over the analysis period, after tax.
- `lifecycle_chp_standby_cost_after_tax` LCC component. Present value of all CHP standby charges, after tax.
- `lifecycle_elecbill_after_tax` LCC component. Present value of all electric utility charges, after tax.
Expand All @@ -57,7 +57,7 @@
- `lifecycle_emissions_cost_health` LCC component if Settings input include_health_in_objective is true. Present value of NOx, SO2, and PM2.5 emissions cost over the analysis period.
calculated in combine_results function if BAU scenario is run:
- `breakeven_cost_of_emissions_reduction_per_tonnes_CO2`
- `breakeven_cost_of_emissions_reduction_per_tonne_CO2`
!!! note "'Series' and 'Annual' energy outputs are average annual"
REopt performs load balances using average annual production values for technologies that include degradation.
Expand Down
6 changes: 4 additions & 2 deletions src/results/results.jl
Original file line number Diff line number Diff line change
Expand Up @@ -142,6 +142,9 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU
("Financial", "lcc"),
("Financial", "lifecycle_emissions_cost_climate"),
("Financial", "lifecycle_emissions_cost_health"),
("Financial", "lifecycle_om_costs_before_tax"),
("Financial", "lifecycle_om_costs_after_tax"),
("Financial", "year_one_om_costs_before_tax"),
("ElectricTariff", "year_one_energy_cost_before_tax"),
("ElectricTariff", "year_one_demand_cost_before_tax"),
("ElectricTariff", "year_one_fixed_cost_before_tax"),
Expand Down Expand Up @@ -221,7 +224,6 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU
end
end
end
opt["Financial"]["lifecycle_om_costs_before_tax_bau"] = bau["Financial"]["lifecycle_om_costs_before_tax"]
opt["Financial"]["npv"] = round(opt["Financial"]["lcc_bau"] - opt["Financial"]["lcc"], digits=2)

opt["ElectricLoad"]["bau_critical_load_met"] = bau_scenario.outage_outputs.bau_critical_load_met
Expand All @@ -247,7 +249,7 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU
bau["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] - opt["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"]
)
if breakeven_cost_denominator != 0.0
opt["Financial"]["breakeven_cost_of_emissions_reduction_per_tonnes_CO2"] = -1 * npv_without_modeled_climate_costs / breakeven_cost_denominator
opt["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] = -1 * npv_without_modeled_climate_costs / breakeven_cost_denominator
end
end

Expand Down
4 changes: 2 additions & 2 deletions test/scenarios/re_emissions_with_thermal.json
Original file line number Diff line number Diff line change
Expand Up @@ -91,7 +91,7 @@
},
"CHP": {
"prime_mover": "recip_engine",
"size_class": 3,
"size_class": 2,
"min_kw": 200.0,
"min_allowable_kw":0.0,
"max_kw": 200.0,
Expand All @@ -104,4 +104,4 @@
"min_gal": 50000.0,
"max_gal": 50000.0
}
}
}
27 changes: 16 additions & 11 deletions test/test_with_xpress.jl
Original file line number Diff line number Diff line change
Expand Up @@ -85,7 +85,7 @@ end
data_cost_curve = JSON.parsefile("./scenarios/chp_sizing.json")
data_cost_curve["CHP"] = Dict()
data_cost_curve["CHP"]["prime_mover"] = "recip_engine"
data_cost_curve["CHP"]["size_class"] = 2
data_cost_curve["CHP"]["size_class"] = 1
data_cost_curve["CHP"]["fuel_cost_per_mmbtu"] = 8.0
data_cost_curve["CHP"]["min_kw"] = 0
data_cost_curve["CHP"]["min_allowable_kw"] = 555.5
Expand Down Expand Up @@ -876,7 +876,7 @@ end
total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.existing_chiller.cop
@test round(total_chiller_electric_consumption, digits=0) 320544.0 atol=1.0 # loads_kw is **electric**, loads_kw_thermal is **thermal**

#Test CHP defaults use average fuel load, size class 3 for recip_engine
#Test CHP defaults use average fuel load, size class 2 for recip_engine
@test inputs.s.chp.min_allowable_kw 50.0 atol=0.01
@test inputs.s.chp.om_cost_per_kwh 0.0225 atol=0.0001

Expand All @@ -893,18 +893,23 @@ end
@test round(total_chiller_electric_consumption, digits=0) round(expected_cooling_electricity) atol=1.0
@test round(total_chiller_electric_consumption, digits=0) 3876410 atol=1.0

# Check that without heating load or max_kw input, CHP.max_kw gets set based on peak electric load
@test inputs.s.chp.max_kw maximum(inputs.s.electric_load.loads_kw) atol=0.01

input_data["SpaceHeatingLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([500.0], 12))
input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([500.0], 12))
input_data["CoolingLoad"] = Dict{Any, Any}("monthly_fractions_of_electric_load" => repeat([0.1], 12))

s = Scenario(input_data)
inputs = REoptInputs(s)
#Test CHP defaults use average fuel load, size class changes to 4
#Test CHP defaults use average fuel load, size class changes to 3
@test inputs.s.chp.min_allowable_kw 315.0 atol=0.1
@test inputs.s.chp.om_cost_per_kwh 0.02 atol=0.0001
#Update CHP prime_mover and test new defaults
input_data["CHP"]["prime_mover"] = "combustion_turbine"
input_data["CHP"]["size_class"] = 2
input_data["CHP"]["size_class"] = 1
# Set max_kw higher than peak electric load so min_allowable_kw doesn't get assigned to max_kw
input_data["CHP"]["max_kw"] = 1000.0

s = Scenario(input_data)
inputs = REoptInputs(s)
Expand Down Expand Up @@ -1283,8 +1288,8 @@ end
yr1_grid_emissions_tonnes_CO2_out = results["ElectricUtility"]["annual_emissions_tonnes_CO2"]
yr1_total_emissions_calced_tonnes_CO2 = yr1_fuel_emissions_tonnes_CO2_out + yr1_grid_emissions_tonnes_CO2_out
@test annual_emissions_tonnes_CO2_out yr1_total_emissions_calced_tonnes_CO2 atol=1e-1
if haskey(results["Financial"],"breakeven_cost_of_emissions_reduction_per_tonnes_CO2")
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonnes_CO2"] >= 0.0
if haskey(results["Financial"],"breakeven_cost_of_emissions_reduction_per_tonne_CO2")
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] >= 0.0
end

if i == 1
Expand All @@ -1300,7 +1305,7 @@ end
@test results["Site"]["total_renewable_energy_fraction"] 0.8
@test results["Site"]["total_renewable_energy_fraction_bau"] 0.14495 atol=1e-4
@test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] 0.61865 atol=1e-4
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonnes_CO2"] 283.5 atol=1
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] 283.5 atol=1
@test results["Site"]["annual_emissions_tonnes_CO2"] 11.36 atol=1e-2
@test results["Site"]["annual_emissions_tonnes_CO2_bau"] 32.16 atol=1e-2
@test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] 6.96
Expand Down Expand Up @@ -1334,7 +1339,7 @@ end
@test results["Site"]["total_renewable_energy_fraction_bau"] 0.1365 atol=1e-3 # 0.1354 atol=1e-3
# CO2 emissions - totals ≈ from grid, from fuelburn, ER, $/tCO2 breakeven
@test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] 0.8 atol=1e-3 # 0.8
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonnes_CO2"] 351.24 atol=1e-1
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] 351.24 atol=1e-1
@test results["Site"]["annual_emissions_tonnes_CO2"] 14.2 atol=1
@test results["Site"]["annual_emissions_tonnes_CO2_bau"] 70.99 atol=1
@test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] 0.0 atol=1 # 0.0
Expand All @@ -1357,13 +1362,13 @@ end
inputs["ElectricStorage"]["max_kw"] = results["ElectricStorage"]["size_kw"]
inputs["ElectricStorage"]["min_kwh"] = results["ElectricStorage"]["size_kwh"]
inputs["ElectricStorage"]["max_kwh"] = results["ElectricStorage"]["size_kwh"]
inputs["Financial"]["CO2_cost_per_tonne"] = results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonnes_CO2"]
inputs["Financial"]["CO2_cost_per_tonne"] = results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"]
inputs["Settings"]["include_climate_in_objective"] = true
m1 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
m2 = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0))
results = run_reopt([m1, m2], inputs)
@test results["Financial"]["npv"]/expected_npv 0 atol=1e-3
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonnes_CO2"] inputs["Financial"]["CO2_cost_per_tonne"] atol=1e-1
@test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] inputs["Financial"]["CO2_cost_per_tonne"] atol=1e-1
elseif i == 3
@test results["PV"]["size_kw"] 20.0 atol=1e-1
@test !haskey(results, "Wind")
Expand Down Expand Up @@ -1484,7 +1489,7 @@ end
# Add CHP
input_data["CHP"] = Dict{Any, Any}([
("prime_mover", "recip_engine"),
("size_class", 2),
("size_class", 1),
("min_kw", 250.0),
("min_allowable_kw", 0.0),
("max_kw", 250.0),
Expand Down

0 comments on commit bbde91b

Please sign in to comment.