Skip to content

Commit d8b75a0

Browse files
author
An Pham
authored
Merge pull request #466 from NREL/allow_presized_GHP_GHX
Allow presized ghp/ghx
2 parents 347d964 + 7b594b4 commit d8b75a0

8 files changed

Lines changed: 161 additions & 19 deletions

File tree

.github/workflows/CI.yml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,5 +22,5 @@ jobs:
2222
version: ${{ matrix.julia-version }}
2323
- uses: julia-actions/julia-buildpkg@latest
2424
# - uses: mxschmitt/action-tmate@v3 # for interactive debugging
25-
- run: julia --project=. -e 'using Pkg; Pkg.activate("test"); Pkg.rm("Xpress"); Pkg.activate("."); using TestEnv; TestEnv.activate(); cd("test"); include("runtests.jl")'
25+
- run: julia --project=. -e 'using Pkg; Pkg.activate("test"); Pkg.rm("Xpress"); Pkg.activate("."); using TestEnv; TestEnv.activate(); Pkg.add(PackageSpec(name="GhpGhx", url="https://github.com/NREL/GhpGhx.jl.git")); using GhpGhx; cd("test"); include("runtests.jl")'
2626
shell: bash

CHANGELOG.md

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -106,6 +106,7 @@ Classify the change according to the following categories:
106106

107107
## v0.48.2
108108
### Added
109+
- Add new optional parameter **max_ton** to GHP module to allow user to size GHP smaller than peak load
109110
- Battery residual value if choosing replacement strategy for degradation
110111
- Add new **ElectricStorage** parameters **max_duration_hours** and **min_duration_hours** to bound the energy duration of battery storage
111112
### Changed

src/REopt.jl

Lines changed: 5 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -58,11 +58,12 @@ function __init__()
5858
end
5959

6060
const EXISTING_BOILER_EFFICIENCY = 0.8
61-
const GAL_PER_M3 = 264.172 # [gal/m^3]
62-
const KWH_PER_GAL_DIESEL = 40.7 # [kWh/gal_diesel] higher heating value of diesel
63-
const KWH_PER_MMBTU = 293.07107 # [kWh/mmbtu]
61+
const GAL_PER_M3 = 264.172 # [gal/m^3]
62+
const KWH_PER_GAL_DIESEL = 40.7 # [kWh/gal_diesel] higher heating value of diesel
63+
const KWH_PER_MMBTU = 293.07107 # [kWh/mmbtu]
6464
const KWH_THERMAL_PER_TONHOUR = 3.51685
65-
const TONNE_PER_LB = 1/2204.62 # [tonne/lb]
65+
const TONNE_PER_LB = 1/2204.62 # [tonne/lb]
66+
const TONNE_PER_MMBTU_HOUR = 0.012 # [tonne/mmbtu-hour]
6667
const FUEL_TYPES = ["natural_gas", "landfill_bio_gas", "propane", "diesel_oil"]
6768
const BIG_NUMBER = 1.0e10 #used for max size. TODO use this number elsewhere.
6869
const PRIME_MOVERS = ["recip_engine", "micro_turbine", "combustion_turbine", "fuel_cell"] #TODO replace `prime_movers` references in CHP code

src/core/ghp.jl

Lines changed: 11 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -19,11 +19,14 @@ struct with outer constructor:
1919
installed_cost_ghx_per_ft::Float64 = 14.0
2020
installed_cost_building_hydronic_loop_per_sqft = 1.70
2121
om_cost_per_sqft_year::Float64 = -0.51
22-
building_sqft::Float64 # Required input
22+
building_sqft::Float64 # Required input
2323
space_heating_efficiency_thermal_factor::Float64 = NaN # Default depends on building and location
24-
cooling_efficiency_thermal_factor::Float64 = NaN # Default depends on building and location
24+
cooling_efficiency_thermal_factor::Float64 = NaN # Default depends on building and location
2525
ghpghx_response::Dict = Dict()
2626
can_serve_dhw::Bool = false
27+
max_ton::Real # Maximum heat pump capacity size. Default at a big number
28+
max_number_of_boreholes::Real # Maximum GHX size
29+
load_served_by_ghp::String # "scaled" or "nonpeak"
2730
2831
macrs_option_years::Int = 5
2932
macrs_bonus_fraction::Float64 = 0.6
@@ -77,6 +80,9 @@ Base.@kwdef mutable struct GHP <: AbstractGHP
7780
can_serve_space_heating::Bool = true
7881
can_serve_process_heat::Bool = false
7982
can_supply_steam_turbine::Bool = false
83+
max_ton::Real = BIG_NUMBER
84+
max_number_of_boreholes::Real = BIG_NUMBER
85+
load_served_by_ghp::String = "nonpeak"
8086

