Skip to content

Commit

Permalink
Merge pull request #241 from NREL/develop
Browse files Browse the repository at this point in the history
v0.32.4
  • Loading branch information
adfarth authored Jul 28, 2023
2 parents 102d580 + 83c0aec commit c680765
Show file tree
Hide file tree
Showing 20 changed files with 172 additions and 185 deletions.
13 changes: 13 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,19 @@ Classify the change according to the following categories:
### Deprecated
### Removed

## v0.32.4
### Changed
- Consolidated PVWatts API calls to 1 call (previously 3 separate calls existed). API call occurs in `src/core/utils.jl/call_pvwatts_api()`. This function is called for PV in `src/core/production_factor.jl/get_production_factor(PV)` and for GHP in `src/core/scenario.jl`. If GHP and PV are evaluated together, the GHP PVWatts call for ambient temperature is also used to assign the pv.production_factor_series in Scenario.jl so that the PVWatts API does not get called again downstream in `get_production_factor(PV)`.
- In `src/core/utils.jl/call_pvwatts_api()`, updated NSRDB bounds used in PVWatts query (now includes southern New Zealand)
- Updated PV Watts version from v6 to v8. PVWatts V8 updates the weather data to 2020 TMY data from the NREL NSRDB for locations covered by the database. (The NSRDB weather data used in PVWatts V6 is from around 2015.) See other differences at https://developer.nrel.gov/docs/solar/pvwatts/.
- Made PV struct mutable: This allows for assigning pv.production_factor_series when calling PVWatts for GHP, to avoid a extra PVWatts calls later.
- Changed unit test expected values due to update to PVWatts v8, which slightly changed expected PV production factors.
- Changed **fuel_avail_gal** default to 1e9 for on-grid scenarios (same as off-grid)
### Fixed
- Issue with using a leap year with a URDB rate - the URDB rate was creating energy_rate of length 8784 instead of intended 8760
- Don't double add adjustments to urdb rates with non-standard units
- Corrected `Generator` **installed_cost_per_kw** from 500 to 650 if **only_runs_during_grid_outage** is _true_ or 800 if _false_

## v0.32.3
### Fixed
- Calculate **num_battery_bins** default in `backup_reliability.jl` based on battery duration to prevent significant discretization error (and add test)
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.32.3"
version = "0.32.4"

