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

Align heating and cooling load profiles with electric load year #472

Merged
merged 28 commits into from
Jan 14, 2025
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
28 commits
Select commit Hold shift + click to select a range
5511ac2
Improve test description, avoid using Dates.jl in runtests.jl
Bill-Becker Jan 7, 2025
3a620f5
Add leap year test input file
Bill-Becker Jan 7, 2025
fdcdd6a
Fix input file path formatting
Bill-Becker Jan 7, 2025
fc9208b
Include 2023 and 2024 years for all new tests
Bill-Becker Jan 8, 2025
dd13bed
Update year input for HeatingLoad
Bill-Becker Jan 9, 2025
60376db
Add year input for CoolingLoad
Bill-Becker Jan 9, 2025
f472f9a
Remove year=2017 requirement for ElectricLoad
Bill-Becker Jan 9, 2025
160ed67
Avoid passing year twice to constructors, make load consistency requi…
Bill-Becker Jan 9, 2025
93fac74
Shift CRB load profiles to match the starting day of the input year, …
Bill-Becker Jan 9, 2025
a6c42ca
Fix leap year handling for helper function get_monthly_energy for sim…
Bill-Becker Jan 9, 2025
b244565
Update simulated_load() for year input and alignment across loads
Bill-Becker Jan 9, 2025
d3b5c60
Add tests for load alignment based on year input
Bill-Becker Jan 9, 2025
64b54a0
Merge branch 'develop' into load-year-align
Bill-Becker Jan 9, 2025
5384b5f
Update changelog for load align fix
Bill-Becker Jan 9, 2025
be8197c
small text updates
adfarth Jan 9, 2025
7af1601
Require year input, in particular with ElectricLoad.loads_kw input
Bill-Becker Jan 10, 2025
c100eee
Add year input to fix tests with loads_kw
Bill-Becker Jan 10, 2025
9d2ea82
small update to normalize explanation
adfarth Jan 10, 2025
2b10ede
Change CRB profile year alignment strategy
Bill-Becker Jan 13, 2025
e5a55e3
Add more updates to Changelog.
Bill-Becker Jan 13, 2025
c91743c
Merge branch 'load-year-align' of https://github.com/NREL/REopt.jl in…
Bill-Becker Jan 13, 2025
cdcd928
Fix test input for doe_reference_name
Bill-Becker Jan 13, 2025
5f46a78
Update test with get_monthly_energy() now that it's consistent with l…
Bill-Becker Jan 13, 2025
d62ef0e
updating CoolingLoad help text
adfarth Jan 13, 2025
5d700be
Update changelog for required year for heating and cooling load profi…
Bill-Becker Jan 14, 2025
74d362d
Make ElectricTariff.year = nothing by default, passed from ElectricLo…
Bill-Becker Jan 14, 2025
375289e
Include industrial_reference_names for process heat default year of 2017
Bill-Becker Jan 14, 2025
f67b06c
Merge branch 'load-year-align' of https://github.com/NREL/REopt.jl in…
Bill-Becker Jan 14, 2025
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
6 changes: 6 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -25,6 +25,12 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## load-year-align
### Fixed
- Align heating and cooling load profiles based on electric load year input, if using custom electric load profile with simulated (CRB or schedule-based flatloads) heating/cooling loads
### Changed
- Make `year` input required with any custom load profile input (e.g. `ElectricLoad.loads_kw`, `SpaceHeatingLoad.fuel_loads_mmbtu_per_hour`)
- Shift and adjust CRB load profiles (i.e. with `doe_reference_name` input) based on the `year` input