8187
aux_heater_type::String = "electric"
8288
is_ghx_hybrid::Bool = false
@@ -154,7 +160,7 @@ function GHP(response::Dict, d::Dict)
154160
end
155161
# incentives = IncentivesNoProdBased(**d_mod)
156162

157-
setup_installed_cost_curve!(ghp, response)
163+
setup_installed_cost_curve!(d, ghp, response)
158164

159165
setup_om_cost!(ghp)
160166

@@ -168,10 +174,10 @@ function GHP(response::Dict, d::Dict)
168174
end
169175

170176
"""
171-
setup_installed_cost_curve!(response::Dict, ghp::GHP)
177+
setup_installed_cost_curve!(d::Dict, response::Dict, ghp::GHP)
172178
173179
"""
174-
function setup_installed_cost_curve!(ghp::GHP, response::Dict)
180+
function setup_installed_cost_curve!(d::Dict, ghp::GHP, response::Dict)
175181
big_number = 1.0e10
176182
# GHX and GHP sizing metrics for cost calculations
177183
total_ghx_ft = response["outputs"]["number_of_boreholes"] * response["outputs"]["length_boreholes_ft"]

src/core/scenario.jl

Lines changed: 90 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -480,7 +480,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
480480
space_heating_thermal_load_reduction_with_ghp_kw = zeros(8760 * settings.time_steps_per_hour)
481481
cooling_thermal_load_reduction_with_ghp_kw = zeros(8760 * settings.time_steps_per_hour)
482482
eval_ghp = false
483-
get_ghpghx_from_input = false
483+
get_ghpghx_from_input = false
484484
if haskey(d, "GHP") && haskey(d["GHP"],"building_sqft")
485485
eval_ghp = true
486486
if haskey(d["GHP"], "ghpghx_responses") && !isempty(d["GHP"]["ghpghx_responses"])
@@ -620,13 +620,101 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
620620
end
621621