[deps]
ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"
Expand Down
12 changes: 6 additions & 6 deletions src/core/generator.jl
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,19 @@
"""
`Generator` is an optional REopt input with the following keys and default values:
```julia
only_runs_during_grid_outage::Bool = true,
existing_kw::Real = 0,
min_kw::Real = 0,
max_kw::Real = 1.0e6,
installed_cost_per_kw::Real = 500.0,
installed_cost_per_kw::Real = only_runs_during_grid_outage ? 650.0 : 800.0,
om_cost_per_kw::Real = off_grid_flag ? 20.0 : 10.0,
om_cost_per_kwh::Real = 0.0,
fuel_cost_per_gallon::Real = 3.0,
electric_efficiency_full_load::Real = 0.3233,
electric_efficiency_half_load::Real = electric_efficiency_full_load,
fuel_avail_gal::Real = off_grid_flag ? 1.0e9 : 660.0,
fuel_avail_gal::Real = 1.0e9,
fuel_higher_heating_value_kwh_per_gal::Real = 40.7,
min_turn_down_fraction::Real = off_grid_flag ? 0.15 : 0.0,
only_runs_during_grid_outage::Bool = true,
sells_energy_back_to_grid::Bool = false,
can_net_meter::Bool = false,
can_wholesale::Bool = false,
Expand Down Expand Up @@ -125,19 +125,19 @@ struct Generator <: AbstractGenerator
function Generator(;
off_grid_flag::Bool = false,
analysis_years::Int = 25,
only_runs_during_grid_outage::Bool = true,
existing_kw::Real = 0,
min_kw::Real = 0,
max_kw::Real = 1.0e6,
installed_cost_per_kw::Real = 500.0,
installed_cost_per_kw::Real = only_runs_during_grid_outage ? 650.0 : 800.0,
om_cost_per_kw::Real= off_grid_flag ? 20.0 : 10.0,
om_cost_per_kwh::Real = 0.0,
fuel_cost_per_gallon::Real = 3.0,
electric_efficiency_full_load::Real = 0.3233,
electric_efficiency_half_load::Real = electric_efficiency_full_load,
fuel_avail_gal::Real = off_grid_flag ? 1.0e9 : 660.0,
fuel_avail_gal::Real = 1.0e9,
fuel_higher_heating_value_kwh_per_gal::Real = KWH_PER_GAL_DIESEL,
min_turn_down_fraction::Real = off_grid_flag ? 0.15 : 0.0,
only_runs_during_grid_outage::Bool = true,
sells_energy_back_to_grid::Bool = false,
can_net_meter::Bool = false,
can_wholesale::Bool = false,
Expand Down
41 changes: 5 additions & 36 deletions src/core/production_factor.jl
Original file line number Diff line number Diff line change
Expand Up @@ -35,44 +35,13 @@ function get_production_factor(pv::PV, latitude::Real, longitude::Real; timefram
return pv.production_factor_series
end

# Check if site is beyond the bounds of the NRSDB dataset. If so, use the international dataset.
dataset = "nsrdb"
if longitude < -179.5 || longitude > -21.0 || latitude < -21.5 || latitude > 60.0
if longitude < 81.5 || longitude > 179.5 || latitude < -43.8 || latitude > 60.0
if longitude < 67.0 || longitude > 81.5 || latitude < -43.8 || latitude > 38.0
dataset = "intl"
end
end
end
watts, ambient_temp_celcius = call_pvwatts_api(latitude, longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type,
array_type=pv.array_type, losses=round(pv.losses*100, digits=3), dc_ac_ratio=pv.dc_ac_ratio,
gcr=pv.gcr, inv_eff=pv.inv_eff*100, timeframe=timeframe, radius=pv.radius,
time_steps_per_hour=time_steps_per_hour)

url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key,
"&lat=", latitude , "&lon=", longitude, "&tilt=", pv.tilt,
"&system_capacity=1", "&azimuth=", pv.azimuth, "&module_type=", pv.module_type,
"&array_type=", pv.array_type, "&losses=", round(pv.losses*100, digits=3), "&dc_ac_ratio=", pv.dc_ac_ratio,
"&gcr=", pv.gcr, "&inv_eff=", pv.inv_eff*100, "&timeframe=", timeframe, "&dataset=", dataset,
"&radius=", pv.radius
)
return watts

try
@info "Querying PVWatts for production_factor with " pv.name
r = HTTP.get(url, keepalive=true, readtimeout=10)
@info "Response received from PVWatts"
response = JSON.parse(String(r.body))
if r.status != 200
throw(@error("Bad response from PVWatts: $(response["errors"])"))
end
@info "PVWatts success."
watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W)
if length(watts) != 8760
throw(@error("PVWatts did not return a valid production factor. Got $watts"))
end
if time_steps_per_hour > 1
watts = repeat(watts, inner=time_steps_per_hour)
end
return watts
catch e
throw(@error("Error occurred when calling PVWatts: $e"))
end
end


Expand Down
14 changes: 7 additions & 7 deletions src/core/pv.jl
Original file line number Diff line number Diff line change
Expand Up @@ -33,12 +33,12 @@
array_type::Int=1, # PV Watts array type (0: Ground Mount Fixed (Open Rack); 1: Rooftop, Fixed; 2: Ground Mount 1-Axis Tracking; 3 : 1-Axis Backtracking; 4: Ground Mount, 2-Axis Tracking)
tilt::Real= array_type == 1 ? 10 : abs(latitude), # tilt = 10 deg for rooftop systems, abs(lat) for ground-mount
module_type::Int=0, # PV module type (0: Standard; 1: Premium; 2: Thin Film)
losses::Real=0.14,
losses::Real=0.14, # System losses
azimuth::Real = latitude≥0 ? 180 : 0, # set azimuth to zero for southern hemisphere
gcr::Real=0.4,
radius::Int=0,
name::String="PV",
location::String="both",
gcr::Real=0.4, # Ground coverage ratio
radius::Int=0, # Radius, in miles, to use when searching for the closest climate data station. Use zero to use the closest station regardless of the distance
name::String="PV", # for use with multiple pvs
location::String="both", # one of ["roof", "ground", "both"]
existing_kw::Real=0,
min_kw::Real=0,
max_kw::Real=1.0e9,
Expand Down Expand Up @@ -82,7 +82,7 @@
If `azimuth` is not provided, then it is set to 180 if the site is in the northern hemisphere and 0 if in the southern hemisphere.
"""
struct PV <: AbstractTech
mutable struct PV <: AbstractTech
tilt
array_type
module_type
Expand Down Expand Up @@ -151,7 +151,7 @@ struct PV <: AbstractTech
acres_per_kw::Real=6e-3,
inv_eff::Real=0.96,
dc_ac_ratio::Real=1.2,
production_factor_series::Union{Nothing, Array{Real,1}} = nothing,
production_factor_series::Union{Nothing, Array{<:Real,1}} = nothing,
federal_itc_fraction::Real = 0.3,
federal_rebate_per_kw::Real = 0.0,
state_ibi_fraction::Real = 0.0,
Expand Down
36 changes: 13 additions & 23 deletions src/core/scenario.jl
Original file line number Diff line number Diff line change
Expand Up @@ -454,37 +454,27 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
number_of_ghpghx = length(d["GHP"]["ghpghx_inputs"])
end
# Call PVWatts for hourly dry-bulb outdoor air temperature
ambient_temperature_f = []
ambient_temp_degF = []
if !haskey(d["GHP"]["ghpghx_inputs"][1], "ambient_temperature_f") || isempty(d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"])
url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key,
"&lat=", d["Site"]["latitude"] , "&lon=", d["Site"]["longitude"], "&tilt=", d["Site"]["latitude"],
"&system_capacity=1", "&azimuth=", 180, "&module_type=", 0,
"&array_type=", 0, "&losses=", 0.14, "&dc_ac_ratio=", 1.1,
"&gcr=", 0.4, "&inv_eff=", 99, "&timeframe=", "hourly", "&dataset=nsrdb",
"&radius=", 100)
try
@info "Querying PVWatts for ambient temperature"
r = HTTP.get(url)
response = JSON.parse(String(r.body))
if r.status != 200
throw(@error("Bad response from PVWatts: $(response["errors"])"))
# If PV is evaluated and we need to call PVWatts for ambient temperature, assign PV production factor here too with the same call
# By assigning pv.production_factor_series here, it will skip the PVWatts call in get_production_factor(PV) call from reopt_input.jl
if !isempty(pvs)
for pv in pvs
pv.production_factor_series, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type,
array_type=pv.array_type, losses=round(pv.losses*100, digits=3), dc_ac_ratio=pv.dc_ac_ratio,
gcr=pv.gcr, inv_eff=pv.inv_eff*100, timeframe="hourly", radius=pv.radius, time_steps_per_hour=settings.time_steps_per_hour)
end
@info "PVWatts success."
temp_c = get(response["outputs"], "tamb", [])
if length(temp_c) != 8760 || isempty(temp_c)
throw(@error("PVWatts did not return a valid temperature profile. Got $temp_c"))
end
ambient_temperature_f = temp_c * 1.8 .+ 32.0
catch e
throw(@error("Error occurred when calling PVWatts: $e"))
else
pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour)
end
ambient_temp_degF = ambient_temp_celcius * 1.8 .+ 32.0
else
ambient_temperature_f = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"]
ambient_temp_degF = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"]
end

