diff --git a/CHANGELOG.md b/CHANGELOG.md index babfb2cf4..6831b0994 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -81,6 +81,7 @@ Classify the change according to the following categories: ## v0.48.2 ### Added +- Add new optional parameter **max_ton** to GHP module to allow user to size GHP smaller than peak load - Battery residual value if choosing replacement strategy for degradation - Add new **ElectricStorage** parameters **max_duration_hours** and **min_duration_hours** to bound the energy duration of battery storage ### Changed diff --git a/src/core/ghp.jl b/src/core/ghp.jl index 949a6a1f7..a50da84be 100644 --- a/src/core/ghp.jl +++ b/src/core/ghp.jl @@ -19,11 +19,14 @@ struct with outer constructor: installed_cost_ghx_per_ft::Float64 = 14.0 installed_cost_building_hydronic_loop_per_sqft = 1.70 om_cost_per_sqft_year::Float64 = -0.51 - building_sqft::Float64 # Required input + building_sqft::Float64 # Required input space_heating_efficiency_thermal_factor::Float64 = NaN # Default depends on building and location - cooling_efficiency_thermal_factor::Float64 = NaN # Default depends on building and location + cooling_efficiency_thermal_factor::Float64 = NaN # Default depends on building and location ghpghx_response::Dict = Dict() can_serve_dhw::Bool = false + max_ton::Real # Maximum heat pump capacity size. Default at a big number + max_number_of_boreholes::Real # Maximum GHX size + load_served_by_ghp::String # "scaled" or "nonpeak" macrs_option_years::Int = 5 macrs_bonus_fraction::Float64 = 0.6 @@ -80,6 +83,9 @@ Base.@kwdef mutable struct GHP <: AbstractGHP can_serve_space_heating::Bool = true can_serve_process_heat::Bool = false can_supply_steam_turbine::Bool = false + max_ton::Real = BIG_NUMBER + max_number_of_boreholes::Real = BIG_NUMBER + load_served_by_ghp::String = "nonpeak" aux_heater_type::String = "electric" is_ghx_hybrid::Bool = false @@ -157,7 +163,7 @@ function GHP(response::Dict, d::Dict) end # incentives = IncentivesNoProdBased(**d_mod) - setup_installed_cost_curve!(ghp, response) + setup_installed_cost_curve!(d, ghp, response) setup_om_cost!(ghp) @@ -171,10 +177,10 @@ function GHP(response::Dict, d::Dict) end """ - setup_installed_cost_curve!(response::Dict, ghp::GHP) + setup_installed_cost_curve!(d::Dict, response::Dict, ghp::GHP) """ -function setup_installed_cost_curve!(ghp::GHP, response::Dict) +function setup_installed_cost_curve!(d::Dict, ghp::GHP, response::Dict) big_number = 1.0e10 # GHX and GHP sizing metrics for cost calculations total_ghx_ft = response["outputs"]["number_of_boreholes"] * response["outputs"]["length_boreholes_ft"] diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 5db0c584d..4c1e8728a 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -475,7 +475,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) space_heating_thermal_load_reduction_with_ghp_kw = zeros(8760 * settings.time_steps_per_hour) cooling_thermal_load_reduction_with_ghp_kw = zeros(8760 * settings.time_steps_per_hour) eval_ghp = false - get_ghpghx_from_input = false + get_ghpghx_from_input = false if haskey(d, "GHP") && haskey(d["GHP"],"building_sqft") eval_ghp = true if haskey(d["GHP"], "ghpghx_responses") && !isempty(d["GHP"]["ghpghx_responses"]) @@ -619,9 +619,77 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # Call GhpGhx.jl to size GHP and GHX @info "Starting GhpGhx.jl" # Call GhpGhx.jl to size GHP and GHX + # If user provides udersized GHP, calculate load to send to GhpGhx.jl, and load to send to REopt for backup + heating_load_ton = ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"]*1000000/12000 + thermal_load_ton = heating_load_ton + if get(ghpghx_inputs, "cooling_thermal_load_ton", []) in [nothing, []] + cooling_load_ton = ghpghx_inputs["cooling_thermal_load_ton"] + thermal_load_ton = heating_load_ton + cooling_load_ton + end + peak_thermal_load = maximum(thermal_load_ton) + if haskey(d["GHP"],"max_ton") && peak_thermal_load > d["GHP"]["max_ton"] + @info "User entered undersized GHP. Calculating load that can be served by user specified undersized GHP" + # When user specifies undersized GHP, calculate the load to be served by GHP and send the rest to REopt + # If user choose to scale down total load (load_served_by_ghp="scaled"), calculate the ratio of the udersized GHP size and peak load + if d["GHP"]["load_served_by_ghp"] == "scaled" + @info "GHP served scaled down of total thermal load" + peak_ratio = d["GHP"]["max_ton"]/peak_thermal_load + # Scale down the total load profile by the peak ratio and use this scaled down load to rerun GhpGhx.jl + ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"]*peak_ratio + if get(ghpghx_inputs, "cooling_thermal_load_ton", []) in [nothing, []] + ghpghx_inputs["cooling_thermal_load_ton"] = cooling_load_ton*peak_ratio + end + elseif d["GHP"]["load_served_by_ghp"] == "nonpeak" + @info "GHP serves all thermal load below thermal peak load" + heating_load_mmbtu = ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] + #ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"][ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] .>= d["GHP"]["max_ton"]*12000/1000000] .= d["GHP"]["max_ton"]*12000/1000000 + heating_load_mmbtu[heating_load_mmbtu .>=d["GHP"]["max_ton"]*12000/1000000] .= d["GHP"]["max_ton"]*12000/1000000 + ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = heating_load_mmbtu + if get(ghpghx_inputs, "cooling_thermal_load_ton", []) in [nothing, []] + cooling_load_ton = ghpghx_inputs["cooling_thermal_load_ton"] + cooling_load_ton[cooling_load_ton .>=d["GHP"]["max_ton"]] .= d["GHP"]["max_ton"] + ghpghx_inputs["cooling_thermal_load_ton"] = cooling_load_ton + end + + end + end results, inputs_params = GhpGhx.ghp_model(ghpghx_inputs) + # If max_number_of_boreholes is specified, check if number of boreholes sized by GhpGhx.jl greater than user-specified max_number_of_boreholes, + # and if max_number_of_boreholes is less, reduce thermal load served by GHP until max_number_of_boreholes = number of boreholses sized by GhpGhx.jl + if haskey(d["GHP"],"max_number_of_boreholes") + determine_number_of_boreholes = GhpGhx.get_results_for_reopt(results, inputs_params) + optimal_number_of_boreholes = determine_number_of_boreholes["number_of_boreholes"] + if optimal_number_of_boreholes > d["GHP"]["max_number_of_boreholes"] + @info "Max number of boreholes specified less than number of boreholes sized in GhpGhx.jl, reducing thermal load served by GHP further" + max_iter = 10 + for iter = 1:max_iter + borehole_ratio = optimal_number_of_boreholes/d["GHP"]["max_number_of_boreholes"] + new_load_peak = maximum(heating_load_mmbtu)*borehole_ratio + heating_load_mmbtu[heating_load_mmbtu .>=new_load_peak] .= new_load_peak + ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = heating_load_mmbtu + if get(ghpghx_inputs, "cooling_thermal_load_ton", []) in [nothing, []] + cooling_load_ton[cooling_load_ton .>=new_load_peak] .= new_load_peak + ghpghx_inputs["cooling_thermal_load_ton"] = cooling_load_ton + end + # Rerun GhpGhx.jl + results, inputs_params = GhpGhx.ghp_model(ghpghx_inputs) + determine_number_of_boreholes = GhpGhx.get_results_for_reopt(results, inputs_params) + optimal_number_of_boreholes = determine_number_of_boreholes["number_of_boreholes"] + # Solution is found if the new optimal number of boreholes sized by GhpGhx.jl = user-specified max number of boreholes, + # Otherwise, continue solving until reaching max iteration + if -0.5 < optimal_number_of_boreholes-d["GHP"]["max_number_of_boreholes"] < 0.5 + break + end + iter += 1 + end + + end + end + + # Create a dictionary of the results data needed for REopt ghpghx_results = GhpGhx.get_results_for_reopt(results, inputs_params) + # Return results from GhpGhx.jl without load scaling if user does not provide GHP size or if user entered GHP size is greater than GHP size output @info "GhpGhx.jl model solved" #with status $(results["status"])." catch e @info e @@ -639,9 +707,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end append!(ghp_option_list, [GHP(ghpghx_response, ghp_inputs_removed_ghpghx_params)]) # Print out ghpghx_response for loading into a future run without running GhpGhx.jl again - #open("scenarios/ghpghx_response.json","w") do f - # JSON.print(f, ghpghx_response) - #end + # open("scenarios/ghpghx_response.json","w") do f end # If ghpghx_responses is included in inputs, do NOT run GhpGhx.jl model and use already-run ghpghx result as input to REopt elseif eval_ghp && get_ghpghx_from_input diff --git a/src/results/ghp.jl b/src/results/ghp.jl index a9a8a01a7..5e89b95dd 100644 --- a/src/results/ghp.jl +++ b/src/results/ghp.jl @@ -24,6 +24,11 @@ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") # r["size_heat_pump_ton"] = 0.0 # r["size_wwhp_heating_pump_ton"] = 0.0 # r["size_wwhp_cooling_pump_ton"] = 0.0 + + # Set sizing factor = 1 if user inputs their own GHP size + if haskey(d, "GHP") && haskey(d["GHP"],"max_ton") + p.s.ghp_option_list[ghp_option_chosen].heatpump_capacity_sizing_factor_on_peak_load = 1.0 + end if ghp_option_chosen >= 1 r["ghpghx_chosen_outputs"] = p.s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]