622622
ghpghx_results = Dict()
623+
623624
try
624625
# Call GhpGhx.jl to size GHP and GHX
625626
@info "Starting GhpGhx.jl"
626627
# Call GhpGhx.jl to size GHP and GHX
628+
# If user provides undersized GHP, calculate load to send to GhpGhx.jl, and load to send to REopt for backup
629+
thermal_load_ton = ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] .* 1/TONNE_PER_MMBTU_HOUR
630+
if haskey(ghpghx_inputs,"cooling_thermal_load_ton")
631+
cooling_load_ton = ghpghx_inputs["cooling_thermal_load_ton"]
632+
thermal_load_ton .+= cooling_load_ton
633+
end
634+
peak_thermal_load_ton = maximum(thermal_load_ton)
635+
if haskey(d["GHP"],"max_ton") && peak_thermal_load_ton > d["GHP"]["max_ton"]
636+
@info "User entered undersized GHP. Calculating load that can be served by user specified undersized GHP"
637+
# When user specifies undersized GHP, calculate the load to be served by GHP and send the rest to REopt
638+
if !haskey(d["GHP"], "load_served_by_ghp")
639+
d["GHP"]["load_served_by_ghp"] = "nonpeak"
640+
end
641+
# If user choose to scale down total load (load_served_by_ghp="scaled"), calculate the ratio of the udersized GHP size and peak load
642+
if d["GHP"]["load_served_by_ghp"] == "scaled"
643+
@info "GHP served scaled down of total thermal load"
644+
peak_ratio = d["GHP"]["max_ton"] / peak_thermal_load_ton
645+
# Scale the total load profile down by the peak_ratio and use this scaled down load to rerun GhpGhx.jl
646+
heating_load_mmbtu = ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"]
647+
heating_load_mmbtu = heating_load_mmbtu .* peak_ratio
648+
ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = heating_load_mmbtu
649+
if haskey(ghpghx_inputs,"cooling_thermal_load_ton")
650+
ghpghx_inputs["cooling_thermal_load_ton"] = cooling_load_ton .* peak_ratio
651+
end
652+
elseif d["GHP"]["load_served_by_ghp"] == "nonpeak"
653+
@info "GHP serves all thermal load below peak"
654+
heating_load_mmbtu = ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"]
655+
# if cooling load is included, cut down total thermal load and send as much heating load to GhpGhx.jl as possible
656+
if haskey(ghpghx_inputs,"cooling_thermal_load_ton")
657+
# If total thermal load (heating + cooling) is more than user-defined GHP size,
658+
# first reduce heating load as much as possible while keeping cooling load the same
659+
if peak_thermal_load_ton > d["GHP"]["max_ton"]
660+
thermal_load_ton[thermal_load_ton .>= d["GHP"]["max_ton"]] .= d["GHP"]["max_ton"]
661+
heating_load_ton = thermal_load_ton .- cooling_load_ton
662+
# Make sure that the reduced heating load is not negative
663+
heating_load_ton[heating_load_ton .< 0] .= 0
664+
# If the updated peak thermal load is still more than user-defined GHP size,
665+
# reduce cooling load as well
666+
updated_thermal_load_ton = heating_load_ton .+ cooling_load_ton
667+
updated_peak_thermal_load_ton = maximum(updated_thermal_load_ton)
668+
if updated_peak_thermal_load_ton > d["GHP"]["max_ton"]
669+
updated_thermal_load_ton[updated_thermal_load_ton .>= d["GHP"]["max_ton"]] .= d["GHP"]["max_ton"]
670+
cooling_load_ton = updated_thermal_load_ton .- heating_load_ton
671+
ghpghx_inputs["cooling_thermal_load_ton"] = cooling_load_ton
672+
end
673+
heating_load_mmbtu = heating_load_ton .* TONNE_PER_MMBTU_HOUR
674+
ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = heating_load_mmbtu
675+
end
676+
# if cooling load is not included, cut down heating load only and send to GhpGhx.jl
677+
else
678+
heating_load_mmbtu[heating_load_mmbtu .>= d["GHP"]["max_ton"] * TONNE_PER_MMBTU_HOUR] .= d["GHP"]["max_ton"] * TONNE_PER_MMBTU_HOUR
679+
ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = heating_load_mmbtu
680+
end
681+
end
682+
end
627683
results, inputs_params = GhpGhx.ghp_model(ghpghx_inputs)
684+
# If max_number_of_boreholes is specified, check if number of boreholes sized by GhpGhx.jl greater than user-specified max_number_of_boreholes,
685+
# 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
686+
if haskey(d["GHP"],"max_number_of_boreholes")
687+
optimal_number_of_boreholes = GhpGhx.get_results_for_reopt(results, inputs_params)["number_of_boreholes"]
688+
if optimal_number_of_boreholes > d["GHP"]["max_number_of_boreholes"]
689+
@info "Max number of boreholes specified is less than number of boreholes sized in GhpGhx.jl, reducing thermal load served by GHP further"
690+
max_iter = 10
691+
for iter = 1:max_iter
692+
borehole_ratio = d["GHP"]["max_number_of_boreholes"] / optimal_number_of_boreholes
693+
heating_load_mmbtu .*= borehole_ratio
694+
if haskey(ghpghx_inputs,"cooling_thermal_load_ton")
695+
cooling_load_ton .*= borehole_ratio
696+
# if cooling load is not included, cut down heating load only and send to GhpGhx.jl
697+
end
698+
ghpghx_inputs["heating_thermal_load_mmbtu_per_hr"] = heating_load_mmbtu
699+
ghpghx_inputs["cooling_thermal_load_ton"] = cooling_load_ton
700+
701+
# Rerun GhpGhx.jl
702+
results, inputs_params = GhpGhx.ghp_model(ghpghx_inputs)
703+
optimal_number_of_boreholes = GhpGhx.get_results_for_reopt(results, inputs_params)["number_of_boreholes"]
704+
# Solution is found if the new optimal number of boreholes sized by GhpGhx.jl = user-specified max number of boreholes,
705+
# Otherwise, continue solving until reaching max iteration
706+
if -0.5 < optimal_number_of_boreholes - d["GHP"]["max_number_of_boreholes"] < 0.5
707+
break
708+
else
709+
iter += 1
710+
end
711+
end
712+
end
713+
end
714+
628715
# Create a dictionary of the results data needed for REopt
629716
ghpghx_results = GhpGhx.get_results_for_reopt(results, inputs_params)
717+
# 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
630718
@info "GhpGhx.jl model solved" #with status $(results["status"])."
631719
catch e
632720
@info e
@@ -644,9 +732,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false)
644732
end
645733
append!(ghp_option_list, [GHP(ghpghx_response, ghp_inputs_removed_ghpghx_params)])
646734
# Print out ghpghx_response for loading into a future run without running GhpGhx.jl again
647-
#open("scenarios/ghpghx_response.json","w") do f
648-
# JSON.print(f, ghpghx_response)
649-
#end
735+
# open("scenarios/ghpghx_response.json","w") do f
650736
end
651737
# If ghpghx_responses is included in inputs, do NOT run GhpGhx.jl model and use already-run ghpghx result as input to REopt
652738
elseif eval_ghp && get_ghpghx_from_input