for i in 1:number_of_ghpghx
ghpghx_inputs = d["GHP"]["ghpghx_inputs"][i]
d["GHP"]["ghpghx_inputs"][i]["ambient_temperature_f"] = ambient_temperature_f
d["GHP"]["ghpghx_inputs"][i]["ambient_temperature_f"] = ambient_temp_degF
# Only SpaceHeating portion of Heating Load gets served by GHP, unless allowed by can_serve_dhw
if get(ghpghx_inputs, "heating_thermal_load_mmbtu_per_hr", []) in [nothing, []]
if haskey(d["GHP"], "can_serve_dhw") && d["GHP"]["can_serve_dhw"]
Expand Down
2 changes: 1 addition & 1 deletion src/core/site.jl
Original file line number Diff line number Diff line change
Expand Up @@ -36,7 +36,7 @@ Inputs related to the physical location:
longitude::Real,
land_acres::Union{Real, Nothing} = nothing, # acres of land available for PV panels and/or Wind turbines. Constraint applied separately to PV and Wind, meaning the two technologies are assumed to be able to be co-located.
roof_squarefeet::Union{Real, Nothing} = nothing,
min_resil_time_steps::Int=0,
min_resil_time_steps::Int=0, # The minimum number consecutive timesteps that load must be fully met once an outage begins. Only applies to multiple outage modeling using inputs outage_start_time_steps and outage_durations.
mg_tech_sizes_equal_grid_sizes::Bool = true,
node::Int = 1,
CO2_emissions_reduction_min_fraction::Union{Float64, Nothing} = nothing,
Expand Down
15 changes: 8 additions & 7 deletions src/core/urdb.jl
Original file line number Diff line number Diff line change
Expand Up @@ -99,7 +99,7 @@ function URDBrate(urdb_response::Dict, year::Int; time_steps_per_hour=1)
n_monthly_demand_tiers, monthly_demand_tier_limits, monthly_demand_rates,
n_tou_demand_tiers, tou_demand_tier_limits, tou_demand_rates, tou_demand_ratchet_time_steps =
parse_demand_rates(urdb_response, year, time_steps_per_hour=time_steps_per_hour)

