diff --git a/CHANGELOG.md b/CHANGELOG.md index 1a386e8bd..c46b3fd56 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -34,18 +34,27 @@ Classify the change according to the following categories: ### Fixed - Adjust grid emissions profiles for day of week alignment with load_year. - In `test_with_xpress.jl`, updated "Emissions and Renewable Energy Percent" expected values to account for load year adjustment. -## Develop + +## Develop 2023-08-09 ### Changed -- Changed unit test expected values due to update to PVWatts v8, which slightly changed expected PV production factors. +- Updated `get_existing_chiller_cop` function to accept scalar values instead of vectors to allow for faster API transactions. +### Fixed +- Steamturbine defaults processing +- simulated_load monthly values processing + ## 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 +- 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_ +- Corrected `SteamTurbine` defaults population from `get_steam_turbine_defaults_size_class()` ## v0.32.3 ### Fixed diff --git a/src/REopt.jl b/src/REopt.jl index d1d1bfc7b..1fea8faa0 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -52,6 +52,7 @@ export avert_emissions_profiles, cambium_emissions_profile, easiur_data + get_existing_chiller_default_cop import HTTP import JSON diff --git a/src/core/generator.jl b/src/core/generator.jl index 31a9b7969..228febf06 100644 --- a/src/core/generator.jl +++ b/src/core/generator.jl @@ -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, @@ -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, diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index bb50a944a..aabea0b52 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -209,23 +209,23 @@ end """ function get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_on_peak_load=nothing, - loads_kw=nothing, - loads_kw_thermal=nothing) + max_load_kw=nothing, + max_load_kw_thermal=nothing) This function returns the default value for ExistingChiller.cop based on: 1. No information about load, returns average of lower and higher cop default values (`cop_unknown_thermal`) - 2. If the cooling electric `loads_kw` is known, we first guess the thermal load profile using `cop_unknown_thermal`, + 2. If the cooling electric `max_load_kw` is known, we first guess the thermal load profile using `cop_unknown_thermal`, and then we use the default logic to determine the `existing_chiller_cop` based on the peak thermal load with a thermal factor multiplier. - 3. If the cooling thermal `loads_kw_thermal` is known, same as 2. but we don't have to guess the cop to convert electric to thermal load first. + 3. If the cooling thermal `max_load_kw_thermal` is known, same as 2. but we don't have to guess the cop to convert electric to thermal load first. """ -function get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_on_peak_load=nothing, loads_kw=nothing, loads_kw_thermal=nothing) +function get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_on_peak_load=nothing, max_load_kw=nothing, max_load_kw_thermal=nothing) cop_less_than_100_ton = 4.40 cop_more_than_100_ton = 4.69 cop_unknown_thermal = (cop_less_than_100_ton + cop_more_than_100_ton) / 2.0 max_cooling_load_ton = nothing - if !isnothing(loads_kw_thermal) - max_cooling_load_ton = maximum(loads_kw_thermal) / KWH_THERMAL_PER_TONHOUR - elseif !isnothing(loads_kw) - max_cooling_load_ton = maximum(loads_kw) / KWH_THERMAL_PER_TONHOUR * cop_unknown_thermal + if !isnothing(max_load_kw_thermal) + max_cooling_load_ton = max_load_kw_thermal / KWH_THERMAL_PER_TONHOUR + elseif !isnothing(max_load_kw) + max_cooling_load_ton = max_load_kw / KWH_THERMAL_PER_TONHOUR * cop_unknown_thermal end if isnothing(max_cooling_load_ton) || isnothing(existing_chiller_max_thermal_factor_on_peak_load) return cop_unknown_thermal @@ -366,7 +366,7 @@ struct CoolingLoad elseif (!isempty(doe_reference_name) || !isempty(blended_doe_reference_names)) || isnothing(existing_chiller_cop) # Generated loads_kw (electric) above based on the building's default fraction of electric profile chiller_cop = get_existing_chiller_default_cop(;existing_chiller_max_thermal_factor_on_peak_load=existing_chiller_max_thermal_factor_on_peak_load, - loads_kw=loads_kw) + max_load_kw=maximum(loads_kw)) else chiller_cop = existing_chiller_cop end @@ -376,7 +376,7 @@ struct CoolingLoad # Now that cooling thermal loads_kw_thermal is known, update existing_chiller_cop if it was not input if isnothing(existing_chiller_cop) existing_chiller_cop = get_existing_chiller_default_cop(;existing_chiller_max_thermal_factor_on_peak_load=existing_chiller_max_thermal_factor_on_peak_load, - loads_kw_thermal=loads_kw_thermal) + max_load_kw_thermal=maximum(loads_kw_thermal)) end if length(loads_kw_thermal) < 8760*time_steps_per_hour diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 41dd4f115..21ee490fb 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -303,14 +303,14 @@ function Scenario(d::Dict; flex_hvac_from_json=false) if haskey(d, "ExistingChiller") if !haskey(d["ExistingChiller"], "cop") d["ExistingChiller"]["cop"] = get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_on_peak_load=1.25, - loads_kw=nothing, - loads_kw_thermal=chiller_inputs[:loads_kw_thermal]) + max_load_kw=nothing, + max_load_kw_thermal=maximum(chiller_inputs[:loads_kw_thermal])) end chiller_inputs = merge(chiller_inputs, dictkeys_tosymbols(d["ExistingChiller"])) else chiller_inputs[:cop] = get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_on_peak_load=1.25, - loads_kw=nothing, - loads_kw_thermal=chiller_inputs[:loads_kw_thermal]) + max_load_kw=nothing, + max_load_kw_thermal=maximum(chiller_inputs[:loads_kw_thermal])) end existing_chiller = ExistingChiller(; chiller_inputs...) end diff --git a/src/core/simulated_load.jl b/src/core/simulated_load.jl index 5b85a49d0..c8879e6be 100644 --- a/src/core/simulated_load.jl +++ b/src/core/simulated_load.jl @@ -117,9 +117,9 @@ function simulated_load(d::Dict) # Monthly loads (default is empty list) monthly_totals_kwh = get(d, "monthly_totals_kwh", Real[]) if !isempty(monthly_totals_kwh) - if length(monthly_totals_kwh != 12) + if length(monthly_totals_kwh) != 12 throw(@error("monthly_totals_kwh must contain a value for each of the 12 months")) - end + end bad_index = [] for (i, kwh) in enumerate(monthly_totals_kwh) if isnothing(kwh) @@ -398,7 +398,7 @@ function simulated_load(d::Dict) # Monthly loads (default is empty list) monthly_tonhour = get(d, "monthly_tonhour", Real[]) if !isempty(monthly_tonhour) - if length(monthly_tonhour != 12) + if length(monthly_tonhour) != 12 throw(@error("monthly_tonhour must contain a value for each of the 12 months")) end bad_index = [] diff --git a/src/core/steam_turbine.jl b/src/core/steam_turbine.jl index e8c488c37..7359afe1c 100644 --- a/src/core/steam_turbine.jl +++ b/src/core/steam_turbine.jl @@ -104,8 +104,10 @@ function SteamTurbine(d::Dict; avg_boiler_fuel_load_mmbtu_per_hour::Union{Float6 ) # set all missing default values in custom_chp_inputs - defaults = get_steam_turbine_defaults_size_class(;avg_boiler_fuel_load_mmbtu_per_hour=avg_boiler_fuel_load_mmbtu_per_hour, + stm_defaults_response = get_steam_turbine_defaults_size_class(;avg_boiler_fuel_load_mmbtu_per_hour=avg_boiler_fuel_load_mmbtu_per_hour, size_class=st.size_class) + + defaults = stm_defaults_response["default_inputs"] for (k, v) in custom_st_inputs if isnan(v) if !(k == :inlet_steam_temperature_degF && !isnan(st.inlet_steam_superheat_degF)) diff --git a/src/core/urdb.jl b/src/core/urdb.jl index 9f3bae6cd..4c7c2a724 100644 --- a/src/core/urdb.jl +++ b/src/core/urdb.jl @@ -293,14 +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 - total_rate = rate - else - rate = get(d["energyratestructure"][period][tier_use], "rate", 0) - total_rate = rate + get(d["energyratestructure"][period][tier_use], "adj", 0) - end - + 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 diff --git a/src/mpc/structs.jl b/src/mpc/structs.jl index c2a66e087..86450478e 100644 --- a/src/mpc/structs.jl +++ b/src/mpc/structs.jl @@ -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, @@ -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, diff --git a/test/scenarios/backup_reliability_reopt_inputs.json b/test/scenarios/backup_reliability_reopt_inputs.json index cf68d63a4..8b0f72f2c 100644 --- a/test/scenarios/backup_reliability_reopt_inputs.json +++ b/test/scenarios/backup_reliability_reopt_inputs.json @@ -21,6 +21,8 @@ "federal_rebate_per_kw": 350.0 }, "Generator": { + "installed_cost_per_kw": 500.0, + "fuel_avail_gal": 660, "min_kw": 200, "max_kw": 200 }, diff --git a/test/scenarios/emissions.json b/test/scenarios/emissions.json index e3f96e89a..da917a521 100644 --- a/test/scenarios/emissions.json +++ b/test/scenarios/emissions.json @@ -42,6 +42,7 @@ "co2_from_avert": true }, "Generator": { + "installed_cost_per_kw": 500.0, "max_kw": 500.0, "fuel_avail_gal": 125.0, "min_turn_down_fraction": 0.0, diff --git a/test/scenarios/generator.json b/test/scenarios/generator.json index 5cce5d65e..a45b61b04 100644 --- a/test/scenarios/generator.json +++ b/test/scenarios/generator.json @@ -11,6 +11,7 @@ "co2_from_avert": true }, "Generator": { + "installed_cost_per_kw": 500.0, "max_kw": 500.0, "fuel_avail_gal": 125.0, "min_turn_down_fraction": 0.0, diff --git a/test/scenarios/mpc.json b/test/scenarios/mpc.json index 7f6893e83..f0aa70041 100644 --- a/test/scenarios/mpc.json +++ b/test/scenarios/mpc.json @@ -104,6 +104,7 @@ 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05, 0.05] }, "Generator": { + "fuel_avail_gal": 660, "size_kw": 30, "only_runs_during_grid_outage": false }, diff --git a/test/scenarios/nogridcost_minresilhours.json b/test/scenarios/nogridcost_minresilhours.json index 2b13d3458..1736c6e7a 100644 --- a/test/scenarios/nogridcost_minresilhours.json +++ b/test/scenarios/nogridcost_minresilhours.json @@ -61,6 +61,8 @@ "critical_load_fraction": 1 }, "Generator": { + "fuel_avail_gal": 660, + "installed_cost_per_kw": 500.0 }, "Financial": { "value_of_lost_load_per_kwh": 0.001, diff --git a/test/scenarios/nogridcost_multiscenario.json b/test/scenarios/nogridcost_multiscenario.json index 8b8ee17b6..d30e6b5ad 100644 --- a/test/scenarios/nogridcost_multiscenario.json +++ b/test/scenarios/nogridcost_multiscenario.json @@ -54,6 +54,8 @@ "critical_load_fraction": 0.1 }, "Generator": { + "fuel_avail_gal": 660, + "installed_cost_per_kw": 500.0, "max_kw": 0.0 }, "Financial": { diff --git a/test/scenarios/outage.json b/test/scenarios/outage.json index 468c1cb8c..6fb79fefe 100644 --- a/test/scenarios/outage.json +++ b/test/scenarios/outage.json @@ -9,6 +9,8 @@ "co2_from_avert": true }, "Generator": { + "fuel_avail_gal": 660, + "installed_cost_per_kw": 500.0, "existing_kw": 0.0, "min_turn_down_fraction": 0.0, "only_runs_during_grid_outage": true, diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 46bd00527..6dd245745 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -799,7 +799,7 @@ end # When the user specifies inputs["ExistingChiller"]["cop"], this changes the **electric** consumption of the chiller to meet that cooling thermal load crb_cop = REopt.get_existing_chiller_default_cop(; existing_chiller_max_thermal_factor_on_peak_load=s.existing_chiller.max_thermal_factor_on_peak_load, - loads_kw_thermal=s.cooling_load.loads_kw_thermal) + max_load_kw_thermal=maximum(s.cooling_load.loads_kw_thermal)) cooling_thermal_load_tonhour_total = 1427329.0 * crb_cop / REopt.KWH_THERMAL_PER_TONHOUR # From CRB models, in heating_cooling_loads.jl, BuiltInCoolingLoad data for location (SanFrancisco Hospital) cooling_electric_load_total_mod_cop_kwh = cooling_thermal_load_tonhour_total / inputs.s.existing_chiller.cop * REopt.KWH_THERMAL_PER_TONHOUR