Skip to content

Commit

Permalink
Merge branch 'develop' into prevent-simul-charge-discharge
Browse files Browse the repository at this point in the history
  • Loading branch information
hdunham committed Sep 25, 2024
2 parents c6e1b06 + 876c7a6 commit 36a6a6c
Show file tree
Hide file tree
Showing 12 changed files with 440 additions and 178 deletions.
30 changes: 23 additions & 7 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,25 +23,41 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## Develop 08-09-2024
## Develop simul charge discharge
### Changed
- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage
- Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. .
- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings
- Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov.
- 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
### 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
### 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

## Develop
### 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
- In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct
- Financial output **initial_capital_costs_after_incentives_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period"
### Changed
- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage.
- Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail.
- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings.
- Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov.
- Improves DRY coding by replacing multiple instances of the same chunks of code for MACRS deprecation and CHP capital cost into functions that are now in financial.jl.
- Simplifies the CHP sizing test to avoid a ~30 minute solve time, by avoiding the fuel burn y-intercept binaries which come with differences between full-load and part-load efficiency.
- For third party analysis proforma.jl metrics, O&M cost for existing Generator is now kept with offtaker, not the owner/developer
### Fixed
- Proforma calcs including "simple" payback and IRR for thermal techs/scenarios.
- The operating costs of fuel and O&M were missing for all thermal techs such as ExistingBoiler, CHP, and others; this adds those sections of code to properly calculate the operating costs.
- Added a test to validate the simple payback calculation with CHP (and ExistingBoiler) and checks the REopt result value against a spreadsheet proforma calculation (see Bill's spreadsheet).
- Added a couple of missing techs for the initial capital cost calculation in financial.jl.
- An issue with setup_boiler_inputs in reopt_inputs.jl.
- Fuel costs in proforma.jl were not consistent with the optimization costs, so that was corrected so that they are only added to the offtaker cashflows and not the owner/developer cashflows for third party.

## v0.47.2
### Fixed
Expand Down
6 changes: 4 additions & 2 deletions README.md
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
# REopt® Julia package
This package is currently under development, but it now has all of the capabilities of the REopt® model used in the [REopt API](https://github.com/NREL/REopt_API).
REopt.jl is the core module of the [REopt® techno-economic decision support platform](https://www.nrel.gov/reopt/), developed by the National Renewable Energy Laboratory (NREL). REopt® stands for **R**enewable **E**nergy integration and **opt**imization. REopt.jl is used within the publicly-accessible and open-source [REopt API](https://github.com/NREL/REopt_API), and the publicly available [REopt Web Tool](https://reopt.nrel.gov/tool) calls the REopt API.

For more information please see the documentation:
The REopt® techno-economic decision support platform is used by researchers to optimize energy systems for buildings, campuses, communities, microgrids, and more. REopt identifies the optimal mix of renewable energy, conventional generation, storage, and electrification technologies to meet cost savings, resilience, emissions reductions, and energy performance goals.

For more information about REopt.jl please see the Julia documentation:
<!-- [![](https://img.shields.io/badge/docs-stable-blue.svg)](https://nrel.github.io/REopt.jl/stable) -->
[![](https://img.shields.io/badge/docs-dev-blue.svg)](https://nrel.github.io/REopt.jl/dev)

Expand Down
67 changes: 58 additions & 9 deletions src/core/heating_cooling_loads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,7 @@ There are many ways in which a DomesticHotWaterLoad can be defined:
struct DomesticHotWaterLoad
loads_kw::Array{Real, 1}
annual_mmbtu::Real
unaddressable_annual_fuel_mmbtu::Real

function DomesticHotWaterLoad(;
doe_reference_name::String = "",
Expand Down Expand Up @@ -62,6 +63,7 @@ struct DomesticHotWaterLoad
end

loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * existing_boiler_efficiency) .* addressable_load_fraction
unaddressable_annual_fuel_mmbtu = sum(fuel_loads_mmbtu_per_hour .* (1 .- addressable_load_fraction)) / time_steps_per_hour

if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0
@warn "DomesticHotWaterLoad `fuel_loads_mmbtu_per_hour` was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored."
Expand All @@ -72,12 +74,14 @@ struct DomesticHotWaterLoad
if length(blended_doe_reference_names) > 0
@warn "DomesticHotWaterLoad doe_reference_name was provided, so blended_doe_reference_names will be ignored."
end
unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
elseif length(blended_doe_reference_names) > 0 &&
length(blended_doe_reference_names) == length(blended_doe_reference_percents)
loads_kw = blend_and_scale_doe_profiles(BuiltInDomesticHotWaterLoad, latitude, longitude, 2017,
blended_doe_reference_names, blended_doe_reference_percents, city,
annual_mmbtu, monthly_mmbtu, addressable_load_fraction,
existing_boiler_efficiency)
unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
else
throw(@error("Cannot construct DomesticHotWaterLoad. You must provide either [fuel_loads_mmbtu_per_hour],
[doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city]."))
Expand All @@ -90,7 +94,8 @@ struct DomesticHotWaterLoad

new(
loads_kw,
(sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU
(sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU,
unaddressable_annual_fuel_mmbtu
)
end
end
Expand Down Expand Up @@ -133,6 +138,7 @@ In this case the values provided for `doe_reference_name`, or `blended_doe_refe
struct SpaceHeatingLoad
loads_kw::Array{Real, 1}
annual_mmbtu::Real
unaddressable_annual_fuel_mmbtu::Real

function SpaceHeatingLoad(;
doe_reference_name::String = "",
Expand Down Expand Up @@ -170,6 +176,7 @@ struct SpaceHeatingLoad
end

loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * existing_boiler_efficiency) .* addressable_load_fraction
unaddressable_annual_fuel_mmbtu = sum(fuel_loads_mmbtu_per_hour .* (1 .- addressable_load_fraction)) / time_steps_per_hour

if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0
@warn "SpaceHeatingLoad fuel_loads_mmbtu_per_hour was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored."
Expand All @@ -180,12 +187,14 @@ struct SpaceHeatingLoad
if length(blended_doe_reference_names) > 0
@warn "SpaceHeatingLoad doe_reference_name was provided, so blended_doe_reference_names will be ignored."
end
unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
elseif length(blended_doe_reference_names) > 0 &&
length(blended_doe_reference_names) == length(blended_doe_reference_percents)
loads_kw = blend_and_scale_doe_profiles(BuiltInSpaceHeatingLoad, latitude, longitude, 2017,
blended_doe_reference_names, blended_doe_reference_percents, city,
annual_mmbtu, monthly_mmbtu, addressable_load_fraction,
existing_boiler_efficiency)
unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
else
throw(@error("Cannot construct BuiltInSpaceHeatingLoad. You must provide either [fuel_loads_mmbtu_per_hour],
[doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city]."))
Expand All @@ -198,7 +207,8 @@ struct SpaceHeatingLoad

new(
loads_kw,
(sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU
(sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU,
unaddressable_annual_fuel_mmbtu
)
end
end
Expand Down Expand Up @@ -1426,17 +1436,28 @@ end
"""
`ProcessHeatLoad` is an optional REopt input with the following keys and default values:
```julia
annual_mmbtu::Union{Real, Nothing} = nothing
fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[]
industry_reference_name::String = "",
sector::String = "",
blended_industry_reference_names::Array{String, 1} = String[],
blended_industry_reference_percents::Array{<:Real, 1} = Real[],
annual_mmbtu::Union{Real, Nothing} = nothing,
monthly_mmbtu::Array{<:Real,1} = Real[],
addressable_load_fraction::Any = 1.0,
fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[],
time_steps_per_hour::Int = 1, # corresponding to `fuel_loads_mmbtu_per_hour`
latitude::Real = 0.0,
longitude::Real = 0.0,
existing_boiler_efficiency::Real = NaN
```
There are many ways in which a ProcessHeatLoad can be defined:
1. One can provide the `fuel_loads_mmbtu_per_hour` value in the `ProcessHeatLoad` key within the `Scenario`.
2. One can provide the `annual_mmbtu` value in the `ProcessHeatLoad` key within the `Scenario`; this assumes a flat load.
1. When using either `industry_reference_name` or `blended_industry_reference_names`
2. One can provide the `industry_reference_name` or `blended_industry_reference_names` directly in the `ProcessHeatLoad` key within the `Scenario`. These values can be combined with the `annual_mmbtu` or `monthly_mmbtu` inputs to scale the industry reference profile(s).
3. One can provide the `fuel_loads_mmbtu_per_hour` value in the `ProcessHeatLoad` key within the `Scenario`.
!!! note "Process heat loads"
These loads are presented in terms of process heat required without regard to the efficiency of the input heating,
unlike the hot-water and space heating loads which are provided in terms of fuel input.
Process heat "load" inputs are in terms of fuel energy input required (boiler fuel), not the actual thermal demand.
The fuel energy is multiplied by the existing_boiler_efficiency to get the actual energy demand.
"""
function BuiltInProcessHeatLoad(
Expand Down Expand Up @@ -1485,9 +1506,11 @@ function BuiltInProcessHeatLoad(

built_in_load("process_heat", city, buildingtype, year, annual_mmbtu, monthly_mmbtu, existing_boiler_efficiency)
end

struct ProcessHeatLoad
loads_kw::Array{Real, 1}
annual_mmbtu::Real
unaddressable_annual_fuel_mmbtu::Real

function ProcessHeatLoad(;
industry_reference_name::String = "",
Expand Down Expand Up @@ -1532,6 +1555,7 @@ struct ProcessHeatLoad
end

loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * existing_boiler_efficiency) .* addressable_load_fraction
unaddressable_annual_fuel_mmbtu = sum(fuel_loads_mmbtu_per_hour .* (1 .- addressable_load_fraction)) / time_steps_per_hour

if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0
@warn "ProcessHeatLoad fuel_loads_mmbtu_per_hour was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored."
Expand All @@ -1542,12 +1566,15 @@ struct ProcessHeatLoad
if length(blended_doe_reference_names) > 0
@warn "ProcessHeatLoad doe_reference_name was provided, so blended_doe_reference_names will be ignored."
end
unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
elseif length(blended_doe_reference_names) > 0 &&
length(blended_doe_reference_names) == length(blended_doe_reference_percents)
loads_kw = blend_and_scale_doe_profiles(BuiltInProcessHeatLoad, latitude, longitude, 2017,
blended_doe_reference_names, blended_doe_reference_percents, city,
annual_mmbtu, monthly_mmbtu, addressable_load_fraction,
existing_boiler_efficiency)

unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
else
throw(@error("Cannot construct BuiltInProcessHeatLoad. You must provide either [fuel_loads_mmbtu_per_hour],
[doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city]."))
Expand All @@ -1560,7 +1587,29 @@ struct ProcessHeatLoad

new(
loads_kw,
(sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU
(sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU,
unaddressable_annual_fuel_mmbtu

)
end
end

"""
get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
Get unaddressable fuel load, for reporting
:addressable_load_fraction is the fraction of the input fuel load that is addressable to supply by energy technologies, like CHP
:annual_mmbtu and :monthly_mmbtu is assumed to be fuel, not thermal, in this function
:loads_kw is assumed to be thermal in this function, with units of kw_thermal, so needs to be converted to fuel mmbtu
"""
function get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
# Get unaddressable fuel load, for reporting
if !isempty(monthly_mmbtu)
unaddressable_annual_fuel_mmbtu = sum(monthly_mmbtu .* (1 .- addressable_load_fraction))
elseif !isnothing(annual_mmbtu)
unaddressable_annual_fuel_mmbtu = annual_mmbtu * (1 - addressable_load_fraction)
else # using the default CRB annual_mmbtu, so rely on loads_kw (thermal) assuming single addressable_load_fraction
unaddressable_annual_fuel_mmbtu = sum(loads_kw) / (KWH_PER_MMBTU * existing_boiler_efficiency)
end
return unaddressable_annual_fuel_mmbtu
end
10 changes: 6 additions & 4 deletions src/core/reopt_inputs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -396,8 +396,8 @@ function setup_tech_inputs(s::AbstractScenario)
end

if "Boiler" in techs.all
setup_boiler_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope,
boiler_efficiency, production_factor, fuel_cost_per_kwh)
setup_boiler_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency,
om_cost_per_kw, production_factor, fuel_cost_per_kwh)
end

if "CHP" in techs.all
Expand Down Expand Up @@ -693,14 +693,16 @@ end

"""
function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency,
production_factor, fuel_cost_per_kwh)
om_cost_per_kw, production_factor, fuel_cost_per_kwh)
Update tech-indexed data arrays necessary to build the JuMP model with the values for (new) boiler.
This version of this function, used in BAUInputs(), doesn't update renewable energy and emissions arrays.
"""
function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, boiler_efficiency, production_factor, fuel_cost_per_kwh)
function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency,
om_cost_per_kw, production_factor, fuel_cost_per_kwh)
max_sizes["Boiler"] = s.boiler.max_kw
min_sizes["Boiler"] = s.boiler.min_kw
existing_sizes["Boiler"] = 0.0
boiler_efficiency["Boiler"] = s.boiler.efficiency

# The Boiler only has a MACRS benefit, no ITC etc.
Expand Down
3 changes: 2 additions & 1 deletion src/results/existing_boiler.jl
Original file line number Diff line number Diff line change
@@ -1,6 +1,7 @@
# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE.
"""
`ExistingBoiler` results keys:
- `size_mmbtu_per_hour`
- `fuel_consumption_series_mmbtu_per_hour`
- `annual_fuel_consumption_mmbtu`
- `thermal_production_series_mmbtu_per_hour`
Expand All @@ -18,7 +19,7 @@
"""
function add_existing_boiler_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
r = Dict{String, Any}()

r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ExistingBoiler"]) / KWH_PER_MMBTU, digits=3)
r["fuel_consumption_series_mmbtu_per_hour"] =
round.(value.(m[:dvFuelUsage]["ExistingBoiler", ts] for ts in p.time_steps) ./ KWH_PER_MMBTU, digits=5)
r["annual_fuel_consumption_mmbtu"] = round(sum(r["fuel_consumption_series_mmbtu_per_hour"]), digits=5)
Expand Down
Loading

0 comments on commit 36a6a6c

Please sign in to comment.