src/results/existing_boiler.jl

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@
55
- `fuel_consumption_series_mmbtu_per_hour` # Fuel consumption series [MMBtu/hr]
66
- `annual_fuel_consumption_mmbtu` # Fuel consumed in a year [MMBtu]
77
- `thermal_production_series_mmbtu_per_hour` # Thermal energy production series [MMBtu/hr]
8-
- `annual_thermal_production_mmbtu` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr]
8+
- `annual_thermal_production_mmbtu` # Thermal power production in a year [MMBtu]
99
- `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr]
1010
- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr]
1111
- `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr]

src/results/ghp.jl

Lines changed: 17 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,9 @@ GHP results:
1515
- `thermal_to_space_heating_load_series_mmbtu_per_hour`
1616
- `thermal_to_dhw_load_series_mmbtu_per_hour`
1717
- `thermal_to_load_series_ton`
18+
- `annual_thermal_production_mmbtu` # GHP's heating thermal power production in a year [MMBtu]
19+
- `annual_thermal_production_tonhour` # GHP's cooling thermal power production in a year [ton]
20+
1821
"""
1922

2023
function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
@@ -25,6 +28,7 @@ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
2528
# r["size_heat_pump_ton"] = 0.0
2629
# r["size_wwhp_heating_pump_ton"] = 0.0
2730
# r["size_wwhp_cooling_pump_ton"] = 0.0
31+
2832
if ghp_option_chosen >= 1
2933
r["ghpghx_chosen_outputs"] = p.s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]
3034

@@ -42,13 +46,22 @@ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
4246
r["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"] = round.(value.(HeatingThermalReductionWithGHP) ./ KWH_PER_MMBTU, digits=3)
4347
@expression(m, CoolingThermalReductionWithGHP[ts in p.time_steps],
4448
sum(p.cooling_thermal_load_reduction_with_ghp_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options))
49+
50+
@expression(m, HeatingThermalLoadServedWithGHP[ts in p.time_steps],
51+
sum(p.ghp_heating_thermal_load_served_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options))
52+
@expression(m, CoolingThermalLoadServedWithGHP[ts in p.time_steps],
53+
sum(p.ghp_cooling_thermal_load_served_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options))
54+
4555
r["cooling_thermal_load_reduction_with_ghp_ton"] = round.(value.(CoolingThermalReductionWithGHP) ./ KWH_THERMAL_PER_TONHOUR, digits=3)
4656
r["ghx_residual_value_present_value"] = value(m[:ResidualGHXCapCost])
4757
r["avoided_capex_by_ghp_present_value"] = value(m[:AvoidedCapexByGHP])
48-
r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = d["HeatingLoad"]["space_heating_thermal_load_series_mmbtu_per_hour"] .- r["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"]
49-
r["thermal_to_load_series_ton"] = d["CoolingLoad"]["load_series_ton"] .- r["cooling_thermal_load_reduction_with_ghp_ton"]
58+
r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(HeatingThermalLoadServedWithGHP) ./ KWH_PER_MMBTU, digits=3)
59+
r["thermal_to_load_series_ton"] = round.(value.(CoolingThermalLoadServedWithGHP) ./ KWH_THERMAL_PER_TONHOUR, digits=3)
60+
r["annual_thermal_production_mmbtu"] = sum(r["thermal_to_space_heating_load_series_mmbtu_per_hour"])
61+
r["annual_thermal_production_tonhour"] = sum(r["thermal_to_load_series_ton"])
5062
if p.s.ghp_option_list[ghp_option_chosen].can_serve_dhw
5163
r["thermal_to_dhw_load_series_mmbtu_per_hour"] = d["HeatingLoad"]["dhw_thermal_load_series_mmbtu_per_hour"]
64+
r["annual_thermal_production_mmbtu"] = r["annual_thermal_production_mmbtu"] + sum(r["thermal_to_dhw_load_series_mmbtu_per_hour"])
5265
else
5366
r["thermal_to_dhw_load_series_mmbtu_per_hour"] = zeros(length(p.time_steps))
5467
end
@@ -60,6 +73,8 @@ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="")
6073
r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = zeros(length(p.time_steps))
6174
r["thermal_to_load_series_ton"] = zeros(length(p.time_steps))
6275
r["thermal_to_dhw_load_series_mmbtu_per_hour"] = zeros(length(p.time_steps))
76+
r["annual_thermal_production_mmbtu"] = 0.0
77+
r["annual_thermal_production_tonhour"] = 0.0
6378
end
6479
d["GHP"] = r
6580
nothing

0 commit comments

Comments
 (0)