energy_rates, energy_tier_limits, n_energy_tiers, sell_rates =
parse_urdb_energy_costs(urdb_response, year; time_steps_per_hour=time_steps_per_hour)

Expand Down Expand Up @@ -270,6 +270,9 @@ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM

for month in range(1, stop=12)
n_days = daysinmonth(Date(string(year) * "-" * string(month)))
if month == 2 && isleapyear(year)
n_days -= 1
end

for day in range(1, stop=n_days)

Expand All @@ -290,12 +293,10 @@ function parse_urdb_energy_costs(d::Dict, year::Int; time_steps_per_hour=1, bigM
else
tier_use = tier
end
if non_kwh_units
rate = rate_average
else
rate = get(d["energyratestructure"][period][tier_use], "rate", 0)
end
total_rate = rate + get(d["energyratestructure"][period][tier_use], "adj", 0)
total_rate = non_kwh_units ?
rate_average :
(get(d["energyratestructure"][period][tier_use], "rate", 0) +
get(d["energyratestructure"][period][tier_use], "adj", 0))
sell = get(d["energyratestructure"][period][tier_use], "sell", 0)

for step in range(1, stop=time_steps_per_hour) # repeat hourly rates intrahour
Expand Down
73 changes: 38 additions & 35 deletions src/core/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -377,54 +377,57 @@ function generate_year_profile_hourly(year::Int64, consecutive_periods::Abstract
end


function get_ambient_temperature(latitude::Real, longitude::Real; timeframe="hourly")
url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key,
"&lat=", latitude , "&lon=", longitude, "&tilt=", latitude,
"&system_capacity=1", "&azimuth=", 180, "&module_type=", 0,
"&array_type=", 0, "&losses=", 14,
"&timeframe=", timeframe, "&dataset=nsrdb"
)

try
@info "Querying PVWatts for ambient temperature... "
r = HTTP.get(url)
response = JSON.parse(String(r.body))
if r.status != 200
throw(@error("Bad response from PVWatts: $(response["errors"])"))
end
@info "PVWatts success."
tamb = collect(get(response["outputs"], "tamb", [])) # Celcius
if length(tamb) != 8760
throw(@error("PVWatts did not return a valid temperature. Got $tamb"))
"""
call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimuth=180, module_type=0, array_type=1,
losses=14, dc_ac_ratio=1.2, gcr=0.4, inv_eff=96, timeframe="hourly", radius=0, time_steps_per_hour=1)
This calls the PVWatts API and returns both:
- PV production factor
- Ambient outdoor air dry bulb temperature profile [Celcius]
"""
function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimuth=180, module_type=0, array_type=1,
losses=14, dc_ac_ratio=1.2, gcr=0.4, inv_eff=96, timeframe="hourly", radius=0, time_steps_per_hour=1)
# Check if site is beyond the bounds of the NRSDB TMY dataset. If so, use the international dataset.
dataset = "nsrdb"
if longitude < -179.5 || longitude > -21.0 || latitude < -21.5 || latitude > 60.0
if longitude < 81.5 || longitude > 179.5 || latitude < -60.0 || latitude > 60.0
if longitude < 67.0 || latitude < -40.0 || latitude > 38.0
dataset = "intl"
end
end
return tamb
catch e
throw(@error("Error occurred when calling PVWatts: $e"))
end
end