## leap-year-fix
### Fixed
Expand Down
27 changes: 18 additions & 9 deletions src/core/doe_commercial_reference_building_loads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -134,25 +134,38 @@ function built_in_load(

profile_path = joinpath(lib_path, string("crb8760_norm_" * city * "_" * buildingtype * ".dat"))
input_normalized = false
shift_possible = false
if isempty(normalized_profile)
if occursin("FlatLoad", buildingtype)
normalized_profile = custom_normalized_flatload(buildingtype, year)
else
normalized_profile = vec(readdlm(profile_path, '\n', Float64, '\n'))
shift_possible = true
end
else
input_normalized = true
end

# The normalized_profile for CRBs (not FlatLoads, which use the year input) is based on year 2017 which starts on a Sunday.
# If the year is not 2017 and we're using a CRB, we shift the 2017 CRB profile to match the weekday/weekend profile of the input year.
# We remove the CRB start day Sunday, and shift the CRB profile to the left until reaching the start day of the input year (e.g. Friday for 2021), and
# the shifted days (but not Sunday) get wrapped around to the end of the year, and the year's start day gets duplicated at the end of the year to match the year's ending day of the week.
# We then re-normalize the profile because we've removed the previously-normalized year's first day Sunday and duplicated the year's start day profile
if !(year == 2017) && shift_possible
crb_start_day = Dates.dayofweek(DateTime(2017,1,1))
load_start_day = Dates.dayofweek(DateTime(year,1,1))
cut_days = 7 - (crb_start_day - load_start_day) # Ex: = 7-(7-5) = 5 --> cut Sun, Mon, Tues, Wed, Thurs for 2021 load year
wrap_ts = normalized_profile[25:24+24*cut_days] # Ex: = crb_profile[25:144] wrap Mon-Fri to end for 2021
normalized_profile = append!(normalized_profile[24*cut_days+1:end], wrap_ts) # Ex: now starts on Fri and end Fri to align with 2021 cal
normalized_profile = normalized_profile ./ sum(normalized_profile)
end

if length(monthly_energies) == 12
annual_energy = 1.0 # do not scale based on annual_energy
t0 = 1
for month in 1:12
plus_hours = daysinmonth(Date(string(year) * "-" * string(month))) * 24
if month == 2 && isleapyear(year) && !input_normalized # for a leap year with normalized_profile, the last day is assumed to be truncated
plus_hours -= 24
end
if month == 12 && isleapyear(year) && input_normalized
if month == 12 && isleapyear(year) # for a leap year, the last day is assumed to be truncated
plus_hours -= 24
end
month_total = sum(normalized_profile[t0:t0+plus_hours-1])
Expand Down Expand Up @@ -223,10 +236,6 @@ function blend_and_scale_doe_profiles(
)

@assert sum(blended_doe_reference_percents) ≈ 1 "The sum of the blended_doe_reference_percents must equal 1"
if year != 2017
@debug "Changing ElectricLoad.year to 2017 because DOE reference profiles start on a Sunday."
end
year = 2017

if isempty(city)
if heating_load_type === "process_heat"
Expand Down Expand Up @@ -325,7 +334,7 @@ function get_monthly_energy(power_profile::AbstractArray{<:Real,1};
monthly_energy_total = zeros(12)
for month in 1:12
plus_hours = daysinmonth(Date(string(year) * "-" * string(month))) * 24
if month == 2 && isleapyear(year)
if month == 12 && isleapyear(year)
plus_hours -= 24
end
if !isempty(power_profile)
Expand Down
17 changes: 7 additions & 10 deletions src/core/electric_load.jl
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@
`ElectricLoad` is a required REopt input with the following keys and default values:
```julia
loads_kw::Array{<:Real,1} = Real[],
normalize_and_scale_load_profile_input::Bool = false, # Takes loads_kw and normalizes and scales it to annual or monthly energy
normalize_and_scale_load_profile_input::Bool = false, # Takes loads_kw and normalizes and scales it to annual_kwh or monthly_totals_kwh
path_to_csv::String = "", # for csv containing loads_kw
doe_reference_name::String = "",
blended_doe_reference_names::Array{String, 1} = String[],
blended_doe_reference_percents::Array{<:Real,1} = Real[], # Values should be between 0-1 and sum to 1.0
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::Union{Int, Nothing} = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] ? 2017 : nothing, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles defaults to using 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 @@ -92,7 +92,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::Union{Int, Nothing} = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] ? 2017 : nothing, # used in ElectricTariff to align rate schedule with weekdays/weekends. DOE CRB profiles 2017 by default. 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 @@ -128,6 +128,10 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o
end
end

if isnothing(year)
throw(@error("Must provide the year when using loads_kw input."))
end

if length(loads_kw) > 0 && !normalize_and_scale_load_profile_input

if !(length(loads_kw) / time_steps_per_hour ≈ 8760)
Expand All @@ -143,7 +147,6 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o
end
# Using dummy values for all unneeded location and building type arguments for normalizing and scaling load profile input
normalized_profile = loads_kw ./ sum(loads_kw)
# Need year still mainly for
loads_kw = BuiltInElectricLoad("Chicago", "LargeOffice", 41.8333, -88.0616, year, annual_kwh, monthly_totals_kwh, normalized_profile)

elseif !isempty(path_to_csv)
Expand All @@ -158,19 +161,13 @@ mutable struct ElectricLoad # mutable to adjust (critical_)loads_kw based off o
end
Copy link
Collaborator

@adfarth adfarth Jan 9, 2025

Choose a reason for hiding this comment

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

Adding a note here to require the year if a custom load profile is uploaded (here and for the heating loads)

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added this requirement. Validation to check if year is not nothing can happen above the check for loads_kw, doe_reference_name, etc, since the default is assigned for CRB without a year input.

Copy link
Collaborator

Choose a reason for hiding this comment

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

@Bill-Becker I think it might be worth adding this to the CHANGELOG too

Copy link
Collaborator Author

Choose a reason for hiding this comment

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

Added to changelog.


elseif !isempty(doe_reference_name)
# NOTE: must use year that starts on Sunday with DOE reference doe_ref_profiles
if year != 2017
@warn "Changing load profile year to 2017 because DOE reference profiles start on a Sunday."
end
year = 2017
loads_kw = BuiltInElectricLoad(city, doe_reference_name, latitude, longitude, year, annual_kwh, monthly_totals_kwh)

elseif length(blended_doe_reference_names) > 1 &&
length(blended_doe_reference_names) == length(blended_doe_reference_percents)
loads_kw = blend_and_scale_doe_profiles(BuiltInElectricLoad, latitude, longitude, year,
blended_doe_reference_names, blended_doe_reference_percents, city,
annual_kwh, monthly_totals_kwh)
# TODO: Should also warn here about year 2017
else
throw(@error("Cannot construct ElectricLoad. You must provide either [loads_kw], [doe_reference_name, city],
[doe_reference_name, latitude, longitude],
Expand Down
2 changes: 1 addition & 1 deletion src/core/electric_tariff.jl
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ function ElectricTariff(;
urdb_response::Dict=Dict(),
urdb_utility_name::String="",
urdb_rate_name::String="",
year::Int=2022, # Passed from ElectricLoad
year::Union{Int, Nothing}=nothing, # Passed from ElectricLoad
time_steps_per_hour::Int=1,
NEM::Bool=false,
wholesale_rate::T1=nothing,
Expand Down
41 changes: 27 additions & 14 deletions src/core/heating_cooling_loads.jl
Original file line number Diff line number Diff line change
Expand Up @@ -9,11 +9,13 @@
industrial_reference_name::String = "", # For ProcessHeatLoad
blended_industrial_reference_names::Array{String, 1} = String[], # For ProcessHeatLoad
blended_industrial_reference_percents::Array{<:Real,1} = Real[], # For ProcessHeatLoad
addressable_load_fraction::Any = 1.0, # Fraction of input fuel load which is addressable by heating technologies. Can be a scalar or vector with length aligned with use of monthly_mmbtu or fuel_loads_mmbtu_per_hour.
city::String = "",
year::Union{Int, Nothing} = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] || industrial_reference_name ≠ "" || blended_industrial_reference_names ≠ String[] ? 2017 : nothing, # CRB profiles are 2017 by default. If providing load profile, specify year of data.
annual_mmbtu::Union{Real, Nothing} = nothing,
monthly_mmbtu::Array{<:Real,1} = Real[],
addressable_load_fraction::Any = 1.0, # Fraction of input fuel load which is addressable by heating technologies. Can be a scalar or vector with length aligned with use of monthly_mmbtu or fuel_loads_mmbtu_per_hour.
fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[], # Vector of space heating fuel loads [mmbtu/hr]. Length must equal 8760 * `Settings.time_steps_per_hour`
normalize_and_scale_load_profile_input::Bool = false, # Takes fuel_loads_mmbtu_per_hour and normalizes and scales it to annual or monthly energy
normalize_and_scale_load_profile_input::Bool = false, # Takes fuel_loads_mmbtu_per_hour and normalizes and scales it to annual_mmbtu or monthly_mmbtu
existing_boiler_efficiency::Real = NaN
```

Expand Down Expand Up @@ -49,7 +51,7 @@ function HeatingLoad(;
blended_industrial_reference_names::Array{String, 1} = String[],
blended_industrial_reference_percents::Array{<:Real,1} = Real[],
city::String = "",
year::Int = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] ? 2017 : 2022, # CRB profiles must use 2017. If providing load profile, specify year of data.
year::Union{Int, Nothing} = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] || industrial_reference_name ≠ "" || blended_industrial_reference_names ≠ String[] ? 2017 : nothing, # CRB profiles are 2017 by default. If providing load profile, specify year of data.
annual_mmbtu::Union{Real, Nothing} = nothing,
monthly_mmbtu::Array{<:Real,1} = Real[],
addressable_load_fraction::Any = 1.0,
Expand Down Expand Up @@ -79,6 +81,10 @@ function HeatingLoad(;
throw(@error("load_type must be 'space_heating', 'domestic_hot_water', or 'process_heat'"))
end

if isnothing(year)
throw(@error("Must provide the year when using fuel_loads_mmbtu_per_hour input."))
end

if length(addressable_load_fraction) > 1
if !isempty(fuel_loads_mmbtu_per_hour) && length(addressable_load_fraction) != length(fuel_loads_mmbtu_per_hour)
throw(@error("`addressable_load_fraction` must be a scalar or an array of length `fuel_loads_mmbtu_per_hour`"))
Expand Down Expand Up @@ -118,14 +124,14 @@ function HeatingLoad(;
loads_kw = BuiltInHeatingLoad(load_type, "Chicago", "FlatLoad", 41.8333, -88.0616, year, addressable_load_fraction, annual_mmbtu, monthly_mmbtu, existing_boiler_efficiency, normalized_profile)
unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency)
elseif !isempty(doe_reference_name)
loads_kw = BuiltInHeatingLoad(load_type, city, doe_reference_name, latitude, longitude, 2017, addressable_load_fraction, annual_mmbtu, monthly_mmbtu, existing_boiler_efficiency)
loads_kw = BuiltInHeatingLoad(load_type, city, doe_reference_name, latitude, longitude, year, addressable_load_fraction, annual_mmbtu, monthly_mmbtu, existing_boiler_efficiency)
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(BuiltInHeatingLoad, latitude, longitude, 2017,
loads_kw = blend_and_scale_doe_profiles(BuiltInHeatingLoad, latitude, longitude, year,
blended_doe_reference_names, blended_doe_reference_percents, city,
annual_mmbtu, monthly_mmbtu, addressable_load_fraction,
existing_boiler_efficiency, load_type)
Expand Down Expand Up @@ -249,9 +255,10 @@ end
`CoolingLoad` is an optional REopt input with the following keys and default values:
```julia
doe_reference_name::String = "",
city::String = "",
blended_doe_reference_names::Array{String, 1} = String[],
blended_doe_reference_percents::Array{<:Real,1} = Real[],
city::String = "",
year::Int = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] ? 2017 : nothing, # CRB profiles are 2017 by default. If providing load profile, specify year of data.
annual_tonhour::Union{Real, Nothing} = nothing,
monthly_tonhour::Array{<:Real,1} = Real[],
thermal_loads_ton::Array{<:Real,1} = Real[], # Vector of cooling thermal loads [ton] = [short ton hours/hour]. Length must equal 8760 * `Settings.time_steps_per_hour`
Expand Down Expand Up @@ -289,9 +296,10 @@ struct CoolingLoad

function CoolingLoad(;
doe_reference_name::String = "",
city::String = "",
blended_doe_reference_names::Array{String, 1} = String[],
blended_doe_reference_percents::Array{<:Real,1} = Real[],
city::String = "",
year::Union{Int, Nothing} = doe_reference_name ≠ "" || blended_doe_reference_names ≠ String[] ? 2017 : nothing, # CRB profiles are 2017 by default. If providing load profile, specify year of data.
annual_tonhour::Union{Real, Nothing} = nothing,
monthly_tonhour::Array{<:Real,1} = Real[],
thermal_loads_ton::Array{<:Real,1} = Real[], # Vector of cooling thermal loads [ton] = [short ton hours/hour]. Length must equal 8760 * `Settings.time_steps_per_hour`
Expand All @@ -305,6 +313,11 @@ struct CoolingLoad
existing_chiller_cop::Union{Real, Nothing} = nothing, # Passed from ExistingChiller or set to a default
existing_chiller_max_thermal_factor_on_peak_load::Union{Real, Nothing}= nothing # Passed from ExistingChiller or set to a default
)

if isnothing(year)
throw(@error("Must provide the year when using inputs of thermal_loads_ton or per_time_step_fractions_of_electric_load."))
end

# determine the timeseries of loads_kw_thermal
loads_kw_thermal = nothing
loads_kw = nothing
Expand All @@ -324,8 +337,8 @@ struct CoolingLoad
if !(length(monthly_fractions_of_electric_load) ≈ 12)
throw(@error("Provided cooling monthly_fractions_of_electric_load array does not have 12 values."))
end
timeseries = collect(DateTime(2017,1,1) : Minute(60/time_steps_per_hour) :
DateTime(2017,1,1) + Minute(8760*60 - 60/time_steps_per_hour))
timeseries = collect(DateTime(year,1,1) : Minute(60/time_steps_per_hour) :
DateTime(year,1,1) + Minute(8760*60 - 60/time_steps_per_hour))
loads_kw = [monthly_fractions_of_electric_load[month(dt)] * site_electric_load_profile[ts] for (ts, dt)
in enumerate(timeseries)]

Expand All @@ -335,9 +348,9 @@ struct CoolingLoad
elseif !isempty(doe_reference_name)
if isnothing(annual_tonhour) && isempty(monthly_tonhour)
loads_kw = get_default_fraction_of_total_electric(city, doe_reference_name,
latitude, longitude, 2017) .* site_electric_load_profile
latitude, longitude, year) .* site_electric_load_profile
else
loads_kw = BuiltInCoolingLoad(city, doe_reference_name, latitude, longitude, 2017,
loads_kw = BuiltInCoolingLoad(city, doe_reference_name, latitude, longitude, year,
annual_tonhour, monthly_tonhour, existing_chiller_cop)
end

Expand All @@ -346,16 +359,16 @@ struct CoolingLoad
if isnothing(annual_tonhour) && isempty(monthly_tonhour)
loads_kw = zeros(Int(8760/time_steps_per_hour))
for (i, building) in enumerate(blended_doe_reference_names)
default_fraction = get_default_fraction_of_total_electric(city, building, latitude, longitude, 2017)
default_fraction = get_default_fraction_of_total_electric(city, building, latitude, longitude, year)
modified_fraction = default_fraction * blended_doe_reference_percents[i]
if length(site_electric_load_profile) > 8784
modified_fraction = repeat(modified_fraction, inner=time_steps_per_hour / (length(site_electric_load_profile)/8760))
@warn "Repeating cooling electric load in each hour to match the time_steps_per_hour."
end
loads_kw += site_electric_load_profile .* modified_fraction
end
else
loads_kw = blend_and_scale_doe_profiles(BuiltInCoolingLoad, latitude, longitude, 2017,
else
loads_kw = blend_and_scale_doe_profiles(BuiltInCoolingLoad, latitude, longitude, year,
blended_doe_reference_names,
blended_doe_reference_percents, city,
annual_tonhour, monthly_tonhour)
Expand Down
Loading
Loading