function get_pvwatts_prodfactor(latitude::Real, longitude::Real; timeframe="hourly")
url = string("https://developer.nrel.gov/api/pvwatts/v6.json", "?api_key=", nrel_developer_key,
"&lat=", latitude , "&lon=", longitude, "&tilt=", latitude,
"&system_capacity=1", "&azimuth=", 180, "&module_type=", 0,
"&array_type=", 0, "&losses=", 14,
"&timeframe=", timeframe, "&dataset=nsrdb"
)
url = string("https://developer.nrel.gov/api/pvwatts/v8.json", "?api_key=", nrel_developer_key,
"&lat=", latitude , "&lon=", longitude, "&tilt=", tilt,
"&system_capacity=1", "&azimuth=", azimuth, "&module_type=", module_type,
"&array_type=", array_type, "&losses=", losses, "&dc_ac_ratio=", dc_ac_ratio,
"&gcr=", gcr, "&inv_eff=", inv_eff, "&timeframe=", timeframe, "&dataset=", dataset,
"&radius=", radius
)

try
@info "Querying PVWatts for production factor of 1 kW system with tilt set to latitude... "
r = HTTP.get(url)
@info "Querying PVWatts for production factor and ambient air temperature... "
r = HTTP.get(url, keepalive=true, readtimeout=10)
response = JSON.parse(String(r.body))
if r.status != 200
throw(@error("Bad response from PVWatts: $(response["errors"])"))
end
@info "PVWatts success."
# Get both possible data of interest
watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W)
tamb_celcius = collect(get(response["outputs"], "tamb", [])) # Celcius
# Validate outputs
if length(watts) != 8760
throw(@error("PVWatts did not return a valid prodfactor. Got $watts"))
end
return watts
# Validate tamb_celcius
if length(tamb_celcius) != 8760
throw(@error("PVWatts did not return a valid temperature. Got $tamb_celcius"))
end
# Upsample or downsample based on model time_steps_per_hour
if time_steps_per_hour > 1
watts = repeat(watts, inner=time_steps_per_hour)
tamb_celcius = repeat(tamb_celcius, inner=time_steps_per_hour)
end
return watts, tamb_celcius
catch e
throw(@error("Error occurred when calling PVWatts: $e"))
end
Expand Down
4 changes: 2 additions & 2 deletions src/mpc/structs.jl
Original file line number Diff line number Diff line change
Expand Up @@ -283,7 +283,7 @@ function MPCGenerator(;
fuel_cost_per_gallon::Real = 3.0,
electric_efficiency_full_load::Real = 0.3233,
electric_efficiency_half_load::Real = electric_efficiency_full_load,
fuel_avail_gal::Real = 660.0,
fuel_avail_gal::Real = 1.0e9,
fuel_higher_heating_value_kwh_per_gal::Real = KWH_PER_GAL_DIESEL,
min_turn_down_fraction::Real = 0.0, # TODO change this to non-zero value
only_runs_during_grid_outage::Bool = true,
Expand All @@ -310,7 +310,7 @@ struct MPCGenerator <: AbstractGenerator
fuel_cost_per_gallon::Real = 3.0,
electric_efficiency_full_load::Real = 0.3233,
electric_efficiency_half_load::Real = electric_efficiency_full_load,
fuel_avail_gal::Real = 660.0,
fuel_avail_gal::Real = 1.0e9,
fuel_higher_heating_value_kwh_per_gal::Real = KWH_PER_GAL_DIESEL,
min_turn_down_fraction::Real = 0.0, # TODO change this to non-zero value
only_runs_during_grid_outage::Bool = true,
Expand Down
Loading

0 comments on commit c680765

Please sign in to comment.