From 26e7e26e381825a47a99aece6cc66626bb16d404 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 20 Feb 2024 17:11:39 -0700 Subject: [PATCH 001/266] added ASHP defaults --- data/ashp/ashp_defaults.json | 8 +++ src/core/ashp.jl | 110 +++++++++++++++++++++++++++++++++++ 2 files changed, 118 insertions(+) create mode 100644 data/ashp/ashp_defaults.json create mode 100644 src/core/ashp.jl diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json new file mode 100644 index 000000000..c7542b598 --- /dev/null +++ b/data/ashp/ashp_defaults.json @@ -0,0 +1,8 @@ +{ + "installed_cost_per_mmbtu_per_hour": 314444, + "om_cost_per_mmbtu_per_hour": 0.0, + "macrs_option_years": 0, + "macrs_bonus_fraction": 0.0, + "can_supply_steam_turbine": false, + "cop": 3.0 +} diff --git a/src/core/ashp.jl b/src/core/ashp.jl new file mode 100644 index 000000000..d916936d6 --- /dev/null +++ b/src/core/ashp.jl @@ -0,0 +1,110 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. + +struct ASHP <: AbstractThermalTech + min_kw::Real + max_kw::Real + installed_cost_per_kw::Real + om_cost_per_kw::Real + macrs_option_years::Int + macrs_bonus_fraction::Real + can_supply_steam_turbine::Bool + cop::Array{<:Real,1} = Real[] + can_serve_dhw::Bool + can_serve_space_heating::Bool + can_serve_process_heat::Bool +end + + +""" +ASHP + +If a user provides the `ASHP` key then the optimal scenario has the option to purchase +this new `ASHP` to meet the heating load in addition to using the `ExistingBoiler` +to meet the heating load. + +```julia +function ASHP(; + min_mmbtu_per_hour::Real = 0.0, # Minimum thermal power size + max_mmbtu_per_hour::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_mmbtu_per_hour::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_mmbtu_per_hour::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS + can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production + cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) + can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load + can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load + can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load +) +``` +""" +function ASHP(; + min_mmbtu_per_hour::Real = 0.0, + max_mmbtu_per_hour::Real = BIG_NUMBER, + installed_cost_per_mmbtu_per_hour::Union{Real, Nothing} = nothing, + om_cost_per_mmbtu_per_hour::Union{Real, Nothing} = nothing, + macrs_option_years::Int = 0, + macrs_bonus_fraction::Real = 0.0, + can_supply_steam_turbine::Union{Bool, Nothing} = nothing, + cop::Array{<:Real,1} = Real[], + can_serve_dhw::Bool = true, + can_serve_space_heating::Bool = true, + can_serve_process_heat::Bool = true + ) + + defaults = get_ashp_defaults() + + # populate defaults as needed + if isnothing(installed_cost_per_mmbtu_per_hour) + installed_cost_per_mmbtu_per_hour = defaults["installed_cost_per_mmbtu_per_hour"] + end + if isnothing(om_cost_per_mmbtu_per_hour) + om_cost_per_mmbtu_per_hour = defaults["om_cost_per_mmbtu_per_hour"] + end + if isnothing(can_supply_steam_turbine) + can_supply_steam_turbine = defaults["can_supply_steam_turbine"] + end + if isnothing(cop) + cop = defaults["cop"] + end + + # Convert max sizes, cost factors from mmbtu_per_hour to kw + min_kw = min_mmbtu_per_hour * KWH_PER_MMBTU + max_kw = max_mmbtu_per_hour * KWH_PER_MMBTU + + installed_cost_per_kw = installed_cost_per_mmbtu_per_hour / KWH_PER_MMBTU + om_cost_per_kw = om_cost_per_mmbtu_per_hour / KWH_PER_MMBTU + + + ASHP( + min_kw, + max_kw, + installed_cost_per_kw, + om_cost_per_kw, + macrs_option_years, + macrs_bonus_fraction, + can_supply_steam_turbine, + cop, + can_serve_dhw, + can_serve_space_heating, + can_serve_process_heat + ) +end + + + +""" +function get_ashp_defaults() + +Obtains defaults for the ASHP from a JSON data file. + +inputs +None + +returns +ashp_defaults::Dict -- Dictionary containing defaults for ASHP +""" +function get_ashp_defaults() + ashp_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_defaults.json")) + return eh_defaults +end \ No newline at end of file From 45461c97300d1df32e150ec2c4500989785be36d Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 20 Feb 2024 17:13:47 -0700 Subject: [PATCH 002/266] added ashp defaults --- src/core/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index d916936d6..3172045e3 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -106,5 +106,5 @@ ashp_defaults::Dict -- Dictionary containing defaults for ASHP """ function get_ashp_defaults() ashp_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_defaults.json")) - return eh_defaults + return ashp_defaults end \ No newline at end of file From 424e8718b06d465bec8a35aa025173beeb306bdf Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 20 Feb 2024 17:33:05 -0700 Subject: [PATCH 003/266] added ashp to results --- src/REopt.jl | 2 + src/results/ashp.jl | 91 ++++++++++++++++++++++++++++++++++++++++++ src/results/results.jl | 4 ++ 3 files changed, 97 insertions(+) create mode 100644 src/results/ashp.jl diff --git a/src/REopt.jl b/src/REopt.jl index 07e275b00..544f8cb9a 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -127,6 +127,7 @@ include("core/chp.jl") include("core/ghp.jl") include("core/steam_turbine.jl") include("core/electric_heater.jl") +include("core/ashp.jl") include("core/scenario.jl") include("core/bau_scenario.jl") include("core/reopt_inputs.jl") @@ -180,6 +181,7 @@ include("results/flexible_hvac.jl") include("results/ghp.jl") include("results/steam_turbine.jl") include("results/electric_heater.jl") +include("results/ashp.jl") include("results/heating_cooling_load.jl") include("core/reopt.jl") diff --git a/src/results/ashp.jl b/src/results/ashp.jl new file mode 100644 index 000000000..bb3b05bac --- /dev/null +++ b/src/results/ashp.jl @@ -0,0 +1,91 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. + +""" +`ASHP` results keys: +- `size_mmbtu_per_hour` # Thermal production capacity size of the ASHP [MMBtu/hr] +- `electric_consumption_series_kw` # Fuel consumption series [kW] +- `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] +- `thermal_production_series_mmbtu_per_hour` # Thermal energy production series [MMBtu/hr] +- `annual_thermal_production_mmbtu` # Thermal energy produced in a year [MMBtu] +- `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] +- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] +- `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] + +!!! note "'Series' and 'Annual' energy outputs are average annual" + REopt performs load balances using average annual production values for technologies that include degradation. + Therefore, all timeseries (`_series`) and `annual_` results should be interpretted as energy outputs averaged over the analysis period. + +""" + +function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") + r = Dict{String, Any}() + r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU, digits=3) + @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t] + for q in p.heating_loads, t in p.techs.ashp)) + r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) + r["annual_electric_consumption_kwh"] = sum(r["electric_consumption_series_kw"]) + + @expression(m, ASHPThermalProductionSeries[ts in p.time_steps], + sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp)) + r["thermal_production_series_mmbtu_per_hour"] = + round.(value.(ASHPProductionSeries) / KWH_PER_MMBTU, digits=5) + r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) + + if !isempty(p.s.storage.types.hot) + @expression(m, ASHPToHotTESKW[ts in p.time_steps], + sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) + ) + @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], + sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot) + ) + else + @expression(m, ASHPToHotTESKW, 0.0) + @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) + end + r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPToHotTESKW) / KWH_PER_MMBTU, digits=3) + + if !isempty(p.techs.steam_turbine) && p.s.ashp.can_supply_steam_turbine + @expression(m, ASHPToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP",q,ts] for q in p.heating_loads)) + @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP",q,ts]) + else + ASHPToSteamTurbine = zeros(length(p.time_steps)) + @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], 0.0) + end + r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ASHPToSteamTurbine) / KWH_PER_MMBTU, digits=3) + + @expression(m, ASHPToLoad[ts in p.time_steps], + sum(m[:dvHeatingProduction]["ASHP", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] + ) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) / KWH_PER_MMBTU, digits=3) + + if "DomesticHotWater" in p.heating_loads && p.s.ashp.can_serve_dhw + @expression(m, ASHPToDHWKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP","DomesticHotWater",ts] - ASHPToHotTESByQualityKW["DomesticHotWater",ts] - ASHPToSteamTurbineByQuality["DomesticHotWater",ts] + ) + else + @expression(m, ASHPToDHWKW[ts in p.time_steps], 0.0) + end + r["thermal_to_dhw_load_series_mmbtu_per_hour"] = round.(value.(ASHPToDHWKW ./ KWH_PER_MMBTU), digits=5) + + if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating + @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] - ASHPToSteamTurbineByQuality["SpaceHeating",ts] + ) + else + @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], 0.0) + end + r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(ASHPToSpaceHeatingKW ./ KWH_PER_MMBTU), digits=5) + + if "ProcessHeat" in p.heating_loads && p.s.ashp.can_serve_space_heating + @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP","ProcessHeat",ts] - ASHPToHotTESByQualityKW["ProcessHeat",ts] - ASHPToSteamTurbineByQuality["ProcessHeat",ts] + ) + else + @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], 0.0) + end + r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) + + d["ASHP"] = r + nothing +end \ No newline at end of file diff --git a/src/results/results.jl b/src/results/results.jl index 0e2e63d6f..6e7b0ee3e 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -103,6 +103,10 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") if !isempty(p.techs.electric_heater) add_electric_heater_results(m, p, d; _n) end + + if !isempty(p.techs.ashp) + add_ashp_results(m, p, d; _n) + end return d end From 9db934b8e8edca461f2434de30c8cbd8affa1b5e Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 20 Feb 2024 17:36:41 -0700 Subject: [PATCH 004/266] added ashp to load balance --- src/constraints/load_balance.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 9d6980f0d..74a13586a 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -14,6 +14,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) @@ -30,6 +31,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) From 2f4223786c5eac950852b2c5af42d9879d5d1a51 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 21 Feb 2024 11:57:44 -0700 Subject: [PATCH 005/266] check electric heater's cop to backup_heating_cop --- src/constraints/load_balance.jl | 4 ++-- src/core/bau_inputs.jl | 4 ++++ src/core/reopt_inputs.jl | 34 ++++++++++++++++++++++++--------- 3 files changed, 31 insertions(+), 11 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 74a13586a..25aeda379 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -13,7 +13,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] @@ -30,7 +30,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 12eeb0cdc..f054ae167 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -27,7 +27,9 @@ function BAUInputs(p::REoptInputs) om_cost_per_kw = Dict(t => 0.0 for t in techs.all) cop = Dict(t => 0.0 for t in techs.cooling) thermal_cop = Dict{String, Float64}() + backup_heating_cop = Dict{String, Float64}() heating_cop = Dict{String, Float64}() + cooling_cop = Dict{String, Float64}() production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) tech_renewable_energy_fraction = Dict(t => 0.0 for t in techs.all) # !!! note: tech_emissions_factors are in lb / kWh of fuel burned (gets multiplied by kWh of fuel burned, not kWh electricity consumption, ergo the use of the HHV instead of fuel slope) @@ -211,7 +213,9 @@ function BAUInputs(p::REoptInputs) tech_emissions_factors_SO2, tech_emissions_factors_PM25, p.techs_operating_reserve_req_fraction, + backup_heating_cop, heating_cop, + cooling_cop, heating_loads, heating_loads_kw, heating_loads_served_by_tes, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index f986e08e0..83da2b2fa 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -61,7 +61,9 @@ struct REoptInputs <: AbstractInputs tech_emissions_factors_SO2::Dict{String, <:Real} # (techs) tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) - heating_cop::Dict{String, <:Real} # (techs.electric_heater) + backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) + heating_cop::Dict{String, <:Real} # (techs.ashp) + cooling_cop::Dict{String, <:Real} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) unavailability::Dict{String, Array{Float64,1}} # Dict by tech of unavailability profile end @@ -127,7 +129,9 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs tech_emissions_factors_SO2::Dict{String, <:Real} # (techs) tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) - heating_cop::Dict{String, <:Real} # (techs.electric_heater) + backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) + heating_cop::Dict{String, <:Real} # (techs.ashp) + cooling_cop::Dict{String, <:Real} # (techs.ashp) heating_loads::Vector{String} # list of heating loads heating_loads_kw::Dict{String, Array{Real,1}} # (heating_loads) heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) @@ -168,7 +172,7 @@ function REoptInputs(s::AbstractScenario) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, cop, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - heating_cop = setup_tech_inputs(s) + backup_heating_cop, heating_cop, cooling_cop = setup_tech_inputs(s) pbi_pwf, pbi_max_benefit, pbi_max_kw, pbi_benefit_per_kwh = setup_pbi_inputs(s, techs) @@ -305,7 +309,9 @@ function REoptInputs(s::AbstractScenario) tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, + backup_heating_cop, heating_cop, + cooling_cop, heating_loads, heating_loads_kw, heating_loads_served_by_tes, @@ -344,7 +350,9 @@ function setup_tech_inputs(s::AbstractScenario) cop = Dict(t => 0.0 for t in techs.cooling) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) - heating_cop = Dict(t => 0.0 for t in techs.electric_heater) + backup_heating_cop = Dict(t => 0.0 for t in techs.electric_heater) + heating_cop = Dict(t => 0.0 for t in techs.ashp) + cooling_cop = Dict(t => 0.0 for t in techs.ashp) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) @@ -415,9 +423,16 @@ function setup_tech_inputs(s::AbstractScenario) end if "ElectricHeater" in techs.all - setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) + setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, backup_heating_cop) else - heating_cop["ElectricHeater"] = 1.0 + backup_heating_cop["ElectricHeater"] = 1.0 + end + + if "ASHP" in techs.all + setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + else + heating_cop["ASHP"] = 3.0 + cooling_cop["ASHP"] = 3.0 end # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in @@ -433,7 +448,8 @@ function setup_tech_inputs(s::AbstractScenario) production_factor, max_sizes, min_sizes, existing_sizes, cap_cost_slope, om_cost_per_kw, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, - tech_emissions_factors_PM25, cop, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, heating_cop + tech_emissions_factors_PM25, cop, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, + backup_heating_cop, heating_cop, cooling_cop end @@ -849,11 +865,11 @@ function setup_steam_turbine_inputs(s::AbstractScenario, max_sizes, min_sizes, c return nothing end -function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) +function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, backup_heating_cop) max_sizes["ElectricHeater"] = s.electric_heater.max_kw min_sizes["ElectricHeater"] = s.electric_heater.min_kw om_cost_per_kw["ElectricHeater"] = s.electric_heater.om_cost_per_kw - heating_cop["ElectricHeater"] = s.electric_heater.cop + backup_heating_cop["ElectricHeater"] = s.electric_heater.cop if s.electric_heater.macrs_option_years in [5, 7] cap_cost_slope["ElectricHeater"] = effective_cost(; From 18fb79d0f0426116ae8291abc7459b86d2f32c3e Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 23 Feb 2024 16:53:20 -0700 Subject: [PATCH 006/266] added ASHP for heating --- src/constraints/load_balance.jl | 4 +-- src/constraints/storage_constraints.jl | 22 ++++++++++++++-- src/core/reopt_inputs.jl | 28 ++++++++++++++++++++- src/core/scenario.jl | 9 ++++++- src/core/techs.jl | 23 ++++++++++++++++- src/core/types.jl | 4 ++- src/results/ashp.jl | 7 ++++-- src/results/electric_heater.jl | 2 +- test/runtests.jl | 35 ++++++++++++++++++++++++++ test/test_with_xpress.jl | 35 ++++++++++++++++++++++++++ 10 files changed, 158 insertions(+), 11 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 25aeda379..e8fd5522b 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -14,7 +14,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) #need to add cooling + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) @@ -31,7 +31,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) # need to add cooling + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index 12af6c5ef..130e015a1 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -118,7 +118,8 @@ function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="") ) end end - + + # # Constraint (4f)-1b: Electric Heater if !isempty(p.techs.electric_heater) for t in p.techs.electric_heater if !isempty(p.techs.steam_turbine) && (t in p.techs.can_supply_steam_turbine) @@ -134,8 +135,25 @@ function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="") end end end + + # # Constraint (4f)-1c: Air-source Heat Pump (ASHP) + if !isempty(p.techs.ashp) + for t in p.techs.ashp + if !isempty(p.techs.steam_turbine) && (t in p.techs.can_supply_steam_turbine) + @constraint(m, [b in p.s.storage.types.hot, q in p.heating_loads, ts in p.time_steps], + m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] + m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] <= + m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + else + @constraint(m, [b in p.s.storage.types.hot, q in p.heating_loads, ts in p.time_steps], + m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] <= + m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + end + end + end - # Constraint (4f)-1b: SteamTurbineTechs + # Constraint (4f)-1d: SteamTurbineTechs if !isempty(p.techs.steam_turbine) @constraint(m, SteamTurbineTechProductionFlowCon[b in p.s.storage.types.hot, t in p.techs.steam_turbine, q in p.heating_loads, ts in p.time_steps], m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] <= m[Symbol("dvHeatingProduction"*_n)][t,q,ts] diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 83da2b2fa..bc93f2af0 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -429,7 +429,7 @@ function setup_tech_inputs(s::AbstractScenario) end if "ASHP" in techs.all - setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + setup_airsource_hp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) else heating_cop["ASHP"] = 3.0 cooling_cop["ASHP"] = 3.0 @@ -890,6 +890,32 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end +function setup_airsource_hp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + max_sizes["ASHP"] = s.ashp.max_kw + min_sizes["ASHP"] = s.ashp.min_kw + om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw + heating_cop["ASHP"] = s.ashp.cop + cooling_cop["ASHP"] = s.ashp.cop + + if s.ashp.macrs_option_years in [5, 7] + cap_cost_slope["ASHP"] = effective_cost(; + itc_basis = s.ashp.installed_cost_per_kw, + replacement_cost = 0.0, + replacement_year = s.financial.analysis_years, + discount_rate = s.financial.owner_discount_rate_fraction, + tax_rate = s.financial.owner_tax_rate_fraction, + itc = 0.0, + macrs_schedule = s.ashp.macrs_option_years == 5 ? s.financial.macrs_five_year : s.financial.macrs_seven_year, + macrs_bonus_fraction = s.ashp.macrs_bonus_fraction, + macrs_itc_reduction = 0.0, + rebate_per_kw = 0.0 + ) + else + cap_cost_slope["ASHP"] = s.ashp.installed_cost_per_kw + end + +end + function setup_present_worth_factors(s::AbstractScenario, techs::Techs) lvl_factor = Dict(t => 1.0 for t in techs.all) # default levelization_factor of 1.0 diff --git a/src/core/scenario.jl b/src/core/scenario.jl index c6918db8b..5e76ee2a0 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -25,6 +25,7 @@ struct Scenario <: AbstractScenario cooling_thermal_load_reduction_with_ghp_kw::Union{Vector{Float64}, Nothing} steam_turbine::Union{SteamTurbine, Nothing} electric_heater::Union{ElectricHeater, Nothing} + ashp::Union{ASHP, Nothing} end """ @@ -633,6 +634,11 @@ function Scenario(d::Dict; flex_hvac_from_json=false) electric_heater = ElectricHeater(;dictkeys_tosymbols(d["ElectricHeater"])...) end + ashp = nothing + if haskey(d, "ASHP") && d["ASHP"]["max_mmbtu_per_hour"] > 0.0 + ashp = ASHP(;dictkeys_tosymbols(d["ASHP"])...) + end + return Scenario( settings, site, @@ -658,7 +664,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) space_heating_thermal_load_reduction_with_ghp_kw, cooling_thermal_load_reduction_with_ghp_kw, steam_turbine, - electric_heater + electric_heater, + ashp ) end diff --git a/src/core/techs.jl b/src/core/techs.jl index 76f9b0a85..82e9e12c9 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -29,6 +29,7 @@ function Techs(p::REoptInputs, s::BAUScenario) techs_can_serve_dhw = String[] techs_can_serve_process_heat = String[] ghp_techs = String[] + ashp_techs = String[] if p.s.generator.existing_kw > 0 push!(all_techs, "Generator") @@ -85,7 +86,8 @@ function Techs(p::REoptInputs, s::BAUScenario) techs_can_serve_space_heating, techs_can_serve_dhw, techs_can_serve_process_heat, - ghp_techs + ghp_techs, + ashp_techs ) end @@ -124,6 +126,7 @@ function Techs(s::Scenario) techs_can_serve_dhw = String[] techs_can_serve_process_heat = String[] ghp_techs = String[] + ashp_techs = String[] if s.wind.max_kw > 0 push!(all_techs, "Wind") @@ -263,6 +266,24 @@ function Techs(s::Scenario) end end + if !isnothing(s.ashp) + push!(all_techs, "ASHP") + push!(heating_techs, "ASHP") + push!(ashp_techs, "ASHP") + if s.ashp.can_supply_steam_turbine + push!(techs_can_supply_steam_turbine, "ASHP") + end + if s.ashp.can_serve_space_heating + push!(techs_can_serve_space_heating, "ASHP") + end + if s.ashp.can_serve_dhw + push!(techs_can_serve_dhw, "ASHP") + end + if s.ashp.can_serve_process_heat + push!(techs_can_serve_process_heat, "ASHP") + end + end + if s.settings.off_grid_flag append!(requiring_oper_res, pvtechs) append!(providing_oper_res, pvtechs) diff --git a/src/core/types.jl b/src/core/types.jl index 4d31ac61f..4ddba9cc6 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -45,7 +45,8 @@ mutable struct Techs can_serve_dhw::Vector{String} can_serve_space_heating::Vector{String} can_serve_process_heat::Vector{String} - ghp_techs::Vector{String} + ghp::Vector{String} + ashp::Vector{String} end ``` """ @@ -75,4 +76,5 @@ mutable struct Techs can_serve_space_heating::Vector{String} can_serve_process_heat::Vector{String} ghp::Vector{String} + ashp::Vector{String} end diff --git a/src/results/ashp.jl b/src/results/ashp.jl index bb3b05bac..e5c884ca4 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -22,12 +22,15 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t] - for q in p.heating_loads, t in p.techs.ashp)) + for q in p.heating_loads, t in p.techs.ashp) + #+ p.hours_per_time_step * sum(m[:dvCoolingProduction][t,q,ts] / p.cooling_cop[t] + #for q in p.cooling_loads, t in p.techs.ashp) + ) r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = sum(r["electric_consumption_series_kw"]) @expression(m, ASHPThermalProductionSeries[ts in p.time_steps], - sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp)) + sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp)) # TODO add cooling r["thermal_production_series_mmbtu_per_hour"] = round.(value.(ASHPProductionSeries) / KWH_PER_MMBTU, digits=5) r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) diff --git a/src/results/electric_heater.jl b/src/results/electric_heater.jl index 362970035..e14b045af 100644 --- a/src/results/electric_heater.jl +++ b/src/results/electric_heater.jl @@ -21,7 +21,7 @@ function add_electric_heater_results(m::JuMP.AbstractModel, p::REoptInputs, d::D r = Dict{String, Any}() r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ElectricHeater"]) / KWH_PER_MMBTU, digits=3) @expression(m, ElectricHeaterElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t] + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater)) r["electric_consumption_series_kw"] = round.(value.(ElectricHeaterElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = sum(r["electric_consumption_series_kw"]) diff --git a/test/runtests.jl b/test/runtests.jl index 7d402fb8f..046f2377b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2205,6 +2205,41 @@ else # run HiGHS tests end + @testset "ASHP" begin + d = JSON.parsefile("./scenarios/ashp.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + #first run: Boiler produces the required heat instead of ASHP - ASHP is invested here + @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP + annual_energy_supplied = 87600 + annual_ashp_consumption + + #Second run: ASHP produces the required heat with free electricity + @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 + @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + + end + @testset "Custom REopt logger" begin # Throw a handled error diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 64d93a19c..9991216e5 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -1743,6 +1743,41 @@ end end +@testset "ASHP" begin + d = JSON.parsefile("./scenarios/ashp.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + results = run_reopt(m, p) + + #first run: Boiler produces the required heat instead of ASHP - ASHP is not purchased here + @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + results = run_reopt(m, p) + + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP + annual_energy_supplied = 87600 + annual_ashp_consumption + + #Second run: ASHP produces the required heat with free electricity + @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 + @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + +end + @testset "Custom REopt logger" begin # Throw a handled error From 52d751d3e09ab41e403402d9e8cf5640f54acd39 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 23 Feb 2024 17:02:44 -0700 Subject: [PATCH 007/266] added testing file for ashp --- test/scenarios/ashp.json | 50 ++++++++++++++++++++++++++++++++++++++++ 1 file changed, 50 insertions(+) create mode 100644 test/scenarios/ashp.json diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json new file mode 100644 index 000000000..e51b462be --- /dev/null +++ b/test/scenarios/ashp.json @@ -0,0 +1,50 @@ +{ + "Site": { + "latitude": 37.78, + "longitude": -122.45 + }, + "ExistingBoiler": { + "production_type": "steam", + "efficiency": 0.8, + "fuel_type": "natural_gas", + "fuel_cost_per_mmbtu": 10 + }, + "ASHP": { + "min_mmbtu_per_hour": 0.0, + "max_mmbtu_per_hour": 100000, + "cop": 3.0, + "installed_cost_per_mmbtu_per_hour": 314000, + "om_cost_per_mmbtu_per_hour": 0.0, + "macrs_option_years": 0, + "macrs_bonus_fraction": 0.0, + "can_supply_steam_turbine": false + }, + "Financial": { + "om_cost_escalation_rate_fraction": 0.025, + "elec_cost_escalation_rate_fraction": 0.023, + "existing_boiler_fuel_cost_escalation_rate_fraction": 0.034, + "boiler_fuel_cost_escalation_rate_fraction": 0.034, + "offtaker_tax_rate_fraction": 0.26, + "offtaker_discount_rate_fraction": 0.083, + "third_party_ownership": false, + "owner_tax_rate_fraction": 0.26, + "owner_discount_rate_fraction": 0.083, + "analysis_years": 25 + }, + "ElectricLoad": { + "doe_reference_name": "FlatLoad", + "annual_kwh": 87600.0 + }, + "SpaceHeatingLoad": { + "doe_reference_name": "FlatLoad" + }, + "DomesticHotWaterLoad": { + "doe_reference_name": "FlatLoad" + }, + "ElectricTariff": { + "monthly_energy_rates": [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1] + }, + "HotThermalStorage":{ + "max_gal":2500 + } + } \ No newline at end of file From 395526687804c90d74485606de418fce8f041bfd Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 23 Feb 2024 17:12:27 -0700 Subject: [PATCH 008/266] tested ASHP with cplex --- test/test_with_cplex.jl | 38 ++++++++++++++++++++++++++++++++++++++ 1 file changed, 38 insertions(+) diff --git a/test/test_with_cplex.jl b/test/test_with_cplex.jl index fc342e683..a548ef71f 100644 --- a/test/test_with_cplex.jl +++ b/test/test_with_cplex.jl @@ -163,6 +163,44 @@ end @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost end +@testset "ASHP" begin + d = JSON.parsefile("./scenarios/ashp.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) + results = run_reopt(m, p) + + #first run: Boiler produces the required heat instead of ASHP - ASHP is not purchased here + @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + s = Scenario(d) + p = REoptInputs(s) + m = Model(GAMS.Optimizer) + set_optimizer_attribute(Model(GAMS.Optimizer, "OUTPUTLOG" => 0), "Solver", "CPLEX") + set_optimizer_attribute(Model(GAMS.Optimizer, "OUTPUTLOG" => 0), GAMS.Solver(), "CPLEX") + results = run_reopt(m, p) + + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP + annual_energy_supplied = 87600 + annual_ashp_consumption + + #Second run: ASHP produces the required heat with free electricity + @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 + @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + +end + + ## equivalent REopt API Post for test 2: # NOTE have to hack in API levelization_factor to get LCC within 5e-5 (Mosel tol) # {"Scenario": { From 83820cc10f9d53da431b5625b73bafdcc8cc1d7e Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 23 Feb 2024 17:16:40 -0700 Subject: [PATCH 009/266] test ashp with cplex --- test/test_with_cplex.jl | 9 +++++---- 1 file changed, 5 insertions(+), 4 deletions(-) diff --git a/test/test_with_cplex.jl b/test/test_with_cplex.jl index a548ef71f..a8c3af163 100644 --- a/test/test_with_cplex.jl +++ b/test/test_with_cplex.jl @@ -1,6 +1,6 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. using CPLEX - +# using GAMS #= add a time-of-export rate that is greater than retail rate for the month of January, @@ -183,9 +183,10 @@ end d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] s = Scenario(d) p = REoptInputs(s) - m = Model(GAMS.Optimizer) - set_optimizer_attribute(Model(GAMS.Optimizer, "OUTPUTLOG" => 0), "Solver", "CPLEX") - set_optimizer_attribute(Model(GAMS.Optimizer, "OUTPUTLOG" => 0), GAMS.Solver(), "CPLEX") + m = Model(optimizer_with_attributes(CPLEX.Optimizer, "CPX_PARAM_SCRIND" => 0)) + #m = Model(GAMS.Optimizer) + #set_optimizer_attribute(Model(GAMS.Optimizer, "CPX_PARAM_SCRIND" => 0), "Solver", "CPLEX") + #set_optimizer_attribute(Model(GAMS.Optimizer, "CPX_PARAM_SCRIND" => 0), GAMS.Solver(), "CPLEX") results = run_reopt(m, p) annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour From 586a656dcdb57d3821299d06fef5ee1b422b51ed Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 23 Feb 2024 17:20:26 -0700 Subject: [PATCH 010/266] add default ASHP COP --- src/core/ashp.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 3172045e3..039a34711 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -8,7 +8,8 @@ struct ASHP <: AbstractThermalTech macrs_option_years::Int macrs_bonus_fraction::Real can_supply_steam_turbine::Bool - cop::Array{<:Real,1} = Real[] + #cop::Array{<:Real,1} = Real[] + cop::Real can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -46,7 +47,8 @@ function ASHP(; macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, - cop::Array{<:Real,1} = Real[], + #cop::Array{<:Real,1} = Real[], + cop::Real, can_serve_dhw::Bool = true, can_serve_space_heating::Bool = true, can_serve_process_heat::Bool = true From 771efee8a45d306282a674775733e35c281d16f4 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 23 Feb 2024 22:50:20 -0700 Subject: [PATCH 011/266] Update techs.jl --- src/core/techs.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/techs.jl b/src/core/techs.jl index 82e9e12c9..6ee346276 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -317,7 +317,8 @@ function Techs(s::Scenario) techs_can_serve_space_heating, techs_can_serve_dhw, techs_can_serve_process_heat, - ghp_techs + ghp_techs, + ashp_techs ) end From fa5f2a21bbdd844e71334d46a6762e4a6e5dab57 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 27 Feb 2024 12:05:35 -0700 Subject: [PATCH 012/266] Update scenario.jl --- src/core/scenario.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 5e76ee2a0..a283fd870 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -596,9 +596,9 @@ 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 + # JSON.print(f, ghpghx_response) + #end 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 From 3510dce1b66810a020dbcbfb3176b6fb4786578f Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 27 Feb 2024 12:30:57 -0700 Subject: [PATCH 013/266] Update ashp.jl --- src/results/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index e5c884ca4..aac7b2d3e 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -43,7 +43,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot) ) else - @expression(m, ASHPToHotTESKW, 0.0) + @expression(m, ASHPToHotTESKW[ts in p.time_steps], 0.0) @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) end r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPToHotTESKW) / KWH_PER_MMBTU, digits=3) From 3aa110585397be05706c0009f67bf8bd6ea92a36 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 28 Feb 2024 10:38:06 -0700 Subject: [PATCH 014/266] separated heating and cooling COPs --- data/ashp/ashp_defaults.json | 3 ++- src/core/ashp.jl | 18 +++++++++++++----- src/core/reopt_inputs.jl | 8 ++++---- 3 files changed, 19 insertions(+), 10 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index c7542b598..3b697c34d 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -4,5 +4,6 @@ "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, - "cop": 3.0 + "heating_cop": 3.0, + "cooling_cop": 3.0 } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 039a34711..f52ef6774 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -9,7 +9,8 @@ struct ASHP <: AbstractThermalTech macrs_bonus_fraction::Real can_supply_steam_turbine::Bool #cop::Array{<:Real,1} = Real[] - cop::Real + heating_cop::Real + cooling_cop::Real can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -33,6 +34,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) + heating_cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) + cooling_cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load @@ -48,7 +51,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, #cop::Array{<:Real,1} = Real[], - cop::Real, + heating_cop::Real, + cooling_cop::Real, can_serve_dhw::Bool = true, can_serve_space_heating::Bool = true, can_serve_process_heat::Bool = true @@ -66,8 +70,11 @@ function ASHP(; if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] end - if isnothing(cop) - cop = defaults["cop"] + if isnothing(heating_cop) + heating_cop = defaults["heating_cop"] + end + if isnothing(cooling_cop) + cooling_cop = defaults["cooling_cop"] end # Convert max sizes, cost factors from mmbtu_per_hour to kw @@ -86,7 +93,8 @@ function ASHP(; macrs_option_years, macrs_bonus_fraction, can_supply_steam_turbine, - cop, + heating_cop, + cooling_cop, can_serve_dhw, can_serve_space_heating, can_serve_process_heat diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index bc93f2af0..ba5d63af5 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -429,7 +429,7 @@ function setup_tech_inputs(s::AbstractScenario) end if "ASHP" in techs.all - setup_airsource_hp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) else heating_cop["ASHP"] = 3.0 cooling_cop["ASHP"] = 3.0 @@ -890,12 +890,12 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end -function setup_airsource_hp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) +function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) max_sizes["ASHP"] = s.ashp.max_kw min_sizes["ASHP"] = s.ashp.min_kw om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw - heating_cop["ASHP"] = s.ashp.cop - cooling_cop["ASHP"] = s.ashp.cop + heating_cop["ASHP"] = s.ashp.heating_cop + cooling_cop["ASHP"] = s.ashp.cooling_cop if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; From ccc78277922c4b94401f1c07bacbb63894f05507 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 28 Feb 2024 11:00:08 -0700 Subject: [PATCH 015/266] fix minor name error --- src/results/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index aac7b2d3e..50cd8e2b1 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -32,7 +32,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPThermalProductionSeries[ts in p.time_steps], sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp)) # TODO add cooling r["thermal_production_series_mmbtu_per_hour"] = - round.(value.(ASHPProductionSeries) / KWH_PER_MMBTU, digits=5) + round.(value.(ASHPThermalProductionSeries) / KWH_PER_MMBTU, digits=5) r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) if !isempty(p.s.storage.types.hot) From 9ae9feedbdf89aa1ba93d21b172bc46bdb073461 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 28 Feb 2024 11:58:45 -0700 Subject: [PATCH 016/266] minor name changes --- data/ashp/ashp_defaults.json | 4 ++-- src/core/ashp.jl | 24 ++++++++++++------------ src/core/reopt_inputs.jl | 4 ++-- 3 files changed, 16 insertions(+), 16 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 3b697c34d..61245e978 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -4,6 +4,6 @@ "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, - "heating_cop": 3.0, - "cooling_cop": 3.0 + "cop_heating": 3.0, + "cop_cooling": 3.0 } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index f52ef6774..faa075395 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -9,8 +9,8 @@ struct ASHP <: AbstractThermalTech macrs_bonus_fraction::Real can_supply_steam_turbine::Bool #cop::Array{<:Real,1} = Real[] - heating_cop::Real - cooling_cop::Real + cop_heating::Real + cop_cooling::Real can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -34,8 +34,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - heating_cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - cooling_cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) + cop_heating::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) + cop_cooling::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load @@ -51,8 +51,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, #cop::Array{<:Real,1} = Real[], - heating_cop::Real, - cooling_cop::Real, + cop_heating::Real, + cop_cooling::Real, can_serve_dhw::Bool = true, can_serve_space_heating::Bool = true, can_serve_process_heat::Bool = true @@ -70,11 +70,11 @@ function ASHP(; if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] end - if isnothing(heating_cop) - heating_cop = defaults["heating_cop"] + if isnothing(cop_heating) + cop_heating = defaults["cop_heating"] end - if isnothing(cooling_cop) - cooling_cop = defaults["cooling_cop"] + if isnothing(cop_cooling) + cop_cooling = defaults["cop_cooling"] end # Convert max sizes, cost factors from mmbtu_per_hour to kw @@ -93,8 +93,8 @@ function ASHP(; macrs_option_years, macrs_bonus_fraction, can_supply_steam_turbine, - heating_cop, - cooling_cop, + cop_heating, + cop_cooling, can_serve_dhw, can_serve_space_heating, can_serve_process_heat diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index ba5d63af5..a963c71a7 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -894,8 +894,8 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ max_sizes["ASHP"] = s.ashp.max_kw min_sizes["ASHP"] = s.ashp.min_kw om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw - heating_cop["ASHP"] = s.ashp.heating_cop - cooling_cop["ASHP"] = s.ashp.cooling_cop + heating_cop["ASHP"] = s.ashp.cop_heating + cooling_cop["ASHP"] = s.ashp.cop_cooling if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; From ab5b5c7651e2d8d3296b38bfd9f98f5c479e3107 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 5 Mar 2024 15:15:41 -0700 Subject: [PATCH 017/266] add heating COP curve for ASHP --- src/core/ashp.jl | 29 ++++++++++++++++------------- src/core/reopt_inputs.jl | 6 +++--- src/core/scenario.jl | 38 +++++++++++++++++++++++++++++++++++++- 3 files changed, 56 insertions(+), 17 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index faa075395..9d7d8fc36 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -8,9 +8,10 @@ struct ASHP <: AbstractThermalTech macrs_option_years::Int macrs_bonus_fraction::Real can_supply_steam_turbine::Bool - #cop::Array{<:Real,1} = Real[] - cop_heating::Real - cop_cooling::Real + #cop_heating::Real + #cop_cooling::Real + cop_heating::Vector{<:Real} + cop_cooling::Vector{<:Real} can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -34,8 +35,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - cop_heating::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) + cop_heating::Vector{<:Real}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_cooling::Vector{<:Real}, # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load @@ -51,8 +52,10 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, #cop::Array{<:Real,1} = Real[], - cop_heating::Real, - cop_cooling::Real, + #cop_heating::Real, + #cop_cooling::Real, + cop_heating::Vector{<:Real}, + cop_cooling::Vector{<:Real}, can_serve_dhw::Bool = true, can_serve_space_heating::Bool = true, can_serve_process_heat::Bool = true @@ -70,12 +73,12 @@ function ASHP(; if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] end - if isnothing(cop_heating) - cop_heating = defaults["cop_heating"] - end - if isnothing(cop_cooling) - cop_cooling = defaults["cop_cooling"] - end + #if isnothing(cop_heating) + # cop_heating = defaults["cop_heating"] + #end + #if isnothing(cop_cooling) + # cop_cooling = defaults["cop_cooling"] + #end # Convert max sizes, cost factors from mmbtu_per_hour to kw min_kw = min_mmbtu_per_hour * KWH_PER_MMBTU diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index a963c71a7..e3f285ea2 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -430,9 +430,9 @@ function setup_tech_inputs(s::AbstractScenario) if "ASHP" in techs.all setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) - else - heating_cop["ASHP"] = 3.0 - cooling_cop["ASHP"] = 3.0 + #else + # heating_cop["ASHP"] = 3.0 + # cooling_cop["ASHP"] = 3.0 end # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in diff --git a/src/core/scenario.jl b/src/core/scenario.jl index a283fd870..d8500d876 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -629,13 +629,49 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end end + # Electric Heater electric_heater = nothing if haskey(d, "ElectricHeater") && d["ElectricHeater"]["max_mmbtu_per_hour"] > 0.0 electric_heater = ElectricHeater(;dictkeys_tosymbols(d["ElectricHeater"])...) end - ashp = nothing + # ASHP + #ashp = nothing + cop_heating = [] + cop_cooling = [] if haskey(d, "ASHP") && d["ASHP"]["max_mmbtu_per_hour"] > 0.0 + # If user does not provide heating cop series then assign cop curves based on ambient temperature + if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") + # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor + 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 + else + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + end + + if !haskey(d["ASHP"], "cop_heating") + cop_heating = 1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696 + else + cop_heating = d["ASHP"]["cop_heating"] + end + + if !haskey(d["ASHP"], "cop_cooling") # need to update (do we have diff curve for cooling cop?) + cop_cooling = 1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696 + else + cop_cooling = d["ASHP"]["cop_cooling"] + end + else + # Else if the user already provide cop series, use that + cop_heating = d["ASHP"]["cop_heating"] + cop_cooling = d["ASHP"]["cop_cooling"] + end + d["ASHP"]["cop_heating"] = cop_heating + d["ASHP"]["cop_cooling"] = cop_cooling ashp = ASHP(;dictkeys_tosymbols(d["ASHP"])...) end From 1c4ddb73d53801f17e218fbb68fbdf33fe975b5a Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 5 Mar 2024 22:12:01 -0700 Subject: [PATCH 018/266] add ashp COP curve --- src/constraints/load_balance.jl | 4 ++-- src/core/ashp.jl | 14 ++++++-------- src/core/bau_inputs.jl | 4 ++-- src/core/reopt_inputs.jl | 19 ++++++++----------- src/core/scenario.jl | 2 +- src/results/ashp.jl | 5 +++-- test/runtests.jl | 2 +- 7 files changed, 23 insertions(+), 27 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index e8fd5522b..ca87bf238 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -14,7 +14,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) #need to add cooling + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t, ts] for q in p.heating_loads, t in p.techs.ashp) #need to add cooling + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) @@ -31,7 +31,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t] for q in p.heating_loads, t in p.techs.ashp) # need to add cooling + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t, ts] for q in p.heating_loads, t in p.techs.ashp) # need to add cooling + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 9d7d8fc36..cccd513ae 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -8,10 +8,8 @@ struct ASHP <: AbstractThermalTech macrs_option_years::Int macrs_bonus_fraction::Real can_supply_steam_turbine::Bool - #cop_heating::Real - #cop_cooling::Real - cop_heating::Vector{<:Real} - cop_cooling::Vector{<:Real} + cop_heating::Array{Float64,1} + cop_cooling::Array{Float64,1} can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -35,8 +33,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - cop_heating::Vector{<:Real}, # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Vector{<:Real}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_heating::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_cooling::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load @@ -54,8 +52,8 @@ function ASHP(; #cop::Array{<:Real,1} = Real[], #cop_heating::Real, #cop_cooling::Real, - cop_heating::Vector{<:Real}, - cop_cooling::Vector{<:Real}, + cop_heating::Array{Float64,1}, + cop_cooling::Array{Float64,1}, can_serve_dhw::Bool = true, can_serve_space_heating::Bool = true, can_serve_process_heat::Bool = true diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index f054ae167..52cf2fa12 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -28,8 +28,8 @@ function BAUInputs(p::REoptInputs) cop = Dict(t => 0.0 for t in techs.cooling) thermal_cop = Dict{String, Float64}() backup_heating_cop = Dict{String, Float64}() - heating_cop = Dict{String, Float64}() - cooling_cop = Dict{String, Float64}() + heating_cop = Dict{String, Array{Float64,1}}() + cooling_cop = Dict{String, Array{Float64,1}}() production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) tech_renewable_energy_fraction = Dict(t => 0.0 for t in techs.all) # !!! note: tech_emissions_factors are in lb / kWh of fuel burned (gets multiplied by kWh of fuel burned, not kWh electricity consumption, ergo the use of the HHV instead of fuel slope) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index e3f285ea2..a676d71a2 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -62,8 +62,8 @@ struct REoptInputs <: AbstractInputs tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) - heating_cop::Dict{String, <:Real} # (techs.ashp) - cooling_cop::Dict{String, <:Real} # (techs.ashp) + heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp) + cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) unavailability::Dict{String, Array{Float64,1}} # Dict by tech of unavailability profile end @@ -130,8 +130,8 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) - heating_cop::Dict{String, <:Real} # (techs.ashp) - cooling_cop::Dict{String, <:Real} # (techs.ashp) + heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp) + cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp) heating_loads::Vector{String} # list of heating loads heating_loads_kw::Dict{String, Array{Real,1}} # (heating_loads) heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) @@ -351,8 +351,8 @@ function setup_tech_inputs(s::AbstractScenario) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) backup_heating_cop = Dict(t => 0.0 for t in techs.electric_heater) - heating_cop = Dict(t => 0.0 for t in techs.ashp) - cooling_cop = Dict(t => 0.0 for t in techs.ashp) + heating_cop = Dict{String, Array{Float64,1}}() + cooling_cop = Dict{String, Array{Float64,1}}() # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) @@ -430,9 +430,6 @@ function setup_tech_inputs(s::AbstractScenario) if "ASHP" in techs.all setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) - #else - # heating_cop["ASHP"] = 3.0 - # cooling_cop["ASHP"] = 3.0 end # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in @@ -894,8 +891,8 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ max_sizes["ASHP"] = s.ashp.max_kw min_sizes["ASHP"] = s.ashp.min_kw om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw - heating_cop["ASHP"] = s.ashp.cop_heating - cooling_cop["ASHP"] = s.ashp.cop_cooling + #heating_cop["ASHP"] = s.ashp.cop_heating + #cooling_cop["ASHP"] = s.ashp.cop_cooling if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; diff --git a/src/core/scenario.jl b/src/core/scenario.jl index d8500d876..9c4d278ba 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -636,7 +636,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # ASHP - #ashp = nothing + ashp = nothing cop_heating = [] cop_cooling = [] if haskey(d, "ASHP") && d["ASHP"]["max_mmbtu_per_hour"] > 0.0 diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 50cd8e2b1..6a4fc49eb 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -20,10 +20,11 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU, digits=3) + r["cop_heating"] = Vector(p.heating_cop) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t] + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t,ts] for q in p.heating_loads, t in p.techs.ashp) - #+ p.hours_per_time_step * sum(m[:dvCoolingProduction][t,q,ts] / p.cooling_cop[t] + #+ p.hours_per_time_step * sum(m[:dvCoolingProduction][t,q,ts] / p.cooling_cop[t,ts] #for q in p.cooling_loads, t in p.techs.ashp) ) r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) diff --git a/test/runtests.jl b/test/runtests.jl index 046f2377b..535a0e32c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2221,7 +2221,7 @@ else # run HiGHS tests @test results["ASHP"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 300 d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] s = Scenario(d) p = REoptInputs(s) From 50748b6d6784142f59ef4f3132bcf5bb54e7e56d Mon Sep 17 00:00:00 2001 From: An Pham Date: Thu, 14 Mar 2024 14:51:51 -0600 Subject: [PATCH 019/266] add ASHP COP curves --- src/core/ashp.jl | 7 ++----- src/core/bau_inputs.jl | 4 ++-- src/core/reopt_inputs.jl | 16 ++++++++-------- src/core/scenario.jl | 12 ++++++------ test/scenarios/ashp.json | 1 - 5 files changed, 18 insertions(+), 22 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index cccd513ae..92b80c875 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -33,8 +33,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - cop_heating::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_heating::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_cooling::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load @@ -49,9 +49,6 @@ function ASHP(; macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, - #cop::Array{<:Real,1} = Real[], - #cop_heating::Real, - #cop_cooling::Real, cop_heating::Array{Float64,1}, cop_cooling::Array{Float64,1}, can_serve_dhw::Bool = true, diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 52cf2fa12..9658c1a75 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -28,8 +28,8 @@ function BAUInputs(p::REoptInputs) cop = Dict(t => 0.0 for t in techs.cooling) thermal_cop = Dict{String, Float64}() backup_heating_cop = Dict{String, Float64}() - heating_cop = Dict{String, Array{Float64,1}}() - cooling_cop = Dict{String, Array{Float64,1}}() + heating_cop = Dict(t => zeros(length(p.time_steps)) for t in techs.all) + cooling_cop = Dict(t => zeros(length(p.time_steps)) for t in techs.all) production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) tech_renewable_energy_fraction = Dict(t => 0.0 for t in techs.all) # !!! note: tech_emissions_factors are in lb / kWh of fuel burned (gets multiplied by kWh of fuel burned, not kWh electricity consumption, ergo the use of the HHV instead of fuel slope) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index a676d71a2..652ed0209 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -62,8 +62,8 @@ struct REoptInputs <: AbstractInputs tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) - heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp) - cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp) + heating_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) + cooling_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) unavailability::Dict{String, Array{Float64,1}} # Dict by tech of unavailability profile end @@ -130,8 +130,8 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) - heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp) - cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp) + heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) + cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) heating_loads::Vector{String} # list of heating loads heating_loads_kw::Dict{String, Array{Real,1}} # (heating_loads) heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) @@ -351,8 +351,8 @@ function setup_tech_inputs(s::AbstractScenario) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) backup_heating_cop = Dict(t => 0.0 for t in techs.electric_heater) - heating_cop = Dict{String, Array{Float64,1}}() - cooling_cop = Dict{String, Array{Float64,1}}() + heating_cop = Dict(t => zeros(length(s.electric_load.loads_kw)) for t in techs.ashp) + cooling_cop = Dict(t => zeros(length(s.electric_load.loads_kw)) for t in techs.ashp) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) @@ -891,8 +891,8 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ max_sizes["ASHP"] = s.ashp.max_kw min_sizes["ASHP"] = s.ashp.min_kw om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw - #heating_cop["ASHP"] = s.ashp.cop_heating - #cooling_cop["ASHP"] = s.ashp.cop_cooling + heating_cop["ASHP"] = s.ashp.cop_heating + cooling_cop["ASHP"] = s.ashp.cop_cooling if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 9c4d278ba..105727733 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -655,20 +655,20 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end if !haskey(d["ASHP"], "cop_heating") - cop_heating = 1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696 + cop_heating = round.(1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696, digits=2) else - cop_heating = d["ASHP"]["cop_heating"] + cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) end if !haskey(d["ASHP"], "cop_cooling") # need to update (do we have diff curve for cooling cop?) - cop_cooling = 1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696 + cop_cooling = round.(1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696, digits=2) else - cop_cooling = d["ASHP"]["cop_cooling"] + cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=2) end else # Else if the user already provide cop series, use that - cop_heating = d["ASHP"]["cop_heating"] - cop_cooling = d["ASHP"]["cop_cooling"] + cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) + cop_cooling = round.(d["ASHP"]["cop_cooling"],digits=2) end d["ASHP"]["cop_heating"] = cop_heating d["ASHP"]["cop_cooling"] = cop_cooling diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index e51b462be..70bdf048c 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -12,7 +12,6 @@ "ASHP": { "min_mmbtu_per_hour": 0.0, "max_mmbtu_per_hour": 100000, - "cop": 3.0, "installed_cost_per_mmbtu_per_hour": 314000, "om_cost_per_mmbtu_per_hour": 0.0, "macrs_option_years": 0, From e94c6b36b956eb7780dfce333a01e2b508be584d Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 15 Mar 2024 02:51:05 -0600 Subject: [PATCH 020/266] tested new ASHP COP curves --- src/constraints/load_balance.jl | 4 ++-- src/core/ashp.jl | 4 ++-- src/core/bau_inputs.jl | 4 ++-- src/core/reopt_inputs.jl | 18 ++++++++++-------- src/core/scenario.jl | 1 + src/results/ashp.jl | 3 +-- 6 files changed, 18 insertions(+), 16 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index ca87bf238..29445f1b4 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -14,7 +14,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t, ts] for q in p.heating_loads, t in p.techs.ashp) #need to add cooling + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) #need to add cooling + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) @@ -31,7 +31,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t, ts] for q in p.heating_loads, t in p.techs.ashp) # need to add cooling + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) # need to add cooling + p.s.electric_load.loads_kw[ts] - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 92b80c875..c08444098 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -33,8 +33,8 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) - cop_heating::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_heating::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_cooling::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index c0ffd62e7..00f2af94c 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -28,8 +28,8 @@ function BAUInputs(p::REoptInputs) cop = Dict(t => 0.0 for t in techs.cooling) thermal_cop = Dict{String, Float64}() backup_heating_cop = Dict{String, Float64}() - heating_cop = Dict(t => zeros(length(p.time_steps)) for t in techs.all) - cooling_cop = Dict(t => zeros(length(p.time_steps)) for t in techs.all) + heating_cop = Dict{String, Array{Float64,1}}() + cooling_cop = Dict{String, Array{Float64,1}}() production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) tech_renewable_energy_fraction = Dict(t => 0.0 for t in techs.all) # !!! note: tech_emissions_factors are in lb / kWh of fuel burned (gets multiplied by kWh of fuel burned, not kWh electricity consumption, ergo the use of the HHV instead of fuel slope) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 734e72851..1e18ef2ee 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -62,8 +62,8 @@ struct REoptInputs <: AbstractInputs tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) - heating_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) - cooling_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) + heating_cop::Dict{String, Array{<:Real, 2}} # (techs.ashp) + cooling_cop::Dict{String, Array{<:Real, 2}} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) unavailability::Dict{String, Array{Float64,1}} # Dict by tech of unavailability profile end @@ -131,7 +131,7 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) - cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) + cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) heating_loads::Vector{String} # list of heating loads heating_loads_kw::Dict{String, Array{Real,1}} # (heating_loads) heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) @@ -172,7 +172,7 @@ function REoptInputs(s::AbstractScenario) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, cop, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - backup_heating_cop, heating_cop, cooling_cop = setup_tech_inputs(s) + backup_heating_cop, heating_cop, cooling_cop = setup_tech_inputs(s,time_steps) pbi_pwf, pbi_max_benefit, pbi_max_kw, pbi_benefit_per_kwh = setup_pbi_inputs(s, techs) @@ -336,9 +336,9 @@ end Create data arrays associated with techs necessary to build the JuMP model. """ -function setup_tech_inputs(s::AbstractScenario) +function setup_tech_inputs(s::AbstractScenario, time_steps) #TODO: create om_cost_per_kwh in here as well as om_cost_per_kw? (Generator, CHP, SteamTurbine, and Boiler have this) - + techs = Techs(s) boiler_efficiency = Dict{String, Float64}() @@ -361,8 +361,8 @@ function setup_tech_inputs(s::AbstractScenario) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) backup_heating_cop = Dict(t => 0.0 for t in techs.electric_heater) - heating_cop = Dict(t => zeros(length(s.electric_load.loads_kw)) for t in techs.ashp) - cooling_cop = Dict(t => zeros(length(s.electric_load.loads_kw)) for t in techs.ashp) + heating_cop = Dict(t => zeros(length(time_steps)) for t in techs.ashp) + cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.ashp) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) @@ -903,6 +903,8 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw heating_cop["ASHP"] = s.ashp.cop_heating cooling_cop["ASHP"] = s.ashp.cop_cooling + #heating_cop = s.ashp.cop_heating + #cooling_cop = s.ashp.cop_heating if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 53dc41cd7..3f33f0709 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -26,6 +26,7 @@ struct Scenario <: AbstractScenario steam_turbine::Union{SteamTurbine, Nothing} electric_heater::Union{ElectricHeater, Nothing} ashp::Union{ASHP, Nothing} + #ashp::Array{Union{ASHP, Nothing}, 1} end """ diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 6a4fc49eb..4bc45c95a 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -20,9 +20,8 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU, digits=3) - r["cop_heating"] = Vector(p.heating_cop) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t,ts] + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] #p.heating_cop[t,ts] for q in p.heating_loads, t in p.techs.ashp) #+ p.hours_per_time_step * sum(m[:dvCoolingProduction][t,q,ts] / p.cooling_cop[t,ts] #for q in p.cooling_loads, t in p.techs.ashp) From ce8cae0771345a7e76683b49a2cb26a0f7915b64 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 15 Mar 2024 11:15:56 -0600 Subject: [PATCH 021/266] updated linear ASHP COP curve --- src/core/scenario.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 3f33f0709..f11e3c994 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -671,13 +671,13 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end if !haskey(d["ASHP"], "cop_heating") - cop_heating = round.(1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696, digits=2) + cop_heating = round.(0.083 .*ambient_temp_celcius .+ 2.8255, digits=2) else cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) end if !haskey(d["ASHP"], "cop_cooling") # need to update (do we have diff curve for cooling cop?) - cop_cooling = round.(1e-08.*ambient_temp_celcius.^4 - 2e-05.*ambient_temp_celcius.^3 - 0.0007.*ambient_temp_celcius.^2 + 0.0897.*ambient_temp_celcius .+ 3.7696, digits=2) + cop_cooling = round.(-0.08.*ambient_temp_celcius .+ 5.4, digits=2) else cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=2) end From 6d606fbe34d0a5554fa6cfbe8eae5df4b2c0319e Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 26 Mar 2024 17:23:54 -0600 Subject: [PATCH 022/266] update defaults using Grant's numbers --- data/ashp/ashp_defaults.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 61245e978..ca26bd968 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -1,6 +1,6 @@ { - "installed_cost_per_mmbtu_per_hour": 314444, - "om_cost_per_mmbtu_per_hour": 0.0, + "installed_cost_per_mmbtu_per_hour": 337500, + "om_cost_per_mmbtu_per_hour": 0.02, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, From 608cece245e2b3a69be918589d8fa7a6ef5b108b Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 12:12:00 -0600 Subject: [PATCH 023/266] add empty ashp_techs field to MPC Techs object --- src/core/techs.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/techs.jl b/src/core/techs.jl index 6ee346276..e15a259bc 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -367,6 +367,7 @@ function Techs(s::MPCScenario) String[], String[], String[], + String[], String[] ) end \ No newline at end of file From 1fce12990a12836af5d673ff0d639e928990a9c7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 13:13:06 -0600 Subject: [PATCH 024/266] rm cop from docstrings --- src/core/ashp.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index c08444098..57c952658 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -32,7 +32,6 @@ function ASHP(; macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production - cop::Array{<:Real,1} = Real[], # COP of the heating (i.e., thermal produced / electricity consumed) cop_heating::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) cop_cooling::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load From 9536fa384841c17217b9ff87c27fdfc39a417bd7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 13:14:19 -0600 Subject: [PATCH 025/266] allow nothing for ASHP load-serving booleans --- src/core/ashp.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 57c952658..fc99fa1c0 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -33,10 +33,10 @@ function ASHP(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop_heating::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) - can_serve_dhw::Bool = true # If ASHP can supply heat to the domestic hot water load - can_serve_space_heating::Bool = true # If ASHP can supply heat to the space heating load - can_serve_process_heat::Bool = true # If ASHP can supply heat to the process heating load + cop_cooling::Array{<:Real,2}, # COP of the cooling (i.e., thermal produced / electricity consumed) + can_serve_dhw::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the domestic hot water load + can_serve_space_heating::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the space heating load + can_serve_process_heat::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the process heating load ) ``` """ @@ -50,9 +50,9 @@ function ASHP(; can_supply_steam_turbine::Union{Bool, Nothing} = nothing, cop_heating::Array{Float64,1}, cop_cooling::Array{Float64,1}, - can_serve_dhw::Bool = true, - can_serve_space_heating::Bool = true, - can_serve_process_heat::Bool = true + can_serve_dhw::Union{Bool, Nothing} = nothing, + can_serve_space_heating::Union{Bool, Nothing} = nothing, + can_serve_process_heat::Union{Bool, Nothing} = nothing, ) defaults = get_ashp_defaults() From b776a0a3398d7398e6f5cb31d103650cd9826d43 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 13:28:29 -0600 Subject: [PATCH 026/266] add ASHP option can_serve_cooling --- src/core/ashp.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index fc99fa1c0..58413c6f1 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -13,6 +13,7 @@ struct ASHP <: AbstractThermalTech can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool + can_serve_cooling::Bool end @@ -37,6 +38,7 @@ function ASHP(; can_serve_dhw::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the space heating load can_serve_process_heat::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the process heating load + can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load ) ``` """ @@ -53,6 +55,7 @@ function ASHP(; can_serve_dhw::Union{Bool, Nothing} = nothing, can_serve_space_heating::Union{Bool, Nothing} = nothing, can_serve_process_heat::Union{Bool, Nothing} = nothing, + can_serve_cooling::Union{Bool, Nothing} = nothing ) defaults = get_ashp_defaults() @@ -94,7 +97,8 @@ function ASHP(; cop_cooling, can_serve_dhw, can_serve_space_heating, - can_serve_process_heat + can_serve_process_heat, + can_serve_cooling ) end From 0ab16fcd1e5afa4a12615401824a69cd526290e1 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 13:28:59 -0600 Subject: [PATCH 027/266] add, link ASHP defaults for load-serving booleans --- data/ashp/ashp_defaults.json | 6 +++++- src/core/ashp.jl | 13 +++++++++++++ 2 files changed, 18 insertions(+), 1 deletion(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index ca26bd968..b357b1cc2 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -5,5 +5,9 @@ "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, "cop_heating": 3.0, - "cop_cooling": 3.0 + "cop_cooling": 3.0, + "can_serve_process_heat": false, + "can_serve_dhw": false, + "can_serve_space_heating": true, + "can_serve_cooling": true } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 58413c6f1..d9a0d7ed1 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -70,6 +70,19 @@ function ASHP(; if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] end + if isnothing(can_serve_dhw) + can_serve_dhw = defaults["can_serve_dhw"] + end + if isnothing(can_serve_space_heating) + can_serve_space_heating = defaults["can_serve_space_heating"] + end + if isnothing(can_serve_process_heat) + can_serve_process_heat = defaults["can_serve_process_heat"] + end + if isnothing(can_serve_cooling) + can_serve_cooling = defaults["can_serve_cooling"] + end + #if isnothing(cop_heating) # cop_heating = defaults["cop_heating"] #end From 6a7c12c9f4ba58acd68f9616f5b13cd95fe283c2 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 13:29:25 -0600 Subject: [PATCH 028/266] condition to add ASHP to cooling_techs --- src/core/techs.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/core/techs.jl b/src/core/techs.jl index e15a259bc..a0234860d 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -282,6 +282,9 @@ function Techs(s::Scenario) if s.ashp.can_serve_process_heat push!(techs_can_serve_process_heat, "ASHP") end + if s.ashp.can_serve_cooling + push!(cooling_techs, "ASHP") + end end if s.settings.off_grid_flag From a62c8cd9d7968bf9f93852f04212c3ba88eb20ea Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 14:46:06 -0600 Subject: [PATCH 029/266] only call PVWatts for ASHP if no PV or GHP --- src/core/scenario.jl | 19 +++++++++++-------- 1 file changed, 11 insertions(+), 8 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 9578f5bf4..71dae0949 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -446,6 +446,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # GHP + ambient_temp_celcius = nothing ghp_option_list = [] 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) @@ -664,15 +665,17 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # If user does not provide heating cop series then assign cop curves based on ambient temperature if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - 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) + if isnothing(ambient_temp_celcius) + 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 + else + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - else - # if PV is not evaluated, call PVWatts to get ambient temperature series - pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end if !haskey(d["ASHP"], "cop_heating") From a8165a8553b96122751cf854608a35443f302aee Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 14:47:20 -0600 Subject: [PATCH 030/266] refactoring: ren ambient_temp_celcius ambient_temp_celsius --- src/core/production_factor.jl | 2 +- src/core/scenario.jl | 18 +++++++++--------- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/production_factor.jl b/src/core/production_factor.jl index 51b8c18e8..35e513519 100644 --- a/src/core/production_factor.jl +++ b/src/core/production_factor.jl @@ -7,7 +7,7 @@ function get_production_factor(pv::PV, latitude::Real, longitude::Real; timefram return pv.production_factor_series end - watts, ambient_temp_celcius = call_pvwatts_api(latitude, longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, + watts, ambient_temp_celsius = 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) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 71dae0949..e01ee2fc2 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -446,7 +446,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # GHP - ambient_temp_celcius = nothing + ambient_temp_celsius = nothing ghp_option_list = [] 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) @@ -486,14 +486,14 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # 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, + pv.production_factor_series, ambient_temp_celsius = 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 else - pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + pv_prodfactor, ambient_temp_celsius = 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 + ambient_temp_degF = ambient_temp_celsius * 1.8 .+ 32.0 else ambient_temp_degF = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"] end @@ -665,27 +665,27 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # If user does not provide heating cop series then assign cop curves based on ambient temperature if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celcius) + if isnothing(ambient_temp_celsius) 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, + pv.production_factor_series, ambient_temp_celsius = 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 else # if PV is not evaluated, call PVWatts to get ambient temperature series - pv_prodfactor, ambient_temp_celcius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end end if !haskey(d["ASHP"], "cop_heating") - cop_heating = round.(0.083 .*ambient_temp_celcius .+ 2.8255, digits=2) + cop_heating = round.(0.083 .*ambient_temp_celsius .+ 2.8255, digits=2) else cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) end if !haskey(d["ASHP"], "cop_cooling") # need to update (do we have diff curve for cooling cop?) - cop_cooling = round.(-0.08.*ambient_temp_celcius .+ 5.4, digits=2) + cop_cooling = round.(-0.08.*ambient_temp_celsius .+ 5.4, digits=2) else cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=2) end From 70c809270895e418c96eea208756de5dd55a47f2 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 16:10:50 -0600 Subject: [PATCH 031/266] convert cop to cooling_cop --- src/constraints/load_balance.jl | 12 ++++------ src/core/bau_inputs.jl | 7 ++---- src/core/reopt_inputs.jl | 39 ++++++++++++++------------------- 3 files changed, 23 insertions(+), 35 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 29445f1b4..aad15f1a9 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -12,11 +12,9 @@ function add_elec_load_balance_constraints(m, p; _n="") sum(sum(m[Symbol("dvProductionToStorage"*_n)][b, t, ts] for b in p.s.storage.types.elec) + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) - + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) #need to add cooling + + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) #need to add cooling + p.s.electric_load.loads_kw[ts] - - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) else @@ -29,11 +27,9 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvProductionToGrid"*_n)][t, u, ts] for u in p.export_bins_by_tech[t]) + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) - + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cop[t] for t in setdiff(p.techs.cooling,p.techs.ghp)) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.backup_heating_cop[t] for q in p.heating_loads, t in p.techs.electric_heater) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) # need to add cooling + + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) # need to add cooling + p.s.electric_load.loads_kw[ts] - - p.s.cooling_load.loads_kw_thermal[ts] / p.cop["ExistingChiller"] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) end diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 6025fdf8c..765ae5c6c 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -25,7 +25,6 @@ function BAUInputs(p::REoptInputs) existing_sizes = Dict(t => 0.0 for t in techs.all) cap_cost_slope = Dict{String, Any}() om_cost_per_kw = Dict(t => 0.0 for t in techs.all) - cop = Dict(t => 0.0 for t in techs.cooling) thermal_cop = Dict{String, Float64}() backup_heating_cop = Dict{String, Float64}() heating_cop = Dict{String, Array{Float64,1}}() @@ -95,10 +94,9 @@ function BAUInputs(p::REoptInputs) tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) end + cooling_cop["ExistingChiller"] = ones(length(time_steps)) if "ExistingChiller" in techs.all - setup_existing_chiller_inputs(bau_scenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cop) - else - cop["ExistingChiller"] = 1.0 + setup_existing_chiller_inputs(bau_scenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) end # Assign null GHP parameters for REoptInputs @@ -172,7 +170,6 @@ function BAUInputs(p::REoptInputs) existing_sizes, cap_cost_slope, om_cost_per_kw, - cop, thermal_cop, p.time_steps, p.time_steps_with_grid, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 1e18ef2ee..6c9d1db5a 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -12,7 +12,6 @@ struct REoptInputs <: AbstractInputs existing_sizes::Dict{String, <:Real} # (techs) cap_cost_slope::Dict{String, Any} # (techs) om_cost_per_kw::Dict{String, <:Real} # (techs) - cop::Dict{String, <:Real} # (techs.cooling) thermal_cop::Dict{String, <:Real} # (techs.absorption_chiller) time_steps::UnitRange time_steps_with_grid::Array{Int, 1} @@ -77,7 +76,6 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs existing_sizes::Dict{String, <:Real} # (techs) cap_cost_slope::Dict{String, Any} # (techs) om_cost_per_kw::Dict{String, <:Real} # (techs) - cop::Dict{String, <:Real} # (techs.cooling) thermal_cop::Dict{String, <:Real} # (techs.absorption_chiller) time_steps::UnitRange time_steps_with_grid::Array{Int, 1} @@ -171,7 +169,7 @@ function REoptInputs(s::AbstractScenario) production_factor, max_sizes, min_sizes, existing_sizes, cap_cost_slope, om_cost_per_kw, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, - tech_emissions_factors_PM25, cop, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, + tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, backup_heating_cop, heating_cop, cooling_cop = setup_tech_inputs(s,time_steps) pbi_pwf, pbi_max_benefit, pbi_max_kw, pbi_benefit_per_kwh = setup_pbi_inputs(s, techs) @@ -267,7 +265,6 @@ function REoptInputs(s::AbstractScenario) existing_sizes, cap_cost_slope, om_cost_per_kw, - cop, thermal_cop, time_steps, time_steps_with_grid, @@ -357,12 +354,10 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) tech_emissions_factors_NOx = Dict(t => 0.0 for t in techs.all) tech_emissions_factors_SO2 = Dict(t => 0.0 for t in techs.all) tech_emissions_factors_PM25 = Dict(t => 0.0 for t in techs.all) - cop = Dict(t => 0.0 for t in techs.cooling) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) - backup_heating_cop = Dict(t => 0.0 for t in techs.electric_heater) - heating_cop = Dict(t => zeros(length(time_steps)) for t in techs.ashp) - cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.ashp) + heating_cop = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) + cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.cooling) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) @@ -416,15 +411,15 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ExistingChiller" in techs.all - setup_existing_chiller_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cop) + setup_existing_chiller_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) else - cop["ExistingChiller"] = 1.0 + cooling_cop["ExistingChiller"] .= 1.0 end if "AbsorptionChiller" in techs.all - setup_absorption_chiller_inputs(s, max_sizes, min_sizes, cap_cost_slope, cop, thermal_cop, om_cost_per_kw) + setup_absorption_chiller_inputs(s, max_sizes, min_sizes, cap_cost_slope, cooling_cop, thermal_cop, om_cost_per_kw) else - cop["AbsorptionChiller"] = 1.0 + cooling_cop["AbsorptionChiller"] .= 1.0 thermal_cop["AbsorptionChiller"] = 1.0 end @@ -433,9 +428,9 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ElectricHeater" in techs.all - setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, backup_heating_cop) + setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) else - backup_heating_cop["ElectricHeater"] = 1.0 + heating_cop["ElectricHeater"] .= 1.0 end if "ASHP" in techs.all @@ -455,7 +450,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) production_factor, max_sizes, min_sizes, existing_sizes, cap_cost_slope, om_cost_per_kw, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, - tech_emissions_factors_PM25, cop, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, + tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, backup_heating_cop, heating_cop, cooling_cop end @@ -745,23 +740,23 @@ end """ - function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cop) + function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) Update tech-indexed data arrays necessary to build the JuMP model with the values for existing chiller. """ -function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cop) +function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) max_sizes["ExistingChiller"] = s.existing_chiller.max_kw min_sizes["ExistingChiller"] = 0.0 existing_sizes["ExistingChiller"] = 0.0 cap_cost_slope["ExistingChiller"] = 0.0 - cop["ExistingChiller"] = s.existing_chiller.cop + cooling_cop["ExistingChiller"] .= s.existing_chiller.cop # om_cost_per_kw["ExistingChiller"] = 0.0 return nothing end function setup_absorption_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, - cop, thermal_cop, om_cost_per_kw + cooling_cop, thermal_cop, om_cost_per_kw ) max_sizes["AbsorptionChiller"] = s.absorption_chiller.max_kw min_sizes["AbsorptionChiller"] = s.absorption_chiller.min_kw @@ -786,7 +781,7 @@ function setup_absorption_chiller_inputs(s::AbstractScenario, max_sizes, min_siz cap_cost_slope["AbsorptionChiller"] = s.absorption_chiller.installed_cost_per_kw end - cop["AbsorptionChiller"] = s.absorption_chiller.cop_electric + cooling_cop["AbsorptionChiller"] .= s.absorption_chiller.cop_electric if isnothing(s.chp) thermal_factor = 1.0 elseif s.chp.cooling_thermal_factor == 0.0 @@ -872,11 +867,11 @@ function setup_steam_turbine_inputs(s::AbstractScenario, max_sizes, min_sizes, c return nothing end -function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, backup_heating_cop) +function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) max_sizes["ElectricHeater"] = s.electric_heater.max_kw min_sizes["ElectricHeater"] = s.electric_heater.min_kw om_cost_per_kw["ElectricHeater"] = s.electric_heater.om_cost_per_kw - backup_heating_cop["ElectricHeater"] = s.electric_heater.cop + heating_cop["ElectricHeater"] .= s.electric_heater.cop if s.electric_heater.macrs_option_years in [5, 7] cap_cost_slope["ElectricHeater"] = effective_cost(; From 7a925c9e17c5bcc554be2fa89afbf9727f7ec000 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 16:20:46 -0600 Subject: [PATCH 032/266] add ASHP to electric_heater set --- src/constraints/storage_constraints.jl | 17 ----------------- src/core/techs.jl | 1 + 2 files changed, 1 insertion(+), 17 deletions(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index 130e015a1..3ce2549d8 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -135,23 +135,6 @@ function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="") end end end - - # # Constraint (4f)-1c: Air-source Heat Pump (ASHP) - if !isempty(p.techs.ashp) - for t in p.techs.ashp - if !isempty(p.techs.steam_turbine) && (t in p.techs.can_supply_steam_turbine) - @constraint(m, [b in p.s.storage.types.hot, q in p.heating_loads, ts in p.time_steps], - m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] + m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] <= - m[Symbol("dvHeatingProduction"*_n)][t,q,ts] - ) - else - @constraint(m, [b in p.s.storage.types.hot, q in p.heating_loads, ts in p.time_steps], - m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] <= - m[Symbol("dvHeatingProduction"*_n)][t,q,ts] - ) - end - end - end # Constraint (4f)-1d: SteamTurbineTechs if !isempty(p.techs.steam_turbine) diff --git a/src/core/techs.jl b/src/core/techs.jl index a0234860d..85e12b4dc 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -269,6 +269,7 @@ function Techs(s::Scenario) if !isnothing(s.ashp) push!(all_techs, "ASHP") push!(heating_techs, "ASHP") + push!(electric_heater, "ASHP") push!(ashp_techs, "ASHP") if s.ashp.can_supply_steam_turbine push!(techs_can_supply_steam_turbine, "ASHP") From ddac677f723c3b86303987f991526cd168054e04 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 25 Apr 2024 20:11:39 -0600 Subject: [PATCH 033/266] new constraint restrictign heating and cooling power from ASHP --- src/constraints/thermal_tech_constraints.jl | 6 ++++++ src/core/reopt.jl | 4 ++++ 2 files changed, 10 insertions(+) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index a03d2b080..1bf124878 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -50,6 +50,12 @@ function add_heating_tech_constraints(m, p; _n="") end end +function add_ashp_heating_cooling_constraints(m, p; _n="") + @constraint(m, [t in intersect(p.techs.cooling, p.techs.heating), ts in p.time_steps], + sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] + ) +end + function no_existing_boiler_production(m, p; _n="") for ts in p.time_steps for q in p.heating_loads diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 7baefaed3..930593ce1 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -313,6 +313,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) if !isempty(p.techs.cooling) add_cooling_tech_constraints(m, p) end + + if !isempty(intersect(p.techs.heating, p.techs.cooling)) + add_ashp_heating_cooling_constraints(m, p) + end if !isempty(p.techs.thermal) add_thermal_load_constraints(m, p) # split into heating and cooling constraints? From 572a6d2ec8a8ae077a6257a81f890a9a1c49e419 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:00:02 -0600 Subject: [PATCH 034/266] rm default cop's from ASHP defaults --- data/ashp/ashp_defaults.json | 2 -- 1 file changed, 2 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index b357b1cc2..009ceab89 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -4,8 +4,6 @@ "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, - "cop_heating": 3.0, - "cop_cooling": 3.0, "can_serve_process_heat": false, "can_serve_dhw": false, "can_serve_space_heating": true, From a325ff9dfec2cc7e03d41191f41b11fd35ec395e Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:16:50 -0600 Subject: [PATCH 035/266] update default inputs, rm commented text in ASHP constructor --- src/core/ashp.jl | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index d9a0d7ed1..11a97fa38 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -50,8 +50,8 @@ function ASHP(; macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, - cop_heating::Array{Float64,1}, - cop_cooling::Array{Float64,1}, + cop_heating::Array{Float64,1} = Float64[], + cop_cooling::Array{Float64,1} = Float64[], can_serve_dhw::Union{Bool, Nothing} = nothing, can_serve_space_heating::Union{Bool, Nothing} = nothing, can_serve_process_heat::Union{Bool, Nothing} = nothing, @@ -83,13 +83,6 @@ function ASHP(; can_serve_cooling = defaults["can_serve_cooling"] end - #if isnothing(cop_heating) - # cop_heating = defaults["cop_heating"] - #end - #if isnothing(cop_cooling) - # cop_cooling = defaults["cop_cooling"] - #end - # Convert max sizes, cost factors from mmbtu_per_hour to kw min_kw = min_mmbtu_per_hour * KWH_PER_MMBTU max_kw = max_mmbtu_per_hour * KWH_PER_MMBTU From 613356641cd0410d6cdad43bc7f6eadd7a12c710 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:19:36 -0600 Subject: [PATCH 036/266] rm backup_heating_cop, replace with time series heating_cop --- src/core/bau_inputs.jl | 2 -- src/core/reopt_inputs.jl | 8 ++------ src/results/electric_heater.jl | 2 +- 3 files changed, 3 insertions(+), 9 deletions(-) diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 765ae5c6c..9ca13b6f5 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -26,7 +26,6 @@ function BAUInputs(p::REoptInputs) cap_cost_slope = Dict{String, Any}() om_cost_per_kw = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict{String, Float64}() - backup_heating_cop = Dict{String, Float64}() heating_cop = Dict{String, Array{Float64,1}}() cooling_cop = Dict{String, Array{Float64,1}}() production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) @@ -221,7 +220,6 @@ function BAUInputs(p::REoptInputs) tech_emissions_factors_SO2, tech_emissions_factors_PM25, p.techs_operating_reserve_req_fraction, - backup_heating_cop, heating_cop, cooling_cop, heating_loads, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 6c9d1db5a..29bf3e4d2 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -60,7 +60,6 @@ struct REoptInputs <: AbstractInputs tech_emissions_factors_SO2::Dict{String, <:Real} # (techs) tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) - backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) heating_cop::Dict{String, Array{<:Real, 2}} # (techs.ashp) cooling_cop::Dict{String, Array{<:Real, 2}} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) @@ -170,7 +169,7 @@ function REoptInputs(s::AbstractScenario) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - backup_heating_cop, heating_cop, cooling_cop = setup_tech_inputs(s,time_steps) + heating_cop, cooling_cop = setup_tech_inputs(s,time_steps) pbi_pwf, pbi_max_benefit, pbi_max_kw, pbi_benefit_per_kwh = setup_pbi_inputs(s, techs) @@ -316,7 +315,6 @@ function REoptInputs(s::AbstractScenario) tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, - backup_heating_cop, heating_cop, cooling_cop, heating_loads, @@ -451,7 +449,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - backup_heating_cop, heating_cop, cooling_cop + heating_cop, cooling_cop end @@ -898,8 +896,6 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw heating_cop["ASHP"] = s.ashp.cop_heating cooling_cop["ASHP"] = s.ashp.cop_cooling - #heating_cop = s.ashp.cop_heating - #cooling_cop = s.ashp.cop_heating if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; diff --git a/src/results/electric_heater.jl b/src/results/electric_heater.jl index a12e81fc7..be16ad2e1 100644 --- a/src/results/electric_heater.jl +++ b/src/results/electric_heater.jl @@ -21,7 +21,7 @@ function add_electric_heater_results(m::JuMP.AbstractModel, p::REoptInputs, d::D r = Dict{String, Any}() r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ElectricHeater"]) / KWH_PER_MMBTU, digits=3) @expression(m, ElectricHeaterElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.backup_heating_cop[t] + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater)) r["electric_consumption_series_kw"] = round.(value.(ElectricHeaterElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = sum(r["electric_consumption_series_kw"]) From 175491118e90442be56ea9f6d8b54ef5e3055978 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:21:08 -0600 Subject: [PATCH 037/266] change cop to cooling_cop in cooling tech results --- src/results/absorption_chiller.jl | 4 ++-- src/results/existing_chiller.jl | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/results/absorption_chiller.jl b/src/results/absorption_chiller.jl index 86f424b99..8bdd47069 100644 --- a/src/results/absorption_chiller.jl +++ b/src/results/absorption_chiller.jl @@ -44,10 +44,10 @@ function add_absorption_chiller_results(m::JuMP.AbstractModel, p::REoptInputs, d for t in p.techs.absorption_chiller, ts in p.time_steps)) r["annual_thermal_production_tonhour"] = round(value(Year1ABSORPCHLThermalProdKWH) / KWH_THERMAL_PER_TONHOUR, digits=5) @expression(m, ABSORPCHLElectricConsumptionSeries[ts in p.time_steps], - sum(m[:dvCoolingProduction][t,ts] / p.cop[t] for t in p.techs.absorption_chiller) ) + sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] for t in p.techs.absorption_chiller) ) r["electric_consumption_series_kw"] = round.(value.(ABSORPCHLElectricConsumptionSeries), digits=3) @expression(m, Year1ABSORPCHLElectricConsumption, - p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cop[t] + p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] for t in p.techs.absorption_chiller, ts in p.time_steps)) r["annual_electric_consumption_kwh"] = round(value(Year1ABSORPCHLElectricConsumption), digits=3) diff --git a/src/results/existing_chiller.jl b/src/results/existing_chiller.jl index 7ea5eb592..284848209 100644 --- a/src/results/existing_chiller.jl +++ b/src/results/existing_chiller.jl @@ -23,12 +23,12 @@ function add_existing_chiller_results(m::JuMP.AbstractModel, p::REoptInputs, d:: r["thermal_to_load_series_ton"] = round.(value.(ELECCHLtoLoad / KWH_THERMAL_PER_TONHOUR).data, digits=3) @expression(m, ELECCHLElecConsumptionSeries[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cop["ExistingChiller"]) + sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cooling_cop["ExistingChiller", ts]) ) r["electric_consumption_series_kw"] = round.(value.(ELECCHLElecConsumptionSeries).data, digits=3) @expression(m, Year1ELECCHLElecConsumption, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cop["ExistingChiller"] + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cooling_cop["ExistingChiller", ts] for ts in p.time_steps) ) r["annual_electric_consumption_kwh"] = round(value(Year1ELECCHLElecConsumption), digits=3) From b5031e6900c3853811ffc0adc9d3304f4fb5d0ba Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:22:02 -0600 Subject: [PATCH 038/266] fixes in ASHP-specific inputs --- src/core/reopt_inputs.jl | 13 +++++++------ src/core/scenario.jl | 1 - src/core/techs.jl | 2 +- 3 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 29bf3e4d2..f8b1d6fc1 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -60,8 +60,8 @@ struct REoptInputs <: AbstractInputs tech_emissions_factors_SO2::Dict{String, <:Real} # (techs) tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) - heating_cop::Dict{String, Array{<:Real, 2}} # (techs.ashp) - cooling_cop::Dict{String, Array{<:Real, 2}} # (techs.ashp) + heating_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) + cooling_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) unavailability::Dict{String, Array{Float64,1}} # Dict by tech of unavailability profile end @@ -126,7 +126,6 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs tech_emissions_factors_SO2::Dict{String, <:Real} # (techs) tech_emissions_factors_PM25::Dict{String, <:Real} # (techs) techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) - backup_heating_cop::Dict{String, <:Real} # (techs.electric_heater) heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) heating_loads::Vector{String} # list of heating loads @@ -411,13 +410,13 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) if "ExistingChiller" in techs.all setup_existing_chiller_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) else - cooling_cop["ExistingChiller"] .= 1.0 + cooling_cop["ExistingChiller"] = ones(length(time_steps)) end if "AbsorptionChiller" in techs.all setup_absorption_chiller_inputs(s, max_sizes, min_sizes, cap_cost_slope, cooling_cop, thermal_cop, om_cost_per_kw) else - cooling_cop["AbsorptionChiller"] .= 1.0 + cooling_cop["AbsorptionChiller"] = ones(length(time_steps)) thermal_cop["AbsorptionChiller"] = 1.0 end @@ -428,11 +427,13 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) if "ElectricHeater" in techs.all setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) else - heating_cop["ElectricHeater"] .= 1.0 + heating_cop["ElectricHeater"] = ones(length(time_steps)) end if "ASHP" in techs.all setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + else + heating_cop["ASHP"] = ones(length(time_steps)) end # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in diff --git a/src/core/scenario.jl b/src/core/scenario.jl index e01ee2fc2..6135979fc 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -26,7 +26,6 @@ struct Scenario <: AbstractScenario steam_turbine::Union{SteamTurbine, Nothing} electric_heater::Union{ElectricHeater, Nothing} ashp::Union{ASHP, Nothing} - #ashp::Array{Union{ASHP, Nothing}, 1} end """ diff --git a/src/core/techs.jl b/src/core/techs.jl index 85e12b4dc..3c3996495 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -269,7 +269,7 @@ function Techs(s::Scenario) if !isnothing(s.ashp) push!(all_techs, "ASHP") push!(heating_techs, "ASHP") - push!(electric_heater, "ASHP") + push!(electric_heaters, "ASHP") push!(ashp_techs, "ASHP") if s.ashp.can_supply_steam_turbine push!(techs_can_supply_steam_turbine, "ASHP") From 05511f47543f5b7fd32c59adc700e198e661452d Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:22:43 -0600 Subject: [PATCH 039/266] add cooling to ASHP electric consumption stats --- src/results/ashp.jl | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 4bc45c95a..97de9b0ae 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -23,9 +23,13 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] #p.heating_cop[t,ts] for q in p.heating_loads, t in p.techs.ashp) - #+ p.hours_per_time_step * sum(m[:dvCoolingProduction][t,q,ts] / p.cooling_cop[t,ts] - #for q in p.cooling_loads, t in p.techs.ashp) ) + if "ASHP" in p.techs.cooling + add_to_expression!(m, ASHPElectricConsumptionSeries[ts in p.time_steps], + p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] + for t in p.techs.ashp) + ) + end r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = sum(r["electric_consumption_series_kw"]) From 2b34fe924b4c38e7b2f0793a4373e701180b7e88 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 14:23:10 -0600 Subject: [PATCH 040/266] check for "ElectricHeater" before generating electric heater results --- src/results/results.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/results.jl b/src/results/results.jl index c772de82c..2cea92a79 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -100,7 +100,7 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") add_steam_turbine_results(m, p, d; _n) end - if !isempty(p.techs.electric_heater) + if "ElectricHeater" in p.techs.electric_heater add_electric_heater_results(m, p, d; _n) end From 6069afb019d771765530af1b18fa364ace9fa69a Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 22:59:50 -0600 Subject: [PATCH 041/266] fix cooling_cop references --- src/results/existing_chiller.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/results/existing_chiller.jl b/src/results/existing_chiller.jl index 284848209..63fb81603 100644 --- a/src/results/existing_chiller.jl +++ b/src/results/existing_chiller.jl @@ -23,12 +23,12 @@ function add_existing_chiller_results(m::JuMP.AbstractModel, p::REoptInputs, d:: r["thermal_to_load_series_ton"] = round.(value.(ELECCHLtoLoad / KWH_THERMAL_PER_TONHOUR).data, digits=3) @expression(m, ELECCHLElecConsumptionSeries[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cooling_cop["ExistingChiller", ts]) + sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cooling_cop["ExistingChiller"][ts]) ) r["electric_consumption_series_kw"] = round.(value.(ELECCHLElecConsumptionSeries).data, digits=3) @expression(m, Year1ELECCHLElecConsumption, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cooling_cop["ExistingChiller", ts] + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] / p.cooling_cop["ExistingChiller"][ts] for ts in p.time_steps) ) r["annual_electric_consumption_kwh"] = round(value(Year1ELECCHLElecConsumption), digits=3) From 4b4cd53fda79c585cd1f182be61afd79e7c62cef Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 23:00:12 -0600 Subject: [PATCH 042/266] add cooling results for ASHP --- src/results/ashp.jl | 53 +++++++++++++++++++++++++++++++++++---------- 1 file changed, 41 insertions(+), 12 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 97de9b0ae..74a92018d 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -10,6 +10,12 @@ - `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] - `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] - `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] +- `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage +- `thermal_to_load_series_ton` # Thermal production to cooling load +- `electric_consumption_series_kw` +- `annual_electric_consumption_kwh` +- `annual_thermal_production_tonhour` + !!! note "'Series' and 'Annual' energy outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. @@ -23,15 +29,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] #p.heating_cop[t,ts] for q in p.heating_loads, t in p.techs.ashp) - ) - if "ASHP" in p.techs.cooling - add_to_expression!(m, ASHPElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] - for t in p.techs.ashp) - ) - end - r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) - r["annual_electric_consumption_kwh"] = sum(r["electric_consumption_series_kw"]) + ) @expression(m, ASHPThermalProductionSeries[ts in p.time_steps], sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp)) # TODO add cooling @@ -42,10 +40,10 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if !isempty(p.s.storage.types.hot) @expression(m, ASHPToHotTESKW[ts in p.time_steps], sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) - ) - @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], + ) + @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot) - ) + ) else @expression(m, ASHPToHotTESKW[ts in p.time_steps], 0.0) @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) @@ -92,6 +90,37 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], 0.0) end r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) + + if "ASHP" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 + + @expression(m, ASHPtoColdTES[ts in p.time_steps], + sum(m[:dvProductionToStorage][b,"ExistingChiller",ts] for b in p.s.storage.types.cold) + ) + r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES / KWH_THERMAL_PER_TONHOUR), digits=3) + + @expression(m, ASHPtoColdLoad[ts in p.time_steps], + sum(m[:dvCoolingProduction]["ExistingChiller", ts]) + - ASHPtoColdTES[ts] + ) + r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad / KWH_THERMAL_PER_TONHOUR).data, digits=3) + + @expression(m, Year1ELECCHLThermalProd, + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] + for ts in p.time_steps) + ) + r["annual_thermal_production_tonhour"] = round(value(Year1ELECCHLThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) + + add_to_expression!(m, ASHPElectricConsumptionSeries[ts in p.time_steps], + p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] + for t in p.techs.ashp) + ) + else + r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) + r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) + r["annual_thermal_production_tonhour"] = 0.0 + end + r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) + r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) d["ASHP"] = r nothing From df121cea36337b0dfcf0ef9a41c270313d204df2 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 26 Apr 2024 23:01:32 -0600 Subject: [PATCH 043/266] update ASHP test JSON --- test/scenarios/ashp.json | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 70bdf048c..08c373a1b 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -16,7 +16,10 @@ "om_cost_per_mmbtu_per_hour": 0.0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, - "can_supply_steam_turbine": false + "can_supply_steam_turbine": false, + "can_serve_space_heating": true, + "can_serve_dhw": true, + "can_serve_cooling": false }, "Financial": { "om_cost_escalation_rate_fraction": 0.025, From 8b5835b78c79c45eb791bfa5b895f4ca36d5664b Mon Sep 17 00:00:00 2001 From: Zolan Date: Sat, 27 Apr 2024 23:19:10 -0600 Subject: [PATCH 044/266] Add symbolic names to constraints (similar to other parts of formulation) --- src/constraints/load_balance.jl | 12 ++++-------- src/results/ashp.jl | 21 +++++++++++---------- 2 files changed, 15 insertions(+), 18 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index aad15f1a9..1a54168fc 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -4,7 +4,7 @@ function add_elec_load_balance_constraints(m, p; _n="") ##Constraint (8a): Electrical Load Balancing with Grid if isempty(p.s.electric_tariff.export_bins) - conrefs = @constraint(m, [ts in p.time_steps_with_grid], + @constraint(m, ElecLoadBalanceCon[ts in p.time_steps_with_grid], sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts] for t in p.techs.elec) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) @@ -18,7 +18,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) else - conrefs = @constraint(m, [ts in p.time_steps_with_grid], + @constraint(m, ElecLoadBalanceCon[ts in p.time_steps_with_grid], sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts] for t in p.techs.elec) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.elec ) + sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) @@ -33,14 +33,10 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) end - - for (i, cr) in enumerate(conrefs) - JuMP.set_name(cr, "con_load_balance"*_n*string("_t", i)) - end ##Constraint (8b): Electrical Load Balancing without Grid if !p.s.settings.off_grid_flag # load balancing constraint for grid-connected runs - @constraint(m, [ts in p.time_steps_without_grid], + @constraint(m, ElecLoadBalanceOffgridCon[ts in p.time_steps_without_grid], sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts] for t in p.techs.elec) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.elec) == @@ -143,7 +139,7 @@ function add_thermal_load_constraints(m, p; _n="") if !isempty(p.techs.cooling) ##Constraint (5a): Cold thermal loads - @constraint(m, [ts in p.time_steps_with_grid], + @constraint(m, ColdLoadBalanceCon[ts in p.time_steps_with_grid], sum(m[Symbol("dvCoolingProduction"*_n)][t,ts] for t in p.techs.cooling) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.cold) == diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 74a92018d..99bdc1b36 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -62,7 +62,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPToLoad[ts in p.time_steps], sum(m[:dvHeatingProduction]["ASHP", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] ) - r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) / KWH_PER_MMBTU, digits=3) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp.can_serve_dhw @expression(m, ASHPToDHWKW[ts in p.time_steps], @@ -94,23 +94,23 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if "ASHP" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 @expression(m, ASHPtoColdTES[ts in p.time_steps], - sum(m[:dvProductionToStorage][b,"ExistingChiller",ts] for b in p.s.storage.types.cold) + sum(m[:dvProductionToStorage][b,"ASHP",ts] for b in p.s.storage.types.cold) ) - r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES / KWH_THERMAL_PER_TONHOUR), digits=3) + r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES ./ KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPtoColdLoad[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ExistingChiller", ts]) + sum(m[:dvCoolingProduction]["ASHP", ts]) - ASHPtoColdTES[ts] ) - r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad / KWH_THERMAL_PER_TONHOUR).data, digits=3) + r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad ./ KWH_THERMAL_PER_TONHOUR).data, digits=3) - @expression(m, Year1ELECCHLThermalProd, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] + @expression(m, Year1ASHPColdThermalProd, + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHP", ts] for ts in p.time_steps) ) - r["annual_thermal_production_tonhour"] = round(value(Year1ELECCHLThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) + r["annual_thermal_production_tonhour"] = round(value(Year1ASHPColdThermalProd ./ KWH_THERMAL_PER_TONHOUR), digits=3) - add_to_expression!(m, ASHPElectricConsumptionSeries[ts in p.time_steps], + @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] for t in p.techs.ashp) ) @@ -118,8 +118,9 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) r["annual_thermal_production_tonhour"] = 0.0 + @expression(m, ASHPColdElectricConsumptionSeries, 0.0) end - r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) + r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) d["ASHP"] = r From 378f38bf9cd7040427de3a7a8405b21235e88ac9 Mon Sep 17 00:00:00 2001 From: Zolan Date: Sat, 27 Apr 2024 23:19:54 -0600 Subject: [PATCH 045/266] Update ASHP tests and add new test case --- test/runtests.jl | 36 ++++++++++++++++++++++++++++-------- 1 file changed, 28 insertions(+), 8 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index b529b0034..85e1fcae3 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2298,33 +2298,53 @@ else # run HiGHS tests end @testset "ASHP" begin + #Case 1: Boiler produces the required heat instead of ASHP - ASHP is not purchased d = JSON.parsefile("./scenarios/ashp.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - #first run: Boiler produces the required heat instead of ASHP - ASHP is invested here + d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1e8 + + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + #Case 2: ASHP has temperature-dependent output and serves all heating load d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 300 d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] - + s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP + annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - - #Second run: ASHP produces the required heat with free electricity @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 + + #Case 3: ASHP can serve cooling, add cooling load + d["CoolingLoad"] = Dict("thermal_loads_ton" => ones(8760)*0.1) + d["ExistingChiller"] = Dict("cop" => 0.5) + d["ASHP"]["can_serve_cooling"] = true + + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + annual_ashp_consumption += sum(0.1 * REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) + annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR + @test results["ASHP"]["size_mmbtu_per_hour"] > 0.8 #size increases when cooling load also served + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 end @testset "Process Heat Load" begin From dac9b34b16f7d374269bd5d6b40319b1429a71b1 Mon Sep 17 00:00:00 2001 From: Zolan Date: Sun, 28 Apr 2024 07:33:37 -0600 Subject: [PATCH 046/266] Revert "Add symbolic names to constraints (similar to other parts of formulation)" This reverts commit 8b5835b78c79c45eb791bfa5b895f4ca36d5664b. --- src/constraints/load_balance.jl | 12 ++++++++---- src/results/ashp.jl | 21 ++++++++++----------- 2 files changed, 18 insertions(+), 15 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 1a54168fc..aad15f1a9 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -4,7 +4,7 @@ function add_elec_load_balance_constraints(m, p; _n="") ##Constraint (8a): Electrical Load Balancing with Grid if isempty(p.s.electric_tariff.export_bins) - @constraint(m, ElecLoadBalanceCon[ts in p.time_steps_with_grid], + conrefs = @constraint(m, [ts in p.time_steps_with_grid], sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts] for t in p.techs.elec) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) @@ -18,7 +18,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) else - @constraint(m, ElecLoadBalanceCon[ts in p.time_steps_with_grid], + conrefs = @constraint(m, [ts in p.time_steps_with_grid], sum(p.production_factor[t, ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts] for t in p.techs.elec) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.elec ) + sum(m[Symbol("dvGridPurchase"*_n)][ts, tier] for tier in 1:p.s.electric_tariff.n_energy_tiers) @@ -33,10 +33,14 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) end + + for (i, cr) in enumerate(conrefs) + JuMP.set_name(cr, "con_load_balance"*_n*string("_t", i)) + end ##Constraint (8b): Electrical Load Balancing without Grid if !p.s.settings.off_grid_flag # load balancing constraint for grid-connected runs - @constraint(m, ElecLoadBalanceOffgridCon[ts in p.time_steps_without_grid], + @constraint(m, [ts in p.time_steps_without_grid], sum(p.production_factor[t,ts] * p.levelization_factor[t] * m[Symbol("dvRatedProduction"*_n)][t,ts] for t in p.techs.elec) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.elec) == @@ -139,7 +143,7 @@ function add_thermal_load_constraints(m, p; _n="") if !isempty(p.techs.cooling) ##Constraint (5a): Cold thermal loads - @constraint(m, ColdLoadBalanceCon[ts in p.time_steps_with_grid], + @constraint(m, [ts in p.time_steps_with_grid], sum(m[Symbol("dvCoolingProduction"*_n)][t,ts] for t in p.techs.cooling) + sum(m[Symbol("dvDischargeFromStorage"*_n)][b,ts] for b in p.s.storage.types.cold) == diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 99bdc1b36..74a92018d 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -62,7 +62,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPToLoad[ts in p.time_steps], sum(m[:dvHeatingProduction]["ASHP", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] ) - r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) / KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp.can_serve_dhw @expression(m, ASHPToDHWKW[ts in p.time_steps], @@ -94,23 +94,23 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if "ASHP" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 @expression(m, ASHPtoColdTES[ts in p.time_steps], - sum(m[:dvProductionToStorage][b,"ASHP",ts] for b in p.s.storage.types.cold) + sum(m[:dvProductionToStorage][b,"ExistingChiller",ts] for b in p.s.storage.types.cold) ) - r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES ./ KWH_THERMAL_PER_TONHOUR), digits=3) + r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES / KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPtoColdLoad[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ASHP", ts]) + sum(m[:dvCoolingProduction]["ExistingChiller", ts]) - ASHPtoColdTES[ts] ) - r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad ./ KWH_THERMAL_PER_TONHOUR).data, digits=3) + r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad / KWH_THERMAL_PER_TONHOUR).data, digits=3) - @expression(m, Year1ASHPColdThermalProd, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHP", ts] + @expression(m, Year1ELECCHLThermalProd, + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] for ts in p.time_steps) ) - r["annual_thermal_production_tonhour"] = round(value(Year1ASHPColdThermalProd ./ KWH_THERMAL_PER_TONHOUR), digits=3) + r["annual_thermal_production_tonhour"] = round(value(Year1ELECCHLThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) - @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], + add_to_expression!(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] for t in p.techs.ashp) ) @@ -118,9 +118,8 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) r["annual_thermal_production_tonhour"] = 0.0 - @expression(m, ASHPColdElectricConsumptionSeries, 0.0) end - r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) + r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) d["ASHP"] = r From 1d2e98aa17070881c0dc11cdd0ef117a35bb0437 Mon Sep 17 00:00:00 2001 From: Zolan Date: Sun, 28 Apr 2024 10:51:08 -0600 Subject: [PATCH 047/266] fixes for ASHP results --- src/results/ashp.jl | 23 +++++++++++------------ 1 file changed, 11 insertions(+), 12 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 74a92018d..69c180314 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -62,7 +62,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPToLoad[ts in p.time_steps], sum(m[:dvHeatingProduction]["ASHP", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] ) - r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) / KWH_PER_MMBTU, digits=3) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp.can_serve_dhw @expression(m, ASHPToDHWKW[ts in p.time_steps], @@ -94,23 +94,21 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if "ASHP" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 @expression(m, ASHPtoColdTES[ts in p.time_steps], - sum(m[:dvProductionToStorage][b,"ExistingChiller",ts] for b in p.s.storage.types.cold) + sum(m[:dvProductionToStorage][b,"ASHP",ts] for b in p.s.storage.types.cold) ) - r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES / KWH_THERMAL_PER_TONHOUR), digits=3) + r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES ./ KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPtoColdLoad[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ExistingChiller", ts]) - - ASHPtoColdTES[ts] + sum(m[:dvCoolingProduction]["ASHP", ts]) - ASHPtoColdTES[ts] ) - r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad / KWH_THERMAL_PER_TONHOUR).data, digits=3) + r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad ./ KWH_THERMAL_PER_TONHOUR), digits=3) - @expression(m, Year1ELECCHLThermalProd, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ExistingChiller", ts] - for ts in p.time_steps) + @expression(m, Year1ASHPColdThermalProd, + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHP", ts] for ts in p.time_steps) ) - r["annual_thermal_production_tonhour"] = round(value(Year1ELECCHLThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) + r["annual_thermal_production_tonhour"] = round(value(Year1ASHPColdThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) - add_to_expression!(m, ASHPElectricConsumptionSeries[ts in p.time_steps], + @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] for t in p.techs.ashp) ) @@ -118,8 +116,9 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) r["annual_thermal_production_tonhour"] = 0.0 + @expression(m, ASHPColdElectricConsumptionSeries, 0.0) end - r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) + r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) d["ASHP"] = r From e7b92b50aa8e1573542092367cb5904e70479b71 Mon Sep 17 00:00:00 2001 From: Zolan Date: Sun, 28 Apr 2024 11:21:01 -0600 Subject: [PATCH 048/266] update ASHP tests --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 85e1fcae3..71a64541f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2316,7 +2316,7 @@ else # run HiGHS tests #Case 2: ASHP has temperature-dependent output and serves all heating load d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 300 - d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + d["ElectricTariff"]["monthly_energy_rates"] = [0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01] s = Scenario(d) p = REoptInputs(s) @@ -2340,7 +2340,7 @@ else # run HiGHS tests p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - annual_ashp_consumption += sum(0.1 * REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) + annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR @test results["ASHP"]["size_mmbtu_per_hour"] > 0.8 #size increases when cooling load also served @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 From f71b5b5727b32ce45ce9786cf84aa5fcfdd35b21 Mon Sep 17 00:00:00 2001 From: Zolan Date: Sun, 28 Apr 2024 21:19:00 -0600 Subject: [PATCH 049/266] fix ExistingChiller default COP assignment --- src/core/bau_inputs.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 9ca13b6f5..8340f52c7 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -93,7 +93,7 @@ function BAUInputs(p::REoptInputs) tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) end - cooling_cop["ExistingChiller"] = ones(length(time_steps)) + cooling_cop["ExistingChiller"] = ones(length(p.time_steps)) if "ExistingChiller" in techs.all setup_existing_chiller_inputs(bau_scenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) end From 6347211048a596c22e0ecf5ea2341253e3751d18 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 29 Apr 2024 08:54:27 -0600 Subject: [PATCH 050/266] update sets for ASHP constraints to exclude GHP --- src/constraints/thermal_tech_constraints.jl | 2 +- src/core/reopt.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 1bf124878..98d7f6f0c 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -51,7 +51,7 @@ function add_heating_tech_constraints(m, p; _n="") end function add_ashp_heating_cooling_constraints(m, p; _n="") - @constraint(m, [t in intersect(p.techs.cooling, p.techs.heating), ts in p.time_steps], + @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] ) end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 930593ce1..605458d9b 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -314,7 +314,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_cooling_tech_constraints(m, p) end - if !isempty(intersect(p.techs.heating, p.techs.cooling)) + if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) add_ashp_heating_cooling_constraints(m, p) end From 04de091f9e9cf6247c75f3d1262d6b6313c9ddd0 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 29 Apr 2024 08:54:53 -0600 Subject: [PATCH 051/266] default cooling and heating COPs for GHP formulation --- src/core/reopt_inputs.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index f8b1d6fc1..b4229608d 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -436,6 +436,11 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) heating_cop["ASHP"] = ones(length(time_steps)) end + if !isempty(techs.ghp) + cooling_cop["GHP"] = ones(length(time_steps)) + heating_cop["GHP"] = ones(length(time_steps)) + end + # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] From 3c23e4620035cade7ca8363b328abfb47d76752e Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 30 Apr 2024 12:02:08 +0700 Subject: [PATCH 052/266] minor clean up --- src/constraints/load_balance.jl | 4 ++-- src/results/ashp.jl | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index aad15f1a9..859afdce8 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -13,7 +13,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) #need to add cooling + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) + p.s.electric_load.loads_kw[ts] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) @@ -28,7 +28,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + m[Symbol("dvCurtail"*_n)][t, ts] for t in p.techs.elec) + sum(m[Symbol("dvGridToStorage"*_n)][b, ts] for b in p.s.storage.types.elec) + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) - + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) # need to add cooling + + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) + p.s.electric_load.loads_kw[ts] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 69c180314..49703d819 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -27,7 +27,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] #p.heating_cop[t,ts] + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) ) From 21750e08cbe35203f97ad7f003c6b2fa71b83f3b Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 30 Apr 2024 13:27:22 +0700 Subject: [PATCH 053/266] Revert "Merge branch 'add-ASHP' of https://github.com/NREL/REopt.jl into add-ASHP" This reverts commit 339156937fc57ea55e4c28d973dd7fb8d8e78e78, reversing changes made to 3c23e4620035cade7ca8363b328abfb47d76752e. --- src/constraints/thermal_tech_constraints.jl | 2 +- src/core/reopt.jl | 2 +- src/core/reopt_inputs.jl | 5 ----- 3 files changed, 2 insertions(+), 7 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 98d7f6f0c..1bf124878 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -51,7 +51,7 @@ function add_heating_tech_constraints(m, p; _n="") end function add_ashp_heating_cooling_constraints(m, p; _n="") - @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], + @constraint(m, [t in intersect(p.techs.cooling, p.techs.heating), ts in p.time_steps], sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] ) end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 605458d9b..930593ce1 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -314,7 +314,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_cooling_tech_constraints(m, p) end - if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) + if !isempty(intersect(p.techs.heating, p.techs.cooling)) add_ashp_heating_cooling_constraints(m, p) end diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index b4229608d..f8b1d6fc1 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -436,11 +436,6 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) heating_cop["ASHP"] = ones(length(time_steps)) end - if !isempty(techs.ghp) - cooling_cop["GHP"] = ones(length(time_steps)) - heating_cop["GHP"] = ones(length(time_steps)) - end - # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] From 22c0a5791a6f10fec5e5f1f8f688fd0ec473158f Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 30 Apr 2024 14:33:53 -0600 Subject: [PATCH 054/266] cooling_cop, heating_cop for GHP --- src/core/reopt_inputs.jl | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index f8b1d6fc1..b4229608d 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -436,6 +436,11 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) heating_cop["ASHP"] = ones(length(time_steps)) end + if !isempty(techs.ghp) + cooling_cop["GHP"] = ones(length(time_steps)) + heating_cop["GHP"] = ones(length(time_steps)) + end + # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in for t in techs.elec export_bins_by_tech[t] = [bin for (bin, ts) in techs_by_exportbin if t in ts] From 2c12c6b55f48b8074c10b127e88da46e8981720e Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 30 Apr 2024 14:37:22 -0600 Subject: [PATCH 055/266] update sets for add_ashp_heating_cooling_constraints --- src/constraints/thermal_tech_constraints.jl | 2 +- src/core/reopt.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 1bf124878..98d7f6f0c 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -51,7 +51,7 @@ function add_heating_tech_constraints(m, p; _n="") end function add_ashp_heating_cooling_constraints(m, p; _n="") - @constraint(m, [t in intersect(p.techs.cooling, p.techs.heating), ts in p.time_steps], + @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] ) end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 930593ce1..605458d9b 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -314,7 +314,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_cooling_tech_constraints(m, p) end - if !isempty(intersect(p.techs.heating, p.techs.cooling)) + if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) add_ashp_heating_cooling_constraints(m, p) end From d21b6d79eafff197a5b72dd9541f5a0bd692461b Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 30 Apr 2024 15:13:27 -0600 Subject: [PATCH 056/266] Add ASHP docs --- docs/src/reopt/inputs.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/docs/src/reopt/inputs.md b/docs/src/reopt/inputs.md index dad72ede7..967d544ad 100644 --- a/docs/src/reopt/inputs.md +++ b/docs/src/reopt/inputs.md @@ -176,3 +176,8 @@ REopt.SteamTurbine ```@docs REopt.ElectricHeater ``` + +## ASHP +```@docs +REopt.ASHP +``` From 1bba5dd9d7d4a90fc118c9b1374f519cacade5d5 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 13 May 2024 16:03:05 -0600 Subject: [PATCH 057/266] update tech max sizes to use time-series COPs --- src/constraints/electric_utility_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraints/electric_utility_constraints.jl b/src/constraints/electric_utility_constraints.jl index a721b584e..c8b8c69c0 100644 --- a/src/constraints/electric_utility_constraints.jl +++ b/src/constraints/electric_utility_constraints.jl @@ -72,7 +72,7 @@ function add_export_constraints(m, p; _n="") sum(p.max_sizes[t] for t in NEM_techs), p.hours_per_time_step * maximum([sum(( p.s.electric_load.loads_kw[ts] + - p.s.cooling_load.loads_kw_thermal[ts]/p.cop["ExistingChiller"] + + p.s.cooling_load.loads_kw_thermal[ts]/p.cooling_cop["ExistingChiller"][ts] + (p.s.space_heating_load.loads_kw[ts] + p.s.dhw_load.loads_kw[ts] + p.s.process_heat_load.loads_kw[ts]) ) for ts in p.s.electric_tariff.time_steps_monthly[m]) for m in p.months ]) From 0c763e6837540c7434e3ba1af8c5314553ddc79a Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 13 May 2024 16:38:44 -0600 Subject: [PATCH 058/266] rm note on process heat loads (which are now in fuel input) --- src/core/heating_cooling_loads.jl | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index 1bb4e8798..72c2ad21d 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -1435,10 +1435,6 @@ There are many ways in which a ProcessHeatLoad can be defined: 1. One can provide the `fuel_loads_mmbtu_per_hour` value in the `ProcessHeatLoad` key within the `Scenario`. 2. One can provide the `annual_mmbtu` value in the `ProcessHeatLoad` key within the `Scenario`; this assumes a flat load. -!!! note "Process heat loads" - These loads are presented in terms of process heat required without regard to the efficiency of the input heating, - unlike the hot-water and space heating loads which are provided in terms of fuel input. - """ struct ProcessHeatLoad loads_kw::Array{Real, 1} From 0b1683e7bc5d719a4baed4d2f5a12f4a700fad3f Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 13 May 2024 17:00:19 -0600 Subject: [PATCH 059/266] update notes for heating loads in documentation --- src/core/heating_cooling_loads.jl | 16 +++++++++++----- 1 file changed, 11 insertions(+), 5 deletions(-) diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index 72c2ad21d..092148b3d 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -18,8 +18,9 @@ There are many ways in which a DomesticHotWaterLoad can be defined: 3. One can provide the `fuel_loads_mmbtu_per_hour` value in the `DomesticHotWaterLoad` key within the `Scenario`. !!! note "Hot water loads" - Hot water and space heating thermal "load" inputs are in terms of energy input required (boiler fuel), not the actual energy demand. - The fuel energy is multiplied by the boiler_efficiency to get the actual energy demand. + Hot water, space heating, and process heat thermal "load" inputs are in terms of energy input required (boiler fuel), + not the actual energy demand. The fuel energy is multiplied by the existing_boiler_efficiency to get the actual energy + demand. """ struct DomesticHotWaterLoad @@ -127,8 +128,9 @@ In this case the values provided for `doe_reference_name`, or `blended_doe_refe `blended_doe_reference_percents` are copied from the `ElectricLoad` to the `SpaceHeatingLoad`. !!! note "Space heating loads" - Hot water and space heating thermal "load" inputs are in terms of energy input required (boiler fuel), not the actual energy demand. - The fuel energy is multiplied by the boiler_efficiency to get the actual energy demand. + Hot water, space heating, and process heat thermal "load" inputs are in terms of energy input required (boiler fuel), + not the actual energy demand. The fuel energy is multiplied by the existing_boiler_efficiency to get the actual energy + emand. """ struct SpaceHeatingLoad loads_kw::Array{Real, 1} @@ -1431,10 +1433,14 @@ end fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[] ``` -There are many ways in which a ProcessHeatLoad can be defined: +There are two ways in which a ProcessHeatLoad can be defined: 1. One can provide the `fuel_loads_mmbtu_per_hour` value in the `ProcessHeatLoad` key within the `Scenario`. 2. One can provide the `annual_mmbtu` value in the `ProcessHeatLoad` key within the `Scenario`; this assumes a flat load. +!!! note "Process heat loads" + Hot water, space heating, and process heat thermal "load" inputs are in terms of energy input required (boiler fuel), + not the actual energy demand. The fuel energy is multiplied by the existing_boiler_efficiency to get the actual energy + demand. """ struct ProcessHeatLoad loads_kw::Array{Real, 1} From 05bcdc048fed964dab2e094c6144d087901e7abb Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 14 May 2024 07:53:53 -0600 Subject: [PATCH 060/266] rm tests for ASHP in cplex and xpress --- test/test_with_cplex.jl | 39 --------------------------------------- test/test_with_xpress.jl | 35 ----------------------------------- 2 files changed, 74 deletions(-) diff --git a/test/test_with_cplex.jl b/test/test_with_cplex.jl index 6d901f3ab..b32ac7756 100644 --- a/test/test_with_cplex.jl +++ b/test/test_with_cplex.jl @@ -163,45 +163,6 @@ end @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost end -@testset "ASHP" begin - d = JSON.parsefile("./scenarios/ashp.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) - results = run_reopt(m, p) - - #first run: Boiler produces the required heat instead of ASHP - ASHP is not purchased here - @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1.0 - d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(CPLEX.Optimizer, "CPX_PARAM_SCRIND" => 0)) - #m = Model(GAMS.Optimizer) - #set_optimizer_attribute(Model(GAMS.Optimizer, "CPX_PARAM_SCRIND" => 0), "Solver", "CPLEX") - #set_optimizer_attribute(Model(GAMS.Optimizer, "CPX_PARAM_SCRIND" => 0), GAMS.Solver(), "CPLEX") - results = run_reopt(m, p) - - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP - annual_energy_supplied = 87600 + annual_ashp_consumption - - #Second run: ASHP produces the required heat with free electricity - @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 - @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - -end - - ## equivalent REopt API Post for test 2: # NOTE have to hack in API levelization_factor to get LCC within 5e-5 (Mosel tol) # {"Scenario": { diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index c0012d8f3..c368d88b4 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -1734,41 +1734,6 @@ end end -@testset "ASHP" begin - d = JSON.parsefile("./scenarios/ashp.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) - results = run_reopt(m, p) - - #first run: Boiler produces the required heat instead of ASHP - ASHP is not purchased here - @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1.0 - d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(Xpress.Optimizer, "OUTPUTLOG" => 0)) - results = run_reopt(m, p) - - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP - annual_energy_supplied = 87600 + annual_ashp_consumption - - #Second run: ASHP produces the required heat with free electricity - @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 - @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - -end - @testset "Custom REopt logger" begin # Throw a handled error From dc8d326feffafea97708b60f456b3818b6d9d300 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 14 May 2024 09:46:50 -0600 Subject: [PATCH 061/266] add todo for cooling COP review cc @atpham88 Co-Authored-By: An Pham --- src/core/scenario.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 98d66f7e9..875d62dfe 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -674,13 +674,13 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end if !haskey(d["ASHP"], "cop_heating") - cop_heating = round.(0.083 .*ambient_temp_celsius .+ 2.8255, digits=2) + cop_heating = round.(0.083 .* ambient_temp_celsius .+ 2.8255, digits=2) else cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) end - if !haskey(d["ASHP"], "cop_cooling") # need to update (do we have diff curve for cooling cop?) - cop_cooling = round.(-0.08.*ambient_temp_celsius .+ 5.4, digits=2) + if !haskey(d["ASHP"], "cop_cooling") # TODO review cooling COP with design docs + cop_cooling = round.(-0.08 .* ambient_temp_celsius .+ 5.4, digits=2) else cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=2) end From 032e0d6cd659b47851b2a41c45bfe06c434afd4f Mon Sep 17 00:00:00 2001 From: An Pham Date: Thu, 23 May 2024 16:02:04 -0600 Subject: [PATCH 062/266] convert ASHP size from mmbtu to ton --- data/ashp/ashp_defaults.json | 4 ++-- src/core/ashp.jl | 32 ++++++++++++++++---------------- src/core/scenario.jl | 2 +- src/results/ashp.jl | 4 ++-- test/scenarios/ashp.json | 8 ++++---- 5 files changed, 25 insertions(+), 25 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 009ceab89..aa2e00562 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -1,6 +1,6 @@ { - "installed_cost_per_mmbtu_per_hour": 337500, - "om_cost_per_mmbtu_per_hour": 0.02, + "installed_cost_per_ton_per_hour": 4050, + "om_cost_per_ton_per_hour": 0.00024, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 11a97fa38..f5e6b019a 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -26,10 +26,10 @@ to meet the heating load. ```julia function ASHP(; - min_mmbtu_per_hour::Real = 0.0, # Minimum thermal power size - max_mmbtu_per_hour::Real = BIG_NUMBER, # Maximum thermal power size - installed_cost_per_mmbtu_per_hour::Union{Real, nothing} = nothing, # Thermal power-based cost - om_cost_per_mmbtu_per_hour::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + min_ton_per_hour::Real = 0.0, # Minimum thermal power size + max_ton_per_hour::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production @@ -43,10 +43,10 @@ function ASHP(; ``` """ function ASHP(; - min_mmbtu_per_hour::Real = 0.0, - max_mmbtu_per_hour::Real = BIG_NUMBER, - installed_cost_per_mmbtu_per_hour::Union{Real, Nothing} = nothing, - om_cost_per_mmbtu_per_hour::Union{Real, Nothing} = nothing, + min_ton_per_hour::Real = 0.0, + max_ton_per_hour::Real = BIG_NUMBER, + installed_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, + om_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, @@ -61,11 +61,11 @@ function ASHP(; defaults = get_ashp_defaults() # populate defaults as needed - if isnothing(installed_cost_per_mmbtu_per_hour) - installed_cost_per_mmbtu_per_hour = defaults["installed_cost_per_mmbtu_per_hour"] + if isnothing(installed_cost_per_ton_per_hour) + installed_cost_per_ton_per_hour = defaults["installed_cost_per_ton_per_hour"] end - if isnothing(om_cost_per_mmbtu_per_hour) - om_cost_per_mmbtu_per_hour = defaults["om_cost_per_mmbtu_per_hour"] + if isnothing(om_cost_per_ton_per_hour) + om_cost_per_ton_per_hour = defaults["om_cost_per_ton_per_hour"] end if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -84,11 +84,11 @@ function ASHP(; end # Convert max sizes, cost factors from mmbtu_per_hour to kw - min_kw = min_mmbtu_per_hour * KWH_PER_MMBTU - max_kw = max_mmbtu_per_hour * KWH_PER_MMBTU + min_kw = min_ton_per_hour * KWH_PER_MMBTU * 0.012 + max_kw = max_ton_per_hour * KWH_PER_MMBTU * 0.012 - installed_cost_per_kw = installed_cost_per_mmbtu_per_hour / KWH_PER_MMBTU - om_cost_per_kw = om_cost_per_mmbtu_per_hour / KWH_PER_MMBTU + installed_cost_per_kw = installed_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) + om_cost_per_kw = om_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) ASHP( diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 875d62dfe..fbd6f0ea7 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -656,7 +656,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ashp = nothing cop_heating = [] cop_cooling = [] - if haskey(d, "ASHP") && d["ASHP"]["max_mmbtu_per_hour"] > 0.0 + if haskey(d, "ASHP") && d["ASHP"]["max_ton_per_hour"] > 0.0 # If user does not provide heating cop series then assign cop curves based on ambient temperature if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 49703d819..7c0f2ce9d 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -2,7 +2,7 @@ """ `ASHP` results keys: -- `size_mmbtu_per_hour` # Thermal production capacity size of the ASHP [MMBtu/hr] +- `size_ton_per_hour` # Thermal production capacity size of the ASHP [ton/hr] - `electric_consumption_series_kw` # Fuel consumption series [kW] - `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] - `thermal_production_series_mmbtu_per_hour` # Thermal energy production series [MMBtu/hr] @@ -25,7 +25,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU, digits=3) + r["size_ton_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU * 0.012, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 08c373a1b..210aae9a4 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -10,10 +10,10 @@ "fuel_cost_per_mmbtu": 10 }, "ASHP": { - "min_mmbtu_per_hour": 0.0, - "max_mmbtu_per_hour": 100000, - "installed_cost_per_mmbtu_per_hour": 314000, - "om_cost_per_mmbtu_per_hour": 0.0, + "min_ton_per_hour": 0.0, + "max_ton_per_hour": 100000, + "installed_cost_per_ton_per_hour": 4050, + "om_cost_per_ton_per_hour": 0.0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, From 2e7b6c42cc2de581dcc44281acd8c0aa0092d8e4 Mon Sep 17 00:00:00 2001 From: An Pham Date: Thu, 23 May 2024 19:46:04 -0600 Subject: [PATCH 063/266] updated ashp test --- test/runtests.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9a1f28e6b..c7f7e2e16 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2364,13 +2364,13 @@ else # run HiGHS tests d = JSON.parsefile("./scenarios/ashp.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 1e8 + d["ASHP"]["installed_cost_per_ton_per_hour"] = 1050 s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["size_ton_per_hour"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 @@ -2387,7 +2387,7 @@ else # run HiGHS tests annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 + @test results["ASHP"]["size_ton_per_hour"] ≈ 0.8 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2404,7 +2404,7 @@ else # run HiGHS tests results = run_reopt(m, p) annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP"]["size_mmbtu_per_hour"] > 0.8 #size increases when cooling load also served + @test results["ASHP"]["size_ton_per_hour"] > 0.8 #size increases when cooling load also served @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 end From 03f7172af860c06d6dbe994f343afb5d299d0572 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 11:26:46 -0600 Subject: [PATCH 064/266] ren ton_per_hour ton (which is a rate) --- data/ashp/ashp_defaults.json | 4 ++-- src/core/ashp.jl | 32 ++++++++++++++++---------------- src/core/scenario.jl | 2 +- src/results/ashp.jl | 4 ++-- test/runtests.jl | 8 ++++---- test/scenarios/ashp.json | 8 ++++---- 6 files changed, 29 insertions(+), 29 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index aa2e00562..027c751c2 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -1,6 +1,6 @@ { - "installed_cost_per_ton_per_hour": 4050, - "om_cost_per_ton_per_hour": 0.00024, + "installed_cost_per_ton": 4050, + "om_cost_per_ton": 0.00024, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, diff --git a/src/core/ashp.jl b/src/core/ashp.jl index f5e6b019a..84552dad7 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -26,10 +26,10 @@ to meet the heating load. ```julia function ASHP(; - min_ton_per_hour::Real = 0.0, # Minimum thermal power size - max_ton_per_hour::Real = BIG_NUMBER, # Maximum thermal power size - installed_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based cost - om_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + min_ton::Real = 0.0, # Minimum thermal power size + max_ton::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production @@ -43,10 +43,10 @@ function ASHP(; ``` """ function ASHP(; - min_ton_per_hour::Real = 0.0, - max_ton_per_hour::Real = BIG_NUMBER, - installed_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, - om_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, + min_ton::Real = 0.0, + max_ton::Real = BIG_NUMBER, + installed_cost_per_ton::Union{Real, Nothing} = nothing, + om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, can_supply_steam_turbine::Union{Bool, Nothing} = nothing, @@ -61,11 +61,11 @@ function ASHP(; defaults = get_ashp_defaults() # populate defaults as needed - if isnothing(installed_cost_per_ton_per_hour) - installed_cost_per_ton_per_hour = defaults["installed_cost_per_ton_per_hour"] + if isnothing(installed_cost_per_ton) + installed_cost_per_ton = defaults["installed_cost_per_ton"] end - if isnothing(om_cost_per_ton_per_hour) - om_cost_per_ton_per_hour = defaults["om_cost_per_ton_per_hour"] + if isnothing(om_cost_per_ton) + om_cost_per_ton = defaults["om_cost_per_ton"] end if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -84,11 +84,11 @@ function ASHP(; end # Convert max sizes, cost factors from mmbtu_per_hour to kw - min_kw = min_ton_per_hour * KWH_PER_MMBTU * 0.012 - max_kw = max_ton_per_hour * KWH_PER_MMBTU * 0.012 + min_kw = min_ton * KWH_PER_MMBTU * 0.012 + max_kw = max_ton * KWH_PER_MMBTU * 0.012 - installed_cost_per_kw = installed_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) - om_cost_per_kw = om_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) + installed_cost_per_kw = installed_cost_per_ton / (KWH_PER_MMBTU * 0.012) + om_cost_per_kw = om_cost_per_ton / (KWH_PER_MMBTU * 0.012) ASHP( diff --git a/src/core/scenario.jl b/src/core/scenario.jl index fbd6f0ea7..de4ba4c37 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -656,7 +656,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ashp = nothing cop_heating = [] cop_cooling = [] - if haskey(d, "ASHP") && d["ASHP"]["max_ton_per_hour"] > 0.0 + if haskey(d, "ASHP") && d["ASHP"]["max_ton"] > 0.0 # If user does not provide heating cop series then assign cop curves based on ambient temperature if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 7c0f2ce9d..bd8a8d9df 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -2,7 +2,7 @@ """ `ASHP` results keys: -- `size_ton_per_hour` # Thermal production capacity size of the ASHP [ton/hr] +- `size_ton` # Thermal production capacity size of the ASHP [ton/hr] - `electric_consumption_series_kw` # Fuel consumption series [kW] - `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] - `thermal_production_series_mmbtu_per_hour` # Thermal energy production series [MMBtu/hr] @@ -25,7 +25,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU * 0.012, digits=3) + r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU * 0.012, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) diff --git a/test/runtests.jl b/test/runtests.jl index c7f7e2e16..3c90d8ed9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2364,13 +2364,13 @@ else # run HiGHS tests d = JSON.parsefile("./scenarios/ashp.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["ASHP"]["installed_cost_per_ton_per_hour"] = 1050 + d["ASHP"]["installed_cost_per_ton"] = 1050 s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP"]["size_ton_per_hour"] ≈ 0.0 atol=0.1 + @test results["ASHP"]["size_ton"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 @@ -2387,7 +2387,7 @@ else # run HiGHS tests annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP"]["size_ton_per_hour"] ≈ 0.8 atol=0.1 + @test results["ASHP"]["size_ton"] ≈ 0.8 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2404,7 +2404,7 @@ else # run HiGHS tests results = run_reopt(m, p) annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP"]["size_ton_per_hour"] > 0.8 #size increases when cooling load also served + @test results["ASHP"]["size_ton"] > 0.8 #size increases when cooling load also served @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 end diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 210aae9a4..f66ea1aa6 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -10,10 +10,10 @@ "fuel_cost_per_mmbtu": 10 }, "ASHP": { - "min_ton_per_hour": 0.0, - "max_ton_per_hour": 100000, - "installed_cost_per_ton_per_hour": 4050, - "om_cost_per_ton_per_hour": 0.0, + "min_ton": 0.0, + "max_ton": 100000, + "installed_cost_per_ton": 4050, + "om_cost_per_ton": 0.0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, From 4b3d6701748b84f49b4d421077695f7e141558fe Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 13:53:51 -0600 Subject: [PATCH 065/266] convert cost conversions to reflect ASHP size in tons --- src/core/ashp.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 84552dad7..b1a93560e 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -84,11 +84,11 @@ function ASHP(; end # Convert max sizes, cost factors from mmbtu_per_hour to kw - min_kw = min_ton * KWH_PER_MMBTU * 0.012 - max_kw = max_ton * KWH_PER_MMBTU * 0.012 + min_kw = min_ton * KWH_THERMAL_PER_TONHOUR * 0.012 + max_kw = max_ton * KWH_THERMAL_PER_TONHOUR * 0.012 - installed_cost_per_kw = installed_cost_per_ton / (KWH_PER_MMBTU * 0.012) - om_cost_per_kw = om_cost_per_ton / (KWH_PER_MMBTU * 0.012) + installed_cost_per_kw = installed_cost_per_ton / (KWH_THERMAL_PER_TONHOUR * 0.012) + om_cost_per_kw = om_cost_per_ton / (KWH_THERMAL_PER_TONHOUR * 0.012) ASHP( From 2ee26456756d57f936f7e54061e46d2e652e7dc7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 14:12:28 -0600 Subject: [PATCH 066/266] update ASHP test units --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 3c90d8ed9..7bece45f4 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2377,7 +2377,7 @@ else # run HiGHS tests #Case 2: ASHP has temperature-dependent output and serves all heating load d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP"]["installed_cost_per_mmbtu_per_hour"] = 300 + d["ASHP"]["installed_cost_per_ton"] = 300 d["ElectricTariff"]["monthly_energy_rates"] = [0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01] s = Scenario(d) From 55f5dc5eac48dee49fb34f1128ee6428fc7f340c Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 14:13:05 -0600 Subject: [PATCH 067/266] add get_electric_heater_defaults and get_ashp_defaults to exported functions for API --- src/REopt.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/REopt.jl b/src/REopt.jl index 1c6c33b04..14929ba08 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -24,7 +24,9 @@ export avert_emissions_profiles, cambium_emissions_profile, easiur_data, - get_existing_chiller_default_cop + get_existing_chiller_default_cop, + get_electric_heater_defaults, + get_ashp_defaults import HTTP import JSON From ab0d73c05bcc9e73f6eca10b74b3364f94fc3cd2 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 15:11:08 -0600 Subject: [PATCH 068/266] docstrings update for ASHP --- src/core/scenario.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index de4ba4c37..3278cada9 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -53,6 +53,7 @@ A Scenario struct can contain the following keys: - [GHP](@ref) (optional, can be Array) - [SteamTurbine](@ref) (optional) - [ElectricHeater](@ref) (optional) +- [ASHP](@ref) (optional) All values of `d` are expected to be `Dicts` except for `PV` and `GHP`, which can be either a `Dict` or `Dict[]` (for multiple PV arrays or GHP options). From 3ea9714a645eeaf876ca547566e57b2dce0ea89a Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 22:56:49 -0600 Subject: [PATCH 069/266] update units in ashp test --- src/results/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index bd8a8d9df..63c867a26 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -25,7 +25,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_PER_MMBTU * 0.012, digits=3) + r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) From 012cc400bdbf7e3c75f460810db26c4e02e9e6c3 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 28 May 2024 13:43:23 -0600 Subject: [PATCH 070/266] fixed ASHP size conversion --- src/core/ashp.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index b1a93560e..00a2b6b52 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -84,11 +84,11 @@ function ASHP(; end # Convert max sizes, cost factors from mmbtu_per_hour to kw - min_kw = min_ton * KWH_THERMAL_PER_TONHOUR * 0.012 - max_kw = max_ton * KWH_THERMAL_PER_TONHOUR * 0.012 + min_kw = min_ton * KWH_THERMAL_PER_TONHOUR + max_kw = max_ton * KWH_THERMAL_PER_TONHOUR - installed_cost_per_kw = installed_cost_per_ton / (KWH_THERMAL_PER_TONHOUR * 0.012) - om_cost_per_kw = om_cost_per_ton / (KWH_THERMAL_PER_TONHOUR * 0.012) + installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR + om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR ASHP( From a48573840eaf19a1356966344c035d5fe4514659 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 29 May 2024 11:05:40 -0600 Subject: [PATCH 071/266] add attribute retire_in_optimal to ExistingChiller --- src/core/existing_chiller.jl | 8 ++++++-- test/scenarios/ashp.json | 2 +- 2 files changed, 7 insertions(+), 3 deletions(-) diff --git a/src/core/existing_chiller.jl b/src/core/existing_chiller.jl index 73f97b5f1..bb20830d3 100644 --- a/src/core/existing_chiller.jl +++ b/src/core/existing_chiller.jl @@ -5,6 +5,7 @@ loads_kw_thermal::Vector{<:Real}, cop::Union{Real, Nothing} = nothing, max_thermal_factor_on_peak_load::Real=1.25 + retire_in_optimal::Bool = false # Do NOT use in the optimal case (still used in BAU) ``` !!! note "Max ExistingChiller size" @@ -17,19 +18,22 @@ struct ExistingChiller <: AbstractThermalTech max_kw::Real cop::Union{Real, Nothing} max_thermal_factor_on_peak_load::Real + retire_in_optimal::Bool end function ExistingChiller(; loads_kw_thermal::Vector{<:Real}, cop::Union{Real, Nothing} = nothing, - max_thermal_factor_on_peak_load::Real=1.25 + max_thermal_factor_on_peak_load::Real=1.25, + retire_in_optimal::Bool = false ) max_kw = maximum(loads_kw_thermal) * max_thermal_factor_on_peak_load ExistingChiller( max_kw, cop, - max_thermal_factor_on_peak_load + max_thermal_factor_on_peak_load, + retire_in_optimal ) end diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index f66ea1aa6..d1e538ff5 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -7,7 +7,7 @@ "production_type": "steam", "efficiency": 0.8, "fuel_type": "natural_gas", - "fuel_cost_per_mmbtu": 10 + "fuel_cost_per_mmbtu": 5 }, "ASHP": { "min_ton": 0.0, From cef75a6c74dec759b0ac238fd05f9e3550d3d3ae Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 29 May 2024 11:07:00 -0600 Subject: [PATCH 072/266] add function no_existing_chiller_production when ExistingChiller is retired --- src/constraints/thermal_tech_constraints.jl | 7 +++++++ src/core/reopt.jl | 8 ++++++++ 2 files changed, 15 insertions(+) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 98d7f6f0c..be5053593 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -77,3 +77,10 @@ function add_cooling_tech_constraints(m, p; _n="") end end end + +function no_existing_chiller_production(m, p; _n="") + for ts in p.time_steps + fix(m[Symbol("dvCoolingProduction"*_n)]["ExistingBoiler",ts], 0.0, force=true) + end + fix(m[Symbol("dvSize"*_n)]["ExistingChiller"], 0.0, force=true) +end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index b420543af..70000acfc 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -314,6 +314,14 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_cooling_tech_constraints(m, p) end + # Zero out ExistingChiller production if retire_in_optimal; setdiff avoids zeroing for BAU + if (!isempty(setdiff(p.techs.cooling, ["ExistingChiller"])) && + !isnothing(p.s.existing_chiller) && + p.s.existing_chiller.retire_in_optimal + ) + no_existing_chiller_production(m, p) + end + if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) add_ashp_heating_cooling_constraints(m, p) end From cd4a91c3b4244a36f5078edd5ef8cdfdf37b2e3c Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 29 May 2024 11:22:10 -0600 Subject: [PATCH 073/266] use set of heating techs to check for retiring boiler --- src/core/reopt.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 70000acfc..968261730 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -296,9 +296,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_heating_tech_constraints(m, p) end - # Zero out ExistingBoiler production if retire_in_optimal; new_heating_techs avoids zeroing for BAU - new_heating_techs = ["CHP", "Boiler", "ElectricHeater", "SteamTurbine"] - if !isempty(intersect(new_heating_techs, p.techs.all)) + # Zero out ExistingBoiler production if retire_in_optimal; setdiff avoids zeroing for BAU + if !isempty(setdiff(p.techs.heating, "ExistingBoiler")) if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal no_existing_boiler_production(m, p) end From 40ffa2367d859c6bdb4699fb91ac7886726e9758 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 29 May 2024 11:58:36 -0600 Subject: [PATCH 074/266] update ASHP tests and add case for retired chiller, boiler --- test/runtests.jl | 38 ++++++++++++++++++++++---------------- 1 file changed, 22 insertions(+), 16 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 7bece45f4..c0c2ef71c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2360,34 +2360,31 @@ else # run HiGHS tests end @testset "ASHP" begin - #Case 1: Boiler produces the required heat instead of ASHP - ASHP is not purchased + #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP is not purchased d = JSON.parsefile("./scenarios/ashp.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["ASHP"]["installed_cost_per_ton"] = 1050 - - s = Scenario(d) - p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) + results = run_reopt(m, d) @test results["ASHP"]["size_ton"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - + #Case 2: ASHP has temperature-dependent output and serves all heating load + d["ExistingChiller"] = Dict("retire_in_optimal" => false) + d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 d["ASHP"]["installed_cost_per_ton"] = 300 - d["ElectricTariff"]["monthly_energy_rates"] = [0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01,0.01] - s = Scenario(d) - p = REoptInputs(s) + p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) + results = run_reopt(m, d) annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP"]["size_ton"] ≈ 0.8 atol=0.1 + @test results["ASHP"]["size_ton"] ≈ 0.8 * REopt.KWH_PER_MMBTU / REopt.KWH_THERMAL_PER_TONHOUR atol=0.01 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2398,15 +2395,24 @@ else # run HiGHS tests d["ExistingChiller"] = Dict("cop" => 0.5) d["ASHP"]["can_serve_cooling"] = true - s = Scenario(d) - p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) + results = run_reopt(m, d) + annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP"]["size_ton"] > 0.8 #size increases when cooling load also served + @test results["ASHP"]["size_ton"] ≈ 0.1 + 0.8 * REopt.KWH_PER_MMBTU / REopt.KWH_THERMAL_PER_TONHOUR atol=0.01 #size increases when cooling load also served + @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 + + #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate + d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) + d["ExistingBoiler"]["retire_in_optimal"] = true + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 + end @testset "Process Heat Load" begin From 78c1a05177653727d8a5f9f24cdce9868191756c Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 29 May 2024 14:52:53 -0600 Subject: [PATCH 075/266] add warnings when new heating techs can't serve all heating loads without ExistingBoiler --- src/core/reopt.jl | 31 ++++++++++++++++++++++--------- 1 file changed, 22 insertions(+), 9 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 968261730..c493941ed 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -296,12 +296,26 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_heating_tech_constraints(m, p) end - # Zero out ExistingBoiler production if retire_in_optimal; setdiff avoids zeroing for BAU - if !isempty(setdiff(p.techs.heating, "ExistingBoiler")) - if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal - no_existing_boiler_production(m, p) + # Zero out ExistingBoiler production if retire_in_optimal; length check avoids zeroing for BAU + if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal && length(p.techs.heating) > 1 + can_retire = true + if !isnothing(p.s.dhw_load) && p.s.dhw_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_dhw, "ExistingBoiler")) + @warn "ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load. ExistingBoiler will not be retired in the optimal case." + can_retire = false end + if !isnothing(p.s.space_heating_load) && p.s.space_heating_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_space_heating, "ExistingBoiler")) + @warn "ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load. ExistingBoiler will not be retired in the optimal case." + can_retire = false + end + if !isnothing(p.s.process_heat_load) && p.s.process_heat_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_process_heat, "ExistingBoiler")) + @warn "ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load. ExistingBoiler will not be retired in the optimal case." + can_retire = false + end + if can_retire + no_existing_boiler_production(m, p) + end end + if !isempty(p.techs.boiler) add_boiler_tech_constraints(m, p) @@ -314,11 +328,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) end # Zero out ExistingChiller production if retire_in_optimal; setdiff avoids zeroing for BAU - if (!isempty(setdiff(p.techs.cooling, ["ExistingChiller"])) && - !isnothing(p.s.existing_chiller) && - p.s.existing_chiller.retire_in_optimal - ) - no_existing_chiller_production(m, p) + if !isnothing(p.s.existing_chiller) && p.s.existing_chiller.retire_in_optimal + if !isempty(setdiff(p.techs.cooling, ["ExistingChiller"])) + no_existing_chiller_production(m, p) + end end if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) From 13c4927aedce4daf81f1719a8be0af9364ed6f97 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 29 May 2024 14:53:15 -0600 Subject: [PATCH 076/266] fix no_existing_chiller_production --- src/constraints/thermal_tech_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index be5053593..9b8d67ef5 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -80,7 +80,7 @@ end function no_existing_chiller_production(m, p; _n="") for ts in p.time_steps - fix(m[Symbol("dvCoolingProduction"*_n)]["ExistingBoiler",ts], 0.0, force=true) + fix(m[Symbol("dvCoolingProduction"*_n)]["ExistingChiller",ts], 0.0, force=true) end fix(m[Symbol("dvSize"*_n)]["ExistingChiller"], 0.0, force=true) end From efc94a29a501b0bf3d283cfba63ab3edc01f5896 Mon Sep 17 00:00:00 2001 From: Alex Zolan Date: Wed, 29 May 2024 15:16:31 -0600 Subject: [PATCH 077/266] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 216800123..72031d1c9 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## Develop 05-29-2024 +### Added +- Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly +- In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct + ## v0.46.2 ### Changed - When the URDB response `energyratestructure` has a "unit" value that is not "kWh", throw an error instead of averaging rates in each energy tier. From 0285146d41df11a6c9da5eb910f991dd18f3ce1b Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 30 May 2024 09:38:01 -0600 Subject: [PATCH 078/266] change warning to throw an error if additional technology sets aren't sufficient to meet heating loads in optimal case and existing boiler is retired --- src/core/reopt.jl | 14 ++++---------- 1 file changed, 4 insertions(+), 10 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index c493941ed..41e90a5c8 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -298,22 +298,16 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) # Zero out ExistingBoiler production if retire_in_optimal; length check avoids zeroing for BAU if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal && length(p.techs.heating) > 1 - can_retire = true if !isnothing(p.s.dhw_load) && p.s.dhw_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_dhw, "ExistingBoiler")) - @warn "ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load. ExistingBoiler will not be retired in the optimal case." - can_retire = false + @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load.")) end if !isnothing(p.s.space_heating_load) && p.s.space_heating_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_space_heating, "ExistingBoiler")) - @warn "ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load. ExistingBoiler will not be retired in the optimal case." - can_retire = false + @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load.")) end if !isnothing(p.s.process_heat_load) && p.s.process_heat_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_process_heat, "ExistingBoiler")) - @warn "ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load. ExistingBoiler will not be retired in the optimal case." - can_retire = false + @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load.")) end - if can_retire - no_existing_boiler_production(m, p) - end + no_existing_boiler_production(m, p) end From cc8025613e3b9348553079409aadcd53c99ecb4b Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 30 May 2024 09:44:42 -0600 Subject: [PATCH 079/266] move errors to when techs are built (catches error earlier) --- src/core/reopt.jl | 11 +---------- src/core/techs.jl | 13 +++++++++++++ 2 files changed, 14 insertions(+), 10 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 41e90a5c8..93f017043 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -297,16 +297,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) end # Zero out ExistingBoiler production if retire_in_optimal; length check avoids zeroing for BAU - if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal && length(p.techs.heating) > 1 - if !isnothing(p.s.dhw_load) && p.s.dhw_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_dhw, "ExistingBoiler")) - @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load.")) - end - if !isnothing(p.s.space_heating_load) && p.s.space_heating_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_space_heating, "ExistingBoiler")) - @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load.")) - end - if !isnothing(p.s.process_heat_load) && p.s.process_heat_load.annual_mmbtu > 0 && isempty(setdiff(p.techs.can_serve_process_heat, "ExistingBoiler")) - @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load.")) - end + if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal no_existing_boiler_production(m, p) end diff --git a/src/core/techs.jl b/src/core/techs.jl index 3c3996495..3df46718c 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -296,6 +296,19 @@ function Techs(s::Scenario) thermal_techs = union(heating_techs, boiler_techs, chp_techs, cooling_techs) fuel_burning_techs = union(gentechs, boiler_techs, chp_techs) + # check for ability of new technologies to meet heating loads if retire_in_optimal + if !isnothing(s.existing_boiler) && s.existing_boiler.retire_in_optimal + if !isnothing(s.dhw_load) && s.dhw_load.annual_mmbtu > 0 && isempty(setdiff(techs_can_serve_dhw, "ExistingBoiler")) + @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load.")) + end + if !isnothing(s.space_heating_load) && s.space_heating_load.annual_mmbtu > 0 && isempty(setdiff(techs_can_serve_space_heating, "ExistingBoiler")) + @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load.")) + end + if !isnothing(s.process_heat_load) && s.process_heat_load.annual_mmbtu > 0 && isempty(setdiff(techs_can_serve_process_heat, "ExistingBoiler")) + @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load.")) + end + end + end Techs( all_techs, elec, From b5b0eaceb0564ffb9b7b0e989fe2215d847e8056 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 30 May 2024 09:47:17 -0600 Subject: [PATCH 080/266] throw error when new techs cannot meet cooling load and existing chiller is retired --- src/core/techs.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/core/techs.jl b/src/core/techs.jl index 3df46718c..f2795d1a7 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -308,7 +308,12 @@ function Techs(s::Scenario) @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load.")) end end - end + if !isnothing(s.existing_chiller) && s.existing_chiller.retire_in_optimal + if !isnothing(s.cooling_load) && sum(s.cooling_load.loads_kw_thermal) > 0 && isempty(setdiff(cooling_techs, "ExistingChiller")) + @throw(@error("ExisitingChiller.retire_in_optimal is true, but no other technologies can meet cooling load.")) + end + end + Techs( all_techs, elec, From 19575a9fea79b2d89ff12826e177046d1b4a6735 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 30 May 2024 10:11:10 -0600 Subject: [PATCH 081/266] fix error throwing syntax --- src/core/techs.jl | 13 +++++++++---- 1 file changed, 9 insertions(+), 4 deletions(-) diff --git a/src/core/techs.jl b/src/core/techs.jl index 3df46718c..f3bf74d01 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -299,16 +299,21 @@ function Techs(s::Scenario) # check for ability of new technologies to meet heating loads if retire_in_optimal if !isnothing(s.existing_boiler) && s.existing_boiler.retire_in_optimal if !isnothing(s.dhw_load) && s.dhw_load.annual_mmbtu > 0 && isempty(setdiff(techs_can_serve_dhw, "ExistingBoiler")) - @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load.")) + throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet DomesticHotWater load.")) end if !isnothing(s.space_heating_load) && s.space_heating_load.annual_mmbtu > 0 && isempty(setdiff(techs_can_serve_space_heating, "ExistingBoiler")) - @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load.")) + throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet SpaceHeating load.")) end if !isnothing(s.process_heat_load) && s.process_heat_load.annual_mmbtu > 0 && isempty(setdiff(techs_can_serve_process_heat, "ExistingBoiler")) - @throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load.")) + throw(@error("ExisitingBoiler.retire_in_optimal is true, but no other heating technologies can meet ProcessHeat load.")) end end - end + if !isnothing(s.existing_chiller) && s.existing_chiller.retire_in_optimal + if !isnothing(s.cooling_load) && sum(s.cooling_load.loads_kw_thermal) > 0 && isempty(setdiff(cooling_techs, "ExistingChiller")) + throw(@error("ExisitingChiller.retire_in_optimal is true, but no other technologies can meet cooling load.")) + end + end + Techs( all_techs, elec, From b3f39713e37a146007a855c0de74a49069c89872 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 30 May 2024 10:18:56 -0600 Subject: [PATCH 082/266] restore setdiff check before no_existing_boiler_production --- src/core/reopt.jl | 10 ++++------ 1 file changed, 4 insertions(+), 6 deletions(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 93f017043..10c118eba 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -296,8 +296,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) add_heating_tech_constraints(m, p) end - # Zero out ExistingBoiler production if retire_in_optimal; length check avoids zeroing for BAU - if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal + # Zero out ExistingBoiler production if retire_in_optimal + if !isnothing(p.s.existing_boiler) && p.s.existing_boiler.retire_in_optimal && !isempty(setdiff(union(p.techs.chp,p.techs.heating), ["ExistingBoiler"])) no_existing_boiler_production(m, p) end @@ -313,10 +313,8 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) end # Zero out ExistingChiller production if retire_in_optimal; setdiff avoids zeroing for BAU - if !isnothing(p.s.existing_chiller) && p.s.existing_chiller.retire_in_optimal - if !isempty(setdiff(p.techs.cooling, ["ExistingChiller"])) - no_existing_chiller_production(m, p) - end + if !isnothing(p.s.existing_chiller) && p.s.existing_chiller.retire_in_optimal && !isempty(setdiff(p.techs.cooling, ["ExistingChiller"])) + no_existing_chiller_production(m, p) end if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) From 2a77f89edac6d4560b8c7e3d7a3b66bd8e730d77 Mon Sep 17 00:00:00 2001 From: An Pham Date: Thu, 30 May 2024 15:37:38 -0600 Subject: [PATCH 083/266] update heating and cooling cops w/ curves in design document --- src/core/scenario.jl | 19 ++++++++++++------- 1 file changed, 12 insertions(+), 7 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 3278cada9..49b07216d 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -673,22 +673,27 @@ function Scenario(d::Dict; flex_hvac_from_json=false) pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end end + ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 if !haskey(d["ASHP"], "cop_heating") - cop_heating = round.(0.083 .* ambient_temp_celsius .+ 2.8255, digits=2) + cop_heating = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) + cop_heating[ambient_temp_fahrenheit .< -7.6] .= 1 + cop_heating[ambient_temp_fahrenheit .> 79] .= 999999 else - cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) + cop_heating = round.(d["ASHP"]["cop_heating"],digits=3) end - if !haskey(d["ASHP"], "cop_cooling") # TODO review cooling COP with design docs - cop_cooling = round.(-0.08 .* ambient_temp_celsius .+ 5.4, digits=2) + if !haskey(d["ASHP"], "cop_cooling") + cop_cooling = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) + cop_cooling[ambient_temp_celsius .< 25] .= 999999 + cop_cooling[ambient_temp_celsius .> 40] .= 1 else - cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=2) + cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=3) end else # Else if the user already provide cop series, use that - cop_heating = round.(d["ASHP"]["cop_heating"],digits=2) - cop_cooling = round.(d["ASHP"]["cop_cooling"],digits=2) + cop_heating = round.(d["ASHP"]["cop_heating"],digits=3) + cop_cooling = round.(d["ASHP"]["cop_cooling"],digits=3) end d["ASHP"]["cop_heating"] = cop_heating d["ASHP"]["cop_cooling"] = cop_cooling From ab4b107873012068db45e6906a5d30eb094819cc Mon Sep 17 00:00:00 2001 From: An Pham Date: Thu, 30 May 2024 16:01:31 -0600 Subject: [PATCH 084/266] distinguish between heating and cooling thermal production --- src/results/ashp.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 63c867a26..fd662884a 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -5,8 +5,8 @@ - `size_ton` # Thermal production capacity size of the ASHP [ton/hr] - `electric_consumption_series_kw` # Fuel consumption series [kW] - `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] -- `thermal_production_series_mmbtu_per_hour` # Thermal energy production series [MMBtu/hr] -- `annual_thermal_production_mmbtu` # Thermal energy produced in a year [MMBtu] +- `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] +- `annual_thermal_production_mmbtu` # Thermal heating energy produced in a year [MMBtu] - `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] - `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] - `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] @@ -14,7 +14,7 @@ - `thermal_to_load_series_ton` # Thermal production to cooling load - `electric_consumption_series_kw` - `annual_electric_consumption_kwh` -- `annual_thermal_production_tonhour` +- `annual_thermal_production_tonhour` Thermal cooling energy produced in a year !!! note "'Series' and 'Annual' energy outputs are average annual" From 0ee336c2b918982ac60a672daf82c83d11f3b680 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 31 May 2024 16:29:44 -0600 Subject: [PATCH 085/266] define capacity factor curves --- src/core/ashp.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 00a2b6b52..763d3e327 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -10,6 +10,8 @@ struct ASHP <: AbstractThermalTech can_supply_steam_turbine::Bool cop_heating::Array{Float64,1} cop_cooling::Array{Float64,1} + cf_heating::Array{Float64,1} + cf_cooling::Array{Float64,1} can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -33,8 +35,10 @@ function ASHP(; macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production - cop_heating::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Array{<:Real,2}, # COP of the cooling (i.e., thermal produced / electricity consumed) + cop_heating::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cop_cooling::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) + cf_heating::Array{Float64,1}, # ASHP's heating capacity factor curves + cf_cooling::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_dhw::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the domestic hot water load can_serve_space_heating::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the space heating load can_serve_process_heat::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the process heating load @@ -101,6 +105,8 @@ function ASHP(; can_supply_steam_turbine, cop_heating, cop_cooling, + cf_heating, + cf_cooling, can_serve_dhw, can_serve_space_heating, can_serve_process_heat, From 3d7316573c3199a36f22ba99a0f291708d925abc Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 31 May 2024 16:42:46 -0600 Subject: [PATCH 086/266] added capacity factor curves formulas --- src/core/reopt_inputs.jl | 6 ++++-- src/core/scenario.jl | 24 ++++++++++++++++++++++++ 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index b4229608d..f3be4b428 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -431,7 +431,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ASHP" in techs.all - setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) else heating_cop["ASHP"] = ones(length(time_steps)) end @@ -896,12 +896,14 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end -function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) +function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) max_sizes["ASHP"] = s.ashp.max_kw min_sizes["ASHP"] = s.ashp.min_kw om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw heating_cop["ASHP"] = s.ashp.cop_heating cooling_cop["ASHP"] = s.ashp.cop_cooling + heating_cf["ASHP"] = s.ashp.cf_heating + cooling_cf["ASHP"] = s.ashp.cf_cooling if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP"] = effective_cost(; diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 49b07216d..0ef816b96 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -657,7 +657,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ashp = nothing cop_heating = [] cop_cooling = [] + cf_heating = [] + cf_cooling = [] if haskey(d, "ASHP") && d["ASHP"]["max_ton"] > 0.0 + # Add ASHP's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor @@ -697,6 +700,27 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end d["ASHP"]["cop_heating"] = cop_heating d["ASHP"]["cop_cooling"] = cop_cooling + + # Add ASHP's capacity factor curves + if !haskey(d["ASHP"], "cf_heating") || !haskey(d["ASHP"], "cf_cooling") + if !haskey(d["ASHP"], "cf_heating") + cf_heating = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) + else + cf_heating = round.(d["ASHP"]["cf_heating"],digits=3) + end + + if !haskey(d["ASHP"], "cf_cooling") + cf_cooling = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) + else + cf_cooling = round.(d["ASHP"]["cf_cooling"],digits=3) + end + + else + # Else if the user already provide cf curves, use them + cf_heating = round.(d["ASHP"]["cf_heating"],digits=3) + cf_cooling = round.(d["ASHP"]["cf_cooling"],digits=3) + end + ashp = ASHP(;dictkeys_tosymbols(d["ASHP"])...) end From 68c114e7b3fb0ccaef2b3181f1006a4d098a8147 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 31 May 2024 16:50:36 -0600 Subject: [PATCH 087/266] define capacity factor curves as reopt inputs --- src/core/reopt_inputs.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index f3be4b428..a33ff0a1b 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -62,6 +62,8 @@ struct REoptInputs <: AbstractInputs techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) heating_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) cooling_cop::Dict{String, Array{<:Real, 1}} # (techs.ashp) + heating_cf::Dict{String, Array{<:Real, 1}} # (techs.ashp) + cooling_cf::Dict{String, Array{<:Real, 1}} # (techs.ashp) heating_loads_kw::Dict{String, <:Real} # (heating_loads) unavailability::Dict{String, Array{Float64,1}} # Dict by tech of unavailability profile end @@ -128,6 +130,8 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) + heating_cf::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) + cooling_cf::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) heating_loads::Vector{String} # list of heating loads heating_loads_kw::Dict{String, Array{Real,1}} # (heating_loads) heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) @@ -168,7 +172,7 @@ function REoptInputs(s::AbstractScenario) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - heating_cop, cooling_cop = setup_tech_inputs(s,time_steps) + heating_cop, cooling_cop, heating_cf, cooling_cf = setup_tech_inputs(s,time_steps) pbi_pwf, pbi_max_benefit, pbi_max_kw, pbi_benefit_per_kwh = setup_pbi_inputs(s, techs) @@ -316,6 +320,8 @@ function REoptInputs(s::AbstractScenario) techs_operating_reserve_req_fraction, heating_cop, cooling_cop, + heating_cf, + cooling_cf, heating_loads, heating_loads_kw, heating_loads_served_by_tes, @@ -354,6 +360,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) heating_cop = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) + heating_cf = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) + cooling_cf = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.cooling) # export related inputs @@ -455,7 +463,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - heating_cop, cooling_cop + heating_cop, cooling_cop, heating_cf, cooling_cf end From 51460a004f79d832b8013fa147d3e7296e77dd42 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 31 May 2024 17:01:01 -0600 Subject: [PATCH 088/266] Update ashp.jl --- src/core/ashp.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 763d3e327..c23771297 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -56,6 +56,8 @@ function ASHP(; can_supply_steam_turbine::Union{Bool, Nothing} = nothing, cop_heating::Array{Float64,1} = Float64[], cop_cooling::Array{Float64,1} = Float64[], + cf_heating::Array{Float64,1} = Float64[], + cf_cooling::Array{Float64,1} = Float64[], can_serve_dhw::Union{Bool, Nothing} = nothing, can_serve_space_heating::Union{Bool, Nothing} = nothing, can_serve_process_heat::Union{Bool, Nothing} = nothing, From 5b25cf39cf5e66d7b28318b1dd4f112051156823 Mon Sep 17 00:00:00 2001 From: An Pham Date: Fri, 31 May 2024 17:26:30 -0600 Subject: [PATCH 089/266] add capacity factor to tech constraints --- src/constraints/thermal_tech_constraints.jl | 6 +++--- src/core/reopt_inputs.jl | 2 ++ 2 files changed, 5 insertions(+), 3 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 9b8d67ef5..9c173035f 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -27,7 +27,7 @@ function add_heating_tech_constraints(m, p; _n="") # Constraint (7_heating_prod_size): Production limit based on size for non-electricity-producing heating techs if !isempty(setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp))) @constraint(m, [t in setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp)), ts in p.time_steps], - sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) <= m[Symbol("dvSize"*_n)][t] + sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) <= m[Symbol("dvSize"*_n)][t]*p.heating_cf[t][ts] ) end # Constraint (7_heating_load_compatability): Set production variables for incompatible heat loads to zero @@ -52,7 +52,7 @@ end function add_ashp_heating_cooling_constraints(m, p; _n="") @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], - sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] + sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t]*p.heating_cf[t][ts] ) end @@ -68,7 +68,7 @@ end function add_cooling_tech_constraints(m, p; _n="") # Constraint (7_cooling_prod_size): Production limit based on size for boiler @constraint(m, [t in setdiff(p.techs.cooling, p.techs.ghp), ts in p.time_steps_with_grid], - m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t]*p.cooling_cf[t][ts] ) # The load balance for cooling is only applied to time_steps_with_grid, so make sure we don't arbitrarily show cooling production for time_steps_without_grid for t in setdiff(p.techs.cooling, p.techs.ghp) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index a33ff0a1b..939ef575a 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -442,6 +442,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) else heating_cop["ASHP"] = ones(length(time_steps)) + heating_cf["ASHP"] = ones(length(time_steps)) + cooling_cf["ASHP"] = ones(length(time_steps)) end if !isempty(techs.ghp) From 2fcea87ae733c8a7fb3c79770712dc85fa5beb1b Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 3 Jun 2024 12:05:28 -0600 Subject: [PATCH 090/266] set capacity factors of other technologies to default of one --- src/core/reopt_inputs.jl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 939ef575a..782e8905b 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -360,8 +360,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) heating_cop = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) - heating_cf = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) - cooling_cf = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) + heating_cf = Dict(t => zeros(length(time_steps)) for t in union(techs.electric_heater, techs.chp)) + cooling_cf = Dict(t => zeros(length(time_steps)) for t in techs.cooling) cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.cooling) # export related inputs @@ -709,7 +709,8 @@ function setup_existing_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, tech_emissions_factors_SO2["ExistingBoiler"] = s.existing_boiler.emissions_factor_lb_SO2_per_mmbtu / KWH_PER_MMBTU tech_emissions_factors_PM25["ExistingBoiler"] = s.existing_boiler.emissions_factor_lb_PM25_per_mmbtu / KWH_PER_MMBTU existing_boiler_fuel_cost_per_kwh = s.existing_boiler.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU - fuel_cost_per_kwh["ExistingBoiler"] = per_hour_value_to_time_series(existing_boiler_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "ExistingBoiler") + fuel_cost_per_kwh["ExistingBoiler"] = per_hour_value_to_time_series(existing_boiler_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "ExistingBoiler") + heating_cf["ExistingBoiler"] .= 1.0 return nothing end @@ -749,6 +750,7 @@ function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost production_factor["Boiler", :] = get_production_factor(s.boiler) boiler_fuel_cost_per_kwh = s.boiler.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU fuel_cost_per_kwh["Boiler"] = per_hour_value_to_time_series(boiler_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "Boiler") + heating_cf["Boiler"] .= 1.0 return nothing end @@ -764,6 +766,7 @@ function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes existing_sizes["ExistingChiller"] = 0.0 cap_cost_slope["ExistingChiller"] = 0.0 cooling_cop["ExistingChiller"] .= s.existing_chiller.cop + cooling_cf["ExistingChiller"] .= 1.0 # om_cost_per_kw["ExistingChiller"] = 0.0 return nothing end @@ -796,6 +799,7 @@ function setup_absorption_chiller_inputs(s::AbstractScenario, max_sizes, min_siz end cooling_cop["AbsorptionChiller"] .= s.absorption_chiller.cop_electric + cooling_cf["AbsorptionChiller"] .= 1.0 if isnothing(s.chp) thermal_factor = 1.0 elseif s.chp.cooling_thermal_factor == 0.0 @@ -839,7 +843,8 @@ function setup_chp_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_sl tech_emissions_factors_SO2["CHP"] = s.chp.emissions_factor_lb_SO2_per_mmbtu / KWH_PER_MMBTU tech_emissions_factors_PM25["CHP"] = s.chp.emissions_factor_lb_PM25_per_mmbtu / KWH_PER_MMBTU chp_fuel_cost_per_kwh = s.chp.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU - fuel_cost_per_kwh["CHP"] = per_hour_value_to_time_series(chp_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "CHP") + fuel_cost_per_kwh["CHP"] = per_hour_value_to_time_series(chp_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "CHP") + heating_cf["CHP"] .= 1.0 return nothing end @@ -878,6 +883,8 @@ function setup_steam_turbine_inputs(s::AbstractScenario, max_sizes, min_sizes, c push!(techs.no_curtail, "SteamTurbine") end + heating_cf["SteamTurbine"] .= 1.0 + return nothing end From b9ab386e40bd6edd8a8916a1df860278c849337b Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 3 Jun 2024 15:17:37 -0600 Subject: [PATCH 091/266] add heating_cf and cooling_cf to tech-specific input functions --- src/core/reopt_inputs.jl | 60 ++++++++++++++++++++++++---------------- 1 file changed, 36 insertions(+), 24 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 782e8905b..d4ef654a5 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -400,50 +400,56 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) if "ExistingBoiler" in techs.all setup_existing_boiler_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf) end if "Boiler" in techs.all setup_boiler_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, - boiler_efficiency, production_factor, fuel_cost_per_kwh) + boiler_efficiency, production_factor, fuel_cost_per_kwh, heating_cf) end if "CHP" in techs.all setup_chp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, production_factor, techs_by_exportbin, techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf) end if "ExistingChiller" in techs.all - setup_existing_chiller_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) + setup_existing_chiller_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop, cooling_cf) else cooling_cop["ExistingChiller"] = ones(length(time_steps)) + cooling_cf["ExistingChiller"] = zeros(length(time_steps)) end if "AbsorptionChiller" in techs.all - setup_absorption_chiller_inputs(s, max_sizes, min_sizes, cap_cost_slope, cooling_cop, thermal_cop, om_cost_per_kw) + setup_absorption_chiller_inputs(s, max_sizes, min_sizes, cap_cost_slope, cooling_cop, thermal_cop, om_cost_per_kw, cooling_cf) else cooling_cop["AbsorptionChiller"] = ones(length(time_steps)) thermal_cop["AbsorptionChiller"] = 1.0 + cooling_cf["ExistingChiller"] = zeros(length(time_steps)) end if "SteamTurbine" in techs.all - setup_steam_turbine_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, production_factor, techs_by_exportbin, techs) + setup_steam_turbine_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, production_factor, techs_by_exportbin, techs, heating_cf) end if "ElectricHeater" in techs.all - setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) + setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) else heating_cop["ElectricHeater"] = ones(length(time_steps)) + heating_cf["ExistingChiller"] = zeros(length(time_steps)) end if "ASHP" in techs.all setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) else heating_cop["ASHP"] = ones(length(time_steps)) - heating_cf["ASHP"] = ones(length(time_steps)) - cooling_cf["ASHP"] = ones(length(time_steps)) + cooling_cop["ASHP"] = ones(length(time_steps)) + heating_cf["ASHP"] = zeros(length(time_steps)) + cooling_cf["ASHP"] = zeros(length(time_steps)) end if !isempty(techs.ghp) @@ -689,14 +695,16 @@ end """ function setup_existing_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf) Update tech-indexed data arrays necessary to build the JuMP model with the values for existing boiler. This version of this function, used in BAUInputs(), doesn't update renewable energy and emissions arrays. """ function setup_existing_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf) max_sizes["ExistingBoiler"] = s.existing_boiler.max_kw min_sizes["ExistingBoiler"] = 0.0 existing_sizes["ExistingBoiler"] = 0.0 @@ -710,7 +718,7 @@ function setup_existing_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, tech_emissions_factors_PM25["ExistingBoiler"] = s.existing_boiler.emissions_factor_lb_PM25_per_mmbtu / KWH_PER_MMBTU existing_boiler_fuel_cost_per_kwh = s.existing_boiler.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU fuel_cost_per_kwh["ExistingBoiler"] = per_hour_value_to_time_series(existing_boiler_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "ExistingBoiler") - heating_cf["ExistingBoiler"] .= 1.0 + heating_cf["ExistingBoiler"] = ones(8760*s.settings.time_steps_per_hour) return nothing end @@ -721,7 +729,8 @@ end Update tech-indexed data arrays necessary to build the JuMP model with the values for (new) boiler. This version of this function, used in BAUInputs(), doesn't update renewable energy and emissions arrays. """ -function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, boiler_efficiency, production_factor, fuel_cost_per_kwh) +function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, boiler_efficiency, production_factor, fuel_cost_per_kwh, + heating_cf) max_sizes["Boiler"] = s.boiler.max_kw min_sizes["Boiler"] = s.boiler.min_kw boiler_efficiency["Boiler"] = s.boiler.efficiency @@ -750,30 +759,30 @@ function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost production_factor["Boiler", :] = get_production_factor(s.boiler) boiler_fuel_cost_per_kwh = s.boiler.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU fuel_cost_per_kwh["Boiler"] = per_hour_value_to_time_series(boiler_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "Boiler") - heating_cf["Boiler"] .= 1.0 + heating_cf["Boiler"] = ones(8760*s.settings.time_steps_per_hour) return nothing end """ - function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) + function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop, cooling_cf) Update tech-indexed data arrays necessary to build the JuMP model with the values for existing chiller. """ -function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) +function setup_existing_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop, cooling_cf) max_sizes["ExistingChiller"] = s.existing_chiller.max_kw min_sizes["ExistingChiller"] = 0.0 existing_sizes["ExistingChiller"] = 0.0 cap_cost_slope["ExistingChiller"] = 0.0 cooling_cop["ExistingChiller"] .= s.existing_chiller.cop - cooling_cf["ExistingChiller"] .= 1.0 + cooling_cf["ExistingChiller"] = ones(8760*s.settings.time_steps_per_hour) # om_cost_per_kw["ExistingChiller"] = 0.0 return nothing end function setup_absorption_chiller_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, - cooling_cop, thermal_cop, om_cost_per_kw + cooling_cop, thermal_cop, om_cost_per_kw, cooling_cf ) max_sizes["AbsorptionChiller"] = s.absorption_chiller.max_kw min_sizes["AbsorptionChiller"] = s.absorption_chiller.min_kw @@ -816,14 +825,16 @@ end """ function setup_chp_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, production_factor, techs_by_exportbin, segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf ) Update tech-indexed data arrays necessary to build the JuMP model with the values for CHP. """ function setup_chp_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, production_factor, techs_by_exportbin, segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf ) max_sizes["CHP"] = s.chp.max_kw min_sizes["CHP"] = s.chp.min_kw @@ -844,12 +855,12 @@ function setup_chp_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_sl tech_emissions_factors_PM25["CHP"] = s.chp.emissions_factor_lb_PM25_per_mmbtu / KWH_PER_MMBTU chp_fuel_cost_per_kwh = s.chp.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU fuel_cost_per_kwh["CHP"] = per_hour_value_to_time_series(chp_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "CHP") - heating_cf["CHP"] .= 1.0 + heating_cf["CHP"] = ones(8760*s.settings.time_steps_per_hour) return nothing end function setup_steam_turbine_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, - om_cost_per_kw, production_factor, techs_by_exportbin, techs + om_cost_per_kw, production_factor, techs_by_exportbin, techs, heating_cf ) max_sizes["SteamTurbine"] = s.steam_turbine.max_kw @@ -883,16 +894,17 @@ function setup_steam_turbine_inputs(s::AbstractScenario, max_sizes, min_sizes, c push!(techs.no_curtail, "SteamTurbine") end - heating_cf["SteamTurbine"] .= 1.0 + heating_cf["SteamTurbine"] = ones(8760*s.settings.time_steps_per_hour) return nothing end -function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop) +function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) max_sizes["ElectricHeater"] = s.electric_heater.max_kw min_sizes["ElectricHeater"] = s.electric_heater.min_kw om_cost_per_kw["ElectricHeater"] = s.electric_heater.om_cost_per_kw heating_cop["ElectricHeater"] .= s.electric_heater.cop + heating_cf["ElectricHeater"] = ones(8760*s.settings.time_steps_per_hour) #TODO: add timem series input for Electric Heater if using as AShP DHW heater? or use ASHP object? if s.electric_heater.macrs_option_years in [5, 7] cap_cost_slope["ElectricHeater"] = effective_cost(; From 8cdc86df1b693820addf9ee5d1560237635343a7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 3 Jun 2024 20:20:42 -0600 Subject: [PATCH 092/266] finish setup of capacity factors --- src/constraints/thermal_tech_constraints.jl | 6 +++--- src/core/reopt_inputs.jl | 16 +++++++++------- src/core/scenario.jl | 3 ++- 3 files changed, 14 insertions(+), 11 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 9c173035f..5fecc6981 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -27,7 +27,7 @@ function add_heating_tech_constraints(m, p; _n="") # Constraint (7_heating_prod_size): Production limit based on size for non-electricity-producing heating techs if !isempty(setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp))) @constraint(m, [t in setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp)), ts in p.time_steps], - sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) <= m[Symbol("dvSize"*_n)][t]*p.heating_cf[t][ts] + sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) <= m[Symbol("dvSize"*_n)][t] * p.heating_cf[t][ts] ) end # Constraint (7_heating_load_compatability): Set production variables for incompatible heat loads to zero @@ -52,7 +52,7 @@ end function add_ashp_heating_cooling_constraints(m, p; _n="") @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], - sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t]*p.heating_cf[t][ts] + sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) / p.heating_cf[t][ts] + m[Symbol("dvCoolingProduction"*_n)][t,ts] / p.cooling_cf[t][ts] <= m[Symbol("dvSize"*_n)][t] ) end @@ -68,7 +68,7 @@ end function add_cooling_tech_constraints(m, p; _n="") # Constraint (7_cooling_prod_size): Production limit based on size for boiler @constraint(m, [t in setdiff(p.techs.cooling, p.techs.ghp), ts in p.time_steps_with_grid], - m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t]*p.cooling_cf[t][ts] + m[Symbol("dvCoolingProduction"*_n)][t,ts] <= m[Symbol("dvSize"*_n)][t] * p.cooling_cf[t][ts] ) # The load balance for cooling is only applied to time_steps_with_grid, so make sure we don't arbitrarily show cooling production for time_steps_without_grid for t in setdiff(p.techs.cooling, p.techs.ghp) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index d4ef654a5..31cbeef2f 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -130,8 +130,8 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs techs_operating_reserve_req_fraction::Dict{String, <:Real} # (techs.all) heating_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) cooling_cop::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) - heating_cf::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) - cooling_cf::Dict{String, Array{Float64,1}} # (techs.ashp, time_steps) + heating_cf::Dict{String, Array{Float64,1}} # (techs.heating, time_steps) + cooling_cf::Dict{String, Array{Float64,1}} # (techs.cooling, time_steps) heating_loads::Vector{String} # list of heating loads heating_loads_kw::Dict{String, Array{Real,1}} # (heating_loads) heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) @@ -359,8 +359,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) tech_emissions_factors_PM25 = Dict(t => 0.0 for t in techs.all) techs_operating_reserve_req_fraction = Dict(t => 0.0 for t in techs.all) thermal_cop = Dict(t => 0.0 for t in techs.absorption_chiller) - heating_cop = Dict(t => zeros(length(time_steps)) for t in techs.electric_heater) - heating_cf = Dict(t => zeros(length(time_steps)) for t in union(techs.electric_heater, techs.chp)) + heating_cop = Dict(t => zeros(length(time_steps)) for t in union(techs.heating, techs.chp)) + heating_cf = Dict(t => zeros(length(time_steps)) for t in union(techs.heating, techs.chp)) cooling_cf = Dict(t => zeros(length(time_steps)) for t in techs.cooling) cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.cooling) @@ -429,7 +429,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) else cooling_cop["AbsorptionChiller"] = ones(length(time_steps)) thermal_cop["AbsorptionChiller"] = 1.0 - cooling_cf["ExistingChiller"] = zeros(length(time_steps)) + cooling_cf["AbsorptionChiller"] = zeros(length(time_steps)) end if "SteamTurbine" in techs.all @@ -440,7 +440,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) else heating_cop["ElectricHeater"] = ones(length(time_steps)) - heating_cf["ExistingChiller"] = zeros(length(time_steps)) + heating_cf["ElectricHeater"] = zeros(length(time_steps)) end if "ASHP" in techs.all @@ -455,6 +455,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) if !isempty(techs.ghp) cooling_cop["GHP"] = ones(length(time_steps)) heating_cop["GHP"] = ones(length(time_steps)) + heating_cf["GHP"] = ones(length(time_steps)) + cooling_cf["GHP"] = ones(length(time_steps)) end # filling export_bins_by_tech MUST be done after techs_by_exportbin has been filled in @@ -718,7 +720,7 @@ function setup_existing_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, tech_emissions_factors_PM25["ExistingBoiler"] = s.existing_boiler.emissions_factor_lb_PM25_per_mmbtu / KWH_PER_MMBTU existing_boiler_fuel_cost_per_kwh = s.existing_boiler.fuel_cost_per_mmbtu ./ KWH_PER_MMBTU fuel_cost_per_kwh["ExistingBoiler"] = per_hour_value_to_time_series(existing_boiler_fuel_cost_per_kwh, s.settings.time_steps_per_hour, "ExistingBoiler") - heating_cf["ExistingBoiler"] = ones(8760*s.settings.time_steps_per_hour) + heating_cf["ExistingBoiler"] = ones(8760*s.settings.time_steps_per_hour) return nothing end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 0ef816b96..3a9150576 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -720,7 +720,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) cf_heating = round.(d["ASHP"]["cf_heating"],digits=3) cf_cooling = round.(d["ASHP"]["cf_cooling"],digits=3) end - + d["ASHP"]["cf_heating"] = cf_heating + d["ASHP"]["cf_cooling"] = cf_cooling ashp = ASHP(;dictkeys_tosymbols(d["ASHP"])...) end From 0e71126b21f52d24cf93e646132530de12097f84 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 08:34:01 -0600 Subject: [PATCH 093/266] Update ashp.json --- test/scenarios/ashp.json | 3 --- 1 file changed, 3 deletions(-) diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index d1e538ff5..9527e2d04 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -45,8 +45,5 @@ }, "ElectricTariff": { "monthly_energy_rates": [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1] - }, - "HotThermalStorage":{ - "max_gal":2500 } } \ No newline at end of file From 64e2f20608844662a7f6f03a7e04a95c8eb41cff Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 09:30:18 -0600 Subject: [PATCH 094/266] include capacity factor in BAU inputs --- src/core/bau_inputs.jl | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index 8340f52c7..a4777d319 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -28,6 +28,8 @@ function BAUInputs(p::REoptInputs) thermal_cop = Dict{String, Float64}() heating_cop = Dict{String, Array{Float64,1}}() cooling_cop = Dict{String, Array{Float64,1}}() + heating_cf = Dict{String, Array{Float64,1}}() + cooling_cf = Dict{String, Array{Float64,1}}() production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) tech_renewable_energy_fraction = Dict(t => 0.0 for t in techs.all) # !!! note: tech_emissions_factors are in lb / kWh of fuel burned (gets multiplied by kWh of fuel burned, not kWh electricity consumption, ergo the use of the HHV instead of fuel slope) @@ -90,12 +92,13 @@ function BAUInputs(p::REoptInputs) if "ExistingBoiler" in techs.all setup_existing_boiler_inputs(bau_scenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, - tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh) + tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, fuel_cost_per_kwh, + heating_cf) end cooling_cop["ExistingChiller"] = ones(length(p.time_steps)) if "ExistingChiller" in techs.all - setup_existing_chiller_inputs(bau_scenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop) + setup_existing_chiller_inputs(bau_scenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, cooling_cop, cooling_cf) end # Assign null GHP parameters for REoptInputs @@ -222,6 +225,8 @@ function BAUInputs(p::REoptInputs) p.techs_operating_reserve_req_fraction, heating_cop, cooling_cop, + heating_cf, + cooling_cf, heating_loads, heating_loads_kw, heating_loads_served_by_tes, From f565ad4cac0c437aeef26f582fc4898e7ce73dac Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 09:30:36 -0600 Subject: [PATCH 095/266] update ASHP tests to account for CF's --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index d95529001..7773ab108 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2394,7 +2394,7 @@ else # run HiGHS tests annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP"]["size_ton"] ≈ 0.8 * REopt.KWH_PER_MMBTU / REopt.KWH_THERMAL_PER_TONHOUR atol=0.01 + @test results["ASHP"]["size_ton"] ≈ 74.99 atol=0.01 @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2410,7 +2410,7 @@ else # run HiGHS tests annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP"]["size_ton"] ≈ 0.1 + 0.8 * REopt.KWH_PER_MMBTU / REopt.KWH_THERMAL_PER_TONHOUR atol=0.01 #size increases when cooling load also served + @test results["ASHP"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 From 11cf9ed8ea6c91f85abc788316ab4593489ac1ed Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 11:01:00 -0600 Subject: [PATCH 096/266] Update ASHP test value --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 7773ab108..1d0e105f0 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2412,7 +2412,7 @@ else # run HiGHS tests annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR @test results["ASHP"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 + @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 873.9 atol=1e-4 #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) From 62debc5709c428dd84bb70a1e4d3012e5dc38773 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:10:49 -0600 Subject: [PATCH 097/266] Update REopt.jl --- src/REopt.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/REopt.jl b/src/REopt.jl index 14929ba08..98d66abc8 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -137,6 +137,7 @@ include("core/chp.jl") include("core/ghp.jl") include("core/steam_turbine.jl") include("core/electric_heater.jl") +include("core/ashp_wh.jl") include("core/ashp.jl") include("core/scenario.jl") include("core/bau_scenario.jl") @@ -191,6 +192,7 @@ include("results/flexible_hvac.jl") include("results/ghp.jl") include("results/steam_turbine.jl") include("results/electric_heater.jl") +include("results/ashp_wh.jl") include("results/ashp.jl") include("results/heating_cooling_load.jl") From 2a6809edadb8146d274850b3736e620a03a70e9b Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:16:55 -0600 Subject: [PATCH 098/266] Added ashp_wh.jl --- src/core/ashp_wh.jl | 123 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 123 insertions(+) create mode 100644 src/core/ashp_wh.jl diff --git a/src/core/ashp_wh.jl b/src/core/ashp_wh.jl new file mode 100644 index 000000000..67d7fa7c1 --- /dev/null +++ b/src/core/ashp_wh.jl @@ -0,0 +1,123 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. + +struct ASHP_WH <: AbstractThermalTech + min_kw::Real + max_kw::Real + installed_cost_per_kw::Real + om_cost_per_kw::Real + macrs_option_years::Int + macrs_bonus_fraction::Real + can_supply_steam_turbine::Bool + cop_heating::Array{Float64,1} + can_serve_dhw::Bool + can_serve_space_heating::Bool + can_serve_process_heat::Bool + can_serve_cooling::Bool +end + + +""" +ASHP Water Heater + +If a user provides the `ASHP_WH` key then the optimal scenario has the option to purchase +this new `ASHP_WH` to meet the domestic hot water load in addition to using the `ExistingBoiler` +to meet the domestic hot water load. + +```julia +function ASHP_WH(; + min_ton_per_hour::Real = 0.0, # Minimum thermal power size + max_ton_per_hour::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS + can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production + cop_heating::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) + can_serve_dhw::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the domestic hot water load + can_serve_space_heating::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the space heating load + can_serve_process_heat::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the process heating load + can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the cooling load +) +``` +""" +function ASHP_WH(; + min_ton_per_hour::Real = 0.0, + max_ton_per_hour::Real = BIG_NUMBER, + installed_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, + om_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, + macrs_option_years::Int = 0, + macrs_bonus_fraction::Real = 0.0, + can_supply_steam_turbine::Union{Bool, Nothing} = nothing, + cop_heating::Array{Float64,1} = Float64[], + can_serve_dhw::Union{Bool, Nothing} = nothing, + can_serve_space_heating::Union{Bool, Nothing} = nothing, + can_serve_process_heat::Union{Bool, Nothing} = nothing, + can_serve_cooling::Union{Bool, Nothing} = nothing + ) + + defaults = get_ashp_wh_defaults() + + # populate defaults as needed + if isnothing(installed_cost_per_ton_per_hour) + installed_cost_per_ton_per_hour = defaults["installed_cost_per_ton_per_hour"] + end + if isnothing(om_cost_per_ton_per_hour) + om_cost_per_ton_per_hour = defaults["om_cost_per_ton_per_hour"] + end + if isnothing(can_supply_steam_turbine) + can_supply_steam_turbine = defaults["can_supply_steam_turbine"] + end + if isnothing(can_serve_dhw) + can_serve_dhw = defaults["can_serve_dhw"] + end + if isnothing(can_serve_space_heating) + can_serve_space_heating = defaults["can_serve_space_heating"] + end + if isnothing(can_serve_process_heat) + can_serve_process_heat = defaults["can_serve_process_heat"] + end + if isnothing(can_serve_cooling) + can_serve_cooling = defaults["can_serve_cooling"] + end + + # Convert max sizes, cost factors from mmbtu_per_hour to kw + min_kw = min_ton_per_hour * KWH_PER_MMBTU * 0.012 + max_kw = max_ton_per_hour * KWH_PER_MMBTU * 0.012 + + installed_cost_per_kw = installed_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) + om_cost_per_kw = om_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) + + + ASHP_WH( + min_kw, + max_kw, + installed_cost_per_kw, + om_cost_per_kw, + macrs_option_years, + macrs_bonus_fraction, + can_supply_steam_turbine, + cop_heating, + can_serve_dhw, + can_serve_space_heating, + can_serve_process_heat, + can_serve_cooling + ) +end + + + +""" +function get_ashp_wh_defaults() + +Obtains defaults for the ASHP_WH from a JSON data file. + +inputs +None + +returns +ashp_wh_defaults::Dict -- Dictionary containing defaults for ASHP_WH +""" +function get_ashp_wh_defaults() + ashp_wh_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_wh_defaults.json")) + return ashp_wh_defaults +end \ No newline at end of file From f45924d2c6fd1bd5831d1f047c57ce64be980854 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:22:42 -0600 Subject: [PATCH 099/266] added ASHP WH inputs --- src/core/reopt_inputs.jl | 26 ++++++++++++++++++++++++++ 1 file changed, 26 insertions(+) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 5824215ff..588d46d9a 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -955,6 +955,32 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ end +function setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) + max_sizes["ASHP_WH"] = s.ashp_wh.max_kw + min_sizes["ASHP_WH"] = s.ashp_wh.min_kw + om_cost_per_kw["ASHP_WH"] = s.ashp_wh.om_cost_per_kw + heating_cop["ASHP_WH"] = s.ashp_wh.cop_heating + + if s.ashp_wh.macrs_option_years in [5, 7] + cap_cost_slope["ASHP_WH"] = effective_cost(; + itc_basis = s.ashp_wh.installed_cost_per_kw, + replacement_cost = 0.0, + replacement_year = s.financial.analysis_years, + discount_rate = s.financial.owner_discount_rate_fraction, + tax_rate = s.financial.owner_tax_rate_fraction, + itc = 0.0, + macrs_schedule = s.ashp_wh.macrs_option_years == 5 ? s.financial.macrs_five_year : s.financial.macrs_seven_year, + macrs_bonus_fraction = s.ashp_wh.macrs_bonus_fraction, + macrs_itc_reduction = 0.0, + rebate_per_kw = 0.0 + ) + else + cap_cost_slope["ASHP_WH"] = s.ashp_wh.installed_cost_per_kw + end + +end + + function setup_present_worth_factors(s::AbstractScenario, techs::Techs) lvl_factor = Dict(t => 1.0 for t in techs.all) # default levelization_factor of 1.0 From d8b068887aec52d5f7dad429c1d547eeba4a60d0 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:30:40 -0600 Subject: [PATCH 100/266] Update scenario.jl --- src/core/scenario.jl | 56 +++++++++++++++++++++++++++++++++++++++++++- 1 file changed, 55 insertions(+), 1 deletion(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index e37aa2935..5d7a5117d 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -26,6 +26,7 @@ struct Scenario <: AbstractScenario steam_turbine::Union{SteamTurbine, Nothing} electric_heater::Union{ElectricHeater, Nothing} ashp::Union{ASHP, Nothing} + ashp_wh::Union{ASHP_WH, Nothing} end """ @@ -724,6 +725,58 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ashp = ASHP(;dictkeys_tosymbols(d["ASHP"])...) end + # ASHP Water Heater: + ashp_wh = nothing + cop_heating = [] + cf_heating = [] + + if haskey(d, "ASHP_WH") && d["ASHP_WH"]["max_ton"] > 0.0 + # Add ASHP_WH's COPs + # If user does not provide heating cop series then assign cop curves based on ambient temperature + if !haskey(d["ASHP_WH"], "cop_heating") + # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor + if isnothing(ambient_temp_celsius) + if !isempty(pvs) + for pv in pvs + pv.production_factor_series, ambient_temp_celsius = 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 + else + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + end + end + ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 + + if !haskey(d["ASHP_WH"], "cop_heating") + cop_heating = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) + cop_heating[ambient_temp_fahrenheit .< -7.6] .= 1 + cop_heating[ambient_temp_fahrenheit .> 79] .= 999999 + else + cop_heating = round.(d["ASHP_WH"]["cop_heating"],digits=3) + end + else + # Else if the user already provide cop series, use that + cop_heating = round.(d["ASHP_WH"]["cop_heating"],digits=3) + end + d["ASHP_WH"]["cop_heating"] = cop_heating + + # Add ASHP_WH's capacity factor curves + if !haskey(d["ASHP_WH"], "cf_heating") + if !haskey(d["ASHP_WH"], "cf_heating") + cf_heating = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) + else + cf_heating = round.(d["ASHP_WH"]["cf_heating"],digits=3) + end + else + # Else if the user already provide cf curves, use them + cf_heating = round.(d["ASHP_WH"]["cf_heating"],digits=3) + end + d["ASHP_WH"]["cf_heating"] = cf_heating + ashp_wh = ASHP_WH(;dictkeys_tosymbols(d["ASHP_WH"])...) + end + return Scenario( settings, site, @@ -750,7 +803,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) cooling_thermal_load_reduction_with_ghp_kw, steam_turbine, electric_heater, - ashp + ashp, + ashp_wh ) end From 43db840ac38968a5497a78e3359c484a871830ac Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:33:33 -0600 Subject: [PATCH 101/266] Update techs.jl --- src/core/techs.jl | 22 ++++++++++++++++++++++ 1 file changed, 22 insertions(+) diff --git a/src/core/techs.jl b/src/core/techs.jl index f3bf74d01..4d8b8e9f2 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -288,6 +288,28 @@ function Techs(s::Scenario) end end + if !isnothing(s.ashp_wh) + push!(all_techs, "ASHP_WH") + push!(heating_techs, "ASHP_WH") + push!(electric_heaters, "ASHP_WH") + push!(ashp_techs, "ASHP_WH") + if s.ashp_wh.can_supply_steam_turbine + push!(techs_can_supply_steam_turbine, "ASHP_WH") + end + if s.ashp_wh.can_serve_space_heating + push!(techs_can_serve_space_heating, "ASHP_WH") + end + if s.ashp_wh.can_serve_dhw + push!(techs_can_serve_dhw, "ASHP_WH") + end + if s.ashp_wh.can_serve_process_heat + push!(techs_can_serve_process_heat, "ASHP_WH") + end + if s.ashp_wh.can_serve_cooling + push!(cooling_techs, "ASHP_WH") + end + end + if s.settings.off_grid_flag append!(requiring_oper_res, pvtechs) append!(providing_oper_res, pvtechs) From df83ca48cc088a8f63bc22f12b216a4cf867ecb3 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:43:36 -0600 Subject: [PATCH 102/266] resolve conflicts --- src/core/types.jl | 1 + src/results/ashp_wh.jl | 99 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 100 insertions(+) create mode 100644 src/results/ashp_wh.jl diff --git a/src/core/types.jl b/src/core/types.jl index 4ddba9cc6..ee44eec49 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -77,4 +77,5 @@ mutable struct Techs can_serve_process_heat::Vector{String} ghp::Vector{String} ashp::Vector{String} + ashp_wh::Vector{String} end diff --git a/src/results/ashp_wh.jl b/src/results/ashp_wh.jl new file mode 100644 index 000000000..1f41ce8ea --- /dev/null +++ b/src/results/ashp_wh.jl @@ -0,0 +1,99 @@ +# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. + +""" +`ASHP_WH` results keys: +- `size_ton` # Thermal production capacity size of the ASHP_WH [ton/hr] +- `electric_consumption_series_kw` # Fuel consumption series [kW] +- `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] +- `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] +- `annual_thermal_production_mmbtu` # Thermal heating energy produced in a year [MMBtu] +- `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] +- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] +- `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] +- `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage +- `thermal_to_load_series_ton` # Thermal production to cooling load +- `electric_consumption_series_kw` +- `annual_electric_consumption_kwh` +- `annual_thermal_production_tonhour` Thermal cooling energy produced in a year + + +!!! note "'Series' and 'Annual' energy outputs are average annual" + REopt performs load balances using average annual production values for technologies that include degradation. + Therefore, all timeseries (`_series`) and `annual_` results should be interpretted as energy outputs averaged over the analysis period. + +""" + +function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") + r = Dict{String, Any}() + r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_WH"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + @expression(m, ASHPWHElectricConsumptionSeries[ts in p.time_steps], + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] + for q in p.heating_loads, t in p.techs.ashp_wh) + ) + + @expression(m, ASHPWHThermalProductionSeries[ts in p.time_steps], + sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp_wh)) + r["thermal_production_series_mmbtu_per_hour"] = + round.(value.(ASHPWHThermalProductionSeries) / KWH_PER_MMBTU, digits=5) + r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) + + if !isempty(p.s.storage.types.hot) + @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], + sum(m[:dvHeatToStorage][b,"ASHP_WH",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) + ) + @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], + sum(m[:dvHeatToStorage][b,"ASHP_WH",q,ts] for b in p.s.storage.types.hot) + ) + else + @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], 0.0) + @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) + end + r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPWHToHotTESKW) / KWH_PER_MMBTU, digits=3) + + if !isempty(p.techs.steam_turbine) && p.s.ashp.can_supply_steam_turbine + @expression(m, ASHPWHToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP_WH",q,ts] for q in p.heating_loads)) + @expression(m, ASHPWHToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP_WH",q,ts]) + else + ASHPWHToSteamTurbine = zeros(length(p.time_steps)) + @expression(m, ASHPWHToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], 0.0) + end + r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ASHPWHToSteamTurbine) / KWH_PER_MMBTU, digits=3) + + @expression(m, ASHPWHToLoad[ts in p.time_steps], + sum(m[:dvHeatingProduction]["ASHP_WH", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] - ASHPWHToSteamTurbine[ts] + ) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) + + if "DomesticHotWater" in p.heating_loads && p.s.ashp_wh.can_serve_dhw + @expression(m, ASHPWHToDHWKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP_WH","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] - ASHPWHToSteamTurbineByQuality["DomesticHotWater",ts] + ) + else + @expression(m, ASHPWHToDHWKW[ts in p.time_steps], 0.0) + end + r["thermal_to_dhw_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToDHWKW ./ KWH_PER_MMBTU), digits=5) + + if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating + @expression(m, ASHPWHToSpaceHeatingKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP_WH","SpaceHeating",ts] - ASHPWHToHotTESByQualityKW["SpaceHeating",ts] - ASHPWHToSteamTurbineByQuality["SpaceHeating",ts] + ) + else + @expression(m, ASHPWHToSpaceHeatingKW[ts in p.time_steps], 0.0) + end + r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToSpaceHeatingKW ./ KWH_PER_MMBTU), digits=5) + + if "ProcessHeat" in p.heating_loads && p.s.ashp.can_serve_space_heating + @expression(m, ASHPWHToProcessHeatKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP_WH","ProcessHeat",ts] - ASHPWHToHotTESByQualityKW["ProcessHeat",ts] - ASHPWHToSteamTurbineByQuality["ProcessHeat",ts] + ) + else + @expression(m, ASHPWHToProcessHeatKW[ts in p.time_steps], 0.0) + end + r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) + + r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) + r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) + + d["ASHP_WH"] = r + nothing +end \ No newline at end of file From 56f2a30ae20c55d4aed6d55a0130b7bd123a4989 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:47:57 -0600 Subject: [PATCH 103/266] ASHP WH tests --- src/results/results.jl | 4 ++++ test/runtests.jl | 32 ++++++++++++++++++++++++++++++++ 2 files changed, 36 insertions(+) diff --git a/src/results/results.jl b/src/results/results.jl index 2cea92a79..a9b4595c9 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -107,6 +107,10 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") if !isempty(p.techs.ashp) add_ashp_results(m, p, d; _n) end + + if !isempty(p.techs.ashp_wh) + add_ashp_wh_results(m, p, d; _n) + end return d end diff --git a/test/runtests.jl b/test/runtests.jl index 8041a2e00..aa56ae346 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2456,6 +2456,38 @@ else # run HiGHS tests end + @testset "ASHP Water Heater" begin + #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP_WH is not purchased + d = JSON.parsefile("./scenarios/ashp_wh.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["ASHP_WH"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHP_WH"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP_WH"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + #Case 2: ASHP_WH has temperature-dependent output and serves all DHW load + d["ExistingChiller"] = Dict("retire_in_optimal" => false) + d["ExistingBoiler"]["retire_in_optimal"] = false + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ASHP_WH"]["installed_cost_per_ton"] = 300 + + p = REoptInputs(d) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WH"][ts] for ts in p.time_steps) + annual_energy_supplied = 87600 + annual_ashp_consumption + @test results["ASHP_WH"]["size_ton"] ≈ 74.99 atol=0.01 + @test results["ASHP_WH"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP_WH"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + @test results["ASHP_WH"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 + end + @testset "Process Heat Load" begin d = JSON.parsefile("./scenarios/process_heat.json") From 747d2a1d2205493a7004686ee99f75599d0c690c Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:49:58 -0600 Subject: [PATCH 104/266] resolve conflicts --- test/scenarios/ashp_wh.json | 52 +++++++++++++++++++++++++++++++++++++ 1 file changed, 52 insertions(+) create mode 100644 test/scenarios/ashp_wh.json diff --git a/test/scenarios/ashp_wh.json b/test/scenarios/ashp_wh.json new file mode 100644 index 000000000..59b752f70 --- /dev/null +++ b/test/scenarios/ashp_wh.json @@ -0,0 +1,52 @@ +{ + "Site": { + "latitude": 37.78, + "longitude": -122.45 + }, + "ExistingBoiler": { + "production_type": "steam", + "efficiency": 0.8, + "fuel_type": "natural_gas", + "fuel_cost_per_mmbtu": 5 + }, + "ASHP_WH": { + "min_ton": 0.0, + "max_ton": 100000, + "installed_cost_per_ton": 4050, + "om_cost_per_ton": 0.0, + "macrs_option_years": 0, + "macrs_bonus_fraction": 0.0, + "can_supply_steam_turbine": false, + "can_serve_space_heating": false, + "can_serve_dhw": true, + "can_serve_cooling": false + }, + "Financial": { + "om_cost_escalation_rate_fraction": 0.025, + "elec_cost_escalation_rate_fraction": 0.023, + "existing_boiler_fuel_cost_escalation_rate_fraction": 0.034, + "boiler_fuel_cost_escalation_rate_fraction": 0.034, + "offtaker_tax_rate_fraction": 0.26, + "offtaker_discount_rate_fraction": 0.083, + "third_party_ownership": false, + "owner_tax_rate_fraction": 0.26, + "owner_discount_rate_fraction": 0.083, + "analysis_years": 25 + }, + "ElectricLoad": { + "doe_reference_name": "FlatLoad", + "annual_kwh": 87600.0 + }, + "SpaceHeatingLoad": { + "doe_reference_name": "FlatLoad" + }, + "DomesticHotWaterLoad": { + "doe_reference_name": "FlatLoad" + }, + "ElectricTariff": { + "monthly_energy_rates": [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1] + }, + "HotThermalStorage":{ + "max_gal":2500 + } + } \ No newline at end of file From e35f36d9d69232d7a8ded3532ff8b3d89c7954be Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 15:51:17 -0600 Subject: [PATCH 105/266] resolve conflicts --- data/ashp/ashp_wh_defaults.json | 11 +++++++++++ 1 file changed, 11 insertions(+) create mode 100644 data/ashp/ashp_wh_defaults.json diff --git a/data/ashp/ashp_wh_defaults.json b/data/ashp/ashp_wh_defaults.json new file mode 100644 index 000000000..c7f650985 --- /dev/null +++ b/data/ashp/ashp_wh_defaults.json @@ -0,0 +1,11 @@ +{ + "installed_cost_per_ton": 4050, + "om_cost_per_ton": 0.00024, + "macrs_option_years": 0, + "macrs_bonus_fraction": 0.0, + "can_supply_steam_turbine": false, + "can_serve_process_heat": false, + "can_serve_dhw": true, + "can_serve_space_heating": false, + "can_serve_cooling": false +} From 72d1b0f5667b262b09ce44149a52e0ba2f798773 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 17:57:19 -0600 Subject: [PATCH 106/266] added ASHP Water Heater & resolved conflicts --- src/REopt.jl | 4 +-- src/core/ashp_wh.jl | 57 +++++++++++++++++++++++----------------- src/core/reopt_inputs.jl | 10 ++++++- src/core/techs.jl | 7 ++++- src/results/ashp_wh.jl | 10 +++---- src/results/results.jl | 10 +++---- 6 files changed, 60 insertions(+), 38 deletions(-) diff --git a/src/REopt.jl b/src/REopt.jl index 98d66abc8..fa54880de 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -137,8 +137,8 @@ include("core/chp.jl") include("core/ghp.jl") include("core/steam_turbine.jl") include("core/electric_heater.jl") -include("core/ashp_wh.jl") include("core/ashp.jl") +include("core/ashp_wh.jl") include("core/scenario.jl") include("core/bau_scenario.jl") include("core/reopt_inputs.jl") @@ -192,8 +192,8 @@ include("results/flexible_hvac.jl") include("results/ghp.jl") include("results/steam_turbine.jl") include("results/electric_heater.jl") -include("results/ashp_wh.jl") include("results/ashp.jl") +include("results/ashp_wh.jl") include("results/heating_cooling_load.jl") include("core/reopt.jl") diff --git a/src/core/ashp_wh.jl b/src/core/ashp_wh.jl index 67d7fa7c1..5b04ee1a3 100644 --- a/src/core/ashp_wh.jl +++ b/src/core/ashp_wh.jl @@ -9,6 +9,9 @@ struct ASHP_WH <: AbstractThermalTech macrs_bonus_fraction::Real can_supply_steam_turbine::Bool cop_heating::Array{Float64,1} + cop_cooling::Array{Float64,1} + cf_heating::Array{Float64,1} + cf_cooling::Array{Float64,1} can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -25,10 +28,10 @@ to meet the domestic hot water load. ```julia function ASHP_WH(; - min_ton_per_hour::Real = 0.0, # Minimum thermal power size - max_ton_per_hour::Real = BIG_NUMBER, # Maximum thermal power size - installed_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based cost - om_cost_per_ton_per_hour::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + min_ton::Real = 0.0, # Minimum thermal power size + max_ton::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production @@ -41,28 +44,31 @@ function ASHP_WH(; ``` """ function ASHP_WH(; - min_ton_per_hour::Real = 0.0, - max_ton_per_hour::Real = BIG_NUMBER, - installed_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, - om_cost_per_ton_per_hour::Union{Real, Nothing} = nothing, - macrs_option_years::Int = 0, - macrs_bonus_fraction::Real = 0.0, - can_supply_steam_turbine::Union{Bool, Nothing} = nothing, - cop_heating::Array{Float64,1} = Float64[], - can_serve_dhw::Union{Bool, Nothing} = nothing, - can_serve_space_heating::Union{Bool, Nothing} = nothing, - can_serve_process_heat::Union{Bool, Nothing} = nothing, - can_serve_cooling::Union{Bool, Nothing} = nothing + min_ton::Real = 0.0, + max_ton::Real = BIG_NUMBER, + installed_cost_per_ton::Union{Real, Nothing} = nothing, + om_cost_per_ton::Union{Real, Nothing} = nothing, + macrs_option_years::Int = 0, + macrs_bonus_fraction::Real = 0.0, + can_supply_steam_turbine::Union{Bool, Nothing} = nothing, + cop_heating::Array{Float64,1} = Float64[], + cop_cooling::Array{Float64,1} = Float64[], + cf_heating::Array{Float64,1} = Float64[], + cf_cooling::Array{Float64,1} = Float64[], + can_serve_dhw::Union{Bool, Nothing} = nothing, + can_serve_space_heating::Union{Bool, Nothing} = nothing, + can_serve_process_heat::Union{Bool, Nothing} = nothing, + can_serve_cooling::Union{Bool, Nothing} = nothing ) defaults = get_ashp_wh_defaults() # populate defaults as needed - if isnothing(installed_cost_per_ton_per_hour) - installed_cost_per_ton_per_hour = defaults["installed_cost_per_ton_per_hour"] + if isnothing(installed_cost_per_ton) + installed_cost_per_ton = defaults["installed_cost_per_ton"] end - if isnothing(om_cost_per_ton_per_hour) - om_cost_per_ton_per_hour = defaults["om_cost_per_ton_per_hour"] + if isnothing(om_cost_per_ton) + om_cost_per_ton = defaults["om_cost_per_ton"] end if isnothing(can_supply_steam_turbine) can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -81,11 +87,11 @@ function ASHP_WH(; end # Convert max sizes, cost factors from mmbtu_per_hour to kw - min_kw = min_ton_per_hour * KWH_PER_MMBTU * 0.012 - max_kw = max_ton_per_hour * KWH_PER_MMBTU * 0.012 + min_kw = min_ton * KWH_THERMAL_PER_TONHOUR + max_kw = max_ton * KWH_THERMAL_PER_TONHOUR - installed_cost_per_kw = installed_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) - om_cost_per_kw = om_cost_per_ton_per_hour / (KWH_PER_MMBTU * 0.012) + installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR + om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR ASHP_WH( @@ -97,6 +103,9 @@ function ASHP_WH(; macrs_bonus_fraction, can_supply_steam_turbine, cop_heating, + cop_cooling, + cf_heating, + cf_cooling, can_serve_dhw, can_serve_space_heating, can_serve_process_heat, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 588d46d9a..836d38785 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -452,6 +452,13 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) cooling_cf["ASHP"] = zeros(length(time_steps)) end + if "ASHP_WH" in techs.all + setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) + else + heating_cop["ASHP_WH"] = ones(length(time_steps)) + heating_cf["ASHP_WH"] = zeros(length(time_steps)) + end + if !isempty(techs.ghp) cooling_cop["GHP"] = ones(length(time_steps)) heating_cop["GHP"] = ones(length(time_steps)) @@ -955,11 +962,12 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ end -function setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop) +function setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) max_sizes["ASHP_WH"] = s.ashp_wh.max_kw min_sizes["ASHP_WH"] = s.ashp_wh.min_kw om_cost_per_kw["ASHP_WH"] = s.ashp_wh.om_cost_per_kw heating_cop["ASHP_WH"] = s.ashp_wh.cop_heating + heating_cf["ASHP_WH"] = s.ashp_wh.cf_heating if s.ashp_wh.macrs_option_years in [5, 7] cap_cost_slope["ASHP_WH"] = effective_cost(; diff --git a/src/core/techs.jl b/src/core/techs.jl index 4d8b8e9f2..f99f42b89 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -30,6 +30,7 @@ function Techs(p::REoptInputs, s::BAUScenario) techs_can_serve_process_heat = String[] ghp_techs = String[] ashp_techs = String[] + ashp_wh_techs = String[] if p.s.generator.existing_kw > 0 push!(all_techs, "Generator") @@ -87,6 +88,7 @@ function Techs(p::REoptInputs, s::BAUScenario) techs_can_serve_dhw, techs_can_serve_process_heat, ghp_techs, + ashp_techs, ashp_techs ) end @@ -127,6 +129,7 @@ function Techs(s::Scenario) techs_can_serve_process_heat = String[] ghp_techs = String[] ashp_techs = String[] + ashp_wh_techs = String[] if s.wind.max_kw > 0 push!(all_techs, "Wind") @@ -362,7 +365,8 @@ function Techs(s::Scenario) techs_can_serve_dhw, techs_can_serve_process_heat, ghp_techs, - ashp_techs + ashp_techs, + ashp_wh_techs ) end @@ -412,6 +416,7 @@ function Techs(s::MPCScenario) String[], String[], String[], + String[], String[] ) end \ No newline at end of file diff --git a/src/results/ashp_wh.jl b/src/results/ashp_wh.jl index 1f41ce8ea..e4098658d 100644 --- a/src/results/ashp_wh.jl +++ b/src/results/ashp_wh.jl @@ -50,7 +50,7 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= end r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPWHToHotTESKW) / KWH_PER_MMBTU, digits=3) - if !isempty(p.techs.steam_turbine) && p.s.ashp.can_supply_steam_turbine + if !isempty(p.techs.steam_turbine) && p.s.ashp_wh.can_supply_steam_turbine @expression(m, ASHPWHToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP_WH",q,ts] for q in p.heating_loads)) @expression(m, ASHPWHToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP_WH",q,ts]) else @@ -62,7 +62,7 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= @expression(m, ASHPWHToLoad[ts in p.time_steps], sum(m[:dvHeatingProduction]["ASHP_WH", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] - ASHPWHToSteamTurbine[ts] ) - r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToLoad) ./ KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp_wh.can_serve_dhw @expression(m, ASHPWHToDHWKW[ts in p.time_steps], @@ -73,7 +73,7 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= end r["thermal_to_dhw_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToDHWKW ./ KWH_PER_MMBTU), digits=5) - if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating + if "SpaceHeating" in p.heating_loads && p.s.ashp_wh.can_serve_space_heating @expression(m, ASHPWHToSpaceHeatingKW[ts in p.time_steps], m[:dvHeatingProduction]["ASHP_WH","SpaceHeating",ts] - ASHPWHToHotTESByQualityKW["SpaceHeating",ts] - ASHPWHToSteamTurbineByQuality["SpaceHeating",ts] ) @@ -82,7 +82,7 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= end r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToSpaceHeatingKW ./ KWH_PER_MMBTU), digits=5) - if "ProcessHeat" in p.heating_loads && p.s.ashp.can_serve_space_heating + if "ProcessHeat" in p.heating_loads && p.s.ashp_wh.can_serve_space_heating @expression(m, ASHPWHToProcessHeatKW[ts in p.time_steps], m[:dvHeatingProduction]["ASHP_WH","ProcessHeat",ts] - ASHPWHToHotTESByQualityKW["ProcessHeat",ts] - ASHPWHToSteamTurbineByQuality["ProcessHeat",ts] ) @@ -91,7 +91,7 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= end r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) - r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) + r["electric_consumption_series_kw"] = round.(value.(ASHPWHElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) d["ASHP_WH"] = r diff --git a/src/results/results.jl b/src/results/results.jl index a9b4595c9..327522f53 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -105,11 +105,11 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") end if !isempty(p.techs.ashp) - add_ashp_results(m, p, d; _n) - end - - if !isempty(p.techs.ashp_wh) - add_ashp_wh_results(m, p, d; _n) + if "ASHP" in p.techs.ashp + add_ashp_results(m, p, d; _n) + elseif "ASHP_WH" in p.techs.ashp + add_ashp_wh_results(m, p, d; _n) + end end return d From cfde553c8a0388ca951325318fdb8ea434183532 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 4 Jun 2024 18:15:10 -0600 Subject: [PATCH 107/266] print results for both ASHP and ASHP_WH --- src/results/results.jl | 7 +++++-- 1 file changed, 5 insertions(+), 2 deletions(-) diff --git a/src/results/results.jl b/src/results/results.jl index 327522f53..30741859b 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -105,9 +105,12 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") end if !isempty(p.techs.ashp) - if "ASHP" in p.techs.ashp + if ("ASHP" in p.techs.ashp) && !("ASHP_WH" in p.techs.ashp) + add_ashp_results(m, p, d; _n) + elseif ("ASHP_WH" in p.techs.ashp) && !("ASHP" in p.techs.ashp) + add_ashp_wh_results(m, p, d; _n) + elseif ("ASHP" in p.techs.ashp) && ("ASHP_WH" in p.techs.ashp) add_ashp_results(m, p, d; _n) - elseif "ASHP_WH" in p.techs.ashp add_ashp_wh_results(m, p, d; _n) end end From 2a8a903e3e08b532bd75dcfe3595f8fbc7ab77e0 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 22:25:49 -0600 Subject: [PATCH 108/266] update ASHP WH docstrings --- src/results/ashp_wh.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/results/ashp_wh.jl b/src/results/ashp_wh.jl index e4098658d..61bd83806 100644 --- a/src/results/ashp_wh.jl +++ b/src/results/ashp_wh.jl @@ -14,7 +14,6 @@ - `thermal_to_load_series_ton` # Thermal production to cooling load - `electric_consumption_series_kw` - `annual_electric_consumption_kwh` -- `annual_thermal_production_tonhour` Thermal cooling energy produced in a year !!! note "'Series' and 'Annual' energy outputs are average annual" From 706248f726bd227da2c9e83116f0cc766c3235b7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 22:26:24 -0600 Subject: [PATCH 109/266] update tech sets for ASHP WH --- src/core/techs.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/techs.jl b/src/core/techs.jl index f99f42b89..bef74af52 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -89,7 +89,7 @@ function Techs(p::REoptInputs, s::BAUScenario) techs_can_serve_process_heat, ghp_techs, ashp_techs, - ashp_techs + ashp_wh_techs ) end @@ -295,7 +295,7 @@ function Techs(s::Scenario) push!(all_techs, "ASHP_WH") push!(heating_techs, "ASHP_WH") push!(electric_heaters, "ASHP_WH") - push!(ashp_techs, "ASHP_WH") + push!(ashp_wh_techs, "ASHP_WH") if s.ashp_wh.can_supply_steam_turbine push!(techs_can_supply_steam_turbine, "ASHP_WH") end From 59417cfea498114369d80008aaad567faa8d45f9 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 22:55:21 -0600 Subject: [PATCH 110/266] update ASHP WH test suite --- test/runtests.jl | 11 +++++------ test/scenarios/ashp_wh.json | 3 --- 2 files changed, 5 insertions(+), 9 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index aa56ae346..cb2e4785c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2461,14 +2461,14 @@ else # run HiGHS tests d = JSON.parsefile("./scenarios/ashp_wh.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) @test results["ASHP_WH"]["size_ton"] ≈ 0.0 atol=0.1 @test results["ASHP_WH"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP_WH"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - + #Case 2: ASHP_WH has temperature-dependent output and serves all DHW load d["ExistingChiller"] = Dict("retire_in_optimal" => false) d["ExistingBoiler"]["retire_in_optimal"] = false @@ -2478,14 +2478,13 @@ else # run HiGHS tests p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WH"][ts] for ts in p.time_steps) + annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WH"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_WH"]["size_ton"] ≈ 74.99 atol=0.01 + @test results["ASHP_WH"]["size_ton"] ≈ 37.495 atol=0.01 @test results["ASHP_WH"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP_WH"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - @test results["ASHP_WH"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 end @testset "Process Heat Load" begin diff --git a/test/scenarios/ashp_wh.json b/test/scenarios/ashp_wh.json index 59b752f70..27e5abe36 100644 --- a/test/scenarios/ashp_wh.json +++ b/test/scenarios/ashp_wh.json @@ -45,8 +45,5 @@ }, "ElectricTariff": { "monthly_energy_rates": [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1] - }, - "HotThermalStorage":{ - "max_gal":2500 } } \ No newline at end of file From 5a28fbf9af156c4f976d4236eccca5e4dd854292 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 4 Jun 2024 22:55:47 -0600 Subject: [PATCH 111/266] update results population logic for ASHP WH --- src/results/results.jl | 14 +++++--------- 1 file changed, 5 insertions(+), 9 deletions(-) diff --git a/src/results/results.jl b/src/results/results.jl index 30741859b..e3e17fae1 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -104,15 +104,11 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") add_electric_heater_results(m, p, d; _n) end - if !isempty(p.techs.ashp) - if ("ASHP" in p.techs.ashp) && !("ASHP_WH" in p.techs.ashp) - add_ashp_results(m, p, d; _n) - elseif ("ASHP_WH" in p.techs.ashp) && !("ASHP" in p.techs.ashp) - add_ashp_wh_results(m, p, d; _n) - elseif ("ASHP" in p.techs.ashp) && ("ASHP_WH" in p.techs.ashp) - add_ashp_results(m, p, d; _n) - add_ashp_wh_results(m, p, d; _n) - end + if "ASHP" in p.techs.ashp + add_ashp_results(m, p, d; _n) + end + if "ASHP_WH" in p.techs.ashp_wh + add_ashp_wh_results(m, p, d; _n) end return d From fe2c8efa0ac30275ba156262d5675fd6193bb2b0 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 13 Jun 2024 14:29:47 -0600 Subject: [PATCH 112/266] make ASHP defaults a single file --- data/ashp/ashp_defaults.json | 35 +++++++++++++++++++++++---------- data/ashp/ashp_wh_defaults.json | 11 ----------- src/core/ashp.jl | 13 +++++++----- 3 files changed, 33 insertions(+), 26 deletions(-) delete mode 100644 data/ashp/ashp_wh_defaults.json diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 027c751c2..3f498118e 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -1,11 +1,26 @@ -{ - "installed_cost_per_ton": 4050, - "om_cost_per_ton": 0.00024, - "macrs_option_years": 0, - "macrs_bonus_fraction": 0.0, - "can_supply_steam_turbine": false, - "can_serve_process_heat": false, - "can_serve_dhw": false, - "can_serve_space_heating": true, - "can_serve_cooling": true +{ + "SpaceHeating": + { + "installed_cost_per_ton": 4050, + "om_cost_per_ton": 0.00024, + "macrs_option_years": 0, + "macrs_bonus_fraction": 0.0, + "can_supply_steam_turbine": false, + "can_serve_process_heat": false, + "can_serve_dhw": false, + "can_serve_space_heating": true, + "can_serve_cooling": true + }, + "DomesticHotWater": + { + "installed_cost_per_ton": 4050, + "om_cost_per_ton": 0.00024, + "macrs_option_years": 0, + "macrs_bonus_fraction": 0.0, + "can_supply_steam_turbine": false, + "can_serve_process_heat": false, + "can_serve_dhw": true, + "can_serve_space_heating": false, + "can_serve_cooling": false + } } diff --git a/data/ashp/ashp_wh_defaults.json b/data/ashp/ashp_wh_defaults.json deleted file mode 100644 index c7f650985..000000000 --- a/data/ashp/ashp_wh_defaults.json +++ /dev/null @@ -1,11 +0,0 @@ -{ - "installed_cost_per_ton": 4050, - "om_cost_per_ton": 0.00024, - "macrs_option_years": 0, - "macrs_bonus_fraction": 0.0, - "can_supply_steam_turbine": false, - "can_serve_process_heat": false, - "can_serve_dhw": true, - "can_serve_space_heating": false, - "can_serve_cooling": false -} diff --git a/src/core/ashp.jl b/src/core/ashp.jl index c23771297..ae18ae219 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -119,17 +119,20 @@ end """ -function get_ashp_defaults() +function get_ashp_defaults(load_served::String="SpaceHeating") Obtains defaults for the ASHP from a JSON data file. inputs -None +load_served::String -- identifier of heating load served by AHSP system returns ashp_defaults::Dict -- Dictionary containing defaults for ASHP """ -function get_ashp_defaults() - ashp_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_defaults.json")) - return ashp_defaults +function get_ashp_defaults(load_served::String="SpaceHeating") + if !(load_served in ["SpaceHeating", "DomesticHotWater"]) + throw(@error("Invalid inputs: argument `load_served` to function get_ashp_defaults() must be a String in the set ['SpaceHeating', 'DomesticHotWater'].")) + end + all_ashp_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_defaults.json")) + return all_ashp_defaults[load_served] end \ No newline at end of file From fa98a0c776173ad18a86cf1672b53157c9e90e40 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 13 Jun 2024 14:30:40 -0600 Subject: [PATCH 113/266] ren ASHP ASHP_SpaceHeater and fix heat loads served --- src/core/ashp.jl | 35 +++++++++++------------------------ 1 file changed, 11 insertions(+), 24 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index ae18ae219..7b62cc290 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -22,49 +22,41 @@ end """ ASHP -If a user provides the `ASHP` key then the optimal scenario has the option to purchase +If a user provides the `ASHP_SpaceHeater` key then the optimal scenario has the option to purchase this new `ASHP` to meet the heating load in addition to using the `ExistingBoiler` to meet the heating load. ```julia -function ASHP(; +function ASHP_SpaceHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS - can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop_heating::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) cop_cooling::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) cf_heating::Array{Float64,1}, # ASHP's heating capacity factor curves cf_cooling::Array{Float64,1}, # ASHP's cooling capacity factor curves - can_serve_dhw::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the domestic hot water load - can_serve_space_heating::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the space heating load - can_serve_process_heat::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the process heating load can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load ) ``` """ -function ASHP(; +function ASHP_SpaceHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, - can_supply_steam_turbine::Union{Bool, Nothing} = nothing, cop_heating::Array{Float64,1} = Float64[], cop_cooling::Array{Float64,1} = Float64[], cf_heating::Array{Float64,1} = Float64[], cf_cooling::Array{Float64,1} = Float64[], - can_serve_dhw::Union{Bool, Nothing} = nothing, - can_serve_space_heating::Union{Bool, Nothing} = nothing, - can_serve_process_heat::Union{Bool, Nothing} = nothing, can_serve_cooling::Union{Bool, Nothing} = nothing ) - defaults = get_ashp_defaults() + defaults = get_ashp_defaults("SpaceHeating") # populate defaults as needed if isnothing(installed_cost_per_ton) @@ -73,22 +65,17 @@ function ASHP(; if isnothing(om_cost_per_ton) om_cost_per_ton = defaults["om_cost_per_ton"] end - if isnothing(can_supply_steam_turbine) - can_supply_steam_turbine = defaults["can_supply_steam_turbine"] - end - if isnothing(can_serve_dhw) - can_serve_dhw = defaults["can_serve_dhw"] - end - if isnothing(can_serve_space_heating) - can_serve_space_heating = defaults["can_serve_space_heating"] - end - if isnothing(can_serve_process_heat) - can_serve_process_heat = defaults["can_serve_process_heat"] - end if isnothing(can_serve_cooling) can_serve_cooling = defaults["can_serve_cooling"] end + #pre-set defaults that aren't mutable due to technology specifications + can_supply_steam_turbine = defaults["can_supply_steam_turbine"] + can_serve_space_heating = defaults["can_serve_space_heating"] + can_serve_dhw = defaults["can_serve_dhw"] + can_serve_process_heat = defaults["can_serve_process_heat"] + + # Convert max sizes, cost factors from mmbtu_per_hour to kw min_kw = min_ton * KWH_THERMAL_PER_TONHOUR max_kw = max_ton * KWH_THERMAL_PER_TONHOUR From 7028b4c8b84de41c07e916be268bd57ad7ff7001 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 13 Jun 2024 14:31:04 -0600 Subject: [PATCH 114/266] merge ASHP_WH as ASHP_WaterHeater --- src/core/ashp.jl | 76 +++++++++++++++++++++++++ src/core/ashp_wh.jl | 132 -------------------------------------------- 2 files changed, 76 insertions(+), 132 deletions(-) delete mode 100644 src/core/ashp_wh.jl diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 7b62cc290..2b9679e51 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -104,6 +104,82 @@ function ASHP_SpaceHeater(; end +""" +ASHP Water Heater + +If a user provides the `ASHP_WaterHeater` key then the optimal scenario has the option to purchase +this new `ASHP_WaterHeater` to meet the domestic hot water load in addition to using the `ExistingBoiler` +to meet the domestic hot water load. + +```julia +function ASHP_WaterHeater(; + min_ton::Real = 0.0, # Minimum thermal power size + max_ton::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS + can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production + cop_heating::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) +) +``` +""" +function ASHP_WaterHeater(; + min_ton::Real = 0.0, + max_ton::Real = BIG_NUMBER, + installed_cost_per_ton::Union{Real, Nothing} = nothing, + om_cost_per_ton::Union{Real, Nothing} = nothing, + macrs_option_years::Int = 0, + macrs_bonus_fraction::Real = 0.0, + cop_heating::Array{Float64,1} = Float64[], + cf_heating::Array{Float64,1} = Float64[] + ) + + defaults = get_ashp_defaults("DomesticHotWater") + + # populate defaults as needed + if isnothing(installed_cost_per_ton) + installed_cost_per_ton = defaults["installed_cost_per_ton"] + end + if isnothing(om_cost_per_ton) + om_cost_per_ton = defaults["om_cost_per_ton"] + end + + #pre-set defaults that aren't mutable due to technology specifications + can_supply_steam_turbine = defaults["can_supply_steam_turbine"] + can_serve_space_heating = defaults["can_serve_space_heating"] + can_serve_dhw = defaults["can_serve_dhw"] + can_serve_process_heat = defaults["can_serve_process_heat"] + can_serve_cooling = defaults["can_serve_cooling"] + + # Convert max sizes, cost factors from mmbtu_per_hour to kw + min_kw = min_ton * KWH_THERMAL_PER_TONHOUR + max_kw = max_ton * KWH_THERMAL_PER_TONHOUR + + installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR + om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR + + + ASHP( + min_kw, + max_kw, + installed_cost_per_kw, + om_cost_per_kw, + macrs_option_years, + macrs_bonus_fraction, + can_supply_steam_turbine, + cop_heating, + Float64[], + cf_heating, + Float64[], + can_serve_dhw, + can_serve_space_heating, + can_serve_process_heat, + can_serve_cooling + ) +end + + """ function get_ashp_defaults(load_served::String="SpaceHeating") diff --git a/src/core/ashp_wh.jl b/src/core/ashp_wh.jl deleted file mode 100644 index 5b04ee1a3..000000000 --- a/src/core/ashp_wh.jl +++ /dev/null @@ -1,132 +0,0 @@ -# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. - -struct ASHP_WH <: AbstractThermalTech - min_kw::Real - max_kw::Real - installed_cost_per_kw::Real - om_cost_per_kw::Real - macrs_option_years::Int - macrs_bonus_fraction::Real - can_supply_steam_turbine::Bool - cop_heating::Array{Float64,1} - cop_cooling::Array{Float64,1} - cf_heating::Array{Float64,1} - cf_cooling::Array{Float64,1} - can_serve_dhw::Bool - can_serve_space_heating::Bool - can_serve_process_heat::Bool - can_serve_cooling::Bool -end - - -""" -ASHP Water Heater - -If a user provides the `ASHP_WH` key then the optimal scenario has the option to purchase -this new `ASHP_WH` to meet the domestic hot water load in addition to using the `ExistingBoiler` -to meet the domestic hot water load. - -```julia -function ASHP_WH(; - min_ton::Real = 0.0, # Minimum thermal power size - max_ton::Real = BIG_NUMBER, # Maximum thermal power size - installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost - om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost - macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable - macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS - can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production - cop_heating::Array{<:Real,2}, # COP of the heating (i.e., thermal produced / electricity consumed) - can_serve_dhw::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the domestic hot water load - can_serve_space_heating::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the space heating load - can_serve_process_heat::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the process heating load - can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP_WH can supply heat to the cooling load -) -``` -""" -function ASHP_WH(; - min_ton::Real = 0.0, - max_ton::Real = BIG_NUMBER, - installed_cost_per_ton::Union{Real, Nothing} = nothing, - om_cost_per_ton::Union{Real, Nothing} = nothing, - macrs_option_years::Int = 0, - macrs_bonus_fraction::Real = 0.0, - can_supply_steam_turbine::Union{Bool, Nothing} = nothing, - cop_heating::Array{Float64,1} = Float64[], - cop_cooling::Array{Float64,1} = Float64[], - cf_heating::Array{Float64,1} = Float64[], - cf_cooling::Array{Float64,1} = Float64[], - can_serve_dhw::Union{Bool, Nothing} = nothing, - can_serve_space_heating::Union{Bool, Nothing} = nothing, - can_serve_process_heat::Union{Bool, Nothing} = nothing, - can_serve_cooling::Union{Bool, Nothing} = nothing - ) - - defaults = get_ashp_wh_defaults() - - # populate defaults as needed - if isnothing(installed_cost_per_ton) - installed_cost_per_ton = defaults["installed_cost_per_ton"] - end - if isnothing(om_cost_per_ton) - om_cost_per_ton = defaults["om_cost_per_ton"] - end - if isnothing(can_supply_steam_turbine) - can_supply_steam_turbine = defaults["can_supply_steam_turbine"] - end - if isnothing(can_serve_dhw) - can_serve_dhw = defaults["can_serve_dhw"] - end - if isnothing(can_serve_space_heating) - can_serve_space_heating = defaults["can_serve_space_heating"] - end - if isnothing(can_serve_process_heat) - can_serve_process_heat = defaults["can_serve_process_heat"] - end - if isnothing(can_serve_cooling) - can_serve_cooling = defaults["can_serve_cooling"] - end - - # Convert max sizes, cost factors from mmbtu_per_hour to kw - min_kw = min_ton * KWH_THERMAL_PER_TONHOUR - max_kw = max_ton * KWH_THERMAL_PER_TONHOUR - - installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR - om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR - - - ASHP_WH( - min_kw, - max_kw, - installed_cost_per_kw, - om_cost_per_kw, - macrs_option_years, - macrs_bonus_fraction, - can_supply_steam_turbine, - cop_heating, - cop_cooling, - cf_heating, - cf_cooling, - can_serve_dhw, - can_serve_space_heating, - can_serve_process_heat, - can_serve_cooling - ) -end - - - -""" -function get_ashp_wh_defaults() - -Obtains defaults for the ASHP_WH from a JSON data file. - -inputs -None - -returns -ashp_wh_defaults::Dict -- Dictionary containing defaults for ASHP_WH -""" -function get_ashp_wh_defaults() - ashp_wh_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_wh_defaults.json")) - return ashp_wh_defaults -end \ No newline at end of file From 73fc4903684dc731c5a6a0d7240c90c7e6a6467c Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 13 Jun 2024 14:33:05 -0600 Subject: [PATCH 115/266] merge ASHP results and remove unused results --- src/results/ashp.jl | 98 ++++++++++++++++++++++++++++++++++++------ src/results/ashp_wh.jl | 98 ------------------------------------------ 2 files changed, 84 insertions(+), 112 deletions(-) delete mode 100644 src/results/ashp_wh.jl diff --git a/src/results/ashp.jl b/src/results/ashp.jl index fd662884a..2864ff34b 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -25,7 +25,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_SpaceHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp) @@ -39,10 +39,10 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if !isempty(p.s.storage.types.hot) @expression(m, ASHPToHotTESKW[ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) + sum(m[:dvHeatToStorage][b,"ASHP_SpaceHeater",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) ) @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP",q,ts] for b in p.s.storage.types.hot) + sum(m[:dvHeatToStorage][b,"ASHP_SpaceHeater",q,ts] for b in p.s.storage.types.hot) ) else @expression(m, ASHPToHotTESKW[ts in p.time_steps], 0.0) @@ -51,8 +51,8 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPToHotTESKW) / KWH_PER_MMBTU, digits=3) if !isempty(p.techs.steam_turbine) && p.s.ashp.can_supply_steam_turbine - @expression(m, ASHPToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP",q,ts] for q in p.heating_loads)) - @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP",q,ts]) + @expression(m, ASHPToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP_SpaceHeater",q,ts] for q in p.heating_loads)) + @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP_SpaceHeater",q,ts]) else ASHPToSteamTurbine = zeros(length(p.time_steps)) @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], 0.0) @@ -60,13 +60,13 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ASHPToSteamTurbine) / KWH_PER_MMBTU, digits=3) @expression(m, ASHPToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHP", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] + sum(m[:dvHeatingProduction]["ASHP_SpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp.can_serve_dhw @expression(m, ASHPToDHWKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP","DomesticHotWater",ts] - ASHPToHotTESByQualityKW["DomesticHotWater",ts] - ASHPToSteamTurbineByQuality["DomesticHotWater",ts] + m[:dvHeatingProduction]["ASHP_SpaceHeater","DomesticHotWater",ts] - ASHPToHotTESByQualityKW["DomesticHotWater",ts] - ASHPToSteamTurbineByQuality["DomesticHotWater",ts] ) else @expression(m, ASHPToDHWKW[ts in p.time_steps], 0.0) @@ -75,7 +75,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] - ASHPToSteamTurbineByQuality["SpaceHeating",ts] + m[:dvHeatingProduction]["ASHP_SpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] - ASHPToSteamTurbineByQuality["SpaceHeating",ts] ) else @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], 0.0) @@ -84,27 +84,27 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") if "ProcessHeat" in p.heating_loads && p.s.ashp.can_serve_space_heating @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP","ProcessHeat",ts] - ASHPToHotTESByQualityKW["ProcessHeat",ts] - ASHPToSteamTurbineByQuality["ProcessHeat",ts] + m[:dvHeatingProduction]["ASHP_SpaceHeater","ProcessHeat",ts] - ASHPToHotTESByQualityKW["ProcessHeat",ts] - ASHPToSteamTurbineByQuality["ProcessHeat",ts] ) else @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], 0.0) end r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) - if "ASHP" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 + if "ASHP_SpaceHeater" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 @expression(m, ASHPtoColdTES[ts in p.time_steps], - sum(m[:dvProductionToStorage][b,"ASHP",ts] for b in p.s.storage.types.cold) + sum(m[:dvProductionToStorage][b,"ASHP_SpaceHeater",ts] for b in p.s.storage.types.cold) ) r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES ./ KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPtoColdLoad[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ASHP", ts]) - ASHPtoColdTES[ts] + sum(m[:dvCoolingProduction]["ASHP_SpaceHeater", ts]) - ASHPtoColdTES[ts] ) r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad ./ KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, Year1ASHPColdThermalProd, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHP", ts] for ts in p.time_steps) + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHP_SpaceHeater", ts] for ts in p.time_steps) ) r["annual_thermal_production_tonhour"] = round(value(Year1ASHPColdThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) @@ -121,6 +121,76 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) - d["ASHP"] = r + d["ASHP_SpaceHeater"] = r + nothing +end + +""" +`ASHP_WaterHeater` results keys: +- `size_ton` # Thermal production capacity size of the ASHP_WaterHeater [ton/hr] +- `electric_consumption_series_kw` # Fuel consumption series [kW] +- `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] +- `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] +- `annual_thermal_production_mmbtu` # Thermal heating energy produced in a year [MMBtu] +- `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] +- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] +- `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] +- `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage +- `thermal_to_load_series_ton` # Thermal production to cooling load +- `electric_consumption_series_kw` +- `annual_electric_consumption_kwh` + + +!!! note "'Series' and 'Annual' energy outputs are average annual" + REopt performs load balances using average annual production values for technologies that include degradation. + Therefore, all timeseries (`_series`) and `annual_` results should be interpretted as energy outputs averaged over the analysis period. + +""" + +function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") + r = Dict{String, Any}() + r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_WaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + @expression(m, ASHPWHElectricConsumptionSeries[ts in p.time_steps], + p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] + for q in p.heating_loads, t in p.techs.ashp_wh) + ) + + @expression(m, ASHPWHThermalProductionSeries[ts in p.time_steps], + sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp_wh)) + r["thermal_production_series_mmbtu_per_hour"] = + round.(value.(ASHPWHThermalProductionSeries) / KWH_PER_MMBTU, digits=5) + r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) + + if !isempty(p.s.storage.types.hot) + @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], + sum(m[:dvHeatToStorage][b,"ASHP_WaterHeater",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) + ) + @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], + sum(m[:dvHeatToStorage][b,"ASHP_WaterHeater",q,ts] for b in p.s.storage.types.hot) + ) + else + @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], 0.0) + @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) + end + r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPWHToHotTESKW) / KWH_PER_MMBTU, digits=3) + + @expression(m, ASHPWHToLoad[ts in p.time_steps], + sum(m[:dvHeatingProduction]["ASHP_WaterHeater", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] + ) + r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToLoad) ./ KWH_PER_MMBTU, digits=3) + + if "DomesticHotWater" in p.heating_loads && p.s.ashp_wh.can_serve_dhw + @expression(m, ASHPWHToDHWKW[ts in p.time_steps], + m[:dvHeatingProduction]["ASHP_WaterHeater","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] + ) + else + @expression(m, ASHPWHToDHWKW[ts in p.time_steps], 0.0) + end + r["thermal_to_dhw_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToDHWKW ./ KWH_PER_MMBTU), digits=5) + + r["electric_consumption_series_kw"] = round.(value.(ASHPWHElectricConsumptionSeries), digits=3) + r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) + + d["ASHP_WaterHeater"] = r nothing end \ No newline at end of file diff --git a/src/results/ashp_wh.jl b/src/results/ashp_wh.jl deleted file mode 100644 index 61bd83806..000000000 --- a/src/results/ashp_wh.jl +++ /dev/null @@ -1,98 +0,0 @@ -# REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. - -""" -`ASHP_WH` results keys: -- `size_ton` # Thermal production capacity size of the ASHP_WH [ton/hr] -- `electric_consumption_series_kw` # Fuel consumption series [kW] -- `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] -- `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] -- `annual_thermal_production_mmbtu` # Thermal heating energy produced in a year [MMBtu] -- `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] -- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] -- `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] -- `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage -- `thermal_to_load_series_ton` # Thermal production to cooling load -- `electric_consumption_series_kw` -- `annual_electric_consumption_kwh` - - -!!! note "'Series' and 'Annual' energy outputs are average annual" - REopt performs load balances using average annual production values for technologies that include degradation. - Therefore, all timeseries (`_series`) and `annual_` results should be interpretted as energy outputs averaged over the analysis period. - -""" - -function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") - r = Dict{String, Any}() - r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_WH"]) / KWH_THERMAL_PER_TONHOUR, digits=3) - @expression(m, ASHPWHElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] - for q in p.heating_loads, t in p.techs.ashp_wh) - ) - - @expression(m, ASHPWHThermalProductionSeries[ts in p.time_steps], - sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp_wh)) - r["thermal_production_series_mmbtu_per_hour"] = - round.(value.(ASHPWHThermalProductionSeries) / KWH_PER_MMBTU, digits=5) - r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) - - if !isempty(p.s.storage.types.hot) - @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP_WH",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) - ) - @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP_WH",q,ts] for b in p.s.storage.types.hot) - ) - else - @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], 0.0) - @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) - end - r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPWHToHotTESKW) / KWH_PER_MMBTU, digits=3) - - if !isempty(p.techs.steam_turbine) && p.s.ashp_wh.can_supply_steam_turbine - @expression(m, ASHPWHToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP_WH",q,ts] for q in p.heating_loads)) - @expression(m, ASHPWHToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP_WH",q,ts]) - else - ASHPWHToSteamTurbine = zeros(length(p.time_steps)) - @expression(m, ASHPWHToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], 0.0) - end - r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ASHPWHToSteamTurbine) / KWH_PER_MMBTU, digits=3) - - @expression(m, ASHPWHToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHP_WH", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] - ASHPWHToSteamTurbine[ts] - ) - r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToLoad) ./ KWH_PER_MMBTU, digits=3) - - if "DomesticHotWater" in p.heating_loads && p.s.ashp_wh.can_serve_dhw - @expression(m, ASHPWHToDHWKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_WH","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] - ASHPWHToSteamTurbineByQuality["DomesticHotWater",ts] - ) - else - @expression(m, ASHPWHToDHWKW[ts in p.time_steps], 0.0) - end - r["thermal_to_dhw_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToDHWKW ./ KWH_PER_MMBTU), digits=5) - - if "SpaceHeating" in p.heating_loads && p.s.ashp_wh.can_serve_space_heating - @expression(m, ASHPWHToSpaceHeatingKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_WH","SpaceHeating",ts] - ASHPWHToHotTESByQualityKW["SpaceHeating",ts] - ASHPWHToSteamTurbineByQuality["SpaceHeating",ts] - ) - else - @expression(m, ASHPWHToSpaceHeatingKW[ts in p.time_steps], 0.0) - end - r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToSpaceHeatingKW ./ KWH_PER_MMBTU), digits=5) - - if "ProcessHeat" in p.heating_loads && p.s.ashp_wh.can_serve_space_heating - @expression(m, ASHPWHToProcessHeatKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_WH","ProcessHeat",ts] - ASHPWHToHotTESByQualityKW["ProcessHeat",ts] - ASHPWHToSteamTurbineByQuality["ProcessHeat",ts] - ) - else - @expression(m, ASHPWHToProcessHeatKW[ts in p.time_steps], 0.0) - end - r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) - - r["electric_consumption_series_kw"] = round.(value.(ASHPWHElectricConsumptionSeries), digits=3) - r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) - - d["ASHP_WH"] = r - nothing -end \ No newline at end of file From 60dbab1fb37ef2f35cad442dd2544e556bc8641d Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 13 Jun 2024 14:33:54 -0600 Subject: [PATCH 116/266] migrate renaming of ASHP through code --- src/REopt.jl | 2 -- src/core/reopt_inputs.jl | 52 ++++++++++++++-------------- src/core/scenario.jl | 69 +++++++++++++++++++------------------ src/core/techs.jl | 36 +++++++++---------- src/results/results.jl | 4 +-- test/runtests.jl | 50 +++++++++++++-------------- test/scenarios/ashp.json | 2 +- test/scenarios/ashp_wh.json | 8 ++--- 8 files changed, 109 insertions(+), 114 deletions(-) diff --git a/src/REopt.jl b/src/REopt.jl index fa54880de..14929ba08 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -138,7 +138,6 @@ include("core/ghp.jl") include("core/steam_turbine.jl") include("core/electric_heater.jl") include("core/ashp.jl") -include("core/ashp_wh.jl") include("core/scenario.jl") include("core/bau_scenario.jl") include("core/reopt_inputs.jl") @@ -193,7 +192,6 @@ include("results/ghp.jl") include("results/steam_turbine.jl") include("results/electric_heater.jl") include("results/ashp.jl") -include("results/ashp_wh.jl") include("results/heating_cooling_load.jl") include("core/reopt.jl") diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 836d38785..8c3e75aed 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -443,20 +443,20 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) heating_cf["ElectricHeater"] = zeros(length(time_steps)) end - if "ASHP" in techs.all + if "ASHP_SpaceHeater" in techs.all setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) else - heating_cop["ASHP"] = ones(length(time_steps)) - cooling_cop["ASHP"] = ones(length(time_steps)) - heating_cf["ASHP"] = zeros(length(time_steps)) - cooling_cf["ASHP"] = zeros(length(time_steps)) + heating_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) + cooling_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) + heating_cf["ASHP_SpaceHeater"] = zeros(length(time_steps)) + cooling_cf["ASHP_SpaceHeater"] = zeros(length(time_steps)) end - if "ASHP_WH" in techs.all - setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) + if "ASHP_WaterHeater" in techs.all + setup_ASHP_WaterHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) else - heating_cop["ASHP_WH"] = ones(length(time_steps)) - heating_cf["ASHP_WH"] = zeros(length(time_steps)) + heating_cop["ASHP_WaterHeater"] = ones(length(time_steps)) + heating_cf["ASHP_WaterHeater"] = zeros(length(time_steps)) end if !isempty(techs.ghp) @@ -935,16 +935,16 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) - max_sizes["ASHP"] = s.ashp.max_kw - min_sizes["ASHP"] = s.ashp.min_kw - om_cost_per_kw["ASHP"] = s.ashp.om_cost_per_kw - heating_cop["ASHP"] = s.ashp.cop_heating - cooling_cop["ASHP"] = s.ashp.cop_cooling - heating_cf["ASHP"] = s.ashp.cf_heating - cooling_cf["ASHP"] = s.ashp.cf_cooling + max_sizes["ASHP_SpaceHeater"] = s.ashp.max_kw + min_sizes["ASHP_SpaceHeater"] = s.ashp.min_kw + om_cost_per_kw["ASHP_SpaceHeater"] = s.ashp.om_cost_per_kw + heating_cop["ASHP_SpaceHeater"] = s.ashp.cop_heating + cooling_cop["ASHP_SpaceHeater"] = s.ashp.cop_cooling + heating_cf["ASHP_SpaceHeater"] = s.ashp.cf_heating + cooling_cf["ASHP_SpaceHeater"] = s.ashp.cf_cooling if s.ashp.macrs_option_years in [5, 7] - cap_cost_slope["ASHP"] = effective_cost(; + cap_cost_slope["ASHP_SpaceHeater"] = effective_cost(; itc_basis = s.ashp.installed_cost_per_kw, replacement_cost = 0.0, replacement_year = s.financial.analysis_years, @@ -957,20 +957,20 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ rebate_per_kw = 0.0 ) else - cap_cost_slope["ASHP"] = s.ashp.installed_cost_per_kw + cap_cost_slope["ASHP_SpaceHeater"] = s.ashp.installed_cost_per_kw end end -function setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) - max_sizes["ASHP_WH"] = s.ashp_wh.max_kw - min_sizes["ASHP_WH"] = s.ashp_wh.min_kw - om_cost_per_kw["ASHP_WH"] = s.ashp_wh.om_cost_per_kw - heating_cop["ASHP_WH"] = s.ashp_wh.cop_heating - heating_cf["ASHP_WH"] = s.ashp_wh.cf_heating +function setup_ASHP_WaterHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) + max_sizes["ASHP_WaterHeater"] = s.ashp_wh.max_kw + min_sizes["ASHP_WaterHeater"] = s.ashp_wh.min_kw + om_cost_per_kw["ASHP_WaterHeater"] = s.ashp_wh.om_cost_per_kw + heating_cop["ASHP_WaterHeater"] = s.ashp_wh.cop_heating + heating_cf["ASHP_WaterHeater"] = s.ashp_wh.cf_heating if s.ashp_wh.macrs_option_years in [5, 7] - cap_cost_slope["ASHP_WH"] = effective_cost(; + cap_cost_slope["ASHP_WaterHeater"] = effective_cost(; itc_basis = s.ashp_wh.installed_cost_per_kw, replacement_cost = 0.0, replacement_year = s.financial.analysis_years, @@ -983,7 +983,7 @@ function setup_ashp_wh_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_p rebate_per_kw = 0.0 ) else - cap_cost_slope["ASHP_WH"] = s.ashp_wh.installed_cost_per_kw + cap_cost_slope["ASHP_WaterHeater"] = s.ashp_wh.installed_cost_per_kw end end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 5d7a5117d..8e2643a47 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -26,7 +26,7 @@ struct Scenario <: AbstractScenario steam_turbine::Union{SteamTurbine, Nothing} electric_heater::Union{ElectricHeater, Nothing} ashp::Union{ASHP, Nothing} - ashp_wh::Union{ASHP_WH, Nothing} + ashp_wh::Union{ASHP, Nothing} end """ @@ -54,7 +54,8 @@ A Scenario struct can contain the following keys: - [GHP](@ref) (optional, can be Array) - [SteamTurbine](@ref) (optional) - [ElectricHeater](@ref) (optional) -- [ASHP](@ref) (optional) +- [ASHP_SpaceHeater](@ref) (optional) +- [ASHP_WaterHeater](@ref) (optional) All values of `d` are expected to be `Dicts` except for `PV` and `GHP`, which can be either a `Dict` or `Dict[]` (for multiple PV arrays or GHP options). @@ -659,10 +660,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) cop_cooling = [] cf_heating = [] cf_cooling = [] - if haskey(d, "ASHP") && d["ASHP"]["max_ton"] > 0.0 + if haskey(d, "ASHP_SpaceHeater") && d["ASHP_SpaceHeater"]["max_ton"] > 0.0 # Add ASHP's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature - if !haskey(d["ASHP"], "cop_heating") || !haskey(d["ASHP"], "cop_cooling") + if !haskey(d["ASHP_SpaceHeater"], "cop_heating") || !haskey(d["ASHP_SpaceHeater"], "cop_cooling") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor if isnothing(ambient_temp_celsius) if !isempty(pvs) @@ -678,51 +679,51 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - if !haskey(d["ASHP"], "cop_heating") + if !haskey(d["ASHP_SpaceHeater"], "cop_heating") cop_heating = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) cop_heating[ambient_temp_fahrenheit .< -7.6] .= 1 cop_heating[ambient_temp_fahrenheit .> 79] .= 999999 else - cop_heating = round.(d["ASHP"]["cop_heating"],digits=3) + cop_heating = round.(d["ASHP_SpaceHeater"]["cop_heating"],digits=3) end - if !haskey(d["ASHP"], "cop_cooling") + if !haskey(d["ASHP_SpaceHeater"], "cop_cooling") cop_cooling = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) cop_cooling[ambient_temp_celsius .< 25] .= 999999 cop_cooling[ambient_temp_celsius .> 40] .= 1 else - cop_cooling = round.(d["ASHP"]["cop_cooling"], digits=3) + cop_cooling = round.(d["ASHP_SpaceHeater"]["cop_cooling"], digits=3) end else # Else if the user already provide cop series, use that - cop_heating = round.(d["ASHP"]["cop_heating"],digits=3) - cop_cooling = round.(d["ASHP"]["cop_cooling"],digits=3) + cop_heating = round.(d["ASHP_SpaceHeater"]["cop_heating"],digits=3) + cop_cooling = round.(d["ASHP_SpaceHeater"]["cop_cooling"],digits=3) end - d["ASHP"]["cop_heating"] = cop_heating - d["ASHP"]["cop_cooling"] = cop_cooling + d["ASHP_SpaceHeater"]["cop_heating"] = cop_heating + d["ASHP_SpaceHeater"]["cop_cooling"] = cop_cooling # Add ASHP's capacity factor curves - if !haskey(d["ASHP"], "cf_heating") || !haskey(d["ASHP"], "cf_cooling") - if !haskey(d["ASHP"], "cf_heating") + if !haskey(d["ASHP_SpaceHeater"], "cf_heating") || !haskey(d["ASHP_SpaceHeater"], "cf_cooling") + if !haskey(d["ASHP_SpaceHeater"], "cf_heating") cf_heating = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) else - cf_heating = round.(d["ASHP"]["cf_heating"],digits=3) + cf_heating = round.(d["ASHP_SpaceHeater"]["cf_heating"],digits=3) end - if !haskey(d["ASHP"], "cf_cooling") + if !haskey(d["ASHP_SpaceHeater"], "cf_cooling") cf_cooling = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) else - cf_cooling = round.(d["ASHP"]["cf_cooling"],digits=3) + cf_cooling = round.(d["ASHP_SpaceHeater"]["cf_cooling"],digits=3) end else # Else if the user already provide cf curves, use them - cf_heating = round.(d["ASHP"]["cf_heating"],digits=3) - cf_cooling = round.(d["ASHP"]["cf_cooling"],digits=3) + cf_heating = round.(d["ASHP_SpaceHeater"]["cf_heating"],digits=3) + cf_cooling = round.(d["ASHP_SpaceHeater"]["cf_cooling"],digits=3) end - d["ASHP"]["cf_heating"] = cf_heating - d["ASHP"]["cf_cooling"] = cf_cooling - ashp = ASHP(;dictkeys_tosymbols(d["ASHP"])...) + d["ASHP_SpaceHeater"]["cf_heating"] = cf_heating + d["ASHP_SpaceHeater"]["cf_cooling"] = cf_cooling + ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) end # ASHP Water Heater: @@ -730,10 +731,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) cop_heating = [] cf_heating = [] - if haskey(d, "ASHP_WH") && d["ASHP_WH"]["max_ton"] > 0.0 + if haskey(d, "ASHP_WaterHeater") && d["ASHP_WaterHeater"]["max_ton"] > 0.0 # Add ASHP_WH's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature - if !haskey(d["ASHP_WH"], "cop_heating") + if !haskey(d["ASHP_WaterHeater"], "cop_heating") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor if isnothing(ambient_temp_celsius) if !isempty(pvs) @@ -749,32 +750,32 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - if !haskey(d["ASHP_WH"], "cop_heating") + if !haskey(d["ASHP_WaterHeater"], "cop_heating") cop_heating = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) cop_heating[ambient_temp_fahrenheit .< -7.6] .= 1 cop_heating[ambient_temp_fahrenheit .> 79] .= 999999 else - cop_heating = round.(d["ASHP_WH"]["cop_heating"],digits=3) + cop_heating = round.(d["ASHP_WaterHeater"]["cop_heating"],digits=3) end else # Else if the user already provide cop series, use that - cop_heating = round.(d["ASHP_WH"]["cop_heating"],digits=3) + cop_heating = round.(d["ASHP_WaterHeater"]["cop_heating"],digits=3) end - d["ASHP_WH"]["cop_heating"] = cop_heating + d["ASHP_WaterHeater"]["cop_heating"] = cop_heating # Add ASHP_WH's capacity factor curves - if !haskey(d["ASHP_WH"], "cf_heating") - if !haskey(d["ASHP_WH"], "cf_heating") + if !haskey(d["ASHP_WaterHeater"], "cf_heating") + if !haskey(d["ASHP_WaterHeater"], "cf_heating") cf_heating = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) else - cf_heating = round.(d["ASHP_WH"]["cf_heating"],digits=3) + cf_heating = round.(d["ASHP_WaterHeater"]["cf_heating"],digits=3) end else # Else if the user already provide cf curves, use them - cf_heating = round.(d["ASHP_WH"]["cf_heating"],digits=3) + cf_heating = round.(d["ASHP_WaterHeater"]["cf_heating"],digits=3) end - d["ASHP_WH"]["cf_heating"] = cf_heating - ashp_wh = ASHP_WH(;dictkeys_tosymbols(d["ASHP_WH"])...) + d["ASHP_WaterHeater"]["cf_heating"] = cf_heating + ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) end return Scenario( diff --git a/src/core/techs.jl b/src/core/techs.jl index bef74af52..672a7254b 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -270,46 +270,46 @@ function Techs(s::Scenario) end if !isnothing(s.ashp) - push!(all_techs, "ASHP") - push!(heating_techs, "ASHP") - push!(electric_heaters, "ASHP") - push!(ashp_techs, "ASHP") + push!(all_techs, "ASHP_SpaceHeater") + push!(heating_techs, "ASHP_SpaceHeater") + push!(electric_heaters, "ASHP_SpaceHeater") + push!(ashp_techs, "ASHP_SpaceHeater") if s.ashp.can_supply_steam_turbine - push!(techs_can_supply_steam_turbine, "ASHP") + push!(techs_can_supply_steam_turbine, "ASHP_SpaceHeater") end if s.ashp.can_serve_space_heating - push!(techs_can_serve_space_heating, "ASHP") + push!(techs_can_serve_space_heating, "ASHP_SpaceHeater") end if s.ashp.can_serve_dhw - push!(techs_can_serve_dhw, "ASHP") + push!(techs_can_serve_dhw, "ASHP_SpaceHeater") end if s.ashp.can_serve_process_heat - push!(techs_can_serve_process_heat, "ASHP") + push!(techs_can_serve_process_heat, "ASHP_SpaceHeater") end if s.ashp.can_serve_cooling - push!(cooling_techs, "ASHP") + push!(cooling_techs, "ASHP_SpaceHeater") end end if !isnothing(s.ashp_wh) - push!(all_techs, "ASHP_WH") - push!(heating_techs, "ASHP_WH") - push!(electric_heaters, "ASHP_WH") - push!(ashp_wh_techs, "ASHP_WH") + push!(all_techs, "ASHP_WaterHeater") + push!(heating_techs, "ASHP_WaterHeater") + push!(electric_heaters, "ASHP_WaterHeater") + push!(ashp_wh_techs, "ASHP_WaterHeater") if s.ashp_wh.can_supply_steam_turbine - push!(techs_can_supply_steam_turbine, "ASHP_WH") + push!(techs_can_supply_steam_turbine, "ASHP_WaterHeater") end if s.ashp_wh.can_serve_space_heating - push!(techs_can_serve_space_heating, "ASHP_WH") + push!(techs_can_serve_space_heating, "ASHP_WaterHeater") end if s.ashp_wh.can_serve_dhw - push!(techs_can_serve_dhw, "ASHP_WH") + push!(techs_can_serve_dhw, "ASHP_WaterHeater") end if s.ashp_wh.can_serve_process_heat - push!(techs_can_serve_process_heat, "ASHP_WH") + push!(techs_can_serve_process_heat, "ASHP_WaterHeater") end if s.ashp_wh.can_serve_cooling - push!(cooling_techs, "ASHP_WH") + push!(cooling_techs, "ASHP_WaterHeater") end end diff --git a/src/results/results.jl b/src/results/results.jl index e3e17fae1..3c6914928 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -104,10 +104,10 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") add_electric_heater_results(m, p, d; _n) end - if "ASHP" in p.techs.ashp + if "ASHP_SpaceHeater" in p.techs.ashp add_ashp_results(m, p, d; _n) end - if "ASHP_WH" in p.techs.ashp_wh + if "ASHP_WaterHeater" in p.techs.ashp_wh add_ashp_wh_results(m, p, d; _n) end diff --git a/test/runtests.jl b/test/runtests.jl index cb2e4785c..4236454a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2400,7 +2400,7 @@ else # run HiGHS tests end - @testset "ASHP" begin + @testset "ASHP_SpaceHeater" begin #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP is not purchased d = JSON.parsefile("./scenarios/ashp.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 @@ -2408,42 +2408,42 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) - @test results["ASHP"]["size_ton"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 #Case 2: ASHP has temperature-dependent output and serves all heating load d["ExistingChiller"] = Dict("retire_in_optimal" => false) d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP"]["installed_cost_per_ton"] = 300 + d["ASHP_SpaceHeater"]["installed_cost_per_ton"] = 300 p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP"][ts] for ts in p.time_steps) + annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP"]["size_ton"] ≈ 74.99 atol=0.01 - @test results["ASHP"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 74.99 atol=0.01 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 #Case 3: ASHP can serve cooling, add cooling load d["CoolingLoad"] = Dict("thermal_loads_ton" => ones(8760)*0.1) d["ExistingChiller"] = Dict("cop" => 0.5) - d["ASHP"]["can_serve_cooling"] = true + d["ASHP_SpaceHeater"]["can_serve_cooling"] = true m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) - annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP"][ts] for ts in p.time_steps) + annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 873.9 atol=1e-4 + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 873.9 atol=1e-4 #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) @@ -2451,8 +2451,8 @@ else # run HiGHS tests d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) - @test results["ASHP"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 end @@ -2464,26 +2464,26 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) - @test results["ASHP_WH"]["size_ton"] ≈ 0.0 atol=0.1 - @test results["ASHP_WH"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP_WH"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ASHP_WaterHeater"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 #Case 2: ASHP_WH has temperature-dependent output and serves all DHW load d["ExistingChiller"] = Dict("retire_in_optimal" => false) d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP_WH"]["installed_cost_per_ton"] = 300 + d["ASHP_WaterHeater"]["installed_cost_per_ton"] = 300 p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WH"][ts] for ts in p.time_steps) + annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_WH"]["size_ton"] ≈ 37.495 atol=0.01 - @test results["ASHP_WH"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP_WH"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP_WaterHeater"]["size_ton"] ≈ 37.495 atol=0.01 + @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 end diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 9527e2d04..35cd2c477 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -9,7 +9,7 @@ "fuel_type": "natural_gas", "fuel_cost_per_mmbtu": 5 }, - "ASHP": { + "ASHP_SpaceHeater": { "min_ton": 0.0, "max_ton": 100000, "installed_cost_per_ton": 4050, diff --git a/test/scenarios/ashp_wh.json b/test/scenarios/ashp_wh.json index 27e5abe36..ee6725274 100644 --- a/test/scenarios/ashp_wh.json +++ b/test/scenarios/ashp_wh.json @@ -9,17 +9,13 @@ "fuel_type": "natural_gas", "fuel_cost_per_mmbtu": 5 }, - "ASHP_WH": { + "ASHP_WaterHeater": { "min_ton": 0.0, "max_ton": 100000, "installed_cost_per_ton": 4050, "om_cost_per_ton": 0.0, "macrs_option_years": 0, - "macrs_bonus_fraction": 0.0, - "can_supply_steam_turbine": false, - "can_serve_space_heating": false, - "can_serve_dhw": true, - "can_serve_cooling": false + "macrs_bonus_fraction": 0.0 }, "Financial": { "om_cost_escalation_rate_fraction": 0.025, From 4c2a46e6356d28aab327322dbf6347b314c92b72 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 10:54:39 -0600 Subject: [PATCH 117/266] refactor inputs function for ASHP --- src/core/reopt_inputs.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 8c3e75aed..e3853a5de 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -444,7 +444,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ASHP_SpaceHeater" in techs.all - setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) + setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) else heating_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) cooling_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) @@ -453,7 +453,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ASHP_WaterHeater" in techs.all - setup_ASHP_WaterHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) + setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) else heating_cop["ASHP_WaterHeater"] = ones(length(time_steps)) heating_cf["ASHP_WaterHeater"] = zeros(length(time_steps)) @@ -934,7 +934,7 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end -function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) +function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) max_sizes["ASHP_SpaceHeater"] = s.ashp.max_kw min_sizes["ASHP_SpaceHeater"] = s.ashp.min_kw om_cost_per_kw["ASHP_SpaceHeater"] = s.ashp.om_cost_per_kw @@ -962,7 +962,7 @@ function setup_ashp_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_ end -function setup_ASHP_WaterHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) +function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) max_sizes["ASHP_WaterHeater"] = s.ashp_wh.max_kw min_sizes["ASHP_WaterHeater"] = s.ashp_wh.min_kw om_cost_per_kw["ASHP_WaterHeater"] = s.ashp_wh.om_cost_per_kw From 22726262c95e854648d117a5bef6e4e3016d33ce Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 10:55:20 -0600 Subject: [PATCH 118/266] bugfix - reorder sets of heating loads served --- src/core/types.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/types.jl b/src/core/types.jl index ee44eec49..6ed1eb96b 100644 --- a/src/core/types.jl +++ b/src/core/types.jl @@ -42,8 +42,8 @@ mutable struct Techs steam_turbine::Vector{String} can_supply_steam_turbine::Vector{String} electric_heater::Vector{String} - can_serve_dhw::Vector{String} can_serve_space_heating::Vector{String} + can_serve_dhw::Vector{String} can_serve_process_heat::Vector{String} ghp::Vector{String} ashp::Vector{String} @@ -72,8 +72,8 @@ mutable struct Techs steam_turbine::Vector{String} can_supply_steam_turbine::Vector{String} electric_heater::Vector{String} - can_serve_dhw::Vector{String} can_serve_space_heating::Vector{String} + can_serve_dhw::Vector{String} can_serve_process_heat::Vector{String} ghp::Vector{String} ashp::Vector{String} From 7ce8fbb324c19df8e1fa789945cde9530e4a2173 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 10:55:54 -0600 Subject: [PATCH 119/266] update ASHP results to remove unserved loads --- src/results/ashp.jl | 40 ++++++---------------------------------- 1 file changed, 6 insertions(+), 34 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 2864ff34b..917de1c24 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -27,12 +27,12 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_SpaceHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] - for q in p.heating_loads, t in p.techs.ashp) + p.hours_per_time_step * sum(m[:dvHeatingProduction]["ASHP_SpaceHeater",q,ts] for q in p.heating_loads) + / p.heating_cop["ASHP_SpaceHeater"][ts] ) @expression(m, ASHPThermalProductionSeries[ts in p.time_steps], - sum(m[:dvHeatingProduction][t,q,ts] for q in p.heating_loads, t in p.techs.ashp)) # TODO add cooling + sum(m[:dvHeatingProduction]["ASHP_SpaceHeater",q,ts] for q in p.heating_loads)) # TODO add cooling r["thermal_production_series_mmbtu_per_hour"] = round.(value.(ASHPThermalProductionSeries) / KWH_PER_MMBTU, digits=5) r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) @@ -50,47 +50,20 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") end r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPToHotTESKW) / KWH_PER_MMBTU, digits=3) - if !isempty(p.techs.steam_turbine) && p.s.ashp.can_supply_steam_turbine - @expression(m, ASHPToSteamTurbine[ts in p.time_steps], sum(m[:dvThermalToSteamTurbine]["ASHP_SpaceHeater",q,ts] for q in p.heating_loads)) - @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], m[:dvThermalToSteamTurbine]["ASHP_SpaceHeater",q,ts]) - else - ASHPToSteamTurbine = zeros(length(p.time_steps)) - @expression(m, ASHPToSteamTurbineByQuality[q in p.heating_loads, ts in p.time_steps], 0.0) - end - r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ASHPToSteamTurbine) / KWH_PER_MMBTU, digits=3) - @expression(m, ASHPToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHP_SpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToSteamTurbine[ts] + sum(m[:dvHeatingProduction]["ASHP_SpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) - - if "DomesticHotWater" in p.heating_loads && p.s.ashp.can_serve_dhw - @expression(m, ASHPToDHWKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_SpaceHeater","DomesticHotWater",ts] - ASHPToHotTESByQualityKW["DomesticHotWater",ts] - ASHPToSteamTurbineByQuality["DomesticHotWater",ts] - ) - else - @expression(m, ASHPToDHWKW[ts in p.time_steps], 0.0) - end - r["thermal_to_dhw_load_series_mmbtu_per_hour"] = round.(value.(ASHPToDHWKW ./ KWH_PER_MMBTU), digits=5) if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_SpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] - ASHPToSteamTurbineByQuality["SpaceHeating",ts] + m[:dvHeatingProduction]["ASHP_SpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] ) else @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], 0.0) end r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(ASHPToSpaceHeatingKW ./ KWH_PER_MMBTU), digits=5) - if "ProcessHeat" in p.heating_loads && p.s.ashp.can_serve_space_heating - @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_SpaceHeater","ProcessHeat",ts] - ASHPToHotTESByQualityKW["ProcessHeat",ts] - ASHPToSteamTurbineByQuality["ProcessHeat",ts] - ) - else - @expression(m, ASHPToProcessHeatKW[ts in p.time_steps], 0.0) - end - r["thermal_to_process_heat_load_series_mmbtu_per_hour"] = round.(value.(ASHPToProcessHeatKW ./ KWH_PER_MMBTU), digits=5) - if "ASHP_SpaceHeater" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 @expression(m, ASHPtoColdTES[ts in p.time_steps], @@ -109,8 +82,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["annual_thermal_production_tonhour"] = round(value(Year1ASHPColdThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvCoolingProduction][t,ts] / p.cooling_cop[t][ts] - for t in p.techs.ashp) + p.hours_per_time_step * m[:dvCoolingProduction]["ASHP_SpaceHeater",ts] / p.cooling_cop["ASHP_SpaceHeater"][ts] ) else r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) From 1d34879038628795f671aaf625f0d2b636ee096f Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 10:56:36 -0600 Subject: [PATCH 120/266] Update ashp.json --- test/scenarios/ashp.json | 8 +------- 1 file changed, 1 insertion(+), 7 deletions(-) diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 35cd2c477..22cbcedeb 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -16,9 +16,6 @@ "om_cost_per_ton": 0.0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, - "can_supply_steam_turbine": false, - "can_serve_space_heating": true, - "can_serve_dhw": true, "can_serve_cooling": false }, "Financial": { @@ -39,10 +36,7 @@ }, "SpaceHeatingLoad": { "doe_reference_name": "FlatLoad" - }, - "DomesticHotWaterLoad": { - "doe_reference_name": "FlatLoad" - }, + }, "ElectricTariff": { "monthly_energy_rates": [0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1,0.1] } From b32b35a4301f462011baa626f6c8bee326843a1a Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 13:32:58 -0600 Subject: [PATCH 121/266] Update ASHP Space Heater test --- test/runtests.jl | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 4236454a6..911212d98 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2400,11 +2400,10 @@ else # run HiGHS tests end - @testset "ASHP_SpaceHeater" begin + @testset "ASHP Space Heater" begin #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP is not purchased d = JSON.parsefile("./scenarios/ashp.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["SpaceHeatingLoad"]["annual_mmbtu"] = 1.0 * 8760 m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, d) From 9e611ec3e98c720617b2c7d01164499693943979 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 21:27:41 -0600 Subject: [PATCH 122/266] Add ability to force ASHP techs into system --- src/constraints/thermal_tech_constraints.jl | 26 +++++++++++++++++++++ src/core/ashp.jl | 19 ++++++++++----- 2 files changed, 39 insertions(+), 6 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 5add65278..b854811f8 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -95,6 +95,32 @@ function add_ashp_heating_cooling_constraints(m, p; _n="") @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) / p.heating_cf[t][ts] + m[Symbol("dvCoolingProduction"*_n)][t,ts] / p.cooling_cf[t][ts] <= m[Symbol("dvSize"*_n)][t] ) + + if "ASHP_SpaceHeater" in p.techs.heating && p.s.ashp.force_into_system + for t in setdiff(p.techs.can_serve_space_heating, ["ASHP_SpaceHeater"]) + for ts in p.time_steps + fix(m[Symbol("dvHeatingProduction"*_n)][t,"SpaceHeating",ts], 0.0, force=true) + fix(m[Symbol("dvProductionToWaste"*_n)][t,"SpaceHeating",ts], 0.0, force=true) + end + end + end + + if "ASHP_SpaceHeater" in p.techs.cooling && p.s.ashp.force_into_system + for t in setdiff(p.techs.cooling, ["ASHP_SpaceHeater"]) + for ts in p.time_steps + fix(m[Symbol("dvCoolingProduction"*_n)][t,ts], 0.0, force=true) + end + end + end + + if "ASHP_WaterHeater" in p.techs.heating && p.s.ashp.force_into_system + for t in setdiff(p.techs.can_serve_space_heating, ["ASHP_WaterHeater"]) + for ts in p.time_steps + fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) + fix(m[Symbol("dvProductionToWaste"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) + end + end + end end function no_existing_boiler_production(m, p; _n="") diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 2b9679e51..f87cda09b 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -16,11 +16,12 @@ struct ASHP <: AbstractThermalTech can_serve_space_heating::Bool can_serve_process_heat::Bool can_serve_cooling::Bool + force_into_system::Bool end """ -ASHP +ASHP_SpaceHeater If a user provides the `ASHP_SpaceHeater` key then the optimal scenario has the option to purchase this new `ASHP` to meet the heating load in addition to using the `ExistingBoiler` @@ -39,6 +40,7 @@ function ASHP_SpaceHeater(; cf_heating::Array{Float64,1}, # ASHP's heating capacity factor curves cf_cooling::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load + force_into_system::Bool = false # force into system to serve all space heating loads if true ) ``` """ @@ -53,7 +55,8 @@ function ASHP_SpaceHeater(; cop_cooling::Array{Float64,1} = Float64[], cf_heating::Array{Float64,1} = Float64[], cf_cooling::Array{Float64,1} = Float64[], - can_serve_cooling::Union{Bool, Nothing} = nothing + can_serve_cooling::Union{Bool, Nothing} = nothing, + force_into_system::Bool = false ) defaults = get_ashp_defaults("SpaceHeating") @@ -99,13 +102,14 @@ function ASHP_SpaceHeater(; can_serve_dhw, can_serve_space_heating, can_serve_process_heat, - can_serve_cooling + can_serve_cooling, + force_into_system ) end """ -ASHP Water Heater +ASHP Water_Heater If a user provides the `ASHP_WaterHeater` key then the optimal scenario has the option to purchase this new `ASHP_WaterHeater` to meet the domestic hot water load in addition to using the `ExistingBoiler` @@ -121,6 +125,7 @@ function ASHP_WaterHeater(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production cop_heating::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + force_into_system::Bool = false # force into system to serve all hot water loads if true ) ``` """ @@ -132,7 +137,8 @@ function ASHP_WaterHeater(; macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, cop_heating::Array{Float64,1} = Float64[], - cf_heating::Array{Float64,1} = Float64[] + cf_heating::Array{Float64,1} = Float64[], + force_into_system::Bool = false ) defaults = get_ashp_defaults("DomesticHotWater") @@ -175,7 +181,8 @@ function ASHP_WaterHeater(; can_serve_dhw, can_serve_space_heating, can_serve_process_heat, - can_serve_cooling + can_serve_cooling, + force_into_system ) end From 59eac0ee6d39d2401bd54a92423f675032fc8411 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 21:28:34 -0600 Subject: [PATCH 123/266] group ASHP tests and add force-in tests --- test/runtests.jl | 177 ++++++++++++++++++++++++++--------------------- 1 file changed, 100 insertions(+), 77 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 911212d98..c14c5d40d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2400,90 +2400,113 @@ else # run HiGHS tests end - @testset "ASHP Space Heater" begin - #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP is not purchased - d = JSON.parsefile("./scenarios/ashp.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 1.0 * 8760 + @testset "ASHP" begin + @testset "ASHP Space Heater" begin + #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP is not purchased + d = JSON.parsefile("./scenarios/ashp.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 1.0 * 8760 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 0.0 atol=0.1 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - - #Case 2: ASHP has temperature-dependent output and serves all heating load - d["ExistingChiller"] = Dict("retire_in_optimal" => false) - d["ExistingBoiler"]["retire_in_optimal"] = false - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP_SpaceHeater"]["installed_cost_per_ton"] = 300 - - p = REoptInputs(d) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) - annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 74.99 atol=0.01 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + #Case 2: ASHP has temperature-dependent output and serves all heating load + d["ExistingChiller"] = Dict("retire_in_optimal" => false) + d["ExistingBoiler"]["retire_in_optimal"] = false + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ASHP_SpaceHeater"]["installed_cost_per_ton"] = 300 + + p = REoptInputs(d) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) + annual_energy_supplied = 87600 + annual_ashp_consumption + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 74.99 atol=0.01 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 + + #Case 3: ASHP can serve cooling, add cooling load + d["CoolingLoad"] = Dict("thermal_loads_ton" => ones(8760)*0.1) + d["ExistingChiller"] = Dict("cop" => 0.5) + d["ASHP_SpaceHeater"]["can_serve_cooling"] = true - #Case 3: ASHP can serve cooling, add cooling load - d["CoolingLoad"] = Dict("thermal_loads_ton" => ones(8760)*0.1) - d["ExistingChiller"] = Dict("cop" => 0.5) - d["ASHP_SpaceHeater"]["can_serve_cooling"] = true + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) + annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 873.9 atol=1e-4 + + #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate + d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) + d["ExistingBoiler"]["retire_in_optimal"] = true + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 - annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) - annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 873.9 atol=1e-4 - - #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate - d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) - d["ExistingBoiler"]["retire_in_optimal"] = true - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 + end - end + @testset "ASHP Water Heater" begin + #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP_WH is not purchased + d = JSON.parsefile("./scenarios/ashp_wh.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["ASHP_WaterHeater"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + #Case 2: ASHP_WH has temperature-dependent output and serves all DHW load + d["ExistingChiller"] = Dict("retire_in_optimal" => false) + d["ExistingBoiler"]["retire_in_optimal"] = false + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ASHP_WaterHeater"]["installed_cost_per_ton"] = 300 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) + annual_energy_supplied = 87600 + annual_ashp_consumption + @test results["ASHP_WaterHeater"]["size_ton"] ≈ 37.495 atol=0.01 + @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + end - @testset "ASHP Water Heater" begin - #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP_WH is not purchased - d = JSON.parsefile("./scenarios/ashp_wh.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["ASHP_WaterHeater"]["size_ton"] ≈ 0.0 atol=0.1 - @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - - #Case 2: ASHP_WH has temperature-dependent output and serves all DHW load - d["ExistingChiller"] = Dict("retire_in_optimal" => false) - d["ExistingBoiler"]["retire_in_optimal"] = false - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP_WaterHeater"]["installed_cost_per_ton"] = 300 + @testset "Force in ASHP systems" begin + d = JSON.parsefile("./scenarios/ashp.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"] = Dict{String,Any}("annual_mmbtu" => 0.5 * 8760, "doe_reference_name" => "FlatLoad") + d["CoolingLoad"] = Dict{String,Any}("thermal_loads_ton" => ones(8760)*0.1) + d["ExistingChiller"] = Dict{String,Any}("retire_in_optimal" => false, "cop" => 100) + d["ExistingBoiler"]["retire_in_optimal"] = false + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 + d["ASHP_SpaceHeater"]["can_serve_cooling"] = true + d["ASHP_SpaceHeater"]["force_into_system"] = true + d["ASHP_WaterHeater"] = Dict{String,Any}("force_into_system" => true, "max_ton" => 100000) + + p = REoptInputs(d) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) - p = REoptInputs(d) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) - annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_WaterHeater"]["size_ton"] ≈ 37.495 atol=0.01 - @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + end + end @testset "Process Heat Load" begin From bb7361c23870f2969eb1420d986ef8b077e6ccdf Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 14 Jun 2024 21:31:27 -0600 Subject: [PATCH 124/266] ren ASHP cf and cop attributes --- src/core/ashp.jl | 42 +++++++++--------- src/core/reopt_inputs.jl | 12 +++--- src/core/scenario.jl | 92 ++++++++++++++++++++-------------------- 3 files changed, 73 insertions(+), 73 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index f87cda09b..bd5794a7b 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -8,10 +8,10 @@ struct ASHP <: AbstractThermalTech macrs_option_years::Int macrs_bonus_fraction::Real can_supply_steam_turbine::Bool - cop_heating::Array{Float64,1} - cop_cooling::Array{Float64,1} - cf_heating::Array{Float64,1} - cf_cooling::Array{Float64,1} + heating_cop::Array{Float64,1} + cooling_cop::Array{Float64,1} + heating_cf::Array{Float64,1} + cooling_cf::Array{Float64,1} can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -35,10 +35,10 @@ function ASHP_SpaceHeater(; om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS - cop_heating::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - cop_cooling::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) - cf_heating::Array{Float64,1}, # ASHP's heating capacity factor curves - cf_cooling::Array{Float64,1}, # ASHP's cooling capacity factor curves + heating_cop::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) + heating_cf::Array{Float64,1}, # ASHP's heating capacity factor curves + cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Bool = false # force into system to serve all space heating loads if true ) @@ -51,10 +51,10 @@ function ASHP_SpaceHeater(; om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, - cop_heating::Array{Float64,1} = Float64[], - cop_cooling::Array{Float64,1} = Float64[], - cf_heating::Array{Float64,1} = Float64[], - cf_cooling::Array{Float64,1} = Float64[], + heating_cop::Array{Float64,1} = Float64[], + cooling_cop::Array{Float64,1} = Float64[], + heating_cf::Array{Float64,1} = Float64[], + cooling_cf::Array{Float64,1} = Float64[], can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Bool = false ) @@ -95,10 +95,10 @@ function ASHP_SpaceHeater(; macrs_option_years, macrs_bonus_fraction, can_supply_steam_turbine, - cop_heating, - cop_cooling, - cf_heating, - cf_cooling, + heating_cop, + cooling_cop, + heating_cf, + cooling_cf, can_serve_dhw, can_serve_space_heating, can_serve_process_heat, @@ -124,7 +124,7 @@ function ASHP_WaterHeater(; macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production - cop_heating::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) force_into_system::Bool = false # force into system to serve all hot water loads if true ) ``` @@ -136,8 +136,8 @@ function ASHP_WaterHeater(; om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, - cop_heating::Array{Float64,1} = Float64[], - cf_heating::Array{Float64,1} = Float64[], + heating_cop::Array{Float64,1} = Float64[], + heating_cf::Array{Float64,1} = Float64[], force_into_system::Bool = false ) @@ -174,9 +174,9 @@ function ASHP_WaterHeater(; macrs_option_years, macrs_bonus_fraction, can_supply_steam_turbine, - cop_heating, + heating_cop, Float64[], - cf_heating, + heating_cf, Float64[], can_serve_dhw, can_serve_space_heating, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index e3853a5de..93c704296 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -938,10 +938,10 @@ function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, max_sizes["ASHP_SpaceHeater"] = s.ashp.max_kw min_sizes["ASHP_SpaceHeater"] = s.ashp.min_kw om_cost_per_kw["ASHP_SpaceHeater"] = s.ashp.om_cost_per_kw - heating_cop["ASHP_SpaceHeater"] = s.ashp.cop_heating - cooling_cop["ASHP_SpaceHeater"] = s.ashp.cop_cooling - heating_cf["ASHP_SpaceHeater"] = s.ashp.cf_heating - cooling_cf["ASHP_SpaceHeater"] = s.ashp.cf_cooling + heating_cop["ASHP_SpaceHeater"] = s.ashp.heating_cop + cooling_cop["ASHP_SpaceHeater"] = s.ashp.cooling_cop + heating_cf["ASHP_SpaceHeater"] = s.ashp.heating_cf + cooling_cf["ASHP_SpaceHeater"] = s.ashp.cooling_cf if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP_SpaceHeater"] = effective_cost(; @@ -966,8 +966,8 @@ function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, max_sizes["ASHP_WaterHeater"] = s.ashp_wh.max_kw min_sizes["ASHP_WaterHeater"] = s.ashp_wh.min_kw om_cost_per_kw["ASHP_WaterHeater"] = s.ashp_wh.om_cost_per_kw - heating_cop["ASHP_WaterHeater"] = s.ashp_wh.cop_heating - heating_cf["ASHP_WaterHeater"] = s.ashp_wh.cf_heating + heating_cop["ASHP_WaterHeater"] = s.ashp_wh.heating_cop + heating_cf["ASHP_WaterHeater"] = s.ashp_wh.heating_cf if s.ashp_wh.macrs_option_years in [5, 7] cap_cost_slope["ASHP_WaterHeater"] = effective_cost(; diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 8e2643a47..e569eec62 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -656,14 +656,14 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # ASHP ashp = nothing - cop_heating = [] - cop_cooling = [] - cf_heating = [] - cf_cooling = [] + heating_cop = [] + cooling_cop = [] + heating_cf = [] + cooling_cf = [] if haskey(d, "ASHP_SpaceHeater") && d["ASHP_SpaceHeater"]["max_ton"] > 0.0 # Add ASHP's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature - if !haskey(d["ASHP_SpaceHeater"], "cop_heating") || !haskey(d["ASHP_SpaceHeater"], "cop_cooling") + if !haskey(d["ASHP_SpaceHeater"], "heating_cop") || !haskey(d["ASHP_SpaceHeater"], "cooling_cop") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor if isnothing(ambient_temp_celsius) if !isempty(pvs) @@ -679,62 +679,62 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - if !haskey(d["ASHP_SpaceHeater"], "cop_heating") - cop_heating = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - cop_heating[ambient_temp_fahrenheit .< -7.6] .= 1 - cop_heating[ambient_temp_fahrenheit .> 79] .= 999999 + if !haskey(d["ASHP_SpaceHeater"], "heating_cop") + heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) + heating_cop[ambient_temp_fahrenheit .< -7.6] .= 1 + heating_cop[ambient_temp_fahrenheit .> 79] .= 999999 else - cop_heating = round.(d["ASHP_SpaceHeater"]["cop_heating"],digits=3) + heating_cop = round.(d["ASHP_SpaceHeater"]["heating_cop"],digits=3) end - if !haskey(d["ASHP_SpaceHeater"], "cop_cooling") - cop_cooling = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) - cop_cooling[ambient_temp_celsius .< 25] .= 999999 - cop_cooling[ambient_temp_celsius .> 40] .= 1 + if !haskey(d["ASHP_SpaceHeater"], "cooling_cop") + cooling_cop = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) + cooling_cop[ambient_temp_celsius .< 25] .= 999999 + cooling_cop[ambient_temp_celsius .> 40] .= 1 else - cop_cooling = round.(d["ASHP_SpaceHeater"]["cop_cooling"], digits=3) + cooling_cop = round.(d["ASHP_SpaceHeater"]["cooling_cop"], digits=3) end else # Else if the user already provide cop series, use that - cop_heating = round.(d["ASHP_SpaceHeater"]["cop_heating"],digits=3) - cop_cooling = round.(d["ASHP_SpaceHeater"]["cop_cooling"],digits=3) + heating_cop = round.(d["ASHP_SpaceHeater"]["heating_cop"],digits=3) + cooling_cop = round.(d["ASHP_SpaceHeater"]["cooling_cop"],digits=3) end - d["ASHP_SpaceHeater"]["cop_heating"] = cop_heating - d["ASHP_SpaceHeater"]["cop_cooling"] = cop_cooling + d["ASHP_SpaceHeater"]["heating_cop"] = heating_cop + d["ASHP_SpaceHeater"]["cooling_cop"] = cooling_cop # Add ASHP's capacity factor curves - if !haskey(d["ASHP_SpaceHeater"], "cf_heating") || !haskey(d["ASHP_SpaceHeater"], "cf_cooling") - if !haskey(d["ASHP_SpaceHeater"], "cf_heating") - cf_heating = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) + if !haskey(d["ASHP_SpaceHeater"], "heating_cf") || !haskey(d["ASHP_SpaceHeater"], "cooling_cf") + if !haskey(d["ASHP_SpaceHeater"], "heating_cf") + heating_cf = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) else - cf_heating = round.(d["ASHP_SpaceHeater"]["cf_heating"],digits=3) + heating_cf = round.(d["ASHP_SpaceHeater"]["heating_cf"],digits=3) end - if !haskey(d["ASHP_SpaceHeater"], "cf_cooling") - cf_cooling = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) + if !haskey(d["ASHP_SpaceHeater"], "cooling_cf") + cooling_cf = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) else - cf_cooling = round.(d["ASHP_SpaceHeater"]["cf_cooling"],digits=3) + cooling_cf = round.(d["ASHP_SpaceHeater"]["cooling_cf"],digits=3) end else # Else if the user already provide cf curves, use them - cf_heating = round.(d["ASHP_SpaceHeater"]["cf_heating"],digits=3) - cf_cooling = round.(d["ASHP_SpaceHeater"]["cf_cooling"],digits=3) + heating_cf = round.(d["ASHP_SpaceHeater"]["heating_cf"],digits=3) + cooling_cf = round.(d["ASHP_SpaceHeater"]["cooling_cf"],digits=3) end - d["ASHP_SpaceHeater"]["cf_heating"] = cf_heating - d["ASHP_SpaceHeater"]["cf_cooling"] = cf_cooling + d["ASHP_SpaceHeater"]["heating_cf"] = heating_cf + d["ASHP_SpaceHeater"]["cooling_cf"] = cooling_cf ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) end # ASHP Water Heater: ashp_wh = nothing - cop_heating = [] - cf_heating = [] + heating_cop = [] + heating_cf = [] if haskey(d, "ASHP_WaterHeater") && d["ASHP_WaterHeater"]["max_ton"] > 0.0 # Add ASHP_WH's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature - if !haskey(d["ASHP_WaterHeater"], "cop_heating") + if !haskey(d["ASHP_WaterHeater"], "heating_cop") # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor if isnothing(ambient_temp_celsius) if !isempty(pvs) @@ -750,31 +750,31 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - if !haskey(d["ASHP_WaterHeater"], "cop_heating") - cop_heating = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - cop_heating[ambient_temp_fahrenheit .< -7.6] .= 1 - cop_heating[ambient_temp_fahrenheit .> 79] .= 999999 + if !haskey(d["ASHP_WaterHeater"], "heating_cop") + heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) + heating_cop[ambient_temp_fahrenheit .< -7.6] .= 1 + heating_cop[ambient_temp_fahrenheit .> 79] .= 999999 else - cop_heating = round.(d["ASHP_WaterHeater"]["cop_heating"],digits=3) + heating_cop = round.(d["ASHP_WaterHeater"]["heating_cop"],digits=3) end else # Else if the user already provide cop series, use that - cop_heating = round.(d["ASHP_WaterHeater"]["cop_heating"],digits=3) + heating_cop = round.(d["ASHP_WaterHeater"]["heating_cop"],digits=3) end - d["ASHP_WaterHeater"]["cop_heating"] = cop_heating + d["ASHP_WaterHeater"]["heating_cop"] = heating_cop # Add ASHP_WH's capacity factor curves - if !haskey(d["ASHP_WaterHeater"], "cf_heating") - if !haskey(d["ASHP_WaterHeater"], "cf_heating") - cf_heating = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) + if !haskey(d["ASHP_WaterHeater"], "heating_cf") + if !haskey(d["ASHP_WaterHeater"], "heating_cf") + heating_cf = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) else - cf_heating = round.(d["ASHP_WaterHeater"]["cf_heating"],digits=3) + heating_cf = round.(d["ASHP_WaterHeater"]["heating_cf"],digits=3) end else # Else if the user already provide cf curves, use them - cf_heating = round.(d["ASHP_WaterHeater"]["cf_heating"],digits=3) + heating_cf = round.(d["ASHP_WaterHeater"]["heating_cf"],digits=3) end - d["ASHP_WaterHeater"]["cf_heating"] = cf_heating + d["ASHP_WaterHeater"]["heating_cf"] = heating_cf ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) end From b9cdcadd6886022f3519406273031ab6ea7f5ce2 Mon Sep 17 00:00:00 2001 From: Zolan Date: Sat, 15 Jun 2024 11:18:24 -0600 Subject: [PATCH 125/266] load REoptInputs in ASHP tests for test values --- test/runtests.jl | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index c14c5d40d..130b5dd3f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2436,8 +2436,9 @@ else # run HiGHS tests d["ExistingChiller"] = Dict("cop" => 0.5) d["ASHP_SpaceHeater"]["can_serve_cooling"] = true + p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + results = run_reopt(m, p) annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR @@ -2475,8 +2476,9 @@ else # run HiGHS tests d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 d["ASHP_WaterHeater"]["installed_cost_per_ton"] = 300 + p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + results = run_reopt(m, p) annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption From ae577747c023b455063f30f54db91bd6e676ca71 Mon Sep 17 00:00:00 2001 From: An Pham Date: Mon, 17 Jun 2024 23:44:00 -0600 Subject: [PATCH 126/266] updated ashp cost defaults --- data/ashp/ashp_defaults.json | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 3f498118e..9a1184dbf 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -1,8 +1,8 @@ { "SpaceHeating": { - "installed_cost_per_ton": 4050, - "om_cost_per_ton": 0.00024, + "installed_cost_per_ton": 2250, + "om_cost_per_ton": 0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, @@ -13,8 +13,8 @@ }, "DomesticHotWater": { - "installed_cost_per_ton": 4050, - "om_cost_per_ton": 0.00024, + "installed_cost_per_ton": 2250, + "om_cost_per_ton": 0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, From bb4f384acea0d89d2a18c5927b1a1067f47e4b2c Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 24 Jun 2024 12:57:51 -0600 Subject: [PATCH 127/266] add force_into_system to defaults, allow nothing as input --- data/ashp/ashp_defaults.json | 6 ++++-- src/core/ashp.jl | 8 ++++---- 2 files changed, 8 insertions(+), 6 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 9a1184dbf..17bf96adc 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -9,7 +9,8 @@ "can_serve_process_heat": false, "can_serve_dhw": false, "can_serve_space_heating": true, - "can_serve_cooling": true + "can_serve_cooling": true, + "force_into_system": false }, "DomesticHotWater": { @@ -21,6 +22,7 @@ "can_serve_process_heat": false, "can_serve_dhw": true, "can_serve_space_heating": false, - "can_serve_cooling": false + "can_serve_cooling": false, + "force_into_system": false } } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index bd5794a7b..84c3e5223 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -40,7 +40,7 @@ function ASHP_SpaceHeater(; heating_cf::Array{Float64,1}, # ASHP's heating capacity factor curves cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load - force_into_system::Bool = false # force into system to serve all space heating loads if true + force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true ) ``` """ @@ -56,7 +56,7 @@ function ASHP_SpaceHeater(; heating_cf::Array{Float64,1} = Float64[], cooling_cf::Array{Float64,1} = Float64[], can_serve_cooling::Union{Bool, Nothing} = nothing, - force_into_system::Bool = false + force_into_system::Union{Bool, Nothing} = nothing ) defaults = get_ashp_defaults("SpaceHeating") @@ -125,7 +125,7 @@ function ASHP_WaterHeater(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - force_into_system::Bool = false # force into system to serve all hot water loads if true + force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true ) ``` """ @@ -138,7 +138,7 @@ function ASHP_WaterHeater(; macrs_bonus_fraction::Real = 0.0, heating_cop::Array{Float64,1} = Float64[], heating_cf::Array{Float64,1} = Float64[], - force_into_system::Bool = false + force_into_system::Union{Bool, Nothing} = nothing ) defaults = get_ashp_defaults("DomesticHotWater") From f532d7fe9fb80a27b70905bb58e196457b1f7ce3 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 24 Jun 2024 12:58:18 -0600 Subject: [PATCH 128/266] rm unused results from ASHP docstrings --- src/results/ashp.jl | 11 ++--------- 1 file changed, 2 insertions(+), 9 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 917de1c24..b87a02817 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -1,19 +1,17 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. """ -`ASHP` results keys: +`ASHP_SpaceHeater` results keys: - `size_ton` # Thermal production capacity size of the ASHP [ton/hr] - `electric_consumption_series_kw` # Fuel consumption series [kW] - `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] - `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] - `annual_thermal_production_mmbtu` # Thermal heating energy produced in a year [MMBtu] - `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] -- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] - `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] +- `thermal_to_space_heating_load_series_mmbtu_per_hour` # Thermal production to space heating load [MMBTU/hr] - `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage - `thermal_to_load_series_ton` # Thermal production to cooling load -- `electric_consumption_series_kw` -- `annual_electric_consumption_kwh` - `annual_thermal_production_tonhour` Thermal cooling energy produced in a year @@ -105,12 +103,7 @@ end - `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] - `annual_thermal_production_mmbtu` # Thermal heating energy produced in a year [MMBtu] - `thermal_to_storage_series_mmbtu_per_hour` # Thermal power production to TES (HotThermalStorage) series [MMBtu/hr] -- `thermal_to_steamturbine_series_mmbtu_per_hour` # Thermal power production to SteamTurbine series [MMBtu/hr] - `thermal_to_load_series_mmbtu_per_hour` # Thermal power production to serve the heating load series [MMBtu/hr] -- `thermal_to_storage_series_ton` # Thermal production to ColdThermalStorage -- `thermal_to_load_series_ton` # Thermal production to cooling load -- `electric_consumption_series_kw` -- `annual_electric_consumption_kwh` !!! note "'Series' and 'Annual' energy outputs are average annual" From 98ed62cb652353c1d160944ef097d9222b6bfd09 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 24 Jun 2024 12:58:38 -0600 Subject: [PATCH 129/266] bug fix in ASHP_WH constraints --- src/constraints/thermal_tech_constraints.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index b854811f8..ef72bca3f 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -113,8 +113,8 @@ function add_ashp_heating_cooling_constraints(m, p; _n="") end end - if "ASHP_WaterHeater" in p.techs.heating && p.s.ashp.force_into_system - for t in setdiff(p.techs.can_serve_space_heating, ["ASHP_WaterHeater"]) + if "ASHP_WaterHeater" in p.techs.heating && p.s.ashp_wh.force_into_system + for t in setdiff(p.techs.can_serve_dhw, ["ASHP_WaterHeater"]) for ts in p.time_steps fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) fix(m[Symbol("dvProductionToWaste"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) From 975a9711745203fa7c96030e5418d062c4adf92e Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 24 Jun 2024 12:59:29 -0600 Subject: [PATCH 130/266] add more tests to force-in-ASHP testset --- test/runtests.jl | 21 ++++++++++++++++++++- 1 file changed, 20 insertions(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 130b5dd3f..4914b4af9 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2495,7 +2495,7 @@ else # run HiGHS tests d["CoolingLoad"] = Dict{String,Any}("thermal_loads_ton" => ones(8760)*0.1) d["ExistingChiller"] = Dict{String,Any}("retire_in_optimal" => false, "cop" => 100) d["ExistingBoiler"]["retire_in_optimal"] = false - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0.001 d["ASHP_SpaceHeater"]["can_serve_cooling"] = true d["ASHP_SpaceHeater"]["force_into_system"] = true d["ASHP_WaterHeater"] = Dict{String,Any}("force_into_system" => true, "max_ton" => 100000) @@ -2507,6 +2507,25 @@ else # run HiGHS tests @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + + d["ASHP_SpaceHeater"]["force_into_system"] = false + p = REoptInputs(d) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 + @test results["ExistingChiller"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + + d["ASHP_SpaceHeater"]["force_into_system"] = true + d["ASHP_WaterHeater"]["force_into_system"] = false + p = REoptInputs(d) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 end end From 9660a4252f1c7a7bb5294d8f36a4adfee188b176 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 24 Jun 2024 13:49:27 -0600 Subject: [PATCH 131/266] check for nothing, add defaults for force_in_system --- src/core/ashp.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 84c3e5223..a4f80fcc6 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -71,6 +71,9 @@ function ASHP_SpaceHeater(; if isnothing(can_serve_cooling) can_serve_cooling = defaults["can_serve_cooling"] end + if isnothing(force_into_system) + force_into_system = defaults["force_into_system"] + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -150,6 +153,9 @@ function ASHP_WaterHeater(; if isnothing(om_cost_per_ton) om_cost_per_ton = defaults["om_cost_per_ton"] end + if isnothing(force_into_system) + force_into_system = defaults["force_into_system"] + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] From 6cdc80105e85a905aa172bc8703b148e6b2a4f13 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 25 Jun 2024 15:33:47 -0600 Subject: [PATCH 132/266] add ASHP_WaterHeater to techs.ashp --- src/core/techs.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/src/core/techs.jl b/src/core/techs.jl index 672a7254b..b144bda05 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -295,6 +295,7 @@ function Techs(s::Scenario) push!(all_techs, "ASHP_WaterHeater") push!(heating_techs, "ASHP_WaterHeater") push!(electric_heaters, "ASHP_WaterHeater") + push!(ashp_techs, "ASHP_WaterHeater") push!(ashp_wh_techs, "ASHP_WaterHeater") if s.ashp_wh.can_supply_steam_turbine push!(techs_can_supply_steam_turbine, "ASHP_WaterHeater") From a005cae2cae32b86b42475a95492c7c57d977109 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 25 Jun 2024 15:34:10 -0600 Subject: [PATCH 133/266] separate force-in constraints from heat+cool sizing --- src/constraints/thermal_tech_constraints.jl | 9 ++++++--- src/core/reopt.jl | 8 ++++++-- 2 files changed, 12 insertions(+), 5 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index ef72bca3f..11b9c4bc8 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -91,12 +91,15 @@ function add_heating_tech_constraints(m, p; _n="") # Enfore end -function add_ashp_heating_cooling_constraints(m, p; _n="") +function add_heating_cooling_constraints(m, p; _n="") @constraint(m, [t in setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp), ts in p.time_steps], sum(m[Symbol("dvHeatingProduction"*_n)][t,q,ts] for q in p.heating_loads) / p.heating_cf[t][ts] + m[Symbol("dvCoolingProduction"*_n)][t,ts] / p.cooling_cf[t][ts] <= m[Symbol("dvSize"*_n)][t] ) +end - if "ASHP_SpaceHeater" in p.techs.heating && p.s.ashp.force_into_system + +function add_ashp_force_in_constraints(m, p; _n="") + if "ASHP_SpaceHeater" in p.techs.ashp && p.s.ashp.force_into_system for t in setdiff(p.techs.can_serve_space_heating, ["ASHP_SpaceHeater"]) for ts in p.time_steps fix(m[Symbol("dvHeatingProduction"*_n)][t,"SpaceHeating",ts], 0.0, force=true) @@ -113,7 +116,7 @@ function add_ashp_heating_cooling_constraints(m, p; _n="") end end - if "ASHP_WaterHeater" in p.techs.heating && p.s.ashp_wh.force_into_system + if "ASHP_WaterHeater" in p.techs.ashp && p.s.ashp_wh.force_into_system for t in setdiff(p.techs.can_serve_dhw, ["ASHP_WaterHeater"]) for ts in p.time_steps fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 57ae7fcbe..1b8a5ea86 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -317,9 +317,13 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) no_existing_chiller_production(m, p) end - if !isempty(setdiff(intersect(p.techs.heating, p.techs.cooling), p.techs.ghp)) - add_ashp_heating_cooling_constraints(m, p) + if !isempty(setdiff(intersect(p.techs.cooling, p.techs.heating), p.techs.ghp)) + add_heating_cooling_constraints(m, p) end + + if !isempty(p.techs.ashp) + add_ashp_force_in_constraints(m, p) + end if !isempty(p.techs.thermal) add_thermal_load_constraints(m, p) # split into heating and cooling constraints? From a3f28a63e89f88b251636a0372826e7817c60109 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 26 Jun 2024 13:32:50 -0600 Subject: [PATCH 134/266] updated cf to reflect backup resistive heaters performance --- src/core/scenario.jl | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index e569eec62..98da10d51 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -721,6 +721,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) heating_cf = round.(d["ASHP_SpaceHeater"]["heating_cf"],digits=3) cooling_cf = round.(d["ASHP_SpaceHeater"]["cooling_cf"],digits=3) end + + heating_cf[heating_cop .== 1] .= 1 + cooling_cf[cooling_cop .== 1] .= 1 + d["ASHP_SpaceHeater"]["heating_cf"] = heating_cf d["ASHP_SpaceHeater"]["cooling_cf"] = cooling_cf ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) @@ -761,6 +765,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # Else if the user already provide cop series, use that heating_cop = round.(d["ASHP_WaterHeater"]["heating_cop"],digits=3) end + d["ASHP_WaterHeater"]["heating_cop"] = heating_cop # Add ASHP_WH's capacity factor curves @@ -774,6 +779,9 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # Else if the user already provide cf curves, use them heating_cf = round.(d["ASHP_WaterHeater"]["heating_cf"],digits=3) end + + heating_cf[heating_cop .== 1] .= 1 + d["ASHP_WaterHeater"]["heating_cf"] = heating_cf ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) end From 6f875f8df4ad566f56be02130cf649c090177dc6 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 26 Jun 2024 14:36:12 -0600 Subject: [PATCH 135/266] removed cooling cf constraint for cooling cop = 1 --- src/core/scenario.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 98da10d51..0818bc27e 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -723,7 +723,6 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end heating_cf[heating_cop .== 1] .= 1 - cooling_cf[cooling_cop .== 1] .= 1 d["ASHP_SpaceHeater"]["heating_cf"] = heating_cf d["ASHP_SpaceHeater"]["cooling_cf"] = cooling_cf From ffbcefc1ab86296d7ae4fd1301e6839a180634d5 Mon Sep 17 00:00:00 2001 From: An Pham Date: Mon, 1 Jul 2024 16:16:29 -0600 Subject: [PATCH 136/266] set temperature threshold for ASHP-SH to switch to backup --- src/core/scenario.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 0818bc27e..963435259 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -681,7 +681,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) if !haskey(d["ASHP_SpaceHeater"], "heating_cop") heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - heating_cop[ambient_temp_fahrenheit .< -7.6] .= 1 + heating_cop[ambient_temp_fahrenheit .< 10] .= 1 heating_cop[ambient_temp_fahrenheit .> 79] .= 999999 else heating_cop = round.(d["ASHP_SpaceHeater"]["heating_cop"],digits=3) From c79c4c76e6779350856a121690c33da02ee4f43a Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 2 Jul 2024 14:54:45 -0600 Subject: [PATCH 137/266] update error text for BuiltInProcessHeatLoad --- src/core/heating_cooling_loads.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index dcfb51d32..02168ab64 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -1552,7 +1552,7 @@ struct ProcessHeatLoad existing_boiler_efficiency) else throw(@error("Cannot construct BuiltInProcessHeatLoad. You must provide either [fuel_loads_mmbtu_per_hour], - [doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city].")) + [industry_reference_name, city], or [blended_industry_reference_names, blended_industry_reference_percents, city].")) end if length(loads_kw) < 8760*time_steps_per_hour From 51f0d84820216e9de7a0dcd4e6592794c5958050 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 3 Jul 2024 12:26:00 -0600 Subject: [PATCH 138/266] fixed typo --- test/scenarios/logger.json | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/scenarios/logger.json b/test/scenarios/logger.json index ba240aab6..2e540281b 100644 --- a/test/scenarios/logger.json +++ b/test/scenarios/logger.json @@ -8,7 +8,7 @@ }, "ElectricLoad": { "annual_kwh": 100000.0, - "doe_reference_name": "MidriceApartment" + "doe_reference_name": "MidriseApartment" }, "ElectricUtility" : { "co2_from_avert" : true From a31cc12827b2ceaba5589dbff88842817d1ab29b Mon Sep 17 00:00:00 2001 From: An Pham Date: Sun, 7 Jul 2024 21:12:22 -0600 Subject: [PATCH 139/266] added back_up_temp_threshold to ASHP-SH --- data/ashp/ashp_defaults.json | 6 ++++-- src/core/ashp.jl | 21 +++++++++++++++++---- src/core/scenario.jl | 8 +++++++- test/scenarios/ashp.json | 3 ++- 4 files changed, 30 insertions(+), 8 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 17bf96adc..e7de50675 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -10,7 +10,8 @@ "can_serve_dhw": false, "can_serve_space_heating": true, "can_serve_cooling": true, - "force_into_system": false + "force_into_system": false, + "back_up_temp_threshold": 10.0 }, "DomesticHotWater": { @@ -23,6 +24,7 @@ "can_serve_dhw": true, "can_serve_space_heating": false, "can_serve_cooling": false, - "force_into_system": false + "force_into_system": false, + "back_up_temp_threshold": 10.0 } } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index a4f80fcc6..6a8bd0f96 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -17,6 +17,7 @@ struct ASHP <: AbstractThermalTech can_serve_process_heat::Bool can_serve_cooling::Bool force_into_system::Bool + back_up_temp_threshold::Real end @@ -41,6 +42,7 @@ function ASHP_SpaceHeater(; cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true + back_up_temp_threshold::Real = 10 # Degree in F that system switches from ASHP to resistive heater ) ``` """ @@ -56,7 +58,8 @@ function ASHP_SpaceHeater(; heating_cf::Array{Float64,1} = Float64[], cooling_cf::Array{Float64,1} = Float64[], can_serve_cooling::Union{Bool, Nothing} = nothing, - force_into_system::Union{Bool, Nothing} = nothing + force_into_system::Union{Bool, Nothing} = nothing, + back_up_temp_threshold::Real = 10.0 ) defaults = get_ashp_defaults("SpaceHeating") @@ -74,6 +77,9 @@ function ASHP_SpaceHeater(; if isnothing(force_into_system) force_into_system = defaults["force_into_system"] end + if isnothing(back_up_temp_threshold) + back_up_temp_threshold = defaults["back_up_temp_threshold"] + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -106,7 +112,8 @@ function ASHP_SpaceHeater(; can_serve_space_heating, can_serve_process_heat, can_serve_cooling, - force_into_system + force_into_system, + back_up_temp_threshold ) end @@ -129,6 +136,7 @@ function ASHP_WaterHeater(; can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true + back_up_temp_threshold::Real = 10 ) ``` """ @@ -141,7 +149,8 @@ function ASHP_WaterHeater(; macrs_bonus_fraction::Real = 0.0, heating_cop::Array{Float64,1} = Float64[], heating_cf::Array{Float64,1} = Float64[], - force_into_system::Union{Bool, Nothing} = nothing + force_into_system::Union{Bool, Nothing} = nothing, + back_up_temp_threshold::Real = 10.0 ) defaults = get_ashp_defaults("DomesticHotWater") @@ -156,6 +165,9 @@ function ASHP_WaterHeater(; if isnothing(force_into_system) force_into_system = defaults["force_into_system"] end + if isnothing(back_up_temp_threshold) + back_up_temp_threshold = defaults["back_up_temp_threshold"] + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -188,7 +200,8 @@ function ASHP_WaterHeater(; can_serve_space_heating, can_serve_process_heat, can_serve_cooling, - force_into_system + force_into_system, + back_up_temp_threshold ) end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 963435259..288dff937 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -661,6 +661,12 @@ function Scenario(d::Dict; flex_hvac_from_json=false) heating_cf = [] cooling_cf = [] if haskey(d, "ASHP_SpaceHeater") && d["ASHP_SpaceHeater"]["max_ton"] > 0.0 + # ASHP Space Heater's temp back_up_temp_threshold + if !haskey(d["ASHP_SpaceHeater"], "back_up_temp_threshold") + ambient_temp_thres_fahrenheit = get_ashp_defaults("SpaceHeating")["back_up_temp_threshold"] + else + ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold"] + end # Add ASHP's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature if !haskey(d["ASHP_SpaceHeater"], "heating_cop") || !haskey(d["ASHP_SpaceHeater"], "cooling_cop") @@ -681,7 +687,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) if !haskey(d["ASHP_SpaceHeater"], "heating_cop") heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - heating_cop[ambient_temp_fahrenheit .< 10] .= 1 + heating_cop[ambient_temp_fahrenheit .< ambient_temp_thres_fahrenheit] .= 1 heating_cop[ambient_temp_fahrenheit .> 79] .= 999999 else heating_cop = round.(d["ASHP_SpaceHeater"]["heating_cop"],digits=3) diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 22cbcedeb..3ba867da7 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -16,7 +16,8 @@ "om_cost_per_ton": 0.0, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, - "can_serve_cooling": false + "can_serve_cooling": false, + "back_up_temp_threshold": 10.0 }, "Financial": { "om_cost_escalation_rate_fraction": 0.025, From 7297d80518c17dcab07ecea9b06682d9ae116406 Mon Sep 17 00:00:00 2001 From: An Pham Date: Sun, 14 Jul 2024 17:37:13 -0600 Subject: [PATCH 140/266] set default o&m to configs 1 & 3 and to 0 when force_into_system (configs 2&4) --- data/ashp/ashp_defaults.json | 4 ++-- src/core/ashp.jl | 12 ++++++++++-- 2 files changed, 12 insertions(+), 4 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index e7de50675..53d868a06 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -2,7 +2,7 @@ "SpaceHeating": { "installed_cost_per_ton": 2250, - "om_cost_per_ton": 0, + "om_cost_per_ton": 40, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, @@ -16,7 +16,7 @@ "DomesticHotWater": { "installed_cost_per_ton": 2250, - "om_cost_per_ton": 0, + "om_cost_per_ton": 40, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_supply_steam_turbine": false, diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 6a8bd0f96..ae9095469 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -69,7 +69,11 @@ function ASHP_SpaceHeater(; installed_cost_per_ton = defaults["installed_cost_per_ton"] end if isnothing(om_cost_per_ton) - om_cost_per_ton = defaults["om_cost_per_ton"] + if force_into_system == true + om_cost_per_ton = 0 + else + om_cost_per_ton = defaults["om_cost_per_ton"] + end end if isnothing(can_serve_cooling) can_serve_cooling = defaults["can_serve_cooling"] @@ -160,7 +164,11 @@ function ASHP_WaterHeater(; installed_cost_per_ton = defaults["installed_cost_per_ton"] end if isnothing(om_cost_per_ton) - om_cost_per_ton = defaults["om_cost_per_ton"] + if force_into_system == true + om_cost_per_ton = 0 + else + om_cost_per_ton = defaults["om_cost_per_ton"] + end end if isnothing(force_into_system) force_into_system = defaults["force_into_system"] From b76fea8a097fba8443a315de6c9fcaa4143c968d Mon Sep 17 00:00:00 2001 From: An Pham Date: Sun, 14 Jul 2024 17:47:33 -0600 Subject: [PATCH 141/266] updated changelog to resolve conflict --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c307765d..fa22a9979 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## Develop 2024-07-14 +### Added +- Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly +- In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct + ## v0.47.2 ### Fixed - Increased the big-M bound on maximum net metering benefit to prevent artificially low export benefits. From ce7b6826c8009a22999d30e9e203d91a176631c4 Mon Sep 17 00:00:00 2001 From: An Pham Date: Tue, 23 Jul 2024 16:14:59 -0600 Subject: [PATCH 142/266] added _degF to back_up_temp_threshold --- data/ashp/ashp_defaults.json | 4 ++-- src/core/ashp.jl | 22 +++++++++++----------- src/core/scenario.jl | 8 ++++---- test/scenarios/ashp.json | 2 +- 4 files changed, 18 insertions(+), 18 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 53d868a06..d6c53ff83 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -11,7 +11,7 @@ "can_serve_space_heating": true, "can_serve_cooling": true, "force_into_system": false, - "back_up_temp_threshold": 10.0 + "back_up_temp_threshold_degF": 10.0 }, "DomesticHotWater": { @@ -25,6 +25,6 @@ "can_serve_space_heating": false, "can_serve_cooling": false, "force_into_system": false, - "back_up_temp_threshold": 10.0 + "back_up_temp_threshold_degF": 10.0 } } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index ae9095469..40c8033c0 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -17,7 +17,7 @@ struct ASHP <: AbstractThermalTech can_serve_process_heat::Bool can_serve_cooling::Bool force_into_system::Bool - back_up_temp_threshold::Real + back_up_temp_threshold_degF::Real end @@ -42,7 +42,7 @@ function ASHP_SpaceHeater(; cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true - back_up_temp_threshold::Real = 10 # Degree in F that system switches from ASHP to resistive heater + back_up_temp_threshold_degF::Real = 10 # Degree in F that system switches from ASHP to resistive heater ) ``` """ @@ -59,7 +59,7 @@ function ASHP_SpaceHeater(; cooling_cf::Array{Float64,1} = Float64[], can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, - back_up_temp_threshold::Real = 10.0 + back_up_temp_threshold_degF::Real = 10.0 ) defaults = get_ashp_defaults("SpaceHeating") @@ -81,8 +81,8 @@ function ASHP_SpaceHeater(; if isnothing(force_into_system) force_into_system = defaults["force_into_system"] end - if isnothing(back_up_temp_threshold) - back_up_temp_threshold = defaults["back_up_temp_threshold"] + if isnothing(back_up_temp_threshold_degF) + back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] end #pre-set defaults that aren't mutable due to technology specifications @@ -117,7 +117,7 @@ function ASHP_SpaceHeater(; can_serve_process_heat, can_serve_cooling, force_into_system, - back_up_temp_threshold + back_up_temp_threshold_degF ) end @@ -140,7 +140,7 @@ function ASHP_WaterHeater(; can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true - back_up_temp_threshold::Real = 10 + back_up_temp_threshold_degF::Real = 10 ) ``` """ @@ -154,7 +154,7 @@ function ASHP_WaterHeater(; heating_cop::Array{Float64,1} = Float64[], heating_cf::Array{Float64,1} = Float64[], force_into_system::Union{Bool, Nothing} = nothing, - back_up_temp_threshold::Real = 10.0 + back_up_temp_threshold_degF::Real = 10.0 ) defaults = get_ashp_defaults("DomesticHotWater") @@ -173,8 +173,8 @@ function ASHP_WaterHeater(; if isnothing(force_into_system) force_into_system = defaults["force_into_system"] end - if isnothing(back_up_temp_threshold) - back_up_temp_threshold = defaults["back_up_temp_threshold"] + if isnothing(back_up_temp_threshold_degF) + back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] end #pre-set defaults that aren't mutable due to technology specifications @@ -209,7 +209,7 @@ function ASHP_WaterHeater(; can_serve_process_heat, can_serve_cooling, force_into_system, - back_up_temp_threshold + back_up_temp_threshold_degF ) end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 288dff937..0c9f3c875 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -661,11 +661,11 @@ function Scenario(d::Dict; flex_hvac_from_json=false) heating_cf = [] cooling_cf = [] if haskey(d, "ASHP_SpaceHeater") && d["ASHP_SpaceHeater"]["max_ton"] > 0.0 - # ASHP Space Heater's temp back_up_temp_threshold - if !haskey(d["ASHP_SpaceHeater"], "back_up_temp_threshold") - ambient_temp_thres_fahrenheit = get_ashp_defaults("SpaceHeating")["back_up_temp_threshold"] + # ASHP Space Heater's temp back_up_temp_threshold_degF + if !haskey(d["ASHP_SpaceHeater"], "back_up_temp_threshold_degF") + ambient_temp_thres_fahrenheit = get_ashp_defaults("SpaceHeating")["back_up_temp_threshold_degF"] else - ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold"] + ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold_degF"] end # Add ASHP's COPs # If user does not provide heating cop series then assign cop curves based on ambient temperature diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index 3ba867da7..c4371b244 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -17,7 +17,7 @@ "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "can_serve_cooling": false, - "back_up_temp_threshold": 10.0 + "back_up_temp_threshold_degF": 10.0 }, "Financial": { "om_cost_escalation_rate_fraction": 0.025, From 06b938fc58797d3994a2efa86243694323d8394d Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 24 Jul 2024 12:36:56 -0600 Subject: [PATCH 143/266] Update macOS to 13, from 11 which is deprecated by Actions --- .github/workflows/CI.yml | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 4331ae6ab..8ff63e606 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,8 +13,7 @@ jobs: matrix: julia-version: ['1.8'] julia-arch: [x64] - # os: [ubuntu-latest, windows-latest, macOS-11] - os: [windows-latest, macOS-11] + os: [windows-latest, macOS-13] steps: - uses: actions/checkout@v2 From 4c63d767465840f1f3cf93e0be9750858d564923 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 24 Jul 2024 12:42:41 -0600 Subject: [PATCH 144/266] Add back Ubuntu OS for GitHub Actions tests (API uses this) --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8ff63e606..985a73088 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: matrix: julia-version: ['1.8'] julia-arch: [x64] - os: [windows-latest, macOS-13] + os: [ubuntu-latest, windows-latest, macOS-13] steps: - uses: actions/checkout@v2 From decb0ea46f136ef794da90f4ab385b29dc15d6bc Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 24 Jul 2024 16:04:08 -0600 Subject: [PATCH 145/266] Skip this test due to JuMP warning flood: Custom URDB with Sub-Hourly --- test/runtests.jl | 17 +++++++++-------- 1 file changed, 9 insertions(+), 8 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index dd7b5905b..029cd4f57 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1207,14 +1207,15 @@ else # run HiGHS tests @test results["PV"]["size_kw"] ≈ p.max_sizes["PV"] end - @testset "Custom URDB with Sub-Hourly" begin - # Testing a 15-min post with a urdb_response with multiple n_energy_tiers - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) - p = REoptInputs("./scenarios/subhourly_with_urdb.json") - results = run_reopt(model, p) - @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 - @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw - end + # Temporarily skip this test which is flooding our test logs with warnings + # @testset "Custom URDB with Sub-Hourly" begin + # # Testing a 15-min post with a urdb_response with multiple n_energy_tiers + # model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) + # p = REoptInputs("./scenarios/subhourly_with_urdb.json") + # results = run_reopt(model, p) + # @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 + # @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw + # end @testset "Multi-tier demand and energy rates" begin #This test ensures that when multiple energy or demand regimes are included, that the tier limits load appropriately From a1ae11c2055f2e0031fceb409fe9856cff49913b Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 24 Jul 2024 16:53:00 -0600 Subject: [PATCH 146/266] Use local logger to suppress excessive JuMP warning for += expressions --- test/runtests.jl | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 029cd4f57..9671fd466 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -8,6 +8,7 @@ using DotEnv DotEnv.load!() using Random using DelimitedFiles +using Logging Random.seed!(42) if "Xpress" in ARGS @@ -1207,15 +1208,18 @@ else # run HiGHS tests @test results["PV"]["size_kw"] ≈ p.max_sizes["PV"] end - # Temporarily skip this test which is flooding our test logs with warnings - # @testset "Custom URDB with Sub-Hourly" begin - # # Testing a 15-min post with a urdb_response with multiple n_energy_tiers - # model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) - # p = REoptInputs("./scenarios/subhourly_with_urdb.json") - # results = run_reopt(model, p) - # @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 - # @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw - # end + @testset "Custom URDB with Sub-Hourly" begin + # Avoid excessive JuMP warning messages about += with Expressions + logger = SimpleLogger() + with_logger(logger) do + # Testing a 15-min post with a urdb_response with multiple n_energy_tiers + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) + p = REoptInputs("./scenarios/subhourly_with_urdb.json") + results = run_reopt(model, p) + @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 + @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw + end + end @testset "Multi-tier demand and energy rates" begin #This test ensures that when multiple energy or demand regimes are included, that the tier limits load appropriately From 7e9bc861f7d992100ed0b74a83d9cf5c9c18e2ef Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 24 Jul 2024 16:59:17 -0600 Subject: [PATCH 147/266] Suppress JuMP warnings for multiple PVs test --- test/runtests.jl | 39 +++++++++++++++++++++------------------ 1 file changed, 21 insertions(+), 18 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9671fd466..e3c176641 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1312,24 +1312,27 @@ else # run HiGHS tests end @testset "Multiple PVs" begin - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json") - - ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])] - roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])] - roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])] - - @test ground_pv["size_kw"] ≈ 15 atol=0.1 - @test roof_west["size_kw"] ≈ 7 atol=0.1 - @test roof_east["size_kw"] ≈ 4 atol=0.1 - @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 - @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 - @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 - @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 - @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 + logger = SimpleLogger() + with_logger(logger) do + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json") + + ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])] + roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])] + roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])] + + @test ground_pv["size_kw"] ≈ 15 atol=0.1 + @test roof_west["size_kw"] ≈ 7 atol=0.1 + @test roof_east["size_kw"] ≈ 4 atol=0.1 + @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 + @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 + @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 + @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 + @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 + @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 + @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 + end end @testset "Thermal Energy Storage + Absorption Chiller" begin From 44afc8a33e1c26eb0e1a866e09a4556097b36ba3 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 24 Jul 2024 19:12:40 -0600 Subject: [PATCH 148/266] Remove Ubuntu OS from Git Actions runner list Due to consistent cancelling of the job, likely due to memory limitations --- .github/workflows/CI.yml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 985a73088..8ff63e606 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: matrix: julia-version: ['1.8'] julia-arch: [x64] - os: [ubuntu-latest, windows-latest, macOS-13] + os: [windows-latest, macOS-13] steps: - uses: actions/checkout@v2 From 1d3ced197fda666033d8a8f0b66d074d22d2cfaf Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Thu, 25 Jul 2024 21:43:53 -0600 Subject: [PATCH 149/266] Remove MacOS from GitHub Actions runner list for CI testing MacOS-13 is still frequently freezing and getting cancelled after 6+ hours. --- .github/workflows/CI.yml | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.github/workflows/CI.yml b/.github/workflows/CI.yml index 8ff63e606..3b2123337 100644 --- a/.github/workflows/CI.yml +++ b/.github/workflows/CI.yml @@ -13,7 +13,7 @@ jobs: matrix: julia-version: ['1.8'] julia-arch: [x64] - os: [windows-latest, macOS-13] + os: [windows-latest] steps: - uses: actions/checkout@v2 @@ -23,4 +23,4 @@ jobs: - uses: julia-actions/julia-buildpkg@latest # - uses: mxschmitt/action-tmate@v3 # for interactive debugging - run: julia --project=. -e 'using Pkg; Pkg.activate("test"); Pkg.rm("Xpress"); Pkg.activate("."); using TestEnv; TestEnv.activate(); cd("test"); include("runtests.jl")' - shell: bash \ No newline at end of file + shell: bash From 697e3b062f1deb044d3469b3e72c2f9dcf29b8e0 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Sun, 28 Jul 2024 20:58:19 -0600 Subject: [PATCH 150/266] Add header User-Agent=REopt.jl to PVWatts API request --- src/core/utils.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/utils.jl b/src/core/utils.jl index 029050626..1357531f6 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -461,7 +461,7 @@ function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimut try @info "Querying PVWatts for production factor and ambient air temperature... " - r = HTTP.get(url, keepalive=true, readtimeout=10) + r = HTTP.get(url, ["User-Agent" => "REopt.jl"]; keepalive=true, readtimeout=10) response = JSON.parse(String(r.body)) if r.status != 200 throw(@error("Bad response from PVWatts: $(response["errors"])")) From 1cefd8043186886e9171f9a80bf766008b51b25c Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Sun, 28 Jul 2024 20:58:45 -0600 Subject: [PATCH 151/266] Add header User-Agent=REopt.jl to Wind Toolkit API request --- src/core/production_factor.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/production_factor.jl b/src/core/production_factor.jl index 51b8c18e8..ab7463016 100644 --- a/src/core/production_factor.jl +++ b/src/core/production_factor.jl @@ -72,7 +72,7 @@ function get_production_factor(wind::Wind, latitude::Real, longitude::Real, time resource = [] try @info "Querying Wind Toolkit for resource data ..." - r = HTTP.get(url; retries=5) + r = HTTP.get(url, ["User-Agent" => "REopt.jl"]; retries=5) if r.status != 200 throw(@error("Bad response from Wind Toolkit: $(response["errors"])")) end From ef03f6fd250fa0ab2830cfa08c7b1da2d1e020e1 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 30 Jul 2024 10:24:17 -0600 Subject: [PATCH 152/266] add COP, CF calculation using temperature tables --- src/core/ashp.jl | 166 +++++++++++++++++++++++++++++++++++++++---- src/core/scenario.jl | 17 +++-- 2 files changed, 164 insertions(+), 19 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 40c8033c0..85e25fcf7 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -1,5 +1,31 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. +""" +ASHP_SpaceHeater + +If a user provides the `ASHP_SpaceHeater` key then the optimal scenario has the option to purchase +this new `ASHP` to meet the heating load in addition to using the `ExistingBoiler` +to meet the heating load. + +ASHP_SpaceHeater has the following attributes: +```julia +function ASHP_SpaceHeater(; + min_ton::Real = 0.0, # Minimum thermal power size + max_ton::Real = BIG_NUMBER, # Maximum thermal power size + installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost + om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost + macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS + heating_cop::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) + heating_cf::Array{Float64,1}, # ASHP's heating capacity factor curves + cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves + can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load + force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true + back_up_temp_threshold::Real = 10 # Degree in F that system switches from ASHP to resistive heater +) +``` +""" struct ASHP <: AbstractThermalTech min_kw::Real max_kw::Real @@ -36,13 +62,22 @@ function ASHP_SpaceHeater(; om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS - heating_cop::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) - heating_cf::Array{Float64,1}, # ASHP's heating capacity factor curves - cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true - back_up_temp_threshold_degF::Real = 10 # Degree in F that system switches from ASHP to resistive heater + + #The following inputs are used to create the attributes heating_cop and heating cf: + heating_cop_reference::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + heating_cf_reference::Array{Float64,1}, # ASHP's heating capacity factor curves + heating_reference_temps ::Array{Float64,1}, # ASHP's reference temperatures for heating COP and CF + back_up_temp_threshold_degF::Real = 10, # Degree in F that system switches from ASHP to resistive heater + + #The following inputs are used to create the attributes heating_cop and heating cf: + cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) + cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves + heating_reference_temps ::Array{Float64,1}, # ASHP's reference temperatures for cooling COP and CF + + #The following input is taken from the Site object: + ambient_temp_degF::Array{Float64,1} #time series of ambient temperature ) ``` """ @@ -53,13 +88,16 @@ function ASHP_SpaceHeater(; om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, - heating_cop::Array{Float64,1} = Float64[], - cooling_cop::Array{Float64,1} = Float64[], - heating_cf::Array{Float64,1} = Float64[], - cooling_cf::Array{Float64,1} = Float64[], can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, - back_up_temp_threshold_degF::Real = 10.0 + heating_cop_reference::Array{Float64,1} = Float64[], + heating_cf_reference::Array{Float64,1} = Float64[], + heating_reference_temps::Array{Float64,1} = Float64[], + back_up_temp_threshold_degF::Real = 10.0, + cooling_cop_reference::Array{Float64,1} = Float64[], + cooling_cf_reference::Array{Float64,1} = Float64[], + cooling_reference_temps::Array{Float64,1} = Float64[], + ambient_temp_degF::Array{Float64,1} = Float64[] ) defaults = get_ashp_defaults("SpaceHeating") @@ -99,7 +137,33 @@ function ASHP_SpaceHeater(; installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR - + if !isempty(heating_reference_temps) + heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, + heating_cf_reference, + heating_reference_temps, + ambient_temp_degF, + back_up_temp_threshold + ) + else + heating_cop, heating_cf = get_default_ashp_heating(ambient_temp_degF,ambient_temp_degF) + end + + if can_serve_cooling + if !isempty(cooling_reference_temps) + cooling_cop, cooling_cf = get_ashp_performance(cooling_cop_reference, + cooling_cf_reference, + cooling_reference_temps, + ambient_temp_degF, + -460 + ) + else + cooling_cop, cooling_cf = get_default_ashp_cooling(ambient_temp_degF) + end + else + cooling_cop = Float64[] + cooling_cf = Float64[] + end + ASHP( min_kw, max_kw, @@ -151,9 +215,10 @@ function ASHP_WaterHeater(; om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, - heating_cop::Array{Float64,1} = Float64[], - heating_cf::Array{Float64,1} = Float64[], force_into_system::Union{Bool, Nothing} = nothing, + heating_cop_reference::Array{Float64,1} = Float64[], + heating_cf_reference::Array{Float64,1} = Float64[], + heating_reference_temps::Array{Float64,1} = Float64[], back_up_temp_threshold_degF::Real = 10.0 ) @@ -191,7 +256,17 @@ function ASHP_WaterHeater(; installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR - + if !isempty(heating_reference_temps) + heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, + heating_cf_reference, + heating_reference_temps, + ambient_temp_degF, + back_up_temp_threshold + ) + else + heating_cop, heating_cf = get_default_ashp_water_heating(ambient_temp_degF,ambient_temp_degF) + end + ASHP( min_kw, max_kw, @@ -232,4 +307,67 @@ function get_ashp_defaults(load_served::String="SpaceHeating") end all_ashp_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_defaults.json")) return all_ashp_defaults[load_served] +end + +""" +function get_ashp_performance(cop_reference, + cf_reference, + reference_temps, + ambient_temp_degF, + back_up_temp_threshold = 10.0 + ) +""" +function get_ashp_performance(cop_reference, + cf_reference, + reference_temps, + ambient_temp_degF, + back_up_temp_threshold = 10.0 + ) + num_timesteps = length(ambient_temp_degF) + cop = zeros(num_timesteps) + cf = zeros(num_timesteps) + for ts in 1:num_timesteps + if ambient_temp_degF[ts] < reference_temps[1] && ambient_temp_degF[ts] < last(reference_temps) + cop[ts] = cop_reference[argmin(reference_temps)] + cf[ts] = cf_reference[argmin(reference_temps)] + elseif ambient_temp_degF[ts] > reference_temps[1] && ambient_temp_degF[ts] > last(reference_temps) + cop[ts] = cop_reference[argmax(reference_temps)] + cf[ts] = cf_reference[argmax(reference_temps)] + else + for i in 2:length(reference_temps) + if + if ambient_temp_degF[ts] >= min(reference_temps[i-1], reference_temps[i]) && + ambient_temp_degF[ts] <= max(reference_temps[i-1], reference_temps[i]) + cop[ts] = cop_reference[i-1] + (cop_reference[i]-cop_reference[i-1])*(ambient_temp_degF[ts]-reference_temps[i-1])/(reference_temps[i]-reference_temps[i-1]) + cf[ts] = cf_reference[i-1] + (cf_reference[i]-cf_reference[i-1])*(ambient_temp_degF[ts]-reference_temps[i-1])/(reference_temps[i]-reference_temps[i-1]) + break + end + end + end + end + if ambient_temp_degF[ts] < back_up_temp_threshold + cop[ts] = 1.0 + cf[ts] = 1.0 + end + end + return cop, cf +end + +""" +function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold) +""" +function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold) + heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) + heating_cop[ambient_temp_fahrenheit .< ambient_temp_thres_fahrenheit] .= 1 + heating_cf = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) + return heating_cop, heating_cf +end + +""" +function get_default_ashp_cooling(ambient_temp_degF) +""" +function get_default_ashp_cooling(ambient_temp_degF) + cooling_cop = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) + cooling_cf = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) + return cooling_cop, cooling_cf end \ No newline at end of file diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 0c9f3c875..9110f8c2a 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -685,12 +685,19 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - if !haskey(d["ASHP_SpaceHeater"], "heating_cop") - heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - heating_cop[ambient_temp_fahrenheit .< ambient_temp_thres_fahrenheit] .= 1 - heating_cop[ambient_temp_fahrenheit .> 79] .= 999999 + if !haskey(d["ASHP_SpaceHeater"], "heating_cop_reference") + heating_cop, heating_cf = get_default_ashp_heating( + ambient_temp_fahrenheit, + ambient_temp_thres_fahrenheit + ) else - heating_cop = round.(d["ASHP_SpaceHeater"]["heating_cop"],digits=3) + heating_cop, heating_cf = get_ashp_performance( + d["ASHP_SpaceHeater"]["heating_cop_reference"], + d["ASHP_SpaceHeater"]["heating_cf_reference"], + d["ASHP_SpaceHeater"]["heating_reference_temps"], + ambient_temp_fahrenheit, + ambient_temp_thres_fahrenheit + ) end if !haskey(d["ASHP_SpaceHeater"], "cooling_cop") From 1ff84ac761cc2a678a95314c0e41de9787cd69f3 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 2 Aug 2024 17:28:34 -0600 Subject: [PATCH 153/266] migrate COP, CF calcs from scenario to ASHP --- src/core/ashp.jl | 47 +++++++-------- src/core/scenario.jl | 137 ++++++++++--------------------------------- 2 files changed, 55 insertions(+), 129 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 85e25fcf7..19c5e2b0b 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -9,7 +9,6 @@ to meet the heating load. ASHP_SpaceHeater has the following attributes: ```julia -function ASHP_SpaceHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost @@ -22,8 +21,7 @@ function ASHP_SpaceHeater(; cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true - back_up_temp_threshold::Real = 10 # Degree in F that system switches from ASHP to resistive heater -) + back_up_temp_threshold_degF::Real = 10 # Degree in F that system switches from ASHP to resistive heater ``` """ struct ASHP <: AbstractThermalTech @@ -142,12 +140,14 @@ function ASHP_SpaceHeater(; heating_cf_reference, heating_reference_temps, ambient_temp_degF, - back_up_temp_threshold + back_up_temp_threshold_degF ) else heating_cop, heating_cf = get_default_ashp_heating(ambient_temp_degF,ambient_temp_degF) end + heating_cf[heating_cop .== 1] .= 1 + if can_serve_cooling if !isempty(cooling_reference_temps) cooling_cop, cooling_cf = get_ashp_performance(cooling_cop_reference, @@ -219,7 +219,8 @@ function ASHP_WaterHeater(; heating_cop_reference::Array{Float64,1} = Float64[], heating_cf_reference::Array{Float64,1} = Float64[], heating_reference_temps::Array{Float64,1} = Float64[], - back_up_temp_threshold_degF::Real = 10.0 + back_up_temp_threshold_degF::Real = 10.0, + ambient_temp_degF::Array{Float64,1} = Float64[] ) defaults = get_ashp_defaults("DomesticHotWater") @@ -261,11 +262,13 @@ function ASHP_WaterHeater(; heating_cf_reference, heating_reference_temps, ambient_temp_degF, - back_up_temp_threshold + back_up_temp_threshold_degF ) else - heating_cop, heating_cf = get_default_ashp_water_heating(ambient_temp_degF,ambient_temp_degF) + heating_cop, heating_cf = get_default_ashp_heating(ambient_temp_degF,back_up_temp_threshold_degF) end + + heating_cf[heating_cop .== 1] .= 1 ASHP( min_kw, @@ -321,7 +324,7 @@ function get_ashp_performance(cop_reference, cf_reference, reference_temps, ambient_temp_degF, - back_up_temp_threshold = 10.0 + back_up_temp_threshold_degF = 10.0 ) num_timesteps = length(ambient_temp_degF) cop = zeros(num_timesteps) @@ -335,17 +338,15 @@ function get_ashp_performance(cop_reference, cf[ts] = cf_reference[argmax(reference_temps)] else for i in 2:length(reference_temps) - if - if ambient_temp_degF[ts] >= min(reference_temps[i-1], reference_temps[i]) && - ambient_temp_degF[ts] <= max(reference_temps[i-1], reference_temps[i]) - cop[ts] = cop_reference[i-1] + (cop_reference[i]-cop_reference[i-1])*(ambient_temp_degF[ts]-reference_temps[i-1])/(reference_temps[i]-reference_temps[i-1]) - cf[ts] = cf_reference[i-1] + (cf_reference[i]-cf_reference[i-1])*(ambient_temp_degF[ts]-reference_temps[i-1])/(reference_temps[i]-reference_temps[i-1]) - break - end + if ambient_temp_degF[ts] >= min(reference_temps[i-1], reference_temps[i]) && + ambient_temp_degF[ts] <= max(reference_temps[i-1], reference_temps[i]) + cop[ts] = cop_reference[i-1] + (cop_reference[i]-cop_reference[i-1])*(ambient_temp_degF[ts]-reference_temps[i-1])/(reference_temps[i]-reference_temps[i-1]) + cf[ts] = cf_reference[i-1] + (cf_reference[i]-cf_reference[i-1])*(ambient_temp_degF[ts]-reference_temps[i-1])/(reference_temps[i]-reference_temps[i-1]) + break end end end - if ambient_temp_degF[ts] < back_up_temp_threshold + if ambient_temp_degF[ts] < back_up_temp_threshold_degF cop[ts] = 1.0 cf[ts] = 1.0 end @@ -354,12 +355,12 @@ function get_ashp_performance(cop_reference, end """ -function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold) +function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) """ -function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold) - heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - heating_cop[ambient_temp_fahrenheit .< ambient_temp_thres_fahrenheit] .= 1 - heating_cf = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) +function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) + heating_cop = round.(0.0462 .* ambient_temp_degF .+ 1.351, digits=3) + heating_cop[ambient_temp_degF .< back_up_temp_threshold_degF] .= 1 + heating_cf = round.(0.0116 .* ambient_temp_degF .+ 0.4556, digits=3) return heating_cop, heating_cf end @@ -367,7 +368,7 @@ end function get_default_ashp_cooling(ambient_temp_degF) """ function get_default_ashp_cooling(ambient_temp_degF) - cooling_cop = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) - cooling_cf = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) + cooling_cop = round.(-0.044 .* ambient_temp_degF .+ 6.822, digits=3) + cooling_cf = round.(-0.0056 .* ambient_temp_degF .+ 1.4778, digits=3) return cooling_cop, cooling_cf end \ No newline at end of file diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 9110f8c2a..9144a0c1a 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -667,78 +667,24 @@ function Scenario(d::Dict; flex_hvac_from_json=false) else ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold_degF"] end - # Add ASHP's COPs - # If user does not provide heating cop series then assign cop curves based on ambient temperature - if !haskey(d["ASHP_SpaceHeater"], "heating_cop") || !haskey(d["ASHP_SpaceHeater"], "cooling_cop") - # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celsius) - if !isempty(pvs) - for pv in pvs - pv.production_factor_series, ambient_temp_celsius = 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 - else - # if PV is not evaluated, call PVWatts to get ambient temperature series - pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + + # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor + if isnothing(ambient_temp_celsius) + if !isempty(pvs) + for pv in pvs + pv.production_factor_series, ambient_temp_celsius = 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 - end - ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - - if !haskey(d["ASHP_SpaceHeater"], "heating_cop_reference") - heating_cop, heating_cf = get_default_ashp_heating( - ambient_temp_fahrenheit, - ambient_temp_thres_fahrenheit - ) - else - heating_cop, heating_cf = get_ashp_performance( - d["ASHP_SpaceHeater"]["heating_cop_reference"], - d["ASHP_SpaceHeater"]["heating_cf_reference"], - d["ASHP_SpaceHeater"]["heating_reference_temps"], - ambient_temp_fahrenheit, - ambient_temp_thres_fahrenheit - ) - end - - if !haskey(d["ASHP_SpaceHeater"], "cooling_cop") - cooling_cop = round.(-0.044 .* ambient_temp_fahrenheit .+ 6.822, digits=3) - cooling_cop[ambient_temp_celsius .< 25] .= 999999 - cooling_cop[ambient_temp_celsius .> 40] .= 1 else - cooling_cop = round.(d["ASHP_SpaceHeater"]["cooling_cop"], digits=3) + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - else - # Else if the user already provide cop series, use that - heating_cop = round.(d["ASHP_SpaceHeater"]["heating_cop"],digits=3) - cooling_cop = round.(d["ASHP_SpaceHeater"]["cooling_cop"],digits=3) end - d["ASHP_SpaceHeater"]["heating_cop"] = heating_cop - d["ASHP_SpaceHeater"]["cooling_cop"] = cooling_cop - - # Add ASHP's capacity factor curves - if !haskey(d["ASHP_SpaceHeater"], "heating_cf") || !haskey(d["ASHP_SpaceHeater"], "cooling_cf") - if !haskey(d["ASHP_SpaceHeater"], "heating_cf") - heating_cf = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) - else - heating_cf = round.(d["ASHP_SpaceHeater"]["heating_cf"],digits=3) - end + ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - if !haskey(d["ASHP_SpaceHeater"], "cooling_cf") - cooling_cf = round.(-0.0056 .* ambient_temp_fahrenheit .+ 1.4778, digits=3) - else - cooling_cf = round.(d["ASHP_SpaceHeater"]["cooling_cf"],digits=3) - end - - else - # Else if the user already provide cf curves, use them - heating_cf = round.(d["ASHP_SpaceHeater"]["heating_cf"],digits=3) - cooling_cf = round.(d["ASHP_SpaceHeater"]["cooling_cf"],digits=3) - end + d["ASHP_SpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - heating_cf[heating_cop .== 1] .= 1 - - d["ASHP_SpaceHeater"]["heating_cf"] = heating_cf - d["ASHP_SpaceHeater"]["cooling_cf"] = cooling_cf ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) end @@ -748,54 +694,33 @@ function Scenario(d::Dict; flex_hvac_from_json=false) heating_cf = [] if haskey(d, "ASHP_WaterHeater") && d["ASHP_WaterHeater"]["max_ton"] > 0.0 - # Add ASHP_WH's COPs - # If user does not provide heating cop series then assign cop curves based on ambient temperature - if !haskey(d["ASHP_WaterHeater"], "heating_cop") - # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celsius) - if !isempty(pvs) - for pv in pvs - pv.production_factor_series, ambient_temp_celsius = 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 - else - # if PV is not evaluated, call PVWatts to get ambient temperature series - pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) - end - end - ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - - if !haskey(d["ASHP_WaterHeater"], "heating_cop") - heating_cop = round.(0.0462 .* ambient_temp_fahrenheit .+ 1.351, digits=3) - heating_cop[ambient_temp_fahrenheit .< -7.6] .= 1 - heating_cop[ambient_temp_fahrenheit .> 79] .= 999999 - else - heating_cop = round.(d["ASHP_WaterHeater"]["heating_cop"],digits=3) - end + # ASHP Space Heater's temp back_up_temp_threshold_degF + if !haskey(d["ASHP_WaterHeater"], "back_up_temp_threshold_degF") + ambient_temp_thres_fahrenheit = get_ashp_defaults("DomesticHotWater")["back_up_temp_threshold_degF"] else - # Else if the user already provide cop series, use that - heating_cop = round.(d["ASHP_WaterHeater"]["heating_cop"],digits=3) + ambient_temp_thres_fahrenheit = d["ASHP_WaterHeater"]["back_up_temp_threshold_degF"] end - - d["ASHP_WaterHeater"]["heating_cop"] = heating_cop - - # Add ASHP_WH's capacity factor curves - if !haskey(d["ASHP_WaterHeater"], "heating_cf") - if !haskey(d["ASHP_WaterHeater"], "heating_cf") - heating_cf = round.(0.0116 .* ambient_temp_fahrenheit .+ 0.4556, digits=3) + + # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor + if isnothing(ambient_temp_celsius) + if !isempty(pvs) + for pv in pvs + pv.production_factor_series, ambient_temp_celsius = 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 else - heating_cf = round.(d["ASHP_WaterHeater"]["heating_cf"],digits=3) + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - else - # Else if the user already provide cf curves, use them - heating_cf = round.(d["ASHP_WaterHeater"]["heating_cf"],digits=3) end + + ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - heating_cf[heating_cop .== 1] .= 1 + d["ASHP_WaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - d["ASHP_WaterHeater"]["heating_cf"] = heating_cf ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) + end return Scenario( From 3b776d7789e77dd92139156ae13ba89017fb7c20 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 2 Aug 2024 17:35:23 -0600 Subject: [PATCH 154/266] add new ASHP attribute min_allowable_kw (given via min_allowable_ton) --- src/core/ashp.jl | 23 ++++++++++++++++++++--- 1 file changed, 20 insertions(+), 3 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 19c5e2b0b..2f1181ded 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -9,8 +9,9 @@ to meet the heating load. ASHP_SpaceHeater has the following attributes: ```julia - min_ton::Real = 0.0, # Minimum thermal power size - max_ton::Real = BIG_NUMBER, # Maximum thermal power size + min_kw::Real = 0.0, # Minimum thermal power size + max_kw::Real = BIG_NUMBER, # Maximum thermal power size + min_allowable_kw::Real = 0.0 # Minimum nonzero thermal power size if included installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable @@ -27,6 +28,7 @@ ASHP_SpaceHeater has the following attributes: struct ASHP <: AbstractThermalTech min_kw::Real max_kw::Real + min_allowable_kw::Real installed_cost_per_kw::Real om_cost_per_kw::Real macrs_option_years::Int @@ -56,7 +58,7 @@ to meet the heating load. function ASHP_SpaceHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size - installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost + min_allowable_ton::Real = 0.0 # Minimum nonzero thermal power size if included om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS @@ -82,6 +84,7 @@ function ASHP_SpaceHeater(; function ASHP_SpaceHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, + min_allowable_ton::Union{Real, Nothing} = nothing, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, @@ -131,6 +134,11 @@ function ASHP_SpaceHeater(; # Convert max sizes, cost factors from mmbtu_per_hour to kw min_kw = min_ton * KWH_THERMAL_PER_TONHOUR max_kw = max_ton * KWH_THERMAL_PER_TONHOUR + if !isnothing(min_allowable_ton) + min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR + else + min_allowable_kw = 0.0 + end installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR @@ -167,6 +175,7 @@ function ASHP_SpaceHeater(; ASHP( min_kw, max_kw, + min_allowable_kw, installed_cost_per_kw, om_cost_per_kw, macrs_option_years, @@ -197,6 +206,7 @@ to meet the domestic hot water load. function ASHP_WaterHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size + min_allowable_ton::Real = 0.0 # Minimum nonzero thermal power size if included installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable @@ -211,6 +221,7 @@ function ASHP_WaterHeater(; function ASHP_WaterHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, + min_allowable_ton::Union{Real, Nothing} = nothing, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, @@ -242,6 +253,11 @@ function ASHP_WaterHeater(; if isnothing(back_up_temp_threshold_degF) back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] end + if !isnothing(min_allowable_ton) + min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR + else + min_allowable_kw = 0.0 + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -273,6 +289,7 @@ function ASHP_WaterHeater(; ASHP( min_kw, max_kw, + min_allowable_kw, installed_cost_per_kw, om_cost_per_kw, macrs_option_years, From 2f52f6df0e01f3252d25e5bf6db4903c02e8fcef Mon Sep 17 00:00:00 2001 From: An Pham Date: Sun, 4 Aug 2024 18:52:18 -0600 Subject: [PATCH 155/266] set default max_ton to big number --- data/ashp/ashp_defaults.json | 6 ++- src/core/ashp.jl | 8 +++ src/core/scenario.jl | 99 +++++++++++++++++++++--------------- 3 files changed, 69 insertions(+), 44 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index d6c53ff83..1e7d74e9a 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -1,6 +1,7 @@ { "SpaceHeating": - { + { + "max_ton": 99999999, "installed_cost_per_ton": 2250, "om_cost_per_ton": 40, "macrs_option_years": 0, @@ -14,7 +15,8 @@ "back_up_temp_threshold_degF": 10.0 }, "DomesticHotWater": - { + { + "max_ton": 99999999, "installed_cost_per_ton": 2250, "om_cost_per_ton": 40, "macrs_option_years": 0, diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 2f1181ded..aa75562ea 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -123,6 +123,9 @@ function ASHP_SpaceHeater(; if isnothing(back_up_temp_threshold_degF) back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] end + if isnothing(max_ton) + max_ton = defaults["max_ton"] + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -253,6 +256,11 @@ function ASHP_WaterHeater(; if isnothing(back_up_temp_threshold_degF) back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] end + + if isnothing(max_ton) + max_ton = defaults["max_ton"] + end + if !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR else diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 9144a0c1a..a4604f00d 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -660,32 +660,40 @@ function Scenario(d::Dict; flex_hvac_from_json=false) cooling_cop = [] heating_cf = [] cooling_cf = [] - if haskey(d, "ASHP_SpaceHeater") && d["ASHP_SpaceHeater"]["max_ton"] > 0.0 - # ASHP Space Heater's temp back_up_temp_threshold_degF - if !haskey(d["ASHP_SpaceHeater"], "back_up_temp_threshold_degF") - ambient_temp_thres_fahrenheit = get_ashp_defaults("SpaceHeating")["back_up_temp_threshold_degF"] + if haskey(d, "ASHP_SpaceHeater") + if !haskey(d["ASHP_SpaceHeater"], "max_ton") + max_ton = get_ashp_defaults("SpaceHeating")["max_ton"] else - ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold_degF"] + max_ton = d["ASHP_SpaceHeater"]["max_ton"] end - - # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celsius) - if !isempty(pvs) - for pv in pvs - pv.production_factor_series, ambient_temp_celsius = 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 + + if max_ton > 0 + # ASHP Space Heater's temp back_up_temp_threshold_degF + if !haskey(d["ASHP_SpaceHeater"], "back_up_temp_threshold_degF") + ambient_temp_thres_fahrenheit = get_ashp_defaults("SpaceHeating")["back_up_temp_threshold_degF"] else - # if PV is not evaluated, call PVWatts to get ambient temperature series - pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold_degF"] end - end - ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 + + # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor + if isnothing(ambient_temp_celsius) + if !isempty(pvs) + for pv in pvs + pv.production_factor_series, ambient_temp_celsius = 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 + else + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + end + end + ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - d["ASHP_SpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHP_SpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) + ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) + end end # ASHP Water Heater: @@ -693,34 +701,41 @@ function Scenario(d::Dict; flex_hvac_from_json=false) heating_cop = [] heating_cf = [] - if haskey(d, "ASHP_WaterHeater") && d["ASHP_WaterHeater"]["max_ton"] > 0.0 - # ASHP Space Heater's temp back_up_temp_threshold_degF - if !haskey(d["ASHP_WaterHeater"], "back_up_temp_threshold_degF") - ambient_temp_thres_fahrenheit = get_ashp_defaults("DomesticHotWater")["back_up_temp_threshold_degF"] + if haskey(d, "ASHP_WaterHeater") + if !haskey(d["ASHP_WaterHeater"], "max_ton") + max_ton = get_ashp_defaults("DomesticHotWater")["max_ton"] else - ambient_temp_thres_fahrenheit = d["ASHP_WaterHeater"]["back_up_temp_threshold_degF"] + max_ton = d["ASHP_WaterHeater"]["max_ton"] end - - # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celsius) - if !isempty(pvs) - for pv in pvs - pv.production_factor_series, ambient_temp_celsius = 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 + + if max_ton > 0.0 + # ASHP Space Heater's temp back_up_temp_threshold_degF + if !haskey(d["ASHP_WaterHeater"], "back_up_temp_threshold_degF") + ambient_temp_thres_fahrenheit = get_ashp_defaults("DomesticHotWater")["back_up_temp_threshold_degF"] else - # if PV is not evaluated, call PVWatts to get ambient temperature series - pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + ambient_temp_thres_fahrenheit = d["ASHP_WaterHeater"]["back_up_temp_threshold_degF"] end - end - - ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - - d["ASHP_WaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + + # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor + if isnothing(ambient_temp_celsius) + if !isempty(pvs) + for pv in pvs + pv.production_factor_series, ambient_temp_celsius = 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 + else + # if PV is not evaluated, call PVWatts to get ambient temperature series + pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) + end + end + + ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) + d["ASHP_WaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) + end end return Scenario( From 0ba60f23306511fec68160e28b8e70d52f993b39 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 07:45:43 -0600 Subject: [PATCH 156/266] add CF override to 1.0 under backup threshold when calculating default ASHP heating --- src/core/ashp.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index aa75562ea..12001659d 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -384,8 +384,9 @@ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF """ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) heating_cop = round.(0.0462 .* ambient_temp_degF .+ 1.351, digits=3) - heating_cop[ambient_temp_degF .< back_up_temp_threshold_degF] .= 1 + heating_cop[ambient_temp_degF .<= back_up_temp_threshold_degF] .= 1 heating_cf = round.(0.0116 .* ambient_temp_degF .+ 0.4556, digits=3) + heating_cf[heating_cop .<= 1.0] .= 1.0 return heating_cop, heating_cf end From e40c540b626dc846107f1e22502f156645dbcb44 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 07:45:43 -0600 Subject: [PATCH 157/266] add CF override to 1.0 under backup threshold when calculating default ASHP heating --- src/core/ashp.jl | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index aa75562ea..09edf9c72 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -384,8 +384,9 @@ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF """ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) heating_cop = round.(0.0462 .* ambient_temp_degF .+ 1.351, digits=3) - heating_cop[ambient_temp_degF .< back_up_temp_threshold_degF] .= 1 + heating_cop[ambient_temp_degF .<= back_up_temp_threshold_degF] .= 1 heating_cf = round.(0.0116 .* ambient_temp_degF .+ 0.4556, digits=3) + heating_cf[heating_cop .== 1.0] .= 1.0 return heating_cop, heating_cf end From 1fe85ed990eb357dd2db31018e944fb98f1cadd1 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 08:14:03 -0600 Subject: [PATCH 158/266] allow back_up_temp_threshold = nothing for ASHP --- src/core/ashp.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 09edf9c72..f68705382 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -94,7 +94,7 @@ function ASHP_SpaceHeater(; heating_cop_reference::Array{Float64,1} = Float64[], heating_cf_reference::Array{Float64,1} = Float64[], heating_reference_temps::Array{Float64,1} = Float64[], - back_up_temp_threshold_degF::Real = 10.0, + back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, cooling_cop_reference::Array{Float64,1} = Float64[], cooling_cf_reference::Array{Float64,1} = Float64[], cooling_reference_temps::Array{Float64,1} = Float64[], @@ -233,7 +233,7 @@ function ASHP_WaterHeater(; heating_cop_reference::Array{Float64,1} = Float64[], heating_cf_reference::Array{Float64,1} = Float64[], heating_reference_temps::Array{Float64,1} = Float64[], - back_up_temp_threshold_degF::Real = 10.0, + back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, ambient_temp_degF::Array{Float64,1} = Float64[] ) From 4ab937a2fc94785024dbde829f2adad8d6c15f0f Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 08:25:13 -0600 Subject: [PATCH 159/266] Update ashp.jl --- src/core/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index b3a25ba80..f68705382 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -386,7 +386,7 @@ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF heating_cop = round.(0.0462 .* ambient_temp_degF .+ 1.351, digits=3) heating_cop[ambient_temp_degF .<= back_up_temp_threshold_degF] .= 1 heating_cf = round.(0.0116 .* ambient_temp_degF .+ 0.4556, digits=3) - heating_cf[heating_cop .<= 1.0] .= 1.0 + heating_cf[heating_cop .== 1.0] .= 1.0 return heating_cop, heating_cf end From bb9aa5b2be5a3ba77214219dc6502c71d3148366 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 16:33:41 -0600 Subject: [PATCH 160/266] include min_allowable_ton in ASHP tests --- test/runtests.jl | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index d04750a1b..26edb6ed7 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2435,6 +2435,7 @@ else # run HiGHS tests d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 d["ASHP_SpaceHeater"]["installed_cost_per_ton"] = 300 + d["ASHP_SpaceHeater"]["min_allowable_ton"] = 80.0 p = REoptInputs(d) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) @@ -2442,7 +2443,7 @@ else # run HiGHS tests annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 74.99 atol=0.01 + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2459,9 +2460,9 @@ else # run HiGHS tests annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 75.07 atol=0.01 #size increases when cooling load also served + @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 #size increases when cooling load also served @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 873.9 atol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 873.9 rtol=1e-4 #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) From 8bb555d0489ee50e87ffa781f3c9c3025848449c Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 22:35:00 -0600 Subject: [PATCH 161/266] set up min_allowable_kw for ASHP in REoptInputs --- src/core/ashp.jl | 2 +- src/core/reopt_inputs.jl | 30 ++++++++++++++++++++++++++---- test/runtests.jl | 2 +- 3 files changed, 28 insertions(+), 6 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index f68705382..09aa2f799 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -342,7 +342,7 @@ function get_ashp_performance(cop_reference, cf_reference, reference_temps, ambient_temp_degF, - back_up_temp_threshold = 10.0 + back_up_temp_threshold_degF = 10.0 ) """ function get_ashp_performance(cop_reference, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 93c704296..4f4f815b6 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -444,7 +444,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ASHP_SpaceHeater" in techs.all - setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) + setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, + techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) else heating_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) cooling_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) @@ -453,7 +454,8 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) end if "ASHP_WaterHeater" in techs.all - setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) + setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, + techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) else heating_cop["ASHP_WaterHeater"] = ones(length(time_steps)) heating_cf["ASHP_WaterHeater"] = zeros(length(time_steps)) @@ -934,7 +936,8 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end -function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf) +function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, + segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) max_sizes["ASHP_SpaceHeater"] = s.ashp.max_kw min_sizes["ASHP_SpaceHeater"] = s.ashp.min_kw om_cost_per_kw["ASHP_SpaceHeater"] = s.ashp.om_cost_per_kw @@ -943,6 +946,15 @@ function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, heating_cf["ASHP_SpaceHeater"] = s.ashp.heating_cf cooling_cf["ASHP_SpaceHeater"] = s.ashp.cooling_cf + if s.ashp.min_allowable_kw > 0.0 + cap_cost_slope["ASHP_SpaceHeater"] = s.ashp.installed_cost_per_kw + push!(segmented_techs, "ASHP_SpaceHeater") + seg_max_size["ASHP_SpaceHeater"] = Dict{Int,Float64}(1 => s.ashp.max_kw) + seg_min_size["ASHP_SpaceHeater"] = Dict{Int,Float64}(1 => s.ashp.min_allowable_kw) + n_segs_by_tech["ASHP_SpaceHeater"] = 1 + seg_yint["ASHP_SpaceHeater"] = Dict{Int,Float64}(1 => 0.0) + end + if s.ashp.macrs_option_years in [5, 7] cap_cost_slope["ASHP_SpaceHeater"] = effective_cost(; itc_basis = s.ashp.installed_cost_per_kw, @@ -962,13 +974,23 @@ function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, end -function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf) +function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, + segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) max_sizes["ASHP_WaterHeater"] = s.ashp_wh.max_kw min_sizes["ASHP_WaterHeater"] = s.ashp_wh.min_kw om_cost_per_kw["ASHP_WaterHeater"] = s.ashp_wh.om_cost_per_kw heating_cop["ASHP_WaterHeater"] = s.ashp_wh.heating_cop heating_cf["ASHP_WaterHeater"] = s.ashp_wh.heating_cf + if s.ashp_wh.min_allowable_kw > 0.0 + cap_cost_slope["ASHP_WaterHeater"] = s.ashp_wh.installed_cost_per_kw + push!(segmented_techs, "ASHP_WaterHeater") + seg_max_size["ASHP_WaterHeater"] = Dict{Int,Float64}(1 => s.ashp_wh.max_kw) + seg_min_size["ASHP_WaterHeater"] = Dict{Int,Float64}(1 => s.ashp_wh.min_allowable_kw) + n_segs_by_tech["ASHP_WaterHeater"] = 1 + seg_yint["ASHP_WaterHeater"] = Dict{Int,Float64}(1 => 0.0) + end + if s.ashp_wh.macrs_option_years in [5, 7] cap_cost_slope["ASHP_WaterHeater"] = effective_cost(; itc_basis = s.ashp_wh.installed_cost_per_kw, diff --git a/test/runtests.jl b/test/runtests.jl index 26edb6ed7..9d94ff236 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2462,7 +2462,7 @@ else # run HiGHS tests annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 #size increases when cooling load also served @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 873.9 rtol=1e-4 + @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) From b107a909ebf54c9409d5f65abc26b6d7b2f82c37 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 22:35:16 -0600 Subject: [PATCH 162/266] add testset for heating and cooling COP, CF profiles --- test/runtests.jl | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/test/runtests.jl b/test/runtests.jl index 9d94ff236..c54fa737c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,6 +63,39 @@ else # run HiGHS tests dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) @test dataset ≈ "tmy3" end + @testset "ASHP COP and CF Profiles" begin + #Heating profiles + heating_reference_temps = [10,20,30] + heating_cop_reference = [1,3,4] + heating_cf_performance = [1.2,1.3,1.5] + back_up_temp_threshold_degF = 10 + test_temps = [5,15,25,35] + test_cops = [1.0,2.0,3.5,4.0] + test_cfs = [1.0,1.25,1.4,1.5] + cop, cf = REopt.get_ashp_performance(heating_cop_reference, + heating_cf_performance, + heating_reference_temps, + ambient_temp_degF, + back_up_temp_threshold_degF) + @test all(cop .== test_cops) + @test all(cf .== test_cfs) + #Cooling profiles + cooling_reference_temps = [30,20,10] + cooling_cop_reference = [1,3,4] + cooling_cf_performance = [1.2,1.3,1.5] + back_up_temp_threshold_degF = 10 + test_temps = [35,25,15,5] + test_cops = [1.0,2.0,3.5,4.0] + test_cfs = [1.2,1.25,1.4,1.5] + cop, cf = REopt.get_ashp_performance(heating_cop_reference, + heating_cf_performance, + heating_reference_temps, + ambient_temp_degF, + back_up_temp_threshold_degF) + @test all(cop .== test_cops) + @test all(cf .== test_cfs) + + end end @testset "January Export Rates" begin From bc0e3490249dd44ca0dd7b81c1862c7a6e2585c6 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 5 Aug 2024 22:46:02 -0600 Subject: [PATCH 163/266] Update runtests.jl --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index c54fa737c..d04524d0d 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -75,7 +75,7 @@ else # run HiGHS tests cop, cf = REopt.get_ashp_performance(heating_cop_reference, heating_cf_performance, heating_reference_temps, - ambient_temp_degF, + test_temps, back_up_temp_threshold_degF) @test all(cop .== test_cops) @test all(cf .== test_cfs) @@ -90,7 +90,7 @@ else # run HiGHS tests cop, cf = REopt.get_ashp_performance(heating_cop_reference, heating_cf_performance, heating_reference_temps, - ambient_temp_degF, + test_temps, back_up_temp_threshold_degF) @test all(cop .== test_cops) @test all(cf .== test_cfs) From e9c86fc2072674ebfec09ca859b47e2752874d53 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 6 Aug 2024 06:41:45 -0600 Subject: [PATCH 164/266] fix ASHP performance tests --- test/runtests.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index d04524d0d..a5485a5c6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -83,18 +83,17 @@ else # run HiGHS tests cooling_reference_temps = [30,20,10] cooling_cop_reference = [1,3,4] cooling_cf_performance = [1.2,1.3,1.5] - back_up_temp_threshold_degF = 10 + back_up_temp_threshold_degF = -200 test_temps = [35,25,15,5] test_cops = [1.0,2.0,3.5,4.0] test_cfs = [1.2,1.25,1.4,1.5] - cop, cf = REopt.get_ashp_performance(heating_cop_reference, - heating_cf_performance, - heating_reference_temps, + cop, cf = REopt.get_ashp_performance(cooling_cop_reference, + cooling_cf_performance, + cooling_reference_temps, test_temps, back_up_temp_threshold_degF) @test all(cop .== test_cops) @test all(cf .== test_cfs) - end end From 8db09cdaddef5631fc84d3fee50a3b86dbb6cb73 Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:16:14 -0600 Subject: [PATCH 165/266] Update CHANGELOG.md --- CHANGELOG.md | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4c307765d..f1f7627c6 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,11 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## Develop 08-09-2024 +### Changed +- Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. . +- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings + ## v0.47.2 ### Fixed - Increased the big-M bound on maximum net metering benefit to prevent artificially low export benefits. From 1e1362e157b84ce0a76c48f293dabe5363b6b84c Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Fri, 9 Aug 2024 14:21:55 -0600 Subject: [PATCH 166/266] Update CHANGELOG.md --- CHANGELOG.md | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index f1f7627c6..d61265cb3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -26,7 +26,8 @@ Classify the change according to the following categories: ## Develop 08-09-2024 ### Changed - Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. . -- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings +- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings +- Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov. ## v0.47.2 ### Fixed From 1307ec69a10c7b01c0dd7375f88e2031d4e01e6b Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Fri, 9 Aug 2024 22:11:51 -0600 Subject: [PATCH 167/266] Wrap all tests in an @testset to avoid "failfast" behavior unintentionally --- test/runtests.jl | 4803 +++++++++++++++++++++++----------------------- 1 file changed, 2402 insertions(+), 2401 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e3c176641..5094cfc07 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,2605 +23,2606 @@ elseif "CPLEX" in ARGS end else # run HiGHS tests - - @testset "Inputs" begin - @testset "hybrid profile" begin - electric_load = REopt.ElectricLoad(; - blended_doe_reference_percents = [0.2, 0.2, 0.2, 0.2, 0.2], - blended_doe_reference_names = ["RetailStore", "LargeOffice", "MediumOffice", "SmallOffice", "Warehouse"], - annual_kwh = 50000.0, - year = 2017, - city = "Atlanta", - latitude = 35.2468, - longitude = -91.7337 - ) - @test sum(electric_load.loads_kw) ≈ 50000.0 - end - @testset "Solar dataset" begin - - # 1. Dallas TX - latitude, longitude = 32.775212075983646, -96.78105623767185 - radius = 0 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "nsrdb" - - # 2. Merefa, Ukraine - latitude, longitude = 49.80670544975866, 36.05418033509974 - radius = 0 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "nsrdb" - - # 3. Younde, Cameroon - latitude, longitude = 3.8603988398663125, 11.528880303663136 - radius = 0 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "intl" - - # 4. Fairbanks, AK - site = "Fairbanks" - latitude, longitude = 64.84112047064114, -147.71570239058084 - radius = 20 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "tmy3" + @testset verbose=true "REopt test set using HiGHS solver" + @testset "Inputs" begin + @testset "hybrid profile" begin + electric_load = REopt.ElectricLoad(; + blended_doe_reference_percents = [0.2, 0.2, 0.2, 0.2, 0.2], + blended_doe_reference_names = ["RetailStore", "LargeOffice", "MediumOffice", "SmallOffice", "Warehouse"], + annual_kwh = 50000.0, + year = 2017, + city = "Atlanta", + latitude = 35.2468, + longitude = -91.7337 + ) + @test sum(electric_load.loads_kw) ≈ 50000.0 + end + @testset "Solar dataset" begin + + # 1. Dallas TX + latitude, longitude = 32.775212075983646, -96.78105623767185 + radius = 0 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "nsrdb" + + # 2. Merefa, Ukraine + latitude, longitude = 49.80670544975866, 36.05418033509974 + radius = 0 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "nsrdb" + + # 3. Younde, Cameroon + latitude, longitude = 3.8603988398663125, 11.528880303663136 + radius = 0 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "intl" + + # 4. Fairbanks, AK + site = "Fairbanks" + latitude, longitude = 64.84112047064114, -147.71570239058084 + radius = 20 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "tmy3" + end end - end - - @testset "January Export Rates" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - data = JSON.parsefile("./scenarios/monthly_rate.json") - # create wholesale_rate with compensation in January > retail rate - jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] - data["ElectricTariff"]["wholesale_rate"] = - append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) - data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) + @testset "January Export Rates" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + data = JSON.parsefile("./scenarios/monthly_rate.json") - s = Scenario(data) - inputs = REoptInputs(s) - results = run_reopt(model, inputs) + # create wholesale_rate with compensation in January > retail rate + jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] + data["ElectricTariff"]["wholesale_rate"] = + append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) + data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) - @test results["PV"]["size_kw"] ≈ 68.9323 atol=0.01 - @test results["Financial"]["lcc"] ≈ 432681.26 rtol=1e-5 # with levelization_factor hack the LCC is within 5e-5 of REopt API LCC - @test all(x == 0.0 for x in results["PV"]["electric_to_load_series_kw"][1:744]) - end + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(model, inputs) - @testset "Blended tariff" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/no_techs.json") - @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 1000.0 - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 136.99 - end + @test results["PV"]["size_kw"] ≈ 68.9323 atol=0.01 + @test results["Financial"]["lcc"] ≈ 432681.26 rtol=1e-5 # with levelization_factor hack the LCC is within 5e-5 of REopt API LCC + @test all(x == 0.0 for x in results["PV"]["electric_to_load_series_kw"][1:744]) + end - @testset "Solar and Storage" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(model, "./scenarios/pv_storage.json") + @testset "Blended tariff" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/no_techs.json") + @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 1000.0 + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 136.99 + end - @test r["PV"]["size_kw"] ≈ 216.6667 atol=0.01 - @test r["Financial"]["lcc"] ≈ 1.2391786e7 rtol=1e-5 - @test r["ElectricStorage"]["size_kw"] ≈ 49.0 atol=0.1 - @test r["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 - end + @testset "Solar and Storage" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(model, "./scenarios/pv_storage.json") - @testset "Outage with Generator" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/generator.json") - @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 - @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + - sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 - p = REoptInputs("./scenarios/generator.json") - simresults = simulate_outages(results, p) - @test simresults["resilience_hours_max"] == 11 - end + @test r["PV"]["size_kw"] ≈ 216.6667 atol=0.01 + @test r["Financial"]["lcc"] ≈ 1.2391786e7 rtol=1e-5 + @test r["ElectricStorage"]["size_kw"] ≈ 49.0 atol=0.1 + @test r["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 + end - # TODO test MPC with outages - @testset "MPC" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_mpc(model, "./scenarios/mpc.json") - @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0 - @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0 - @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0 - end + @testset "Outage with Generator" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/generator.json") + @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 + @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + + sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 + p = REoptInputs("./scenarios/generator.json") + simresults = simulate_outages(results, p) + @test simresults["resilience_hours_max"] == 11 + end - @testset "MPC Multi-node" begin - # not doing much yet; just testing that two identical sites have the same costs - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - ps = MPCInputs[] - push!(ps, MPCInputs("./scenarios/mpc_multinode1.json")); - push!(ps, MPCInputs("./scenarios/mpc_multinode2.json")); - r = run_mpc(model, ps) - @test r[1]["Costs"] ≈ r[2]["Costs"] - end + # TODO test MPC with outages + @testset "MPC" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_mpc(model, "./scenarios/mpc.json") + @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0 + @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0 + @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0 + end - @testset "Complex Incentives" begin - """ - This test was compared against the API test: - reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives - when using the hardcoded levelization_factor in this package's REoptInputs function. - The two LCC's matched within 0.00005%. (The Julia pkg LCC is 1.0971991e7) - """ - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/incentives.json") - @test results["Financial"]["lcc"] ≈ 1.096852612e7 atol=1e4 - end + @testset "MPC Multi-node" begin + # not doing much yet; just testing that two identical sites have the same costs + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + ps = MPCInputs[] + push!(ps, MPCInputs("./scenarios/mpc_multinode1.json")); + push!(ps, MPCInputs("./scenarios/mpc_multinode2.json")); + r = run_mpc(model, ps) + @test r[1]["Costs"] ≈ r[2]["Costs"] + end - @testset "Fifteen minute load" begin - d = JSON.parsefile("scenarios/no_techs.json") - d["ElectricLoad"] = Dict("loads_kw" => repeat([1.0], 35040)) - d["Settings"] = Dict("time_steps_per_hour" => 4) - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, d) - @test results["ElectricLoad"]["annual_calculated_kwh"] ≈ 8760 - end + @testset "Complex Incentives" begin + """ + This test was compared against the API test: + reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives + when using the hardcoded levelization_factor in this package's REoptInputs function. + The two LCC's matched within 0.00005%. (The Julia pkg LCC is 1.0971991e7) + """ + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/incentives.json") + @test results["Financial"]["lcc"] ≈ 1.096852612e7 atol=1e4 + end - try - rm("Highs.log", force=true) - catch - @warn "Could not delete test/Highs.log" - end + @testset "Fifteen minute load" begin + d = JSON.parsefile("scenarios/no_techs.json") + d["ElectricLoad"] = Dict("loads_kw" => repeat([1.0], 35040)) + d["Settings"] = Dict("time_steps_per_hour" => 4) + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, d) + @test results["ElectricLoad"]["annual_calculated_kwh"] ≈ 8760 + end - @testset "AVERT region abberviations" begin - """ - This test checks 5 scenarios (in order) - 1. Coordinate pair inside an AVERT polygon - 2. Coordinate pair near a US border - 3. Coordinate pair < 5 miles from US border - 4. Coordinate pair > 5 miles from US border - 5. Coordinate pair >> 5 miles from US border - """ - (r, d) = REopt.avert_region_abbreviation(65.27661752129738, -149.59278391820223) - @test r == "AKGD" - (r, d) = REopt.avert_region_abbreviation(21.45440792261567, -157.93648793163402) - @test r == "HIOA" - (r, d) = REopt.avert_region_abbreviation(19.686877556659436, -155.4223641905743) - @test r == "HIMS" - (r, d) = REopt.avert_region_abbreviation(39.86357200140234, -104.67953917092028) - @test r == "RM" - @test d ≈ 0.0 atol=1 - (r, d) = REopt.avert_region_abbreviation(47.49137892652077, -69.3240287592685) - @test r == "NE" - @test d ≈ 7986 atol=1 - (r, d) = REopt.avert_region_abbreviation(47.50448307102053, -69.34882434376593) - @test r === nothing - @test d ≈ 10297 atol=1 - (r, d) = REopt.avert_region_abbreviation(55.860334445251354, -4.286554357755312) - @test r === nothing - end + try + rm("Highs.log", force=true) + catch + @warn "Could not delete test/Highs.log" + end - @testset "PVspecs" begin - ## Scenario 1: Palmdale, CA; array-type = 0 (Ground-mount) - post_name = "pv.json" - post = JSON.parsefile("./scenarios/$post_name") - scen = Scenario(post) - @test scen.pvs[1].tilt ≈ 20 - @test scen.pvs[1].azimuth ≈ 180 - - ## Scenario 2: Palmdale, CA; array-type = 1 (roof) - post["PV"]["array_type"] = 1 - scen = Scenario(post) - - @test scen.pvs[1].tilt ≈ 20 # Correct tilt value for array_type = 1 - - ## Scenario 3: Palmdale, CA; array-type = 2 (axis-tracking) - post["PV"]["array_type"] = 2 - scen = Scenario(post) - - @test scen.pvs[1].tilt ≈ 0 # Correct tilt value for array_type = 2 - - ## Scenario 4: Cape Town; array-type = 0 (ground) - post["Site"]["latitude"] = -33.974732 - post["Site"]["longitude"] = 19.130050 - post["PV"]["array_type"] = 0 - scen = Scenario(post) - - @test scen.pvs[1].tilt ≈ 20 - @test scen.pvs[1].azimuth ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 - - ## Scenario 4:Cape Town; array-type = 0 (ground); user-provided tilt (should not get overwritten) - post["PV"]["tilt"] = 17 - scen = Scenario(post) - @test scen.pvs[1].tilt ≈ 17 - - - end - - - @testset "AlternativeFlatLoads" begin - input_data = JSON.parsefile("./scenarios/flatloads.json") - s = Scenario(input_data) - inputs = REoptInputs(s) - - # FlatLoad_8_5 => 8 hrs/day, 5 days/week, 52 weeks/year - active_hours_8_5 = 8 * 5 * 52 - @test count(x->x>0, s.space_heating_load.loads_kw, dims=1)[1] == active_hours_8_5 - # FlatLoad_16_7 => only hours 6-22 should be >0, and each day is the same portion of the total year - @test sum(s.electric_load.loads_kw[1:5]) + sum(s.electric_load.loads_kw[23:24]) == 0.0 - @test sum(s.electric_load.loads_kw[6:22]) / sum(s.electric_load.loads_kw) - 1/365 ≈ 0.0 atol=0.000001 - end - - # removed Wind test for two reasons - # 1. reduce WindToolKit calls in tests - # 2. HiGHS does not support SOS or indicator constraints, which are needed for export constraints - - @testset "Simulated load function consistency with REoptInputs.s (Scenario)" begin - """ - - This tests the consistency between getting DOE commercial reference building (CRB) load data - from the simulated_load function and the processing of REoptInputs.s (Scenario struct). - - The simulated_load function is used for the /simulated_load endpoint in the REopt API, - in particular for the webtool/UI to display loads before running REopt, but is also generally - an external way to access CRB load data without running REopt. - - One particular test specifically for the webtool/UI is for the heating load because there is just a - single heating load instead of separated space heating and domestic hot water loads. - - """ - input_data = JSON.parsefile("./scenarios/simulated_load.json") + @testset "AVERT region abberviations" begin + """ + This test checks 5 scenarios (in order) + 1. Coordinate pair inside an AVERT polygon + 2. Coordinate pair near a US border + 3. Coordinate pair < 5 miles from US border + 4. Coordinate pair > 5 miles from US border + 5. Coordinate pair >> 5 miles from US border + """ + (r, d) = REopt.avert_region_abbreviation(65.27661752129738, -149.59278391820223) + @test r == "AKGD" + (r, d) = REopt.avert_region_abbreviation(21.45440792261567, -157.93648793163402) + @test r == "HIOA" + (r, d) = REopt.avert_region_abbreviation(19.686877556659436, -155.4223641905743) + @test r == "HIMS" + (r, d) = REopt.avert_region_abbreviation(39.86357200140234, -104.67953917092028) + @test r == "RM" + @test d ≈ 0.0 atol=1 + (r, d) = REopt.avert_region_abbreviation(47.49137892652077, -69.3240287592685) + @test r == "NE" + @test d ≈ 7986 atol=1 + (r, d) = REopt.avert_region_abbreviation(47.50448307102053, -69.34882434376593) + @test r === nothing + @test d ≈ 10297 atol=1 + (r, d) = REopt.avert_region_abbreviation(55.860334445251354, -4.286554357755312) + @test r === nothing + end - input_data["ElectricLoad"] = Dict([("blended_doe_reference_names", ["Hospital", "FlatLoad_16_5"]), - ("blended_doe_reference_percents", [0.2, 0.8]) - ]) - - input_data["CoolingLoad"] = Dict([("blended_doe_reference_names", ["Warehouse", "FlatLoad"]), - ("blended_doe_reference_percents", [0.5, 0.5]) - ]) - - # Heating load from the UI will call the /simulated_load endpoint first to parse single heating mmbtu into separate Space and DHW mmbtu - annual_mmbtu_hvac = 7000.0 - annual_mmbtu_process = 3000.0 - doe_reference_name_heating = ["Warehouse", "FlatLoad"] - percent_share_heating = [0.3, 0.7] + @testset "PVspecs" begin + ## Scenario 1: Palmdale, CA; array-type = 0 (Ground-mount) + post_name = "pv.json" + post = JSON.parsefile("./scenarios/$post_name") + scen = Scenario(post) + @test scen.pvs[1].tilt ≈ 20 + @test scen.pvs[1].azimuth ≈ 180 - d_sim_load_heating = Dict([("latitude", input_data["Site"]["latitude"]), - ("longitude", input_data["Site"]["longitude"]), - ("load_type", "heating"), # since annual_tonhour is not given - ("doe_reference_name", doe_reference_name_heating), - ("percent_share", percent_share_heating), - ("annual_mmbtu", annual_mmbtu_hvac) - ]) + ## Scenario 2: Palmdale, CA; array-type = 1 (roof) + post["PV"]["array_type"] = 1 + scen = Scenario(post) - sim_load_response_heating = simulated_load(d_sim_load_heating) + @test scen.pvs[1].tilt ≈ 20 # Correct tilt value for array_type = 1 - d_sim_load_process = copy(d_sim_load_heating) - d_sim_load_process["load_type"] = "process_heat" - d_sim_load_process["annual_mmbtu"] = annual_mmbtu_process - sim_load_response_process = simulated_load(d_sim_load_process) + ## Scenario 3: Palmdale, CA; array-type = 2 (axis-tracking) + post["PV"]["array_type"] = 2 + scen = Scenario(post) - input_data["SpaceHeatingLoad"] = Dict([("blended_doe_reference_names", doe_reference_name_heating), - ("blended_doe_reference_percents", percent_share_heating), - ("annual_mmbtu", sim_load_response_heating["space_annual_mmbtu"]) - ]) + @test scen.pvs[1].tilt ≈ 0 # Correct tilt value for array_type = 2 - input_data["DomesticHotWaterLoad"] = Dict([("blended_doe_reference_names", doe_reference_name_heating), - ("blended_doe_reference_percents", percent_share_heating), - ("annual_mmbtu", sim_load_response_heating["dhw_annual_mmbtu"]) - ]) + ## Scenario 4: Cape Town; array-type = 0 (ground) + post["Site"]["latitude"] = -33.974732 + post["Site"]["longitude"] = 19.130050 + post["PV"]["array_type"] = 0 + scen = Scenario(post) - input_data["ProcessHeatLoad"] = Dict([("blended_industry_reference_names", doe_reference_name_heating), - ("blended_industry_reference_percents", percent_share_heating), - ("annual_mmbtu", annual_mmbtu_process) - ]) - - s = Scenario(input_data) - inputs = REoptInputs(s) + @test scen.pvs[1].tilt ≈ 20 + @test scen.pvs[1].azimuth ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 + + ## Scenario 4:Cape Town; array-type = 0 (ground); user-provided tilt (should not get overwritten) + post["PV"]["tilt"] = 17 + scen = Scenario(post) + @test scen.pvs[1].tilt ≈ 17 - # Call simulated_load function to check cooling - d_sim_load_elec_and_cooling = Dict([("latitude", input_data["Site"]["latitude"]), - ("longitude", input_data["Site"]["longitude"]), - ("load_type", "electric"), # since annual_tonhour is not given - ("doe_reference_name", input_data["ElectricLoad"]["blended_doe_reference_names"]), - ("percent_share", input_data["ElectricLoad"]["blended_doe_reference_percents"]), - ("cooling_doe_ref_name", input_data["CoolingLoad"]["blended_doe_reference_names"]), - ("cooling_pct_share", input_data["CoolingLoad"]["blended_doe_reference_percents"]), - ]) - sim_load_response_elec_and_cooling = simulated_load(d_sim_load_elec_and_cooling) - sim_electric_kw = sim_load_response_elec_and_cooling["loads_kw"] - sim_cooling_ton = sim_load_response_elec_and_cooling["cooling_defaults"]["loads_ton"] + end - total_heating_thermal_load_reopt_inputs = (s.space_heating_load.loads_kw + s.dhw_load.loads_kw + s.process_heat_load.loads_kw) ./ REopt.KWH_PER_MMBTU ./ REopt.EXISTING_BOILER_EFFICIENCY + + @testset "AlternativeFlatLoads" begin + input_data = JSON.parsefile("./scenarios/flatloads.json") + s = Scenario(input_data) + inputs = REoptInputs(s) + + # FlatLoad_8_5 => 8 hrs/day, 5 days/week, 52 weeks/year + active_hours_8_5 = 8 * 5 * 52 + @test count(x->x>0, s.space_heating_load.loads_kw, dims=1)[1] == active_hours_8_5 + # FlatLoad_16_7 => only hours 6-22 should be >0, and each day is the same portion of the total year + @test sum(s.electric_load.loads_kw[1:5]) + sum(s.electric_load.loads_kw[23:24]) == 0.0 + @test sum(s.electric_load.loads_kw[6:22]) / sum(s.electric_load.loads_kw) - 1/365 ≈ 0.0 atol=0.000001 + end - @test round.(sim_load_response_heating["loads_mmbtu_per_hour"] + - sim_load_response_process["loads_mmbtu_per_hour"], digits=2) ≈ - round.(total_heating_thermal_load_reopt_inputs, digits=2) rtol=0.02 + # removed Wind test for two reasons + # 1. reduce WindToolKit calls in tests + # 2. HiGHS does not support SOS or indicator constraints, which are needed for export constraints - @test sim_electric_kw ≈ s.electric_load.loads_kw atol=0.1 - @test sim_cooling_ton ≈ s.cooling_load.loads_kw_thermal ./ REopt.KWH_THERMAL_PER_TONHOUR atol=0.1 - end + @testset "Simulated load function consistency with REoptInputs.s (Scenario)" begin + """ - @testset "Backup Generator Reliability" begin - - @testset "Compare backup_reliability and simulate_outages" begin - # Tests ensure `backup_reliability()` consistent with `simulate_outages()` - # First, just battery - reopt_inputs = Dict( - "Site" => Dict( - "longitude" => -106.42077256104001, - "latitude" => 31.810468380036337 - ), - "ElectricStorage" => Dict( - "min_kw" => 4000, - "max_kw" => 4000, - "min_kwh" => 400000, - "max_kwh" => 400000, - "soc_min_fraction" => 0.8, - "soc_init_fraction" => 0.9 - ), - "ElectricLoad" => Dict( - "doe_reference_name" => "FlatLoad", - "annual_kwh" => 175200000.0, - "critical_load_fraction" => 0.2 - ), - "ElectricTariff" => Dict( - "urdb_label" => "5ed6c1a15457a3367add15ae" - ), - ) - p = REoptInputs(reopt_inputs) - model = Model(optimizer_with_attributes(HiGHS.Optimizer,"output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, p) - simresults = simulate_outages(results, p) + This tests the consistency between getting DOE commercial reference building (CRB) load data + from the simulated_load function and the processing of REoptInputs.s (Scenario struct). + + The simulated_load function is used for the /simulated_load endpoint in the REopt API, + in particular for the webtool/UI to display loads before running REopt, but is also generally + an external way to access CRB load data without running REopt. - reliability_inputs = Dict( - "generator_size_kw" => 0, - "max_outage_duration" => 100, - "generator_operational_availability" => 1.0, - "generator_failure_to_start" => 0.0, - "generator_mean_time_to_failure" => 10000000000, - "fuel_limit" => 0, - "battery_size_kw" => 4000, - "battery_size_kwh" => 400000, - "battery_charge_efficiency" => 1, - "battery_discharge_efficiency" => 1, - "battery_operational_availability" => 1.0, - "battery_minimum_soc_fraction" => 0.0, - "battery_starting_soc_series_fraction" => results["ElectricStorage"]["soc_series_fraction"], - "critical_loads_kw" => results["ElectricLoad"]["critical_load_series_kw"]#4000*ones(8760)#p.s.electric_load.critical_loads_kw - ) - reliability_results = backup_reliability(reliability_inputs) + One particular test specifically for the webtool/UI is for the heating load because there is just a + single heating load instead of separated space heating and domestic hot water loads. + + """ + input_data = JSON.parsefile("./scenarios/simulated_load.json") + + input_data["ElectricLoad"] = Dict([("blended_doe_reference_names", ["Hospital", "FlatLoad_16_5"]), + ("blended_doe_reference_percents", [0.2, 0.8]) + ]) + + input_data["CoolingLoad"] = Dict([("blended_doe_reference_names", ["Warehouse", "FlatLoad"]), + ("blended_doe_reference_percents", [0.5, 0.5]) + ]) + + # Heating load from the UI will call the /simulated_load endpoint first to parse single heating mmbtu into separate Space and DHW mmbtu + annual_mmbtu_hvac = 7000.0 + annual_mmbtu_process = 3000.0 + doe_reference_name_heating = ["Warehouse", "FlatLoad"] + percent_share_heating = [0.3, 0.7] + + d_sim_load_heating = Dict([("latitude", input_data["Site"]["latitude"]), + ("longitude", input_data["Site"]["longitude"]), + ("load_type", "heating"), # since annual_tonhour is not given + ("doe_reference_name", doe_reference_name_heating), + ("percent_share", percent_share_heating), + ("annual_mmbtu", annual_mmbtu_hvac) + ]) + + sim_load_response_heating = simulated_load(d_sim_load_heating) + + d_sim_load_process = copy(d_sim_load_heating) + d_sim_load_process["load_type"] = "process_heat" + d_sim_load_process["annual_mmbtu"] = annual_mmbtu_process + sim_load_response_process = simulated_load(d_sim_load_process) + + input_data["SpaceHeatingLoad"] = Dict([("blended_doe_reference_names", doe_reference_name_heating), + ("blended_doe_reference_percents", percent_share_heating), + ("annual_mmbtu", sim_load_response_heating["space_annual_mmbtu"]) + ]) + + input_data["DomesticHotWaterLoad"] = Dict([("blended_doe_reference_names", doe_reference_name_heating), + ("blended_doe_reference_percents", percent_share_heating), + ("annual_mmbtu", sim_load_response_heating["dhw_annual_mmbtu"]) + ]) + + input_data["ProcessHeatLoad"] = Dict([("blended_industry_reference_names", doe_reference_name_heating), + ("blended_industry_reference_percents", percent_share_heating), + ("annual_mmbtu", annual_mmbtu_process) + ]) + + s = Scenario(input_data) + inputs = REoptInputs(s) + + # Call simulated_load function to check cooling + d_sim_load_elec_and_cooling = Dict([("latitude", input_data["Site"]["latitude"]), + ("longitude", input_data["Site"]["longitude"]), + ("load_type", "electric"), # since annual_tonhour is not given + ("doe_reference_name", input_data["ElectricLoad"]["blended_doe_reference_names"]), + ("percent_share", input_data["ElectricLoad"]["blended_doe_reference_percents"]), + ("cooling_doe_ref_name", input_data["CoolingLoad"]["blended_doe_reference_names"]), + ("cooling_pct_share", input_data["CoolingLoad"]["blended_doe_reference_percents"]), + ]) + + sim_load_response_elec_and_cooling = simulated_load(d_sim_load_elec_and_cooling) + sim_electric_kw = sim_load_response_elec_and_cooling["loads_kw"] + sim_cooling_ton = sim_load_response_elec_and_cooling["cooling_defaults"]["loads_ton"] + + total_heating_thermal_load_reopt_inputs = (s.space_heating_load.loads_kw + s.dhw_load.loads_kw + s.process_heat_load.loads_kw) ./ REopt.KWH_PER_MMBTU ./ REopt.EXISTING_BOILER_EFFICIENCY + + @test round.(sim_load_response_heating["loads_mmbtu_per_hour"] + + sim_load_response_process["loads_mmbtu_per_hour"], digits=2) ≈ + round.(total_heating_thermal_load_reopt_inputs, digits=2) rtol=0.02 + + @test sim_electric_kw ≈ s.electric_load.loads_kw atol=0.1 + @test sim_cooling_ton ≈ s.cooling_load.loads_kw_thermal ./ REopt.KWH_THERMAL_PER_TONHOUR atol=0.1 + end + + @testset "Backup Generator Reliability" begin + + @testset "Compare backup_reliability and simulate_outages" begin + # Tests ensure `backup_reliability()` consistent with `simulate_outages()` + # First, just battery + reopt_inputs = Dict( + "Site" => Dict( + "longitude" => -106.42077256104001, + "latitude" => 31.810468380036337 + ), + "ElectricStorage" => Dict( + "min_kw" => 4000, + "max_kw" => 4000, + "min_kwh" => 400000, + "max_kwh" => 400000, + "soc_min_fraction" => 0.8, + "soc_init_fraction" => 0.9 + ), + "ElectricLoad" => Dict( + "doe_reference_name" => "FlatLoad", + "annual_kwh" => 175200000.0, + "critical_load_fraction" => 0.2 + ), + "ElectricTariff" => Dict( + "urdb_label" => "5ed6c1a15457a3367add15ae" + ), + ) + p = REoptInputs(reopt_inputs) + model = Model(optimizer_with_attributes(HiGHS.Optimizer,"output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, p) + simresults = simulate_outages(results, p) + + reliability_inputs = Dict( + "generator_size_kw" => 0, + "max_outage_duration" => 100, + "generator_operational_availability" => 1.0, + "generator_failure_to_start" => 0.0, + "generator_mean_time_to_failure" => 10000000000, + "fuel_limit" => 0, + "battery_size_kw" => 4000, + "battery_size_kwh" => 400000, + "battery_charge_efficiency" => 1, + "battery_discharge_efficiency" => 1, + "battery_operational_availability" => 1.0, + "battery_minimum_soc_fraction" => 0.0, + "battery_starting_soc_series_fraction" => results["ElectricStorage"]["soc_series_fraction"], + "critical_loads_kw" => results["ElectricLoad"]["critical_load_series_kw"]#4000*ones(8760)#p.s.electric_load.critical_loads_kw + ) + reliability_results = backup_reliability(reliability_inputs) + + #TODO: resolve bug where unlimted fuel markov portion of results goes to zero 1 timestep early + for i = 1:99#min(length(simresults["probs_of_surviving"]), reliability_inputs["max_outage_duration"]) + @test simresults["probs_of_surviving"][i] ≈ reliability_results["mean_cumulative_survival_by_duration"][i] atol=0.01 + @test simresults["probs_of_surviving"][i] ≈ reliability_results["unlimited_fuel_mean_cumulative_survival_by_duration"][i] atol=0.01 + @test simresults["probs_of_surviving"][i] ≈ reliability_results["mean_fuel_survival_by_duration"][i] atol=0.01 + end - #TODO: resolve bug where unlimted fuel markov portion of results goes to zero 1 timestep early - for i = 1:99#min(length(simresults["probs_of_surviving"]), reliability_inputs["max_outage_duration"]) - @test simresults["probs_of_surviving"][i] ≈ reliability_results["mean_cumulative_survival_by_duration"][i] atol=0.01 - @test simresults["probs_of_surviving"][i] ≈ reliability_results["unlimited_fuel_mean_cumulative_survival_by_duration"][i] atol=0.01 - @test simresults["probs_of_surviving"][i] ≈ reliability_results["mean_fuel_survival_by_duration"][i] atol=0.01 + # Second, gen, PV, Wind, battery + reopt_inputs = JSON.parsefile("./scenarios/backup_reliability_reopt_inputs.json") + reopt_inputs["ElectricLoad"]["annual_kwh"] = 4*reopt_inputs["ElectricLoad"]["annual_kwh"] + p = REoptInputs(reopt_inputs) + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, p) + simresults = simulate_outages(results, p) + reliability_inputs = Dict( + "max_outage_duration" => 48, + "generator_operational_availability" => 1.0, + "generator_failure_to_start" => 0.0, + "generator_mean_time_to_failure" => 10000000000, + "fuel_limit" => 1000000000, + "battery_operational_availability" => 1.0, + "battery_minimum_soc_fraction" => 0.0, + "pv_operational_availability" => 1.0, + "wind_operational_availability" => 1.0 + ) + reliability_results = backup_reliability(results, p, reliability_inputs) + for i = 1:min(length(simresults["probs_of_surviving"]), reliability_inputs["max_outage_duration"]) + @test simresults["probs_of_surviving"][i] ≈ reliability_results["mean_cumulative_survival_by_duration"][i] atol=0.001 + end end - # Second, gen, PV, Wind, battery - reopt_inputs = JSON.parsefile("./scenarios/backup_reliability_reopt_inputs.json") - reopt_inputs["ElectricLoad"]["annual_kwh"] = 4*reopt_inputs["ElectricLoad"]["annual_kwh"] - p = REoptInputs(reopt_inputs) - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, p) - simresults = simulate_outages(results, p) + # Test survival with no generator decreasing and same as with generator but no fuel reliability_inputs = Dict( - "max_outage_duration" => 48, - "generator_operational_availability" => 1.0, - "generator_failure_to_start" => 0.0, - "generator_mean_time_to_failure" => 10000000000, - "fuel_limit" => 1000000000, - "battery_operational_availability" => 1.0, - "battery_minimum_soc_fraction" => 0.0, - "pv_operational_availability" => 1.0, - "wind_operational_availability" => 1.0 + "critical_loads_kw" => 200 .* (2 .+ sin.(collect(1:8760)*2*pi/24)), + "num_generators" => 0, + "generator_size_kw" => 312.0, + "fuel_limit" => 0.0, + "max_outage_duration" => 10, + "battery_size_kw" => 428.0, + "battery_size_kwh" => 1585.0, + "num_battery_bins" => 5 ) - reliability_results = backup_reliability(results, p, reliability_inputs) - for i = 1:min(length(simresults["probs_of_surviving"]), reliability_inputs["max_outage_duration"]) - @test simresults["probs_of_surviving"][i] ≈ reliability_results["mean_cumulative_survival_by_duration"][i] atol=0.001 + reliability_results1 = backup_reliability(reliability_inputs) + reliability_inputs["generator_size_kw"] = 0 + reliability_inputs["fuel_limit"] = 1e10 + reliability_results2 = backup_reliability(reliability_inputs) + for i in 1:reliability_inputs["max_outage_duration"] + if i != 1 + @test reliability_results1["mean_fuel_survival_by_duration"][i] <= reliability_results1["mean_fuel_survival_by_duration"][i-1] + @test reliability_results1["mean_cumulative_survival_by_duration"][i] <= reliability_results1["mean_cumulative_survival_by_duration"][i-1] + end + @test reliability_results2["mean_fuel_survival_by_duration"][i] == reliability_results1["mean_fuel_survival_by_duration"][i] end - end - # Test survival with no generator decreasing and same as with generator but no fuel - reliability_inputs = Dict( - "critical_loads_kw" => 200 .* (2 .+ sin.(collect(1:8760)*2*pi/24)), - "num_generators" => 0, - "generator_size_kw" => 312.0, - "fuel_limit" => 0.0, - "max_outage_duration" => 10, - "battery_size_kw" => 428.0, - "battery_size_kwh" => 1585.0, - "num_battery_bins" => 5 - ) - reliability_results1 = backup_reliability(reliability_inputs) - reliability_inputs["generator_size_kw"] = 0 - reliability_inputs["fuel_limit"] = 1e10 - reliability_results2 = backup_reliability(reliability_inputs) - for i in 1:reliability_inputs["max_outage_duration"] - if i != 1 - @test reliability_results1["mean_fuel_survival_by_duration"][i] <= reliability_results1["mean_fuel_survival_by_duration"][i-1] - @test reliability_results1["mean_cumulative_survival_by_duration"][i] <= reliability_results1["mean_cumulative_survival_by_duration"][i-1] - end - @test reliability_results2["mean_fuel_survival_by_duration"][i] == reliability_results1["mean_fuel_survival_by_duration"][i] - end + #test fuel limit + input_dict = JSON.parsefile("./scenarios/erp_fuel_limit_inputs.json") + results = backup_reliability(input_dict) + @test results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 1 + @test results["cumulative_survival_final_time_step"][1] ≈ 1 + + input_dict = Dict( + "critical_loads_kw" => [1,2,2,1], + "battery_starting_soc_series_fraction" => [0.75,0.75,0.75,0.75], + "max_outage_duration" => 3, + "num_generators" => 2, "generator_size_kw" => 1, + "generator_operational_availability" => 1, + "generator_failure_to_start" => 0.0, + "generator_mean_time_to_failure" => 5, + "battery_operational_availability" => 1, + "num_battery_bins" => 3, + "battery_size_kwh" => 4, + "battery_size_kw" => 1, + "battery_charge_efficiency" => 1, + "battery_discharge_efficiency" => 1, + "battery_minimum_soc_fraction" => 0.5) + + + #Given outage starts in time period 1 + #____________________________________ + #Outage hour 1: + #2 generators: Prob = 0.64, Battery = 2, Survived + #1 generator: Prob = 0.32, Battery = 1, Survived + #0 generator: Prob = 0.04, Battery = 0, Survived + #Survival Probability 1.0 + + #Outage hour 2: + #2 generators: Prob = 0.4096, Battery = 2, Survived + #2 gen -> 1 gen: Prob = 0.2048, Battery = 1, Survived + #1 gen -> 1 gen: Prob = 0.256, Battery = 0, Survived + #0 generators: Prob = 0.1296, Battery = -1, Failed + #Survival Probability: 0.8704 + + #Outage hour 3: + #2 generators: Prob = 0.262144, Battery = 0, Survived + #2 gen -> 2 -> 1 Prob = 0.131072, Battery = 1, Survived + #2 gen -> 1 -> 1 Prob = 0.16384, Battery = 0, Survived + #1 gen -> 1 -> 1 Prob = 0.2048, Battery = -1, Failed + #0 generators Prob = 0.238144, Battery = -1, Failed + #Survival Probability: 0.557056 + @test backup_reliability(input_dict)["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.557056 + + #Test multiple generator types + input_dict = Dict( + "critical_loads_kw" => [1,2,2,1], + "battery_starting_soc_series_fraction" => [0.5,0.5,0.5,0.5], + "max_outage_duration" => 3, + "num_generators" => [1,1], + "generator_size_kw" => [1,1], + "generator_operational_availability" => [1,1], + "generator_failure_to_start" => [0.0, 0.0], + "generator_mean_time_to_failure" => [5, 5], + "battery_operational_availability" => 1.0, + "num_battery_bins" => 3, + "battery_size_kwh" => 2, + "battery_size_kw" => 1, + "battery_charge_efficiency" => 1, + "battery_discharge_efficiency" => 1, + "battery_minimum_soc_fraction" => 0) + + @test backup_reliability(input_dict)["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.557056 + + #8760 of flat load. Battery can survive 4 hours. + #Survival after 24 hours should be chance of generator surviving 20 or more hours + input_dict = Dict( + "critical_loads_kw" => 100 .* ones(8760), + "max_outage_duration" => 24, + "num_generators" => 1, + "generator_size_kw" => 100, + "generator_operational_availability" => 0.98, + "generator_failure_to_start" => 0.1, + "generator_mean_time_to_failure" => 100, + "battery_operational_availability" => 1.0, + "num_battery_bins" => 101, + "battery_size_kwh" => 400, + "battery_size_kw" => 100, + "battery_charge_efficiency" => 1, + "battery_discharge_efficiency" => 1, + "battery_minimum_soc_fraction" => 0) - #test fuel limit - input_dict = JSON.parsefile("./scenarios/erp_fuel_limit_inputs.json") - results = backup_reliability(input_dict) - @test results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 1 - @test results["cumulative_survival_final_time_step"][1] ≈ 1 - - input_dict = Dict( - "critical_loads_kw" => [1,2,2,1], - "battery_starting_soc_series_fraction" => [0.75,0.75,0.75,0.75], - "max_outage_duration" => 3, - "num_generators" => 2, "generator_size_kw" => 1, - "generator_operational_availability" => 1, - "generator_failure_to_start" => 0.0, - "generator_mean_time_to_failure" => 5, - "battery_operational_availability" => 1, - "num_battery_bins" => 3, - "battery_size_kwh" => 4, - "battery_size_kw" => 1, - "battery_charge_efficiency" => 1, - "battery_discharge_efficiency" => 1, - "battery_minimum_soc_fraction" => 0.5) - + reliability_results = backup_reliability(input_dict) + @test reliability_results["unlimited_fuel_mean_cumulative_survival_by_duration"][24] ≈ (0.99^20)*(0.9*0.98) atol=0.00001 - #Given outage starts in time period 1 - #____________________________________ - #Outage hour 1: - #2 generators: Prob = 0.64, Battery = 2, Survived - #1 generator: Prob = 0.32, Battery = 1, Survived - #0 generator: Prob = 0.04, Battery = 0, Survived - #Survival Probability 1.0 - - #Outage hour 2: - #2 generators: Prob = 0.4096, Battery = 2, Survived - #2 gen -> 1 gen: Prob = 0.2048, Battery = 1, Survived - #1 gen -> 1 gen: Prob = 0.256, Battery = 0, Survived - #0 generators: Prob = 0.1296, Battery = -1, Failed - #Survival Probability: 0.8704 - - #Outage hour 3: - #2 generators: Prob = 0.262144, Battery = 0, Survived - #2 gen -> 2 -> 1 Prob = 0.131072, Battery = 1, Survived - #2 gen -> 1 -> 1 Prob = 0.16384, Battery = 0, Survived - #1 gen -> 1 -> 1 Prob = 0.2048, Battery = -1, Failed - #0 generators Prob = 0.238144, Battery = -1, Failed - #Survival Probability: 0.557056 - @test backup_reliability(input_dict)["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.557056 - - #Test multiple generator types - input_dict = Dict( - "critical_loads_kw" => [1,2,2,1], - "battery_starting_soc_series_fraction" => [0.5,0.5,0.5,0.5], - "max_outage_duration" => 3, - "num_generators" => [1,1], - "generator_size_kw" => [1,1], - "generator_operational_availability" => [1,1], - "generator_failure_to_start" => [0.0, 0.0], - "generator_mean_time_to_failure" => [5, 5], - "battery_operational_availability" => 1.0, - "num_battery_bins" => 3, - "battery_size_kwh" => 2, - "battery_size_kw" => 1, - "battery_charge_efficiency" => 1, - "battery_discharge_efficiency" => 1, - "battery_minimum_soc_fraction" => 0) - - @test backup_reliability(input_dict)["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.557056 - - #8760 of flat load. Battery can survive 4 hours. - #Survival after 24 hours should be chance of generator surviving 20 or more hours - input_dict = Dict( - "critical_loads_kw" => 100 .* ones(8760), - "max_outage_duration" => 24, - "num_generators" => 1, - "generator_size_kw" => 100, - "generator_operational_availability" => 0.98, - "generator_failure_to_start" => 0.1, - "generator_mean_time_to_failure" => 100, - "battery_operational_availability" => 1.0, - "num_battery_bins" => 101, - "battery_size_kwh" => 400, - "battery_size_kw" => 100, - "battery_charge_efficiency" => 1, - "battery_discharge_efficiency" => 1, - "battery_minimum_soc_fraction" => 0) - - reliability_results = backup_reliability(input_dict) - @test reliability_results["unlimited_fuel_mean_cumulative_survival_by_duration"][24] ≈ (0.99^20)*(0.9*0.98) atol=0.00001 - - #More complex case of hospital load with 2 generators, PV, wind, and battery - reliability_inputs = JSON.parsefile("./scenarios/backup_reliability_inputs.json") - reliability_results = backup_reliability(reliability_inputs) - @test reliability_results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.858756 atol=0.0001 - @test reliability_results["cumulative_survival_final_time_step"][1] ≈ 0.858756 atol=0.0001 - @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.904242 atol=0.0001#0.833224 - - # Test gens+pv+wind+batt with 3 arg version of backup_reliability - # Attention! REopt optimization results are presaved in erp_gens_batt_pv_wind_reopt_results.json - # If you modify backup_reliability_reopt_inputs.json, you must add this before JSON.parsefile: - # results = run_reopt(model, p) - # open("scenarios/erp_gens_batt_pv_wind_reopt_results.json","w") do f - # JSON.print(f, results, 4) - # end - for input_key in [ - "generator_size_kw", - "battery_size_kw", - "battery_size_kwh", - "pv_size_kw", - "wind_size_kw", - "critical_loads_kw", - "pv_production_factor_series", - "wind_production_factor_series" - ] - delete!(reliability_inputs, input_key) - end - # note: the wind prod series in backup_reliability_reopt_inputs.json is actually a PV profile (to in order to test a wind scenario that should give same results as an existing PV one) - p = REoptInputs("./scenarios/backup_reliability_reopt_inputs.json") - results = JSON.parsefile("./scenarios/erp_gens_batt_pv_wind_reopt_results.json") - reliability_results = backup_reliability(results, p, reliability_inputs) - - @test reliability_results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.802997 atol=0.0001 - @test reliability_results["cumulative_survival_final_time_step"][1] ≈ 0.802997 atol=0.0001 - @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.817586 atol=0.001 - end - - @testset "Disaggregated Heating Loads" begin - @testset "Process Heat Load Inputs" begin - d = JSON.parsefile("./scenarios/electric_heater.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["ProcessHeatLoad"]["annual_mmbtu"] = 0.5 * 8760 - s = Scenario(d) - inputs = REoptInputs(s) - @test inputs.heating_loads_kw["ProcessHeat"][1] ≈ 117.228428 atol=1.0e-3 - end - @testset "Separate Heat Load Results" begin - d = JSON.parsefile("./scenarios/electric_heater.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["ProcessHeatLoad"]["annual_mmbtu"] = 0.5 * 8760 - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0 - d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] - d["HotThermalStorage"]["max_gal"] = 0.0 - s = Scenario(d) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, inputs) - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.01 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.01 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.8*4380.0 atol=0.01 - @test sum(results["ElectricHeater"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.8*4380.0 atol=0.01 - @test sum(results["ElectricHeater"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.8*4380.0 atol=0.01 - @test sum(results["ElectricHeater"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.01 - end - end + #More complex case of hospital load with 2 generators, PV, wind, and battery + reliability_inputs = JSON.parsefile("./scenarios/backup_reliability_inputs.json") + reliability_results = backup_reliability(reliability_inputs) + @test reliability_results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.858756 atol=0.0001 + @test reliability_results["cumulative_survival_final_time_step"][1] ≈ 0.858756 atol=0.0001 + @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.904242 atol=0.0001#0.833224 + + # Test gens+pv+wind+batt with 3 arg version of backup_reliability + # Attention! REopt optimization results are presaved in erp_gens_batt_pv_wind_reopt_results.json + # If you modify backup_reliability_reopt_inputs.json, you must add this before JSON.parsefile: + # results = run_reopt(model, p) + # open("scenarios/erp_gens_batt_pv_wind_reopt_results.json","w") do f + # JSON.print(f, results, 4) + # end + for input_key in [ + "generator_size_kw", + "battery_size_kw", + "battery_size_kwh", + "pv_size_kw", + "wind_size_kw", + "critical_loads_kw", + "pv_production_factor_series", + "wind_production_factor_series" + ] + delete!(reliability_inputs, input_key) + end + # note: the wind prod series in backup_reliability_reopt_inputs.json is actually a PV profile (to in order to test a wind scenario that should give same results as an existing PV one) + p = REoptInputs("./scenarios/backup_reliability_reopt_inputs.json") + results = JSON.parsefile("./scenarios/erp_gens_batt_pv_wind_reopt_results.json") + reliability_results = backup_reliability(results, p, reliability_inputs) - @testset "Net Metering" begin - @testset "Net Metering Limit and Wholesale" begin - #case 1: net metering limit is met by PV - d = JSON.parsefile("./scenarios/net_metering.json") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["PV"]["size_kw"] ≈ 30.0 atol=1e-3 - - #case 2: wholesale rate is high, big-M is met - d["ElectricTariff"]["wholesale_rate"] = 5.0 - d["PV"]["can_wholesale"] = true - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test results["PV"]["size_kw"] ≈ 7440.0 atol=1e-3 #max benefit provides the upper bound - + @test reliability_results["unlimited_fuel_cumulative_survival_final_time_step"][1] ≈ 0.802997 atol=0.0001 + @test reliability_results["cumulative_survival_final_time_step"][1] ≈ 0.802997 atol=0.0001 + @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.817586 atol=0.001 + end + + @testset "Disaggregated Heating Loads" begin + @testset "Process Heat Load Inputs" begin + d = JSON.parsefile("./scenarios/electric_heater.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["ProcessHeatLoad"]["annual_mmbtu"] = 0.5 * 8760 + s = Scenario(d) + inputs = REoptInputs(s) + @test inputs.heating_loads_kw["ProcessHeat"][1] ≈ 117.228428 atol=1.0e-3 + end + @testset "Separate Heat Load Results" begin + d = JSON.parsefile("./scenarios/electric_heater.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["ProcessHeatLoad"]["annual_mmbtu"] = 0.5 * 8760 + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + d["HotThermalStorage"]["max_gal"] = 0.0 + s = Scenario(d) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, inputs) + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.01 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.01 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.8*4380.0 atol=0.01 + @test sum(results["ElectricHeater"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.8*4380.0 atol=0.01 + @test sum(results["ElectricHeater"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.8*4380.0 atol=0.01 + @test sum(results["ElectricHeater"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.01 + end end - end - @testset "Imported Xpress Test Suite" begin - @testset "Heating loads and addressable load fraction" begin - # Default LargeOffice CRB with SpaceHeatingLoad and DomesticHotWaterLoad are served by ExistingBoiler - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, "./scenarios/thermal_load.json") + @testset "Net Metering" begin + @testset "Net Metering Limit and Wholesale" begin + #case 1: net metering limit is met by PV + d = JSON.parsefile("./scenarios/net_metering.json") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["PV"]["size_kw"] ≈ 30.0 atol=1e-3 - @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 12904 - - # Hourly fuel load inputs with addressable_load_fraction are served as expected - data = JSON.parsefile("./scenarios/thermal_load.json") - - data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) - data["DomesticHotWaterLoad"]["addressable_load_fraction"] = 0.6 - data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) - data["SpaceHeatingLoad"]["addressable_load_fraction"] = 0.8 - data["ProcessHeatLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.3], 8760) - data["ProcessHeatLoad"]["addressable_load_fraction"] = 0.7 + #case 2: wholesale rate is high, big-M is met + d["ElectricTariff"]["wholesale_rate"] = 5.0 + d["PV"]["can_wholesale"] = true + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test results["PV"]["size_kw"] ≈ 7440.0 atol=1e-3 #max benefit provides the upper bound + + end + end - s = Scenario(data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, inputs) - @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 8760 * (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) atol = 1.0 + @testset "Imported Xpress Test Suite" begin + @testset "Heating loads and addressable load fraction" begin + # Default LargeOffice CRB with SpaceHeatingLoad and DomesticHotWaterLoad are served by ExistingBoiler + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, "./scenarios/thermal_load.json") - # Monthly fuel load input with addressable_load_fraction is processed to expected thermal load - data = JSON.parsefile("./scenarios/thermal_load.json") - data["DomesticHotWaterLoad"]["monthly_mmbtu"] = repeat([100], 12) - data["DomesticHotWaterLoad"]["addressable_load_fraction"] = repeat([0.6], 12) - data["SpaceHeatingLoad"]["monthly_mmbtu"] = repeat([200], 12) - data["SpaceHeatingLoad"]["addressable_load_fraction"] = repeat([0.8], 12) - data["ProcessHeatLoad"]["monthly_mmbtu"] = repeat([150], 12) - data["ProcessHeatLoad"]["addressable_load_fraction"] = repeat([0.7], 12) - - # Assuming Scenario and REoptInputs are defined functions/classes in your code - s = Scenario(data) - inputs = REoptInputs(s) + @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 12904 + + # Hourly fuel load inputs with addressable_load_fraction are served as expected + data = JSON.parsefile("./scenarios/thermal_load.json") - dhw_thermal_load_expected = sum(data["DomesticHotWaterLoad"]["monthly_mmbtu"] .* data["DomesticHotWaterLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency - space_thermal_load_expected = sum(data["SpaceHeatingLoad"]["monthly_mmbtu"] .* data["SpaceHeatingLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency - process_thermal_load_expected = sum(data["ProcessHeatLoad"]["monthly_mmbtu"] .* data["ProcessHeatLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) + data["DomesticHotWaterLoad"]["addressable_load_fraction"] = 0.6 + data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) + data["SpaceHeatingLoad"]["addressable_load_fraction"] = 0.8 + data["ProcessHeatLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.3], 8760) + data["ProcessHeatLoad"]["addressable_load_fraction"] = 0.7 - @test round(sum(s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(dhw_thermal_load_expected) - @test round(sum(s.space_heating_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(space_thermal_load_expected) - @test round(sum(s.process_heat_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(process_thermal_load_expected) - end - - @testset "CHP" begin - @testset "CHP Sizing" begin - # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods - data_sizing = JSON.parsefile("./scenarios/chp_sizing.json") - s = Scenario(data_sizing) + s = Scenario(data) inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, inputs) - - @test round(results["CHP"]["size_kw"], digits=0) ≈ 400.0 atol=50.0 - @test round(results["Financial"]["lcc"], digits=0) ≈ 1.3476e7 rtol=1.0e-2 - end - - @testset "CHP Cost Curve and Min Allowable Size" begin - # Fixed size CHP with cost curve, no unavailability_periods - data_cost_curve = JSON.parsefile("./scenarios/chp_sizing.json") - data_cost_curve["CHP"] = Dict() - data_cost_curve["CHP"]["prime_mover"] = "recip_engine" - data_cost_curve["CHP"]["size_class"] = 1 - data_cost_curve["CHP"]["fuel_cost_per_mmbtu"] = 8.0 - data_cost_curve["CHP"]["min_kw"] = 0 - data_cost_curve["CHP"]["min_allowable_kw"] = 555.5 - data_cost_curve["CHP"]["max_kw"] = 555.51 - data_cost_curve["CHP"]["installed_cost_per_kw"] = 1800.0 - data_cost_curve["CHP"]["installed_cost_per_kw"] = [2300.0, 1800.0, 1500.0] - data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] = [100.0, 300.0, 1140.0] - - data_cost_curve["CHP"]["federal_itc_fraction"] = 0.1 - data_cost_curve["CHP"]["macrs_option_years"] = 0 - data_cost_curve["CHP"]["macrs_bonus_fraction"] = 0.0 - data_cost_curve["CHP"]["macrs_itc_reduction"] = 0.0 - - expected_x = data_cost_curve["CHP"]["min_allowable_kw"] - cap_cost_y = data_cost_curve["CHP"]["installed_cost_per_kw"] - cap_cost_x = data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] - slope = (cap_cost_x[3] * cap_cost_y[3] - cap_cost_x[2] * cap_cost_y[2]) / (cap_cost_x[3] - cap_cost_x[2]) - init_capex_chp_expected = cap_cost_x[2] * cap_cost_y[2] + (expected_x - cap_cost_x[2]) * slope - lifecycle_capex_chp_expected = init_capex_chp_expected - - REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], - [0, init_capex_chp_expected * data_cost_curve["CHP"]["federal_itc_fraction"]]) - - #PV - data_cost_curve["PV"] = Dict() - data_cost_curve["PV"]["min_kw"] = 1500 - data_cost_curve["PV"]["max_kw"] = 1500 - data_cost_curve["PV"]["installed_cost_per_kw"] = 1600 - data_cost_curve["PV"]["federal_itc_fraction"] = 0.26 - data_cost_curve["PV"]["macrs_option_years"] = 0 - data_cost_curve["PV"]["macrs_bonus_fraction"] = 0.0 - data_cost_curve["PV"]["macrs_itc_reduction"] = 0.0 - - init_capex_pv_expected = data_cost_curve["PV"]["max_kw"] * data_cost_curve["PV"]["installed_cost_per_kw"] - lifecycle_capex_pv_expected = init_capex_pv_expected - - REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], - [0, init_capex_pv_expected * data_cost_curve["PV"]["federal_itc_fraction"]]) - - s = Scenario(data_cost_curve) + @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 8760 * (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) atol = 1.0 + + # Monthly fuel load input with addressable_load_fraction is processed to expected thermal load + data = JSON.parsefile("./scenarios/thermal_load.json") + data["DomesticHotWaterLoad"]["monthly_mmbtu"] = repeat([100], 12) + data["DomesticHotWaterLoad"]["addressable_load_fraction"] = repeat([0.6], 12) + data["SpaceHeatingLoad"]["monthly_mmbtu"] = repeat([200], 12) + data["SpaceHeatingLoad"]["addressable_load_fraction"] = repeat([0.8], 12) + data["ProcessHeatLoad"]["monthly_mmbtu"] = repeat([150], 12) + data["ProcessHeatLoad"]["addressable_load_fraction"] = repeat([0.7], 12) + + # Assuming Scenario and REoptInputs are defined functions/classes in your code + s = Scenario(data) inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - init_capex_total_expected = init_capex_chp_expected + init_capex_pv_expected - lifecycle_capex_total_expected = lifecycle_capex_chp_expected + lifecycle_capex_pv_expected + + dhw_thermal_load_expected = sum(data["DomesticHotWaterLoad"]["monthly_mmbtu"] .* data["DomesticHotWaterLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + space_thermal_load_expected = sum(data["SpaceHeatingLoad"]["monthly_mmbtu"] .* data["SpaceHeatingLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + process_thermal_load_expected = sum(data["ProcessHeatLoad"]["monthly_mmbtu"] .* data["ProcessHeatLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + + @test round(sum(s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(dhw_thermal_load_expected) + @test round(sum(s.space_heating_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(space_thermal_load_expected) + @test round(sum(s.process_heat_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(process_thermal_load_expected) + end - init_capex_total = results["Financial"]["initial_capital_costs"] - lifecycle_capex_total = results["Financial"]["initial_capital_costs_after_incentives"] + @testset "CHP" begin + @testset "CHP Sizing" begin + # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods + data_sizing = JSON.parsefile("./scenarios/chp_sizing.json") + s = Scenario(data_sizing) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt(m, inputs) + + @test round(results["CHP"]["size_kw"], digits=0) ≈ 400.0 atol=50.0 + @test round(results["Financial"]["lcc"], digits=0) ≈ 1.3476e7 rtol=1.0e-2 + end + @testset "CHP Cost Curve and Min Allowable Size" begin + # Fixed size CHP with cost curve, no unavailability_periods + data_cost_curve = JSON.parsefile("./scenarios/chp_sizing.json") + data_cost_curve["CHP"] = Dict() + data_cost_curve["CHP"]["prime_mover"] = "recip_engine" + data_cost_curve["CHP"]["size_class"] = 1 + data_cost_curve["CHP"]["fuel_cost_per_mmbtu"] = 8.0 + data_cost_curve["CHP"]["min_kw"] = 0 + data_cost_curve["CHP"]["min_allowable_kw"] = 555.5 + data_cost_curve["CHP"]["max_kw"] = 555.51 + data_cost_curve["CHP"]["installed_cost_per_kw"] = 1800.0 + data_cost_curve["CHP"]["installed_cost_per_kw"] = [2300.0, 1800.0, 1500.0] + data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] = [100.0, 300.0, 1140.0] + + data_cost_curve["CHP"]["federal_itc_fraction"] = 0.1 + data_cost_curve["CHP"]["macrs_option_years"] = 0 + data_cost_curve["CHP"]["macrs_bonus_fraction"] = 0.0 + data_cost_curve["CHP"]["macrs_itc_reduction"] = 0.0 + + expected_x = data_cost_curve["CHP"]["min_allowable_kw"] + cap_cost_y = data_cost_curve["CHP"]["installed_cost_per_kw"] + cap_cost_x = data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] + slope = (cap_cost_x[3] * cap_cost_y[3] - cap_cost_x[2] * cap_cost_y[2]) / (cap_cost_x[3] - cap_cost_x[2]) + init_capex_chp_expected = cap_cost_x[2] * cap_cost_y[2] + (expected_x - cap_cost_x[2]) * slope + lifecycle_capex_chp_expected = init_capex_chp_expected - + REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], + [0, init_capex_chp_expected * data_cost_curve["CHP"]["federal_itc_fraction"]]) + + #PV + data_cost_curve["PV"] = Dict() + data_cost_curve["PV"]["min_kw"] = 1500 + data_cost_curve["PV"]["max_kw"] = 1500 + data_cost_curve["PV"]["installed_cost_per_kw"] = 1600 + data_cost_curve["PV"]["federal_itc_fraction"] = 0.26 + data_cost_curve["PV"]["macrs_option_years"] = 0 + data_cost_curve["PV"]["macrs_bonus_fraction"] = 0.0 + data_cost_curve["PV"]["macrs_itc_reduction"] = 0.0 + + init_capex_pv_expected = data_cost_curve["PV"]["max_kw"] * data_cost_curve["PV"]["installed_cost_per_kw"] + lifecycle_capex_pv_expected = init_capex_pv_expected - + REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], + [0, init_capex_pv_expected * data_cost_curve["PV"]["federal_itc_fraction"]]) + + s = Scenario(data_cost_curve) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) + + init_capex_total_expected = init_capex_chp_expected + init_capex_pv_expected + lifecycle_capex_total_expected = lifecycle_capex_chp_expected + lifecycle_capex_pv_expected + + init_capex_total = results["Financial"]["initial_capital_costs"] + lifecycle_capex_total = results["Financial"]["initial_capital_costs_after_incentives"] + + + # Check initial CapEx (pre-incentive/tax) and life cycle CapEx (post-incentive/tax) cost with expect + @test init_capex_total_expected ≈ init_capex_total atol=0.0001*init_capex_total_expected + @test lifecycle_capex_total_expected ≈ lifecycle_capex_total atol=0.0001*lifecycle_capex_total_expected + + # Test CHP.min_allowable_kw - the size would otherwise be ~100 kW less by setting min_allowable_kw to zero + @test results["CHP"]["size_kw"] ≈ data_cost_curve["CHP"]["min_allowable_kw"] atol=0.1 + end - # Check initial CapEx (pre-incentive/tax) and life cycle CapEx (post-incentive/tax) cost with expect - @test init_capex_total_expected ≈ init_capex_total atol=0.0001*init_capex_total_expected - @test lifecycle_capex_total_expected ≈ lifecycle_capex_total atol=0.0001*lifecycle_capex_total_expected + @testset "CHP Unavailability and Outage" begin + """ + Validation to ensure that: + 1) CHP meets load during outage without exporting + 2) CHP never exports if chp.can_wholesale and chp.can_net_meter inputs are False (default) + 3) CHP does not "curtail", i.e. send power to a load bank when chp.can_curtail is False (default) + 4) CHP min_turn_down_fraction is ignored during an outage + 5) Cooling tech production gets zeroed out during the outage period because we ignore the cooling load balance for outage + 6) Unavailability intervals that intersect with grid-outages get ignored + 7) Unavailability intervals that do not intersect with grid-outages result in no CHP production + """ + # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods + data = JSON.parsefile("./scenarios/chp_unavailability_outage.json") + + # Add unavailability periods that 1) intersect (ignored) and 2) don't intersect with outage period + data["CHP"]["unavailability_periods"] = [Dict([("month", 1), ("start_week_of_month", 2), + ("start_day_of_week", 1), ("start_hour", 1), ("duration_hours", 8)]), + Dict([("month", 1), ("start_week_of_month", 2), + ("start_day_of_week", 3), ("start_hour", 9), ("duration_hours", 8)])] + + # Manually doing the math from the unavailability defined above + unavail_1_start = 24 + 1 + unavail_1_end = unavail_1_start + 8 - 1 + unavail_2_start = 24*3 + 9 + unavail_2_end = unavail_2_start + 8 - 1 + + # Specify the CHP.min_turn_down_fraction which is NOT used during an outage + data["CHP"]["min_turn_down_fraction"] = 0.5 + # Specify outage period; outage time_steps are 1-indexed + outage_start = unavail_1_start + data["ElectricUtility"]["outage_start_time_step"] = outage_start + outage_end = unavail_1_end + data["ElectricUtility"]["outage_end_time_step"] = outage_end + data["ElectricLoad"]["critical_load_fraction"] = 0.25 + + s = Scenario(data) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) + + tot_elec_load = results["ElectricLoad"]["load_series_kw"] + chp_total_elec_prod = results["CHP"]["electric_production_series_kw"] + chp_to_load = results["CHP"]["electric_to_load_series_kw"] + chp_export = results["CHP"]["electric_to_grid_series_kw"] + cooling_elec_consumption = results["ExistingChiller"]["electric_consumption_series_kw"] + + # The values compared to the expected values + @test sum([(chp_to_load[i] - tot_elec_load[i]*data["ElectricLoad"]["critical_load_fraction"]) for i in outage_start:outage_end]) ≈ 0.0 atol=0.001 + critical_load = tot_elec_load[outage_start:outage_end] * data["ElectricLoad"]["critical_load_fraction"] + @test sum(chp_to_load[outage_start:outage_end]) ≈ sum(critical_load) atol=0.1 + @test sum(chp_export) == 0.0 + @test sum(chp_total_elec_prod) ≈ sum(chp_to_load) atol=1.0e-5*sum(chp_total_elec_prod) + @test sum(cooling_elec_consumption[outage_start:outage_end]) == 0.0 + @test sum(chp_total_elec_prod[unavail_2_start:unavail_2_end]) == 0.0 + end - # Test CHP.min_allowable_kw - the size would otherwise be ~100 kW less by setting min_allowable_kw to zero - @test results["CHP"]["size_kw"] ≈ data_cost_curve["CHP"]["min_allowable_kw"] atol=0.1 + @testset "CHP Supplementary firing and standby" begin + """ + Test to ensure that supplementary firing and standby charges work as intended. The thermal and + electrical loads are constant, and the CHP system size is fixed; the supplementary firing has a + similar cost to the boiler and is purcahsed and used when the boiler efficiency is set to a lower + value than that of the supplementary firing. The test also ensures that demand charges are + correctly calculated when CHP is and is not allowed to reduce demand charges. + """ + data = JSON.parsefile("./scenarios/chp_supplementary_firing.json") + data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10000 + data["ElectricLoad"]["loads_kw"] = repeat([800.0], 8760) + data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) + data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) + #part 1: supplementary firing not used when less efficient than the boiler and expensive + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(m1, inputs) + @test results["CHP"]["size_kw"] == 800 + @test results["CHP"]["size_supplemental_firing_kw"] == 0 + @test results["CHP"]["annual_electric_production_kwh"] ≈ 800*8760 rtol=1e-5 + @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 800*(0.4418/0.3573)*8760/293.07107 rtol=1e-5 + @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] == 0 + @test results["HeatingLoad"]["annual_calculated_total_heating_thermal_load_mmbtu"] == 12.0 * 8760 * data["ExistingBoiler"]["efficiency"] + @test results["HeatingLoad"]["annual_calculated_dhw_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] + @test results["HeatingLoad"]["annual_calculated_space_heating_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] + + #part 2: supplementary firing used when more efficient than the boiler and low-cost; demand charges not reduced by CHP + data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10 + data["CHP"]["reduces_demand_charges"] = false + data["ExistingBoiler"]["efficiency"] = 0.85 + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(m2, inputs) + @test results["CHP"]["size_supplemental_firing_kw"] ≈ 321.71 atol=0.1 + @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 149136.6 rtol=1e-5 + @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] ≈ 5212.7 rtol=1e-5 + end + + @testset "CHP to Waste Heat" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d = JSON.parsefile("./scenarios/chp_waste.json") + results = run_reopt(m, d) + @test sum(results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 4174.455 atol=1e-3 + end end - - @testset "CHP Unavailability and Outage" begin - """ - Validation to ensure that: - 1) CHP meets load during outage without exporting - 2) CHP never exports if chp.can_wholesale and chp.can_net_meter inputs are False (default) - 3) CHP does not "curtail", i.e. send power to a load bank when chp.can_curtail is False (default) - 4) CHP min_turn_down_fraction is ignored during an outage - 5) Cooling tech production gets zeroed out during the outage period because we ignore the cooling load balance for outage - 6) Unavailability intervals that intersect with grid-outages get ignored - 7) Unavailability intervals that do not intersect with grid-outages result in no CHP production - """ - # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods - data = JSON.parsefile("./scenarios/chp_unavailability_outage.json") - - # Add unavailability periods that 1) intersect (ignored) and 2) don't intersect with outage period - data["CHP"]["unavailability_periods"] = [Dict([("month", 1), ("start_week_of_month", 2), - ("start_day_of_week", 1), ("start_hour", 1), ("duration_hours", 8)]), - Dict([("month", 1), ("start_week_of_month", 2), - ("start_day_of_week", 3), ("start_hour", 9), ("duration_hours", 8)])] - - # Manually doing the math from the unavailability defined above - unavail_1_start = 24 + 1 - unavail_1_end = unavail_1_start + 8 - 1 - unavail_2_start = 24*3 + 9 - unavail_2_end = unavail_2_start + 8 - 1 - - # Specify the CHP.min_turn_down_fraction which is NOT used during an outage - data["CHP"]["min_turn_down_fraction"] = 0.5 - # Specify outage period; outage time_steps are 1-indexed - outage_start = unavail_1_start - data["ElectricUtility"]["outage_start_time_step"] = outage_start - outage_end = unavail_1_end - data["ElectricUtility"]["outage_end_time_step"] = outage_end - data["ElectricLoad"]["critical_load_fraction"] = 0.25 - s = Scenario(data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - tot_elec_load = results["ElectricLoad"]["load_series_kw"] - chp_total_elec_prod = results["CHP"]["electric_production_series_kw"] - chp_to_load = results["CHP"]["electric_to_load_series_kw"] - chp_export = results["CHP"]["electric_to_grid_series_kw"] - cooling_elec_consumption = results["ExistingChiller"]["electric_consumption_series_kw"] + @testset "FlexibleHVAC" begin + + @testset "Single RC Model heating only" begin + #= + Single RC model: + 1 state/control node + 2 inputs: Ta and Qheat + A = [1/(RC)], B = [1/(RC) 1/C], u = [Ta; Q] + NOTE exogenous_inputs (u) allows for parasitic heat, but it is input as zeros here + + We start with no technologies except ExistingBoiler and ExistingChiller. + FlexibleHVAC is only worth purchasing if its cost is neglible (i.e. below the lcc_bau * MIPTOL) + or if there is a time-varying fuel and/or electricity cost + (and the FlexibleHVAC installed_cost is less than the achievable savings). + =# + + # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC + pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431); + R = 0.00025 # K/kW + C = 1e5 # kJ/K + # the starting scenario has flat fuel and electricty costs + d = JSON.parsefile("./scenarios/thermal_load.json"); + A = reshape([-1/(R*C)], 1,1) + B = [1/(R*C) 1/C] + u = [tamb zeros(8760)]'; + d["FlexibleHVAC"] = Dict( + "control_node" => 1, + "initial_temperatures" => [21], + "temperature_upper_bound_degC" => 22.0, + "temperature_lower_bound_degC" => 19.8, + "installed_cost" => 300.0, # NOTE cost must be more then the MIPTOL * LCC 5e-5 * 5.79661e6 ≈ 290 to make FlexibleHVAC not worth it + "system_matrix" => A, + "input_matrix" => B, + "exogenous_inputs" => u + ) + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false + # @test r["Financial"]["npv"] == 0 + + # put in a time varying fuel cost, which should make purchasing the FlexibleHVAC system economical + # with flat ElectricTariff the ExistingChiller does not benefit from FlexibleHVAC + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = rand(Float64, (8760))*(50-5).+5; + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # all of the savings are from the ExistingBoiler fuel costs + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === true + # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] + # @test fuel_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 + + # now increase the FlexibleHVAC installed_cost to the fuel costs savings + 100 and expect that the FlexibleHVAC is not purchased + # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + 100 + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false + # @test r["Financial"]["npv"] == 0 + + # add TOU ElectricTariff and expect to benefit from using ExistingChiller intelligently + d["ElectricTariff"] = Dict("urdb_label" => "5ed6c1a15457a3367add15ae") + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + + # elec_cost_savings = r["ElectricTariff"]["lifecycle_demand_cost_after_tax_bau"] + + # r["ElectricTariff"]["lifecycle_energy_cost_after_tax_bau"] - + # r["ElectricTariff"]["lifecycle_demand_cost_after_tax"] - + # r["ElectricTariff"]["lifecycle_energy_cost_after_tax"] + + # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] + # @test fuel_cost_savings + elec_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 + + # now increase the FlexibleHVAC installed_cost to the fuel costs savings + elec_cost_savings + # + 100 and expect that the FlexibleHVAC is not purchased + # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + elec_cost_savings + 100 + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false + # @test r["Financial"]["npv"] == 0 - # The values compared to the expected values - @test sum([(chp_to_load[i] - tot_elec_load[i]*data["ElectricLoad"]["critical_load_fraction"]) for i in outage_start:outage_end]) ≈ 0.0 atol=0.001 - critical_load = tot_elec_load[outage_start:outage_end] * data["ElectricLoad"]["critical_load_fraction"] - @test sum(chp_to_load[outage_start:outage_end]) ≈ sum(critical_load) atol=0.1 - @test sum(chp_export) == 0.0 - @test sum(chp_total_elec_prod) ≈ sum(chp_to_load) atol=1.0e-5*sum(chp_total_elec_prod) - @test sum(cooling_elec_consumption[outage_start:outage_end]) == 0.0 - @test sum(chp_total_elec_prod[unavail_2_start:unavail_2_end]) == 0.0 + end end - - @testset "CHP Supplementary firing and standby" begin - """ - Test to ensure that supplementary firing and standby charges work as intended. The thermal and - electrical loads are constant, and the CHP system size is fixed; the supplementary firing has a - similar cost to the boiler and is purcahsed and used when the boiler efficiency is set to a lower - value than that of the supplementary firing. The test also ensures that demand charges are - correctly calculated when CHP is and is not allowed to reduce demand charges. - """ - data = JSON.parsefile("./scenarios/chp_supplementary_firing.json") - data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10000 - data["ElectricLoad"]["loads_kw"] = repeat([800.0], 8760) - data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) - data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) - #part 1: supplementary firing not used when less efficient than the boiler and expensive - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - s = Scenario(data) - inputs = REoptInputs(s) - results = run_reopt(m1, inputs) - @test results["CHP"]["size_kw"] == 800 - @test results["CHP"]["size_supplemental_firing_kw"] == 0 - @test results["CHP"]["annual_electric_production_kwh"] ≈ 800*8760 rtol=1e-5 - @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 800*(0.4418/0.3573)*8760/293.07107 rtol=1e-5 - @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] == 0 - @test results["HeatingLoad"]["annual_calculated_total_heating_thermal_load_mmbtu"] == 12.0 * 8760 * data["ExistingBoiler"]["efficiency"] - @test results["HeatingLoad"]["annual_calculated_dhw_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] - @test results["HeatingLoad"]["annual_calculated_space_heating_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] - - #part 2: supplementary firing used when more efficient than the boiler and low-cost; demand charges not reduced by CHP - data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10 - data["CHP"]["reduces_demand_charges"] = false - data["ExistingBoiler"]["efficiency"] = 0.85 - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + + #= + add a time-of-export rate that is greater than retail rate for the month of January, + check to make sure that PV does NOT export unless the site load is met first for the month of January. + =# + @testset "Do not allow_simultaneous_export_import" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + data = JSON.parsefile("./scenarios/monthly_rate.json") + + # create wholesale_rate with compensation in January > retail rate + jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] + data["ElectricTariff"]["wholesale_rate"] = + append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) + data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) + data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false) + s = Scenario(data) inputs = REoptInputs(s) - results = run_reopt(m2, inputs) - @test results["CHP"]["size_supplemental_firing_kw"] ≈ 321.71 atol=0.1 - @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 149136.6 rtol=1e-5 - @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] ≈ 5212.7 rtol=1e-5 + results = run_reopt(model, inputs) + + @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"][1:744]) + if results["PV"]["electric_to_grid_series_kw"][i] > 0) end - @testset "CHP to Waste Heat" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d = JSON.parsefile("./scenarios/chp_waste.json") - results = run_reopt(m, d) - @test sum(results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 4174.455 atol=1e-3 - end - end - - @testset "FlexibleHVAC" begin - - @testset "Single RC Model heating only" begin - #= - Single RC model: - 1 state/control node - 2 inputs: Ta and Qheat - A = [1/(RC)], B = [1/(RC) 1/C], u = [Ta; Q] - NOTE exogenous_inputs (u) allows for parasitic heat, but it is input as zeros here - - We start with no technologies except ExistingBoiler and ExistingChiller. - FlexibleHVAC is only worth purchasing if its cost is neglible (i.e. below the lcc_bau * MIPTOL) - or if there is a time-varying fuel and/or electricity cost - (and the FlexibleHVAC installed_cost is less than the achievable savings). - =# - - # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC - pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431); - R = 0.00025 # K/kW - C = 1e5 # kJ/K - # the starting scenario has flat fuel and electricty costs - d = JSON.parsefile("./scenarios/thermal_load.json"); - A = reshape([-1/(R*C)], 1,1) - B = [1/(R*C) 1/C] - u = [tamb zeros(8760)]'; - d["FlexibleHVAC"] = Dict( - "control_node" => 1, - "initial_temperatures" => [21], - "temperature_upper_bound_degC" => 22.0, - "temperature_lower_bound_degC" => 19.8, - "installed_cost" => 300.0, # NOTE cost must be more then the MIPTOL * LCC 5e-5 * 5.79661e6 ≈ 290 to make FlexibleHVAC not worth it - "system_matrix" => A, - "input_matrix" => B, - "exogenous_inputs" => u - ) - + @testset "Solar and ElectricStorage w/BAU and degradation" begin m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false - # @test r["Financial"]["npv"] == 0 - - # put in a time varying fuel cost, which should make purchasing the FlexibleHVAC system economical - # with flat ElectricTariff the ExistingChiller does not benefit from FlexibleHVAC - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = rand(Float64, (8760))*(50-5).+5; - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # all of the savings are from the ExistingBoiler fuel costs - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === true - # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] - # @test fuel_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 - - # now increase the FlexibleHVAC installed_cost to the fuel costs savings + 100 and expect that the FlexibleHVAC is not purchased - # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + 100 - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false - # @test r["Financial"]["npv"] == 0 - - # add TOU ElectricTariff and expect to benefit from using ExistingChiller intelligently - d["ElectricTariff"] = Dict("urdb_label" => "5ed6c1a15457a3367add15ae") - + d = JSON.parsefile("scenarios/pv_storage.json"); + d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) + results = run_reopt([m1,m2], d) + + @test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01 + @test results["PV"]["lcoe_per_kwh"] ≈ 0.0468 atol = 0.001 + @test results["Financial"]["lcc"] ≈ 1.239179e7 rtol=1e-5 + @test results["Financial"]["lcc_bau"] ≈ 12766397 rtol=1e-5 + @test results["ElectricStorage"]["size_kw"] ≈ 49.02 atol=0.1 + @test results["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 + proforma_npv = REopt.npv(results["Financial"]["offtaker_annual_free_cashflows"] - + results["Financial"]["offtaker_annual_free_cashflows_bau"], 0.081) + @test results["Financial"]["npv"] ≈ proforma_npv rtol=0.0001 + + # compare avg soc with and without degradation, + # using default augmentation battery maintenance strategy + avg_soc_no_degr = sum(results["ElectricStorage"]["soc_series_fraction"]) / 8760 + d["ElectricStorage"]["model_degradation"] = true + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r_degr = run_reopt(m, d) + avg_soc_degr = sum(r_degr["ElectricStorage"]["soc_series_fraction"]) / 8760 + @test avg_soc_no_degr > avg_soc_degr + + # test the replacement strategy + d["ElectricStorage"]["degradation"] = Dict("maintenance_strategy" => "replacement") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + set_optimizer_attribute(m, "mip_rel_gap", 0.01) + r = run_reopt(m, d) + @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + # #optimal SOH at end of horizon is 80\% to prevent any replacement + # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 + # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 + # # the maintenance_cost comes out to 3004.39 on Actions, so we test the LCC since it should match + # @test r["Financial"]["lcc"] ≈ 1.240096e7 rtol=0.01 + # @test last(value.(m[:SOH])) ≈ 66.633 rtol=0.01 + # @test r["ElectricStorage"]["size_kwh"] ≈ 83.29 rtol=0.01 + + # test minimum_avg_soc_fraction + d["ElectricStorage"]["minimum_avg_soc_fraction"] = 0.72 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + set_optimizer_attribute(m, "mip_rel_gap", 0.01) + r = run_reopt(m, d) + @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 + end + + @testset "Outage with Generator, outage simulator, BAU critical load outputs" begin m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + p = REoptInputs("./scenarios/generator.json") + results = run_reopt([m1,m2], p) + @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 + @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + + sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 + @test results["ElectricLoad"]["bau_critical_load_met"] == false + @test results["ElectricLoad"]["bau_critical_load_met_time_steps"] == 0 - # elec_cost_savings = r["ElectricTariff"]["lifecycle_demand_cost_after_tax_bau"] + - # r["ElectricTariff"]["lifecycle_energy_cost_after_tax_bau"] - - # r["ElectricTariff"]["lifecycle_demand_cost_after_tax"] - - # r["ElectricTariff"]["lifecycle_energy_cost_after_tax"] - - # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] - # @test fuel_cost_savings + elec_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 - - # now increase the FlexibleHVAC installed_cost to the fuel costs savings + elec_cost_savings - # + 100 and expect that the FlexibleHVAC is not purchased - # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + elec_cost_savings + 100 - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false - # @test r["Financial"]["npv"] == 0 - + simresults = simulate_outages(results, p) + @test simresults["resilience_hours_max"] == 11 end - end - #= - add a time-of-export rate that is greater than retail rate for the month of January, - check to make sure that PV does NOT export unless the site load is met first for the month of January. - =# - @testset "Do not allow_simultaneous_export_import" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - data = JSON.parsefile("./scenarios/monthly_rate.json") - - # create wholesale_rate with compensation in January > retail rate - jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] - data["ElectricTariff"]["wholesale_rate"] = - append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) - data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) - data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false) - - s = Scenario(data) - inputs = REoptInputs(s) - results = run_reopt(model, inputs) - - @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"][1:744]) - if results["PV"]["electric_to_grid_series_kw"][i] > 0) - end - - @testset "Solar and ElectricStorage w/BAU and degradation" begin - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - d = JSON.parsefile("scenarios/pv_storage.json"); - d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) - results = run_reopt([m1,m2], d) - - @test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01 - @test results["PV"]["lcoe_per_kwh"] ≈ 0.0468 atol = 0.001 - @test results["Financial"]["lcc"] ≈ 1.239179e7 rtol=1e-5 - @test results["Financial"]["lcc_bau"] ≈ 12766397 rtol=1e-5 - @test results["ElectricStorage"]["size_kw"] ≈ 49.02 atol=0.1 - @test results["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 - proforma_npv = REopt.npv(results["Financial"]["offtaker_annual_free_cashflows"] - - results["Financial"]["offtaker_annual_free_cashflows_bau"], 0.081) - @test results["Financial"]["npv"] ≈ proforma_npv rtol=0.0001 - - # compare avg soc with and without degradation, - # using default augmentation battery maintenance strategy - avg_soc_no_degr = sum(results["ElectricStorage"]["soc_series_fraction"]) / 8760 - d["ElectricStorage"]["model_degradation"] = true - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r_degr = run_reopt(m, d) - avg_soc_degr = sum(r_degr["ElectricStorage"]["soc_series_fraction"]) / 8760 - @test avg_soc_no_degr > avg_soc_degr - - # test the replacement strategy - d["ElectricStorage"]["degradation"] = Dict("maintenance_strategy" => "replacement") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - set_optimizer_attribute(m, "mip_rel_gap", 0.01) - r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) - # #optimal SOH at end of horizon is 80\% to prevent any replacement - # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 - # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 - # # the maintenance_cost comes out to 3004.39 on Actions, so we test the LCC since it should match - # @test r["Financial"]["lcc"] ≈ 1.240096e7 rtol=0.01 - # @test last(value.(m[:SOH])) ≈ 66.633 rtol=0.01 - # @test r["ElectricStorage"]["size_kwh"] ≈ 83.29 rtol=0.01 - - # test minimum_avg_soc_fraction - d["ElectricStorage"]["minimum_avg_soc_fraction"] = 0.72 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - set_optimizer_attribute(m, "mip_rel_gap", 0.01) - r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) - # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 - end - - @testset "Outage with Generator, outage simulator, BAU critical load outputs" begin - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - p = REoptInputs("./scenarios/generator.json") - results = run_reopt([m1,m2], p) - @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 - @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + - sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 - @test results["ElectricLoad"]["bau_critical_load_met"] == false - @test results["ElectricLoad"]["bau_critical_load_met_time_steps"] == 0 - - simresults = simulate_outages(results, p) - @test simresults["resilience_hours_max"] == 11 - end - - @testset "Minimize Unserved Load" begin - d = JSON.parsefile("./scenarios/outage.json") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - results = run_reopt(m, d) - - @test results["Outages"]["expected_outage_cost"] ≈ 0 atol=0.1 - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 0 atol=0.1 - @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 - @test value(m[:binMGTechUsed]["CHP"]) ≈ 1 - @test value(m[:binMGTechUsed]["PV"]) ≈ 1 - @test value(m[:binMGStorageUsed]) ≈ 1 - - # Increase cost of microgrid upgrade and PV Size, PV not used and some load not met - d["Financial"]["microgrid_upgrade_cost_fraction"] = 0.3 - d["PV"]["min_kw"] = 200.0 - d["PV"]["max_kw"] = 200.0 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - results = run_reopt(m, d) - @test value(m[:binMGTechUsed]["PV"]) ≈ 0 - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 24.16 atol=0.1 - - #= - Scenario with $0.001/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 - - should meet 168 kWh in each outage such that the total unserved load is 12 kWh - =# - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/nogridcost_minresilhours.json") - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 12 - - # testing dvUnserved load, which would output 100 kWh for this scenario before output fix - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/nogridcost_multiscenario.json") - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 60 - @test results["Outages"]["expected_outage_cost"] ≈ 485.43270 atol=1.0e-5 #avg duration (3h) * load per time step (10) * present worth factor (16.18109) - @test results["Outages"]["max_outage_cost_per_outage_duration"][1] ≈ 161.8109 atol=1.0e-5 - - # Scenario with generator, PV, electric storage - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/outages_gen_pv_stor.json") - @test results["Outages"]["expected_outage_cost"] ≈ 3.54476923e6 atol=10 - @test results["Financial"]["lcc"] ≈ 8.6413594727e7 rtol=0.001 - - # Scenario with generator, PV, wind, electric storage - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/outages_gen_pv_wind_stor.json") - @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 - @test value(m[:binMGTechUsed]["PV"]) ≈ 1 - @test value(m[:binMGTechUsed]["Wind"]) ≈ 1 - @test results["Outages"]["expected_outage_cost"] ≈ 1.296319791276051e6 atol=1.0 - @test results["Financial"]["lcc"] ≈ 4.8046446434e6 rtol=0.001 + @testset "Minimize Unserved Load" begin + d = JSON.parsefile("./scenarios/outage.json") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt(m, d) - end - - @testset "Outages with Wind and supply-to-load no greater than critical load" begin - input_data = JSON.parsefile("./scenarios/wind_outages.json") - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - results = run_reopt([m1,m2], inputs) - - # Check that supply-to-load is equal to critical load during outages, including wind - supply_to_load = results["Outages"]["storage_discharge_series_kw"] .+ results["Outages"]["wind_to_load_series_kw"] - supply_to_load = [supply_to_load[:,:,i][1] for i in eachindex(supply_to_load)] - critical_load = results["Outages"]["critical_loads_per_outage_series_kw"][1,1,:] - check = .≈(supply_to_load, critical_load, atol=0.001) - @test !(0 in check) - - # Check that the soc_series_fraction is the same length as the storage_discharge_series_kw - @test size(results["Outages"]["soc_series_fraction"]) == size(results["Outages"]["storage_discharge_series_kw"]) - end - - @testset "Multiple Sites" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - ps = [ - REoptInputs("./scenarios/pv_storage.json"), - REoptInputs("./scenarios/monthly_rate.json"), - ]; - results = run_reopt(m, ps) - @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.2830872235e7 rtol=1e-5 - end + @test results["Outages"]["expected_outage_cost"] ≈ 0 atol=0.1 + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 0 atol=0.1 + @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 + @test value(m[:binMGTechUsed]["CHP"]) ≈ 1 + @test value(m[:binMGTechUsed]["PV"]) ≈ 1 + @test value(m[:binMGStorageUsed]) ≈ 1 + + # Increase cost of microgrid upgrade and PV Size, PV not used and some load not met + d["Financial"]["microgrid_upgrade_cost_fraction"] = 0.3 + d["PV"]["min_kw"] = 200.0 + d["PV"]["max_kw"] = 200.0 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt(m, d) + @test value(m[:binMGTechUsed]["PV"]) ≈ 0 + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 24.16 atol=0.1 + + #= + Scenario with $0.001/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 + - should meet 168 kWh in each outage such that the total unserved load is 12 kWh + =# + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/nogridcost_minresilhours.json") + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 12 + + # testing dvUnserved load, which would output 100 kWh for this scenario before output fix + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/nogridcost_multiscenario.json") + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 60 + @test results["Outages"]["expected_outage_cost"] ≈ 485.43270 atol=1.0e-5 #avg duration (3h) * load per time step (10) * present worth factor (16.18109) + @test results["Outages"]["max_outage_cost_per_outage_duration"][1] ≈ 161.8109 atol=1.0e-5 - @testset "MPC" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_mpc(model, "./scenarios/mpc.json") - @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0 - @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0 - @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0 - grid_draw = r["ElectricUtility"]["to_load_series_kw"] .+ r["ElectricUtility"]["to_battery_series_kw"] - # the grid draw limit in the 10th time step is set to 90 - # without the 90 limit the grid draw is 98 in the 10th time step - @test grid_draw[10] <= 90 - end + # Scenario with generator, PV, electric storage + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/outages_gen_pv_stor.json") + @test results["Outages"]["expected_outage_cost"] ≈ 3.54476923e6 atol=10 + @test results["Financial"]["lcc"] ≈ 8.6413594727e7 rtol=0.001 - @testset "Complex Incentives" begin - """ - This test was compared against the API test: - reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives - when using the hardcoded levelization_factor in this package's REoptInputs function. - The two LCC's matched within 0.00005%. (The Julia pkg LCC is 1.0971991e7) - """ - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, "./scenarios/incentives.json") - @test results["Financial"]["lcc"] ≈ 1.094596365e7 atol=5e4 - end + # Scenario with generator, PV, wind, electric storage + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/outages_gen_pv_wind_stor.json") + @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 + @test value(m[:binMGTechUsed]["PV"]) ≈ 1 + @test value(m[:binMGTechUsed]["Wind"]) ≈ 1 + @test results["Outages"]["expected_outage_cost"] ≈ 1.296319791276051e6 atol=1.0 + @test results["Financial"]["lcc"] ≈ 4.8046446434e6 rtol=0.001 + + end - @testset verbose = true "Rate Structures" begin + @testset "Outages with Wind and supply-to-load no greater than critical load" begin + input_data = JSON.parsefile("./scenarios/wind_outages.json") + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt([m1,m2], inputs) + + # Check that supply-to-load is equal to critical load during outages, including wind + supply_to_load = results["Outages"]["storage_discharge_series_kw"] .+ results["Outages"]["wind_to_load_series_kw"] + supply_to_load = [supply_to_load[:,:,i][1] for i in eachindex(supply_to_load)] + critical_load = results["Outages"]["critical_loads_per_outage_series_kw"][1,1,:] + check = .≈(supply_to_load, critical_load, atol=0.001) + @test !(0 in check) + + # Check that the soc_series_fraction is the same length as the storage_discharge_series_kw + @test size(results["Outages"]["soc_series_fraction"]) == size(results["Outages"]["storage_discharge_series_kw"]) + end - @testset "Tiered Energy" begin + @testset "Multiple Sites" begin m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, "./scenarios/tiered_energy_rate.json") - @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 2342.88 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 24000.0 atol=0.1 - @test results["ElectricLoad"]["annual_calculated_kwh"] ≈ 24000.0 atol=0.1 + ps = [ + REoptInputs("./scenarios/pv_storage.json"), + REoptInputs("./scenarios/monthly_rate.json"), + ]; + results = run_reopt(m, ps) + @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.2830872235e7 rtol=1e-5 end - @testset "Lookback Demand Charges" begin - # 1. Testing rate from URDB - data = JSON.parsefile("./scenarios/lookback_rate.json") - # urdb_label used https://apps.openei.org/IURDB/rate/view/539f6a23ec4f024411ec8bf9#2__Demand - # has a demand charge lookback of 35% for all months with 2 different demand charges based on which month - data["ElectricLoad"]["loads_kw"] = ones(8760) - data["ElectricLoad"]["loads_kw"][8] = 100.0 - inputs = REoptInputs(Scenario(data)) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, inputs) - # Expected result is 100 kW demand for January, 35% of that for all other months and - # with 5x other $10.5/kW cold months and 6x $11.5/kW warm months - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 100 * (10.5 + 0.35*10.5*5 + 0.35*11.5*6) - - # 2. Testing custom rate from user with demand_lookback_months - d = JSON.parsefile("./scenarios/lookback_rate.json") - d["ElectricTariff"] = Dict() - d["ElectricTariff"]["demand_lookback_percent"] = 0.75 - d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] - d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak - d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) - d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) - d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak - d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] - d["ElectricTariff"]["demand_lookback_months"] = [1,0,0,1,0,0,0,0,0,0,0,1] # Jan, April, Dec - d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + @testset "MPC" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_mpc(model, "./scenarios/mpc.json") + @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0 + @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0 + @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0 + grid_draw = r["ElectricUtility"]["to_load_series_kw"] .+ r["ElectricUtility"]["to_battery_series_kw"] + # the grid draw limit in the 10th time step is set to 90 + # without the 90 limit the grid draw is 98 in the 10th time step + @test grid_draw[10] <= 90 + end + @testset "Complex Incentives" begin + """ + This test was compared against the API test: + reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives + when using the hardcoded levelization_factor in this package's REoptInputs function. + The two LCC's matched within 0.00005%. (The Julia pkg LCC is 1.0971991e7) + """ m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, REoptInputs(Scenario(d))) - - monthly_peaks = [300,300,300,400,300,500,300,300,300,300,300,300] # 300 = 400*0.75. Sets peak in all months excpet April and June - expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) - @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost - - # 3. Testing custom rate from user with demand_lookback_range - d = JSON.parsefile("./scenarios/lookback_rate.json") - d["ElectricTariff"] = Dict() - d["ElectricTariff"]["demand_lookback_percent"] = 0.75 - d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] - d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak - d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) - d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) - d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak - d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] - d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 - d["ElectricTariff"]["demand_lookback_range"] = 6 + results = run_reopt(m, "./scenarios/incentives.json") + @test results["Financial"]["lcc"] ≈ 1.094596365e7 atol=5e4 + end - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, REoptInputs(Scenario(d))) + @testset verbose = true "Rate Structures" begin - monthly_peaks = [225, 225, 225, 400, 300, 500, 375, 375, 375, 375, 375, 375] - expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) - @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + @testset "Tiered Energy" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, "./scenarios/tiered_energy_rate.json") + @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 2342.88 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 24000.0 atol=0.1 + @test results["ElectricLoad"]["annual_calculated_kwh"] ≈ 24000.0 atol=0.1 + end - end + @testset "Lookback Demand Charges" begin + # 1. Testing rate from URDB + data = JSON.parsefile("./scenarios/lookback_rate.json") + # urdb_label used https://apps.openei.org/IURDB/rate/view/539f6a23ec4f024411ec8bf9#2__Demand + # has a demand charge lookback of 35% for all months with 2 different demand charges based on which month + data["ElectricLoad"]["loads_kw"] = ones(8760) + data["ElectricLoad"]["loads_kw"][8] = 100.0 + inputs = REoptInputs(Scenario(data)) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, inputs) + # Expected result is 100 kW demand for January, 35% of that for all other months and + # with 5x other $10.5/kW cold months and 6x $11.5/kW warm months + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 100 * (10.5 + 0.35*10.5*5 + 0.35*11.5*6) + + # 2. Testing custom rate from user with demand_lookback_months + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["demand_lookback_months"] = [1,0,0,1,0,0,0,0,0,0,0,1] # Jan, April, Dec + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [300,300,300,400,300,500,300,300,300,300,300,300] # 300 = 400*0.75. Sets peak in all months excpet April and June + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + + # 3. Testing custom rate from user with demand_lookback_range + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + d["ElectricTariff"]["demand_lookback_range"] = 6 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [225, 225, 225, 400, 300, 500, 375, 375, 375, 375, 375, 375] + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost - @testset "Blended tariff" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/no_techs.json") - @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 1000.0 - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 136.99 - end + end - @testset "Coincident Peak Charges" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/coincident_peak.json") - @test results["ElectricTariff"]["year_one_coincident_peak_cost_before_tax"] ≈ 15.0 - end + @testset "Blended tariff" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/no_techs.json") + @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 1000.0 + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 136.99 + end - @testset "URDB sell rate" begin - #= The URDB contains at least one "Customer generation" tariff that only has a "sell" key in the energyratestructure (the tariff tested here) - =# - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - p = REoptInputs("./scenarios/URDB_customer_generation.json") - results = run_reopt(model, p) - @test results["PV"]["size_kw"] ≈ p.max_sizes["PV"] - end + @testset "Coincident Peak Charges" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/coincident_peak.json") + @test results["ElectricTariff"]["year_one_coincident_peak_cost_before_tax"] ≈ 15.0 + end - @testset "Custom URDB with Sub-Hourly" begin - # Avoid excessive JuMP warning messages about += with Expressions - logger = SimpleLogger() - with_logger(logger) do - # Testing a 15-min post with a urdb_response with multiple n_energy_tiers - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) - p = REoptInputs("./scenarios/subhourly_with_urdb.json") + @testset "URDB sell rate" begin + #= The URDB contains at least one "Customer generation" tariff that only has a "sell" key in the energyratestructure (the tariff tested here) + =# + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + p = REoptInputs("./scenarios/URDB_customer_generation.json") results = run_reopt(model, p) - @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 - @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw + @test results["PV"]["size_kw"] ≈ p.max_sizes["PV"] end - end - @testset "Multi-tier demand and energy rates" begin - #This test ensures that when multiple energy or demand regimes are included, that the tier limits load appropriately - d = JSON.parsefile("./scenarios/no_techs.json") - d["ElectricTariff"] = Dict() - d["ElectricTariff"]["urdb_response"] = JSON.parsefile("./scenarios/multi_tier_urdb_response.json") - s = Scenario(d) - p = REoptInputs(s) - @test p.s.electric_tariff.tou_demand_tier_limits[1, 1] ≈ 1.0e8 atol=1.0 - @test p.s.electric_tariff.tou_demand_tier_limits[1, 2] ≈ 1.0e8 atol=1.0 - @test p.s.electric_tariff.tou_demand_tier_limits[2, 1] ≈ 100.0 atol=1.0 - @test p.s.electric_tariff.tou_demand_tier_limits[2, 2] ≈ 1.0e8 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[1, 1] ≈ 1.0e10 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[1, 2] ≈ 1.0e10 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[6, 1] ≈ 20000.0 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[6, 2] ≈ 1.0e10 atol=1.0 - end + @testset "Custom URDB with Sub-Hourly" begin + # Avoid excessive JuMP warning messages about += with Expressions + logger = SimpleLogger() + with_logger(logger) do + # Testing a 15-min post with a urdb_response with multiple n_energy_tiers + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) + p = REoptInputs("./scenarios/subhourly_with_urdb.json") + results = run_reopt(model, p) + @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 + @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw + end + end - @testset "Tiered TOU Demand" begin - data = JSON.parsefile("./scenarios/tiered_tou_demand.json") - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, data) - max_demand = data["ElectricLoad"]["annual_kwh"] / 8760 - tier1_max = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["max"] - tier1_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["rate"] - tier2_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][2]["rate"] - expected_demand_charges = 12 * (tier1_max * tier1_rate + (max_demand - tier1_max) * tier2_rate) - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_charges atol=1 - end + @testset "Multi-tier demand and energy rates" begin + #This test ensures that when multiple energy or demand regimes are included, that the tier limits load appropriately + d = JSON.parsefile("./scenarios/no_techs.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["urdb_response"] = JSON.parsefile("./scenarios/multi_tier_urdb_response.json") + s = Scenario(d) + p = REoptInputs(s) + @test p.s.electric_tariff.tou_demand_tier_limits[1, 1] ≈ 1.0e8 atol=1.0 + @test p.s.electric_tariff.tou_demand_tier_limits[1, 2] ≈ 1.0e8 atol=1.0 + @test p.s.electric_tariff.tou_demand_tier_limits[2, 1] ≈ 100.0 atol=1.0 + @test p.s.electric_tariff.tou_demand_tier_limits[2, 2] ≈ 1.0e8 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[1, 1] ≈ 1.0e10 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[1, 2] ≈ 1.0e10 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[6, 1] ≈ 20000.0 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[6, 2] ≈ 1.0e10 atol=1.0 + end - # # tiered monthly demand rate TODO: expected results? - # m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - # data = JSON.parsefile("./scenarios/tiered_energy_rate.json") - # data["ElectricTariff"]["urdb_label"] = "59bc22705457a3372642da67" - # s = Scenario(data) - # inputs = REoptInputs(s) - # results = run_reopt(m, inputs) - - @testset "Non-Standard Units for Energy Rates" begin - d = JSON.parsefile("./scenarios/no_techs.json") - d["ElectricTariff"] = Dict( - "urdb_label" => "6272e4ae7eb76766c247d469" - ) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test occursin("URDB energy tiers have non-standard units of", string(results["Messages"])) - end + @testset "Tiered TOU Demand" begin + data = JSON.parsefile("./scenarios/tiered_tou_demand.json") + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, data) + max_demand = data["ElectricLoad"]["annual_kwh"] / 8760 + tier1_max = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["max"] + tier1_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["rate"] + tier2_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][2]["rate"] + expected_demand_charges = 12 * (tier1_max * tier1_rate + (max_demand - tier1_max) * tier2_rate) + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_charges atol=1 + end - end + # # tiered monthly demand rate TODO: expected results? + # m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + # data = JSON.parsefile("./scenarios/tiered_energy_rate.json") + # data["ElectricTariff"]["urdb_label"] = "59bc22705457a3372642da67" + # s = Scenario(data) + # inputs = REoptInputs(s) + # results = run_reopt(m, inputs) + + @testset "Non-Standard Units for Energy Rates" begin + d = JSON.parsefile("./scenarios/no_techs.json") + d["ElectricTariff"] = Dict( + "urdb_label" => "6272e4ae7eb76766c247d469" + ) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test occursin("URDB energy tiers have non-standard units of", string(results["Messages"])) + end - @testset "EASIUR" begin - d = JSON.parsefile("./scenarios/pv.json") - d["Site"]["latitude"] = 30.2672 - d["Site"]["longitude"] = -97.7431 - scen = Scenario(d) - @test scen.financial.NOx_grid_cost_per_tonne ≈ 5510.61 atol=0.1 - end + end - @testset "Wind" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d = JSON.parsefile("./scenarios/wind.json") - results = run_reopt(m, d) - @test results["Wind"]["size_kw"] ≈ 3752 atol=0.1 - @test results["Financial"]["lcc"] ≈ 8.591017e6 rtol=1e-5 - #= - 0.5% higher LCC in this package as compared to API ? 8,591,017 vs 8,551,172 - - both have zero curtailment - - same energy to grid: 5,839,317 vs 5,839,322 - - same energy to load: 4,160,683 vs 4,160,677 - - same city: Boulder - - same total wind prod factor - - REopt.jl has: - - bigger turbine: 3752 vs 3735 - - net_capital_costs_plus_om: 8,576,590 vs. 8,537,480 + @testset "EASIUR" begin + d = JSON.parsefile("./scenarios/pv.json") + d["Site"]["latitude"] = 30.2672 + d["Site"]["longitude"] = -97.7431 + scen = Scenario(d) + @test scen.financial.NOx_grid_cost_per_tonne ≈ 5510.61 atol=0.1 + end - TODO: will these discrepancies be addressed once NMIL binaries are added? - =# + @testset "Wind" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d = JSON.parsefile("./scenarios/wind.json") + results = run_reopt(m, d) + @test results["Wind"]["size_kw"] ≈ 3752 atol=0.1 + @test results["Financial"]["lcc"] ≈ 8.591017e6 rtol=1e-5 + #= + 0.5% higher LCC in this package as compared to API ? 8,591,017 vs 8,551,172 + - both have zero curtailment + - same energy to grid: 5,839,317 vs 5,839,322 + - same energy to load: 4,160,683 vs 4,160,677 + - same city: Boulder + - same total wind prod factor + + REopt.jl has: + - bigger turbine: 3752 vs 3735 + - net_capital_costs_plus_om: 8,576,590 vs. 8,537,480 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d["Site"]["land_acres"] = 60 # = 2 MW (with 0.03 acres/kW) - results = run_reopt(m, d) - @test results["Wind"]["size_kw"] == 2000.0 # Wind should be constrained by land_acres + TODO: will these discrepancies be addressed once NMIL binaries are added? + =# - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d["Wind"]["min_kw"] = 2001 # min_kw greater than land-constrained max should error - results = run_reopt(m, d) - @test "errors" ∈ keys(results["Messages"]) - @test length(results["Messages"]["errors"]) > 0 - end + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d["Site"]["land_acres"] = 60 # = 2 MW (with 0.03 acres/kW) + results = run_reopt(m, d) + @test results["Wind"]["size_kw"] == 2000.0 # Wind should be constrained by land_acres - @testset "Multiple PVs" begin - logger = SimpleLogger() - with_logger(logger) do - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json") - - ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])] - roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])] - roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])] - - @test ground_pv["size_kw"] ≈ 15 atol=0.1 - @test roof_west["size_kw"] ≈ 7 atol=0.1 - @test roof_east["size_kw"] ≈ 4 atol=0.1 - @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 - @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 - @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 - @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 - @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d["Wind"]["min_kw"] = 2001 # min_kw greater than land-constrained max should error + results = run_reopt(m, d) + @test "errors" ∈ keys(results["Messages"]) + @test length(results["Messages"]["errors"]) > 0 end - end - @testset "Thermal Energy Storage + Absorption Chiller" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - data = JSON.parsefile("./scenarios/thermal_storage.json") - s = Scenario(data) - p = REoptInputs(s) + @testset "Multiple PVs" begin + logger = SimpleLogger() + with_logger(logger) do + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json") + + ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])] + roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])] + roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])] + + @test ground_pv["size_kw"] ≈ 15 atol=0.1 + @test roof_west["size_kw"] ≈ 7 atol=0.1 + @test roof_east["size_kw"] ≈ 4 atol=0.1 + @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 + @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 + @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 + @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 + @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 + @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 + @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 + end + end + + @testset "Thermal Energy Storage + Absorption Chiller" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + data = JSON.parsefile("./scenarios/thermal_storage.json") + s = Scenario(data) + p = REoptInputs(s) + + #test for get_absorption_chiller_defaults consistency with inputs data and Scenario s. + htf_defaults_response = get_absorption_chiller_defaults(; + thermal_consumption_hot_water_or_steam=get(data["AbsorptionChiller"], "thermal_consumption_hot_water_or_steam", nothing), + boiler_type=get(data["ExistingBoiler"], "production_type", nothing), + load_max_tons=maximum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) + ) - #test for get_absorption_chiller_defaults consistency with inputs data and Scenario s. - htf_defaults_response = get_absorption_chiller_defaults(; - thermal_consumption_hot_water_or_steam=get(data["AbsorptionChiller"], "thermal_consumption_hot_water_or_steam", nothing), - boiler_type=get(data["ExistingBoiler"], "production_type", nothing), - load_max_tons=maximum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) - ) - - expected_installed_cost_per_ton = htf_defaults_response["default_inputs"]["installed_cost_per_ton"] - expected_om_cost_per_ton = htf_defaults_response["default_inputs"]["om_cost_per_ton"] - - @test p.s.absorption_chiller.installed_cost_per_kw ≈ expected_installed_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 - @test p.s.absorption_chiller.om_cost_per_kw ≈ expected_om_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 - @test p.s.absorption_chiller.cop_thermal ≈ htf_defaults_response["default_inputs"]["cop_thermal"] atol=0.001 - - #load test values - p.s.absorption_chiller.installed_cost_per_kw = 500.0 / REopt.KWH_THERMAL_PER_TONHOUR - p.s.absorption_chiller.om_cost_per_kw = 0.5 / REopt.KWH_THERMAL_PER_TONHOUR - p.s.absorption_chiller.cop_thermal = 0.7 - - #Make every other hour zero fuel and electric cost; storage should charge and discharge in each period - for ts in p.time_steps - #heating and cooling loads only - if ts % 2 == 0 #in even periods, there is a nonzero load and energy is higher cost, and storage should discharge - p.s.electric_load.loads_kw[ts] = 10 - p.s.dhw_load.loads_kw[ts] = 5 - p.s.space_heating_load.loads_kw[ts] = 5 - p.s.cooling_load.loads_kw_thermal[ts] = 10 - p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 100 - for tier in 1:p.s.electric_tariff.n_energy_tiers - p.s.electric_tariff.energy_rates[ts, tier] = 100 - end - else #in odd periods, there is no load and energy is cheaper - storage should charge - p.s.electric_load.loads_kw[ts] = 0 - p.s.dhw_load.loads_kw[ts] = 0 - p.s.space_heating_load.loads_kw[ts] = 0 - p.s.cooling_load.loads_kw_thermal[ts] = 0 - p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 1 - for tier in 1:p.s.electric_tariff.n_energy_tiers - p.s.electric_tariff.energy_rates[ts, tier] = 50 + expected_installed_cost_per_ton = htf_defaults_response["default_inputs"]["installed_cost_per_ton"] + expected_om_cost_per_ton = htf_defaults_response["default_inputs"]["om_cost_per_ton"] + + @test p.s.absorption_chiller.installed_cost_per_kw ≈ expected_installed_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 + @test p.s.absorption_chiller.om_cost_per_kw ≈ expected_om_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 + @test p.s.absorption_chiller.cop_thermal ≈ htf_defaults_response["default_inputs"]["cop_thermal"] atol=0.001 + + #load test values + p.s.absorption_chiller.installed_cost_per_kw = 500.0 / REopt.KWH_THERMAL_PER_TONHOUR + p.s.absorption_chiller.om_cost_per_kw = 0.5 / REopt.KWH_THERMAL_PER_TONHOUR + p.s.absorption_chiller.cop_thermal = 0.7 + + #Make every other hour zero fuel and electric cost; storage should charge and discharge in each period + for ts in p.time_steps + #heating and cooling loads only + if ts % 2 == 0 #in even periods, there is a nonzero load and energy is higher cost, and storage should discharge + p.s.electric_load.loads_kw[ts] = 10 + p.s.dhw_load.loads_kw[ts] = 5 + p.s.space_heating_load.loads_kw[ts] = 5 + p.s.cooling_load.loads_kw_thermal[ts] = 10 + p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 100 + for tier in 1:p.s.electric_tariff.n_energy_tiers + p.s.electric_tariff.energy_rates[ts, tier] = 100 + end + else #in odd periods, there is no load and energy is cheaper - storage should charge + p.s.electric_load.loads_kw[ts] = 0 + p.s.dhw_load.loads_kw[ts] = 0 + p.s.space_heating_load.loads_kw[ts] = 0 + p.s.cooling_load.loads_kw_thermal[ts] = 0 + p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 1 + for tier in 1:p.s.electric_tariff.n_energy_tiers + p.s.electric_tariff.energy_rates[ts, tier] = 50 + end end end + + r = run_reopt(model, p) + + #dispatch to load should be 10kW every other period = 4,380 * 10 + @test sum(r["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"]) ≈ 149.45 atol=0.1 + @test sum(r["ColdThermalStorage"]["storage_to_load_series_ton"]) ≈ 12454.33 atol=0.1 + #size should be just over 10kW in gallons, accounting for efficiency losses and min SOC + @test r["HotThermalStorage"]["size_gal"] ≈ 233.0 atol=0.1 + @test r["ColdThermalStorage"]["size_gal"] ≈ 378.0 atol=0.1 + #No production from existing chiller, only absorption chiller, which is sized at ~5kW to manage electric demand charge & capital cost. + @test r["ExistingChiller"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=0.1 + @test r["AbsorptionChiller"]["annual_thermal_production_tonhour"] ≈ 12464.15 atol=0.1 + @test r["AbsorptionChiller"]["size_ton"] ≈ 2.846 atol=0.01 end - - r = run_reopt(model, p) - - #dispatch to load should be 10kW every other period = 4,380 * 10 - @test sum(r["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"]) ≈ 149.45 atol=0.1 - @test sum(r["ColdThermalStorage"]["storage_to_load_series_ton"]) ≈ 12454.33 atol=0.1 - #size should be just over 10kW in gallons, accounting for efficiency losses and min SOC - @test r["HotThermalStorage"]["size_gal"] ≈ 233.0 atol=0.1 - @test r["ColdThermalStorage"]["size_gal"] ≈ 378.0 atol=0.1 - #No production from existing chiller, only absorption chiller, which is sized at ~5kW to manage electric demand charge & capital cost. - @test r["ExistingChiller"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=0.1 - @test r["AbsorptionChiller"]["annual_thermal_production_tonhour"] ≈ 12464.15 atol=0.1 - @test r["AbsorptionChiller"]["size_ton"] ≈ 2.846 atol=0.01 - end - @testset "Heat and cool energy balance" begin - """ - - This is an "energy balance" type of test which tests the model formulation/math as opposed - to a specific scenario. This test is robust to changes in the model "MIPRELSTOP" or "MAXTIME" setting + @testset "Heat and cool energy balance" begin + """ - Validation to ensure that: - 1) The electric and absorption chillers are supplying 100% of the cooling thermal load plus losses from ColdThermalStorage - 2) The boiler and CHP are supplying the heating load plus additional absorption chiller thermal load - 3) The Cold and Hot TES efficiency (charge loss and thermal decay) are being tracked properly + This is an "energy balance" type of test which tests the model formulation/math as opposed + to a specific scenario. This test is robust to changes in the model "MIPRELSTOP" or "MAXTIME" setting - """ - input_data = JSON.parsefile("./scenarios/heat_cool_energy_balance_inputs.json") - s = Scenario(input_data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - # Annual cooling **thermal** energy load of CRB is based on annual cooling electric energy (from CRB models) and a conditional COP depending on the peak cooling thermal load - # 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, - 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 - - #Test cooling load results - @test round(cooling_thermal_load_tonhour_total, digits=1) ≈ results["CoolingLoad"]["annual_calculated_tonhour"] atol=1.0 - - # Convert fuel input to thermal using user input boiler efficiency - boiler_thermal_load_mmbtu_total = (671.40531 + 11570.9155) * input_data["ExistingBoiler"]["efficiency"] # From CRB models, in heating_cooling_loads.jl, BuiltInDomesticHotWaterLoad + BuiltInSpaceHeatingLoad data for location (SanFrancisco Hospital) - boiler_fuel_consumption_total_mod_efficiency = boiler_thermal_load_mmbtu_total / inputs.s.existing_boiler.efficiency - - # Cooling outputs - cooling_elecchl_tons_to_load_series = results["ExistingChiller"]["thermal_to_load_series_ton"] - cooling_elecchl_tons_to_tes_series = results["ExistingChiller"]["thermal_to_storage_series_ton"] - cooling_absorpchl_tons_to_load_series = results["AbsorptionChiller"]["thermal_to_load_series_ton"] - cooling_absorpchl_tons_to_tes_series = results["AbsorptionChiller"]["thermal_to_storage_series_ton"] - cooling_tonhour_to_load_tech_total = sum(cooling_elecchl_tons_to_load_series) + sum(cooling_absorpchl_tons_to_load_series) - cooling_tonhour_to_tes_total = sum(cooling_elecchl_tons_to_tes_series) + sum(cooling_absorpchl_tons_to_tes_series) - cooling_tes_tons_to_load_series = results["ColdThermalStorage"]["storage_to_load_series_ton"] - cooling_extra_from_tes_losses = cooling_tonhour_to_tes_total - sum(cooling_tes_tons_to_load_series) - tes_effic_with_decay = sum(cooling_tes_tons_to_load_series) / cooling_tonhour_to_tes_total - cooling_total_prod_from_techs = cooling_tonhour_to_load_tech_total + cooling_tonhour_to_tes_total - cooling_load_plus_tes_losses = cooling_thermal_load_tonhour_total + cooling_extra_from_tes_losses - - # Absorption Chiller electric consumption addition - absorpchl_total_cooling_produced_series_ton = cooling_absorpchl_tons_to_load_series .+ cooling_absorpchl_tons_to_tes_series - absorpchl_total_cooling_produced_ton_hour = sum(absorpchl_total_cooling_produced_series_ton) - absorpchl_electric_consumption_total_kwh = results["AbsorptionChiller"]["annual_electric_consumption_kwh"] - absorpchl_cop_elec = s.absorption_chiller.cop_electric - - # Check if sum of electric and absorption chillers equals cooling thermal total - @test tes_effic_with_decay < 0.97 - @test round(cooling_total_prod_from_techs, digits=0) ≈ cooling_load_plus_tes_losses atol=5.0 - @test round(absorpchl_electric_consumption_total_kwh, digits=0) ≈ absorpchl_total_cooling_produced_ton_hour * REopt.KWH_THERMAL_PER_TONHOUR / absorpchl_cop_elec atol=1.0 - - # Heating outputs - boiler_fuel_consumption_calculated = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] - boiler_thermal_series = results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"] - boiler_to_load_series = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] - boiler_thermal_to_tes_series = results["ExistingBoiler"]["thermal_to_storage_series_mmbtu_per_hour"] - chp_thermal_to_load_series = results["CHP"]["thermal_to_load_series_mmbtu_per_hour"] - chp_thermal_to_tes_series = results["CHP"]["thermal_to_storage_series_mmbtu_per_hour"] - chp_thermal_to_waste_series = results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"] - absorpchl_thermal_series = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] - hot_tes_mmbtu_per_hour_to_load_series = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] - tes_inflows = sum(chp_thermal_to_tes_series) + sum(boiler_thermal_to_tes_series) - total_chp_production = sum(chp_thermal_to_load_series) + sum(chp_thermal_to_waste_series) + sum(chp_thermal_to_tes_series) - tes_outflows = sum(hot_tes_mmbtu_per_hour_to_load_series) - total_thermal_expected = boiler_thermal_load_mmbtu_total + sum(chp_thermal_to_waste_series) + tes_inflows + sum(absorpchl_thermal_series) - boiler_fuel_expected = (total_thermal_expected - total_chp_production - tes_outflows) / inputs.s.existing_boiler.efficiency - total_thermal_mmbtu_calculated = sum(boiler_thermal_series) + total_chp_production + tes_outflows - - @test round(boiler_fuel_consumption_calculated, digits=0) ≈ boiler_fuel_expected atol=8.0 - @test round(total_thermal_mmbtu_calculated, digits=0) ≈ total_thermal_expected atol=8.0 - - # Test CHP["cooling_thermal_factor"] = 0.8, AbsorptionChiller["cop_thermal"] = 0.7 (from inputs .json) - absorpchl_heat_in_kwh = results["AbsorptionChiller"]["annual_thermal_consumption_mmbtu"] * REopt.KWH_PER_MMBTU - absorpchl_cool_out_kwh = results["AbsorptionChiller"]["annual_thermal_production_tonhour"] * REopt.KWH_THERMAL_PER_TONHOUR - absorpchl_cop = absorpchl_cool_out_kwh / absorpchl_heat_in_kwh - - @test round(absorpchl_cop, digits=5) ≈ 0.8*0.7 rtol=1e-4 - end + Validation to ensure that: + 1) The electric and absorption chillers are supplying 100% of the cooling thermal load plus losses from ColdThermalStorage + 2) The boiler and CHP are supplying the heating load plus additional absorption chiller thermal load + 3) The Cold and Hot TES efficiency (charge loss and thermal decay) are being tracked properly - @testset "Heating and cooling inputs + CHP defaults" begin - """ + """ + input_data = JSON.parsefile("./scenarios/heat_cool_energy_balance_inputs.json") + s = Scenario(input_data) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) - This tests the various ways to input heating and cooling loads to make sure they are processed correctly. - There are no "new" technologies in this test, so heating is served by ExistingBoiler, and - cooling is served by ExistingCooler. Since this is just inputs processing tests, no optimization is needed. + # Annual cooling **thermal** energy load of CRB is based on annual cooling electric energy (from CRB models) and a conditional COP depending on the peak cooling thermal load + # 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, + 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 - """ - input_data = JSON.parsefile("./scenarios/heating_cooling_load_inputs.json") - s = Scenario(input_data) - inputs = REoptInputs(s) + #Test cooling load results + @test round(cooling_thermal_load_tonhour_total, digits=1) ≈ results["CoolingLoad"]["annual_calculated_tonhour"] atol=1.0 + + # Convert fuel input to thermal using user input boiler efficiency + boiler_thermal_load_mmbtu_total = (671.40531 + 11570.9155) * input_data["ExistingBoiler"]["efficiency"] # From CRB models, in heating_cooling_loads.jl, BuiltInDomesticHotWaterLoad + BuiltInSpaceHeatingLoad data for location (SanFrancisco Hospital) + boiler_fuel_consumption_total_mod_efficiency = boiler_thermal_load_mmbtu_total / inputs.s.existing_boiler.efficiency + + # Cooling outputs + cooling_elecchl_tons_to_load_series = results["ExistingChiller"]["thermal_to_load_series_ton"] + cooling_elecchl_tons_to_tes_series = results["ExistingChiller"]["thermal_to_storage_series_ton"] + cooling_absorpchl_tons_to_load_series = results["AbsorptionChiller"]["thermal_to_load_series_ton"] + cooling_absorpchl_tons_to_tes_series = results["AbsorptionChiller"]["thermal_to_storage_series_ton"] + cooling_tonhour_to_load_tech_total = sum(cooling_elecchl_tons_to_load_series) + sum(cooling_absorpchl_tons_to_load_series) + cooling_tonhour_to_tes_total = sum(cooling_elecchl_tons_to_tes_series) + sum(cooling_absorpchl_tons_to_tes_series) + cooling_tes_tons_to_load_series = results["ColdThermalStorage"]["storage_to_load_series_ton"] + cooling_extra_from_tes_losses = cooling_tonhour_to_tes_total - sum(cooling_tes_tons_to_load_series) + tes_effic_with_decay = sum(cooling_tes_tons_to_load_series) / cooling_tonhour_to_tes_total + cooling_total_prod_from_techs = cooling_tonhour_to_load_tech_total + cooling_tonhour_to_tes_total + cooling_load_plus_tes_losses = cooling_thermal_load_tonhour_total + cooling_extra_from_tes_losses + + # Absorption Chiller electric consumption addition + absorpchl_total_cooling_produced_series_ton = cooling_absorpchl_tons_to_load_series .+ cooling_absorpchl_tons_to_tes_series + absorpchl_total_cooling_produced_ton_hour = sum(absorpchl_total_cooling_produced_series_ton) + absorpchl_electric_consumption_total_kwh = results["AbsorptionChiller"]["annual_electric_consumption_kwh"] + absorpchl_cop_elec = s.absorption_chiller.cop_electric + + # Check if sum of electric and absorption chillers equals cooling thermal total + @test tes_effic_with_decay < 0.97 + @test round(cooling_total_prod_from_techs, digits=0) ≈ cooling_load_plus_tes_losses atol=5.0 + @test round(absorpchl_electric_consumption_total_kwh, digits=0) ≈ absorpchl_total_cooling_produced_ton_hour * REopt.KWH_THERMAL_PER_TONHOUR / absorpchl_cop_elec atol=1.0 + + # Heating outputs + boiler_fuel_consumption_calculated = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] + boiler_thermal_series = results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"] + boiler_to_load_series = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] + boiler_thermal_to_tes_series = results["ExistingBoiler"]["thermal_to_storage_series_mmbtu_per_hour"] + chp_thermal_to_load_series = results["CHP"]["thermal_to_load_series_mmbtu_per_hour"] + chp_thermal_to_tes_series = results["CHP"]["thermal_to_storage_series_mmbtu_per_hour"] + chp_thermal_to_waste_series = results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"] + absorpchl_thermal_series = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] + hot_tes_mmbtu_per_hour_to_load_series = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] + tes_inflows = sum(chp_thermal_to_tes_series) + sum(boiler_thermal_to_tes_series) + total_chp_production = sum(chp_thermal_to_load_series) + sum(chp_thermal_to_waste_series) + sum(chp_thermal_to_tes_series) + tes_outflows = sum(hot_tes_mmbtu_per_hour_to_load_series) + total_thermal_expected = boiler_thermal_load_mmbtu_total + sum(chp_thermal_to_waste_series) + tes_inflows + sum(absorpchl_thermal_series) + boiler_fuel_expected = (total_thermal_expected - total_chp_production - tes_outflows) / inputs.s.existing_boiler.efficiency + total_thermal_mmbtu_calculated = sum(boiler_thermal_series) + total_chp_production + tes_outflows + + @test round(boiler_fuel_consumption_calculated, digits=0) ≈ boiler_fuel_expected atol=8.0 + @test round(total_thermal_mmbtu_calculated, digits=0) ≈ total_thermal_expected atol=8.0 + + # Test CHP["cooling_thermal_factor"] = 0.8, AbsorptionChiller["cop_thermal"] = 0.7 (from inputs .json) + absorpchl_heat_in_kwh = results["AbsorptionChiller"]["annual_thermal_consumption_mmbtu"] * REopt.KWH_PER_MMBTU + absorpchl_cool_out_kwh = results["AbsorptionChiller"]["annual_thermal_production_tonhour"] * REopt.KWH_THERMAL_PER_TONHOUR + absorpchl_cop = absorpchl_cool_out_kwh / absorpchl_heat_in_kwh + + @test round(absorpchl_cop, digits=5) ≈ 0.8*0.7 rtol=1e-4 + end - # Heating load is input as **fuel**, not thermal - # If boiler efficiency is not input, we use REopt.EXISTING_BOILER_EFFICIENCY to convert fuel to thermal - expected_fuel = input_data["SpaceHeatingLoad"]["annual_mmbtu"] + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] - total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU - @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY atol=1.0 - total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency - @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY / inputs.s.existing_boiler.efficiency atol=1.0 - # If boiler efficiency is input, use that with annual or monthly mmbtu input to convert fuel to thermal - input_data["ExistingBoiler"]["efficiency"] = 0.72 - s = Scenario(input_data) - inputs = REoptInputs(s) - total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU - @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] atol=1.0 - total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency - @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] / inputs.s.existing_boiler.efficiency atol=1.0 + @testset "Heating and cooling inputs + CHP defaults" begin + """ - # The expected cooling load is based on the default **fraction of total electric** profile for the doe_reference_name when annual_tonhour is NOT input - # the 320540.0 kWh number is from the default LargeOffice fraction of total electric profile applied to the Hospital default total electric profile - total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.existing_chiller.cop - @test round(total_chiller_electric_consumption, digits=0) ≈ 320544.0 atol=1.0 # loads_kw is **electric**, loads_kw_thermal is **thermal** + This tests the various ways to input heating and cooling loads to make sure they are processed correctly. + There are no "new" technologies in this test, so heating is served by ExistingBoiler, and + cooling is served by ExistingCooler. Since this is just inputs processing tests, no optimization is needed. - #Test CHP defaults use average fuel load, size class 2 for recip_engine - @test inputs.s.chp.min_allowable_kw ≈ 50.0 atol=0.01 - @test inputs.s.chp.om_cost_per_kwh ≈ 0.0235 atol=0.0001 + """ + input_data = JSON.parsefile("./scenarios/heating_cooling_load_inputs.json") + s = Scenario(input_data) + inputs = REoptInputs(s) - delete!(input_data, "SpaceHeatingLoad") - delete!(input_data, "DomesticHotWaterLoad") - annual_fraction_of_electric_load_input = 0.5 - input_data["CoolingLoad"] = Dict{Any, Any}("annual_fraction_of_electric_load" => annual_fraction_of_electric_load_input) + # Heating load is input as **fuel**, not thermal + # If boiler efficiency is not input, we use REopt.EXISTING_BOILER_EFFICIENCY to convert fuel to thermal + expected_fuel = input_data["SpaceHeatingLoad"]["annual_mmbtu"] + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] + total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU + @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY atol=1.0 + total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency + @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY / inputs.s.existing_boiler.efficiency atol=1.0 + # If boiler efficiency is input, use that with annual or monthly mmbtu input to convert fuel to thermal + input_data["ExistingBoiler"]["efficiency"] = 0.72 + s = Scenario(input_data) + inputs = REoptInputs(s) + total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU + @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] atol=1.0 + total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency + @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] / inputs.s.existing_boiler.efficiency atol=1.0 + + # The expected cooling load is based on the default **fraction of total electric** profile for the doe_reference_name when annual_tonhour is NOT input + # the 320540.0 kWh number is from the default LargeOffice fraction of total electric profile applied to the Hospital default total electric profile + total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.existing_chiller.cop + @test round(total_chiller_electric_consumption, digits=0) ≈ 320544.0 atol=1.0 # loads_kw is **electric**, loads_kw_thermal is **thermal** + + #Test CHP defaults use average fuel load, size class 2 for recip_engine + @test inputs.s.chp.min_allowable_kw ≈ 50.0 atol=0.01 + @test inputs.s.chp.om_cost_per_kwh ≈ 0.0235 atol=0.0001 + + delete!(input_data, "SpaceHeatingLoad") + delete!(input_data, "DomesticHotWaterLoad") + annual_fraction_of_electric_load_input = 0.5 + input_data["CoolingLoad"] = Dict{Any, Any}("annual_fraction_of_electric_load" => annual_fraction_of_electric_load_input) + + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + expected_cooling_electricity = sum(inputs.s.electric_load.loads_kw) * annual_fraction_of_electric_load_input + total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop + @test round(total_chiller_electric_consumption, digits=0) ≈ round(expected_cooling_electricity) atol=1.0 + @test round(total_chiller_electric_consumption, digits=0) ≈ 3876410 atol=1.0 - expected_cooling_electricity = sum(inputs.s.electric_load.loads_kw) * annual_fraction_of_electric_load_input - total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop - @test round(total_chiller_electric_consumption, digits=0) ≈ round(expected_cooling_electricity) atol=1.0 - @test round(total_chiller_electric_consumption, digits=0) ≈ 3876410 atol=1.0 + # Check that without heating load or max_kw input, CHP.max_kw gets set based on peak electric load + @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.01 - # Check that without heating load or max_kw input, CHP.max_kw gets set based on peak electric load - @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.01 + input_data["SpaceHeatingLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) + input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) + input_data["CoolingLoad"] = Dict{Any, Any}("monthly_fractions_of_electric_load" => repeat([0.1], 12)) - input_data["SpaceHeatingLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) - input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) - input_data["CoolingLoad"] = Dict{Any, Any}("monthly_fractions_of_electric_load" => repeat([0.1], 12)) + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + #Test CHP defaults use average fuel load, size class changes to 3 + @test inputs.s.chp.min_allowable_kw ≈ 125.0 atol=0.1 + @test inputs.s.chp.om_cost_per_kwh ≈ 0.021 atol=0.0001 + #Update CHP prime_mover and test new defaults + input_data["CHP"]["prime_mover"] = "combustion_turbine" + input_data["CHP"]["size_class"] = 1 + # Set max_kw higher than peak electric load so min_allowable_kw doesn't get assigned to max_kw + input_data["CHP"]["max_kw"] = 2500.0 - #Test CHP defaults use average fuel load, size class changes to 3 - @test inputs.s.chp.min_allowable_kw ≈ 125.0 atol=0.1 - @test inputs.s.chp.om_cost_per_kwh ≈ 0.021 atol=0.0001 - #Update CHP prime_mover and test new defaults - input_data["CHP"]["prime_mover"] = "combustion_turbine" - input_data["CHP"]["size_class"] = 1 - # Set max_kw higher than peak electric load so min_allowable_kw doesn't get assigned to max_kw - input_data["CHP"]["max_kw"] = 2500.0 + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + @test inputs.s.chp.min_allowable_kw ≈ 2000.0 atol=0.1 + @test inputs.s.chp.om_cost_per_kwh ≈ 0.014499999999999999 atol=0.0001 - @test inputs.s.chp.min_allowable_kw ≈ 2000.0 atol=0.1 - @test inputs.s.chp.om_cost_per_kwh ≈ 0.014499999999999999 atol=0.0001 + total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + + sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU + @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 24000 atol=1.0 + total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop + @test round(total_chiller_electric_consumption, digits=0) ≈ 775282 atol=1.0 - total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + - sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU - @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 24000 atol=1.0 - total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop - @test round(total_chiller_electric_consumption, digits=0) ≈ 775282 atol=1.0 + input_data["SpaceHeatingLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) + input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) + input_data["CoolingLoad"] = Dict{Any, Any}("per_time_step_fractions_of_electric_load" => repeat([0.01], 8760)) - input_data["SpaceHeatingLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) - input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) - input_data["CoolingLoad"] = Dict{Any, Any}("per_time_step_fractions_of_electric_load" => repeat([0.01], 8760)) + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + + sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU + @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 8760 atol=0.1 + @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop, digits=0) ≈ 77528.0 atol=1.0 - total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + - sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU - @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 8760 atol=0.1 - @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop, digits=0) ≈ 77528.0 atol=1.0 + # Make sure annual_tonhour is preserved with conditional existing_chiller_default logic, where guess-and-correct method is applied + input_data["SpaceHeatingLoad"] = Dict{Any, Any}() + input_data["DomesticHotWaterLoad"] = Dict{Any, Any}() + annual_tonhour = 25000.0 + input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital", + "annual_tonhour" => annual_tonhour) + input_data["ExistingChiller"] = Dict{Any, Any}() - # Make sure annual_tonhour is preserved with conditional existing_chiller_default logic, where guess-and-correct method is applied - input_data["SpaceHeatingLoad"] = Dict{Any, Any}() - input_data["DomesticHotWaterLoad"] = Dict{Any, Any}() - annual_tonhour = 25000.0 - input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital", - "annual_tonhour" => annual_tonhour) - input_data["ExistingChiller"] = Dict{Any, Any}() + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / REopt.KWH_THERMAL_PER_TONHOUR, digits=0) ≈ annual_tonhour atol=1.0 + + # Test for prime generator CHP inputs (electric only) + # First get CHP cost to compare later with prime generator + input_data["ElectricLoad"] = Dict("doe_reference_name" => "FlatLoad", + "annual_kwh" => 876000) + input_data["ElectricTariff"] = Dict("blended_annual_energy_rate" => 0.06, + "blended_annual_demand_rate" => 0.0 ) + s_chp = Scenario(input_data) + inputs_chp = REoptInputs(s) + installed_cost_chp = s_chp.chp.installed_cost_per_kw + + # Now get prime generator (electric only) + input_data["CHP"]["is_electric_only"] = true + delete!(input_data["CHP"], "max_kw") + s = Scenario(input_data) + inputs = REoptInputs(s) + # Costs are 75% of CHP + @test inputs.s.chp.installed_cost_per_kw ≈ (0.75*installed_cost_chp) atol=1.0 + @test inputs.s.chp.om_cost_per_kwh ≈ (0.75*0.0145) atol=0.0001 + @test inputs.s.chp.federal_itc_fraction ≈ 0.0 atol=0.0001 + # Thermal efficiency set to zero + @test inputs.s.chp.thermal_efficiency_full_load == 0 + @test inputs.s.chp.thermal_efficiency_half_load == 0 + # Max size based on electric load, not heating load + @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.001 + end - @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / REopt.KWH_THERMAL_PER_TONHOUR, digits=0) ≈ annual_tonhour atol=1.0 - - # Test for prime generator CHP inputs (electric only) - # First get CHP cost to compare later with prime generator - input_data["ElectricLoad"] = Dict("doe_reference_name" => "FlatLoad", - "annual_kwh" => 876000) - input_data["ElectricTariff"] = Dict("blended_annual_energy_rate" => 0.06, - "blended_annual_demand_rate" => 0.0 ) - s_chp = Scenario(input_data) - inputs_chp = REoptInputs(s) - installed_cost_chp = s_chp.chp.installed_cost_per_kw - - # Now get prime generator (electric only) - input_data["CHP"]["is_electric_only"] = true - delete!(input_data["CHP"], "max_kw") - s = Scenario(input_data) - inputs = REoptInputs(s) - # Costs are 75% of CHP - @test inputs.s.chp.installed_cost_per_kw ≈ (0.75*installed_cost_chp) atol=1.0 - @test inputs.s.chp.om_cost_per_kwh ≈ (0.75*0.0145) atol=0.0001 - @test inputs.s.chp.federal_itc_fraction ≈ 0.0 atol=0.0001 - # Thermal efficiency set to zero - @test inputs.s.chp.thermal_efficiency_full_load == 0 - @test inputs.s.chp.thermal_efficiency_half_load == 0 - # Max size based on electric load, not heating load - @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.001 - end + @testset "Hybrid/blended heating and cooling loads" begin + """ - @testset "Hybrid/blended heating and cooling loads" begin - """ + This tests the hybrid/campus loads for heating and cooling, where a blended_doe_reference_names + and blended_doe_reference_percents are given and blended to create an aggregate load profile - This tests the hybrid/campus loads for heating and cooling, where a blended_doe_reference_names - and blended_doe_reference_percents are given and blended to create an aggregate load profile + """ + input_data = JSON.parsefile("./scenarios/hybrid_loads_heating_cooling_inputs.json") - """ - input_data = JSON.parsefile("./scenarios/hybrid_loads_heating_cooling_inputs.json") + hospital_fraction = 0.75 + hotel_fraction = 1.0 - hospital_fraction - hospital_fraction = 0.75 - hotel_fraction = 1.0 - hospital_fraction + # Hospital only + input_data["ElectricLoad"]["annual_kwh"] = hospital_fraction * 100 + input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hospital_fraction * 100 + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hospital_fraction * 100 + input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "Hospital" + input_data["CoolingLoad"]["doe_reference_name"] = "Hospital" - # Hospital only - input_data["ElectricLoad"]["annual_kwh"] = hospital_fraction * 100 - input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hospital_fraction * 100 - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hospital_fraction * 100 - input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "Hospital" - input_data["CoolingLoad"]["doe_reference_name"] = "Hospital" + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + elec_hospital = inputs.s.electric_load.loads_kw + space_hospital = inputs.s.space_heating_load.loads_kw # thermal + dhw_hospital = inputs.s.dhw_load.loads_kw # thermal + cooling_hospital = inputs.s.cooling_load.loads_kw_thermal # thermal + cooling_elec_frac_of_total_hospital = cooling_hospital / inputs.s.cooling_load.existing_chiller_cop ./ elec_hospital + + # Hotel only + input_data["ElectricLoad"]["annual_kwh"] = hotel_fraction * 100 + input_data["ElectricLoad"]["doe_reference_name"] = "LargeHotel" + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hotel_fraction * 100 + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "LargeHotel" + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hotel_fraction * 100 + input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "LargeHotel" + input_data["CoolingLoad"]["doe_reference_name"] = "LargeHotel" + + s = Scenario(input_data) + inputs = REoptInputs(s) - elec_hospital = inputs.s.electric_load.loads_kw - space_hospital = inputs.s.space_heating_load.loads_kw # thermal - dhw_hospital = inputs.s.dhw_load.loads_kw # thermal - cooling_hospital = inputs.s.cooling_load.loads_kw_thermal # thermal - cooling_elec_frac_of_total_hospital = cooling_hospital / inputs.s.cooling_load.existing_chiller_cop ./ elec_hospital - - # Hotel only - input_data["ElectricLoad"]["annual_kwh"] = hotel_fraction * 100 - input_data["ElectricLoad"]["doe_reference_name"] = "LargeHotel" - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hotel_fraction * 100 - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "LargeHotel" - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hotel_fraction * 100 - input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "LargeHotel" - input_data["CoolingLoad"]["doe_reference_name"] = "LargeHotel" + elec_hotel = inputs.s.electric_load.loads_kw + space_hotel = inputs.s.space_heating_load.loads_kw # thermal + dhw_hotel = inputs.s.dhw_load.loads_kw # thermal + cooling_hotel = inputs.s.cooling_load.loads_kw_thermal # thermal + cooling_elec_frac_of_total_hotel = cooling_hotel / inputs.s.cooling_load.existing_chiller_cop ./ elec_hotel - s = Scenario(input_data) - inputs = REoptInputs(s) + # Hybrid mix of hospital and hotel + # Remove previous assignment of doe_reference_name + for load in ["ElectricLoad", "SpaceHeatingLoad", "DomesticHotWaterLoad", "CoolingLoad"] + delete!(input_data[load], "doe_reference_name") + end + annual_energy = (hospital_fraction + hotel_fraction) * 100 + building_list = ["Hospital", "LargeHotel"] + percent_share_list = [hospital_fraction, hotel_fraction] + input_data["ElectricLoad"]["annual_kwh"] = annual_energy + input_data["ElectricLoad"]["blended_doe_reference_names"] = building_list + input_data["ElectricLoad"]["blended_doe_reference_percents"] = percent_share_list + + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = annual_energy + input_data["SpaceHeatingLoad"]["blended_doe_reference_names"] = building_list + input_data["SpaceHeatingLoad"]["blended_doe_reference_percents"] = percent_share_list + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = annual_energy + input_data["DomesticHotWaterLoad"]["blended_doe_reference_names"] = building_list + input_data["DomesticHotWaterLoad"]["blended_doe_reference_percents"] = percent_share_list + + # CoolingLoad now use a weighted fraction of total electric profile if no annual_tonhour is provided + input_data["CoolingLoad"]["blended_doe_reference_names"] = building_list + input_data["CoolingLoad"]["blended_doe_reference_percents"] = percent_share_list + + s = Scenario(input_data) + inputs = REoptInputs(s) - elec_hotel = inputs.s.electric_load.loads_kw - space_hotel = inputs.s.space_heating_load.loads_kw # thermal - dhw_hotel = inputs.s.dhw_load.loads_kw # thermal - cooling_hotel = inputs.s.cooling_load.loads_kw_thermal # thermal - cooling_elec_frac_of_total_hotel = cooling_hotel / inputs.s.cooling_load.existing_chiller_cop ./ elec_hotel + elec_hybrid = inputs.s.electric_load.loads_kw + space_hybrid = inputs.s.space_heating_load.loads_kw # thermal + dhw_hybrid = inputs.s.dhw_load.loads_kw # thermal + cooling_hybrid = inputs.s.cooling_load.loads_kw_thermal # thermal + cooling_elec_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop # electric + cooling_elec_frac_of_total_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop ./ elec_hybrid + + # Check that the combined/hybrid load is the same as the sum of the individual loads in each time_step + + @test round(sum(elec_hybrid .- (elec_hospital .+ elec_hotel)), digits=1) ≈ 0.0 atol=0.1 + @test round(sum(space_hybrid .- (space_hospital .+ space_hotel)), digits=1) ≈ 0.0 atol=0.1 + @test round(sum(dhw_hybrid .- (dhw_hospital .+ dhw_hotel)), digits=1) ≈ 0.0 atol=0.1 + # Check that the cooling load is the weighted average of the default CRB fraction of total electric profiles + cooling_electric_hybrid_expected = elec_hybrid .* (cooling_elec_frac_of_total_hospital * hospital_fraction .+ + cooling_elec_frac_of_total_hotel * hotel_fraction) + @test round(sum(cooling_electric_hybrid_expected .- cooling_elec_hybrid), digits=1) ≈ 0.0 atol=0.1 + end - # Hybrid mix of hospital and hotel - # Remove previous assignment of doe_reference_name - for load in ["ElectricLoad", "SpaceHeatingLoad", "DomesticHotWaterLoad", "CoolingLoad"] - delete!(input_data[load], "doe_reference_name") + @testset "Boiler (new) test" begin + input_data = JSON.parsefile("scenarios/boiler_new_inputs.json") + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], inputs) + + # BAU boiler loads + load_thermal_mmbtu_bau = sum(s.space_heating_load.loads_kw + s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU + existing_boiler_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) + boiler_thermal_mmbtu = sum(results["Boiler"]["thermal_production_series_mmbtu_per_hour"]) + + # Used monthly fuel cost for ExistingBoiler and Boiler, where ExistingBoiler has lower fuel cost only + # in February (28 days), so expect ExistingBoiler to serve the flat/constant load 28 days of the year + @test existing_boiler_mmbtu ≈ load_thermal_mmbtu_bau * 28 / 365 atol=0.00001 + @test boiler_thermal_mmbtu ≈ load_thermal_mmbtu_bau - existing_boiler_mmbtu atol=0.00001 end - annual_energy = (hospital_fraction + hotel_fraction) * 100 - building_list = ["Hospital", "LargeHotel"] - percent_share_list = [hospital_fraction, hotel_fraction] - input_data["ElectricLoad"]["annual_kwh"] = annual_energy - input_data["ElectricLoad"]["blended_doe_reference_names"] = building_list - input_data["ElectricLoad"]["blended_doe_reference_percents"] = percent_share_list - - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = annual_energy - input_data["SpaceHeatingLoad"]["blended_doe_reference_names"] = building_list - input_data["SpaceHeatingLoad"]["blended_doe_reference_percents"] = percent_share_list - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = annual_energy - input_data["DomesticHotWaterLoad"]["blended_doe_reference_names"] = building_list - input_data["DomesticHotWaterLoad"]["blended_doe_reference_percents"] = percent_share_list - - # CoolingLoad now use a weighted fraction of total electric profile if no annual_tonhour is provided - input_data["CoolingLoad"]["blended_doe_reference_names"] = building_list - input_data["CoolingLoad"]["blended_doe_reference_percents"] = percent_share_list - s = Scenario(input_data) - inputs = REoptInputs(s) + @testset "OffGrid" begin + ## Scenario 1: Solar, Storage, Fixed Generator + post_name = "off_grid.json" + post = JSON.parsefile("./scenarios/$post_name") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, post) + scen = Scenario(post) + + # Test default values + @test scen.electric_utility.outage_start_time_step ≈ 1 + @test scen.electric_utility.outage_end_time_step ≈ 8760 * scen.settings.time_steps_per_hour + @test scen.storage.attr["ElectricStorage"].soc_init_fraction ≈ 1 + @test scen.storage.attr["ElectricStorage"].can_grid_charge ≈ false + @test scen.generator.fuel_avail_gal ≈ 1.0e9 + @test scen.generator.min_turn_down_fraction ≈ 0.15 + @test sum(scen.electric_load.loads_kw) - sum(scen.electric_load.critical_loads_kw) ≈ 0 # critical loads should equal loads_kw + @test scen.financial.microgrid_upgrade_cost_fraction ≈ 0 + + # Test outputs + @test r["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0 # no interaction with grid + @test r["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ 2617.092 atol=0.01 # Check straight line depreciation calc + @test sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) # OR provided >= required + @test r["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction + @test r["PV"]["size_kw"] ≈ 5050.0 + f = r["Financial"] + @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + + f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + + f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + + f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - + f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 + + ## Scenario 2: Fixed Generator only + post["ElectricLoad"]["annual_kwh"] = 100.0 + post["PV"]["max_kw"] = 0.0 + post["ElectricStorage"]["max_kw"] = 0.0 + post["Generator"]["min_turn_down_fraction"] = 0.0 - elec_hybrid = inputs.s.electric_load.loads_kw - space_hybrid = inputs.s.space_heating_load.loads_kw # thermal - dhw_hybrid = inputs.s.dhw_load.loads_kw # thermal - cooling_hybrid = inputs.s.cooling_load.loads_kw_thermal # thermal - cooling_elec_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop # electric - cooling_elec_frac_of_total_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop ./ elec_hybrid - - # Check that the combined/hybrid load is the same as the sum of the individual loads in each time_step - - @test round(sum(elec_hybrid .- (elec_hospital .+ elec_hotel)), digits=1) ≈ 0.0 atol=0.1 - @test round(sum(space_hybrid .- (space_hospital .+ space_hotel)), digits=1) ≈ 0.0 atol=0.1 - @test round(sum(dhw_hybrid .- (dhw_hospital .+ dhw_hotel)), digits=1) ≈ 0.0 atol=0.1 - # Check that the cooling load is the weighted average of the default CRB fraction of total electric profiles - cooling_electric_hybrid_expected = elec_hybrid .* (cooling_elec_frac_of_total_hospital * hospital_fraction .+ - cooling_elec_frac_of_total_hotel * hotel_fraction) - @test round(sum(cooling_electric_hybrid_expected .- cooling_elec_hybrid), digits=1) ≈ 0.0 atol=0.1 - end + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, post) + + # Test generator outputs + @test r["Generator"]["annual_fuel_consumption_gal"] ≈ 7.52 # 99 kWh * 0.076 gal/kWh + @test r["Generator"]["annual_energy_produced_kwh"] ≈ 99.0 + @test r["Generator"]["year_one_fuel_cost_before_tax"] ≈ 22.57 + @test r["Generator"]["lifecycle_fuel_cost_after_tax"] ≈ 205.35 + @test r["Financial"]["initial_capital_costs"] ≈ 100*(700) + @test r["Financial"]["lifecycle_capital_costs"] ≈ 100*(700+324.235442*(1-0.26)) atol=0.1 # replacement in yr 10 is considered tax deductible + @test r["Financial"]["initial_capital_costs_after_incentives"] ≈ 700*100 atol=0.1 + @test r["Financial"]["replacements_future_cost_after_tax"] ≈ 700*100 + @test r["Financial"]["replacements_present_cost_after_tax"] ≈ 100*(324.235442*(1-0.26)) atol=0.1 + + ## Scenario 3: Fixed Generator that can meet load, but cannot meet load operating reserve requirement + ## This test ensures the load operating reserve requirement is being enforced + post["ElectricLoad"]["doe_reference_name"] = "FlatLoad" + post["ElectricLoad"]["annual_kwh"] = 876000.0 # requires 100 kW gen + post["ElectricLoad"]["min_load_met_annual_fraction"] = 1.0 # requires additional generator capacity + post["PV"]["max_kw"] = 0.0 + post["ElectricStorage"]["max_kw"] = 0.0 + post["Generator"]["min_turn_down_fraction"] = 0.0 - @testset "Boiler (new) test" begin - input_data = JSON.parsefile("scenarios/boiler_new_inputs.json") - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], inputs) - - # BAU boiler loads - load_thermal_mmbtu_bau = sum(s.space_heating_load.loads_kw + s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU - existing_boiler_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) - boiler_thermal_mmbtu = sum(results["Boiler"]["thermal_production_series_mmbtu_per_hour"]) - - # Used monthly fuel cost for ExistingBoiler and Boiler, where ExistingBoiler has lower fuel cost only - # in February (28 days), so expect ExistingBoiler to serve the flat/constant load 28 days of the year - @test existing_boiler_mmbtu ≈ load_thermal_mmbtu_bau * 28 / 365 atol=0.00001 - @test boiler_thermal_mmbtu ≈ load_thermal_mmbtu_bau - existing_boiler_mmbtu atol=0.00001 - end + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, post) - @testset "OffGrid" begin - ## Scenario 1: Solar, Storage, Fixed Generator - post_name = "off_grid.json" - post = JSON.parsefile("./scenarios/$post_name") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, post) - scen = Scenario(post) - - # Test default values - @test scen.electric_utility.outage_start_time_step ≈ 1 - @test scen.electric_utility.outage_end_time_step ≈ 8760 * scen.settings.time_steps_per_hour - @test scen.storage.attr["ElectricStorage"].soc_init_fraction ≈ 1 - @test scen.storage.attr["ElectricStorage"].can_grid_charge ≈ false - @test scen.generator.fuel_avail_gal ≈ 1.0e9 - @test scen.generator.min_turn_down_fraction ≈ 0.15 - @test sum(scen.electric_load.loads_kw) - sum(scen.electric_load.critical_loads_kw) ≈ 0 # critical loads should equal loads_kw - @test scen.financial.microgrid_upgrade_cost_fraction ≈ 0 - - # Test outputs - @test r["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0 # no interaction with grid - @test r["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ 2617.092 atol=0.01 # Check straight line depreciation calc - @test sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) # OR provided >= required - @test r["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction - @test r["PV"]["size_kw"] ≈ 5050.0 - f = r["Financial"] - @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + - f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + - f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + - f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - - f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 - - ## Scenario 2: Fixed Generator only - post["ElectricLoad"]["annual_kwh"] = 100.0 - post["PV"]["max_kw"] = 0.0 - post["ElectricStorage"]["max_kw"] = 0.0 - post["Generator"]["min_turn_down_fraction"] = 0.0 - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, post) - - # Test generator outputs - @test r["Generator"]["annual_fuel_consumption_gal"] ≈ 7.52 # 99 kWh * 0.076 gal/kWh - @test r["Generator"]["annual_energy_produced_kwh"] ≈ 99.0 - @test r["Generator"]["year_one_fuel_cost_before_tax"] ≈ 22.57 - @test r["Generator"]["lifecycle_fuel_cost_after_tax"] ≈ 205.35 - @test r["Financial"]["initial_capital_costs"] ≈ 100*(700) - @test r["Financial"]["lifecycle_capital_costs"] ≈ 100*(700+324.235442*(1-0.26)) atol=0.1 # replacement in yr 10 is considered tax deductible - @test r["Financial"]["initial_capital_costs_after_incentives"] ≈ 700*100 atol=0.1 - @test r["Financial"]["replacements_future_cost_after_tax"] ≈ 700*100 - @test r["Financial"]["replacements_present_cost_after_tax"] ≈ 100*(324.235442*(1-0.26)) atol=0.1 - - ## Scenario 3: Fixed Generator that can meet load, but cannot meet load operating reserve requirement - ## This test ensures the load operating reserve requirement is being enforced - post["ElectricLoad"]["doe_reference_name"] = "FlatLoad" - post["ElectricLoad"]["annual_kwh"] = 876000.0 # requires 100 kW gen - post["ElectricLoad"]["min_load_met_annual_fraction"] = 1.0 # requires additional generator capacity - post["PV"]["max_kw"] = 0.0 - post["ElectricStorage"]["max_kw"] = 0.0 - post["Generator"]["min_turn_down_fraction"] = 0.0 - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, post) - - # Test generator outputs - @test typeof(r) == Model # this is true when the model is infeasible - - ### Scenario 3: Indonesia. Wind (custom prod) and Generator only - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - post_name = "wind_intl_offgrid.json" - post = JSON.parsefile("./scenarios/$post_name") - post["ElectricLoad"]["loads_kw"] = [10.0 for i in range(1,8760)] - scen = Scenario(post) - post["Wind"]["production_factor_series"] = reduce(vcat, readdlm("./data/example_wind_prod_factor_kw.csv", '\n', header=true)[1]) + # Test generator outputs + @test typeof(r) == Model # this is true when the model is infeasible - results = run_reopt(m, post) - - @test results["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction - f = results["Financial"] - @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + - f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + - f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + - f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - - f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 - - windOR = sum(results["Wind"]["electric_to_load_series_kw"] * post["Wind"]["operating_reserve_required_fraction"]) - loadOR = sum(post["ElectricLoad"]["loads_kw"] * scen.electric_load.operating_reserve_required_fraction) - @test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) ≈ loadOR + windOR atol=1.0 + ### Scenario 3: Indonesia. Wind (custom prod) and Generator only + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + post_name = "wind_intl_offgrid.json" + post = JSON.parsefile("./scenarios/$post_name") + post["ElectricLoad"]["loads_kw"] = [10.0 for i in range(1,8760)] + scen = Scenario(post) + post["Wind"]["production_factor_series"] = reduce(vcat, readdlm("./data/example_wind_prod_factor_kw.csv", '\n', header=true)[1]) - end + results = run_reopt(m, post) + + @test results["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction + f = results["Financial"] + @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + + f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + + f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + + f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - + f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 + + windOR = sum(results["Wind"]["electric_to_load_series_kw"] * post["Wind"]["operating_reserve_required_fraction"]) + loadOR = sum(post["ElectricLoad"]["loads_kw"] * scen.electric_load.operating_reserve_required_fraction) + @test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) ≈ loadOR + windOR atol=1.0 - @testset "GHP" begin - """ + end - This tests multiple unique aspects of GHP: - 1. REopt takes the output data of GhpGhx, creates multiple GHP options, and chooses the expected one - 2. GHP with heating and cooling "..efficiency_thermal_factors" reduces the net thermal load - 3. GHP serves only the SpaceHeatingLoad by default unless it is allowed to serve DHW - 4. GHP serves all the Cooling load - 5. Input of a custom COP map for GHP and check the GHP performance to make sure it's using it correctly - 6. Hybrid GHP capability functions as expected + @testset "GHP" begin + """ - """ - # Load base inputs - input_data = JSON.parsefile("scenarios/ghp_inputs.json") - - # Modify ["GHP"]["ghpghx_inputs"] for running GhpGhx.jl - # Heat pump performance maps - cop_map_mat_header = readdlm("scenarios/ghp_cop_map_custom.csv", ',', header=true) - data = cop_map_mat_header[1] - headers = cop_map_mat_header[2] - # Generate a "records" style dictionary from the - cop_map_list = [] - for i in axes(data,1) - dict_record = Dict(name=>data[i, col] for (col, name) in enumerate(headers)) - push!(cop_map_list, dict_record) + This tests multiple unique aspects of GHP: + 1. REopt takes the output data of GhpGhx, creates multiple GHP options, and chooses the expected one + 2. GHP with heating and cooling "..efficiency_thermal_factors" reduces the net thermal load + 3. GHP serves only the SpaceHeatingLoad by default unless it is allowed to serve DHW + 4. GHP serves all the Cooling load + 5. Input of a custom COP map for GHP and check the GHP performance to make sure it's using it correctly + 6. Hybrid GHP capability functions as expected + + """ + # Load base inputs + input_data = JSON.parsefile("scenarios/ghp_inputs.json") + + # Modify ["GHP"]["ghpghx_inputs"] for running GhpGhx.jl + # Heat pump performance maps + cop_map_mat_header = readdlm("scenarios/ghp_cop_map_custom.csv", ',', header=true) + data = cop_map_mat_header[1] + headers = cop_map_mat_header[2] + # Generate a "records" style dictionary from the + cop_map_list = [] + for i in axes(data,1) + dict_record = Dict(name=>data[i, col] for (col, name) in enumerate(headers)) + push!(cop_map_list, dict_record) + end + input_data["GHP"]["ghpghx_inputs"][1]["cop_map_eft_heating_cooling"] = cop_map_list + + # Due to GhpGhx not being a registered package (no OSI-approved license), + # the registered REopt package cannot have GhpGhx as a "normal" dependency; + # Therefore, we only use a "ghpghx_response" (the output of GhpGhx) as an + # input to REopt to avoid GhpGhx module calls + response_1 = JSON.parsefile("scenarios/ghpghx_response.json") + response_2 = deepcopy(response_1) + # Reduce the electric consumption of response 2 which should then be the chosen system + response_2["outputs"]["yearly_total_electric_consumption_series_kw"] *= 0.5 + input_data["GHP"]["ghpghx_responses"] = [response_1, response_2] + + # Heating load + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" + input_data["SpaceHeatingLoad"]["monthly_mmbtu"] = fill(1000.0, 12) + input_data["SpaceHeatingLoad"]["monthly_mmbtu"][1] = 500.0 + input_data["SpaceHeatingLoad"]["monthly_mmbtu"][end] = 1500.0 + + # Call REopt + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt([m1,m2], inputs) + + ghp_option_chosen = results["GHP"]["ghp_option_chosen"] + @test ghp_option_chosen == 2 + + # Test GHP heating and cooling load reduced + hot_load_reduced_mmbtu = sum(results["GHP"]["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"]) + cold_load_reduced_tonhour = sum(results["GHP"]["cooling_thermal_load_reduction_with_ghp_ton"]) + @test hot_load_reduced_mmbtu ≈ 1440.00 atol=0.1 + @test cold_load_reduced_tonhour ≈ 761382.78 atol=0.1 + + # Test GHP serving space heating with VAV thermal efficiency improvements + heating_served_mmbtu = sum(s.ghp_option_list[ghp_option_chosen].heating_thermal_kw / REopt.KWH_PER_MMBTU) + expected_heating_served_mmbtu = 12000 * 0.8 * 0.85 # (fuel_mmbtu * boiler_effic * space_heating_efficiency_thermal_factor) + @test round(heating_served_mmbtu, digits=1) ≈ expected_heating_served_mmbtu atol=1.0 + + # Boiler serves all of the DHW load, no DHW thermal reduction due to GHP retrofit + boiler_served_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) + expected_boiler_served_mmbtu = 3000 * 0.8 # (fuel_mmbtu * boiler_effic) + @test round(boiler_served_mmbtu, digits=1) ≈ expected_boiler_served_mmbtu atol=1.0 + + # LoadProfileChillerThermal cooling thermal is 1/cooling_efficiency_thermal_factor of GHP cooling thermal production + bau_chiller_thermal_tonhour = sum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) + ghp_cooling_thermal_tonhour = sum(inputs.ghp_cooling_thermal_load_served_kw[1,:] / REopt.KWH_THERMAL_PER_TONHOUR) + @test round(bau_chiller_thermal_tonhour) ≈ ghp_cooling_thermal_tonhour/0.6 atol=1.0 + + # Custom heat pump COP map is used properly + ghp_option_chosen = results["GHP"]["ghp_option_chosen"] + heating_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["heating_cop_avg"] + cooling_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["cooling_cop_avg"] + # Average COP which includes pump power should be lower than Heat Pump only COP specified by the map + @test heating_cop_avg <= 4.0 + @test cooling_cop_avg <= 8.0 end - input_data["GHP"]["ghpghx_inputs"][1]["cop_map_eft_heating_cooling"] = cop_map_list - - # Due to GhpGhx not being a registered package (no OSI-approved license), - # the registered REopt package cannot have GhpGhx as a "normal" dependency; - # Therefore, we only use a "ghpghx_response" (the output of GhpGhx) as an - # input to REopt to avoid GhpGhx module calls - response_1 = JSON.parsefile("scenarios/ghpghx_response.json") - response_2 = deepcopy(response_1) - # Reduce the electric consumption of response 2 which should then be the chosen system - response_2["outputs"]["yearly_total_electric_consumption_series_kw"] *= 0.5 - input_data["GHP"]["ghpghx_responses"] = [response_1, response_2] - - # Heating load - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" - input_data["SpaceHeatingLoad"]["monthly_mmbtu"] = fill(1000.0, 12) - input_data["SpaceHeatingLoad"]["monthly_mmbtu"][1] = 500.0 - input_data["SpaceHeatingLoad"]["monthly_mmbtu"][end] = 1500.0 - - # Call REopt - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt([m1,m2], inputs) - - ghp_option_chosen = results["GHP"]["ghp_option_chosen"] - @test ghp_option_chosen == 2 - - # Test GHP heating and cooling load reduced - hot_load_reduced_mmbtu = sum(results["GHP"]["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"]) - cold_load_reduced_tonhour = sum(results["GHP"]["cooling_thermal_load_reduction_with_ghp_ton"]) - @test hot_load_reduced_mmbtu ≈ 1440.00 atol=0.1 - @test cold_load_reduced_tonhour ≈ 761382.78 atol=0.1 - - # Test GHP serving space heating with VAV thermal efficiency improvements - heating_served_mmbtu = sum(s.ghp_option_list[ghp_option_chosen].heating_thermal_kw / REopt.KWH_PER_MMBTU) - expected_heating_served_mmbtu = 12000 * 0.8 * 0.85 # (fuel_mmbtu * boiler_effic * space_heating_efficiency_thermal_factor) - @test round(heating_served_mmbtu, digits=1) ≈ expected_heating_served_mmbtu atol=1.0 - - # Boiler serves all of the DHW load, no DHW thermal reduction due to GHP retrofit - boiler_served_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) - expected_boiler_served_mmbtu = 3000 * 0.8 # (fuel_mmbtu * boiler_effic) - @test round(boiler_served_mmbtu, digits=1) ≈ expected_boiler_served_mmbtu atol=1.0 - - # LoadProfileChillerThermal cooling thermal is 1/cooling_efficiency_thermal_factor of GHP cooling thermal production - bau_chiller_thermal_tonhour = sum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) - ghp_cooling_thermal_tonhour = sum(inputs.ghp_cooling_thermal_load_served_kw[1,:] / REopt.KWH_THERMAL_PER_TONHOUR) - @test round(bau_chiller_thermal_tonhour) ≈ ghp_cooling_thermal_tonhour/0.6 atol=1.0 - - # Custom heat pump COP map is used properly - ghp_option_chosen = results["GHP"]["ghp_option_chosen"] - heating_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["heating_cop_avg"] - cooling_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["cooling_cop_avg"] - # Average COP which includes pump power should be lower than Heat Pump only COP specified by the map - @test heating_cop_avg <= 4.0 - @test cooling_cop_avg <= 8.0 - end - @testset "Hybrid GHX and GHP calculated costs validation" begin - ## Hybrid GHP validation. - # Load base inputs - input_data = JSON.parsefile("scenarios/ghp_financial_hybrid.json") + @testset "Hybrid GHX and GHP calculated costs validation" begin + ## Hybrid GHP validation. + # Load base inputs + input_data = JSON.parsefile("scenarios/ghp_financial_hybrid.json") - inputs = REoptInputs(input_data) + inputs = REoptInputs(input_data) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt([m1,m2], inputs) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt([m1,m2], inputs) - calculated_ghp_capital_costs = ((input_data["GHP"]["ghpghx_responses"][1]["outputs"]["number_of_boreholes"]* - input_data["GHP"]["ghpghx_responses"][1]["outputs"]["length_boreholes_ft"]* - inputs.s.ghp_option_list[1].installed_cost_ghx_per_ft) + - (inputs.s.ghp_option_list[1].installed_cost_heatpump_per_ton* - input_data["GHP"]["ghpghx_responses"][1]["outputs"]["peak_combined_heatpump_thermal_ton"]* - inputs.s.ghp_option_list[1].heatpump_capacity_sizing_factor_on_peak_load) + - (inputs.s.ghp_option_list[1].building_sqft* - inputs.s.ghp_option_list[1].installed_cost_building_hydronic_loop_per_sqft)) + calculated_ghp_capital_costs = ((input_data["GHP"]["ghpghx_responses"][1]["outputs"]["number_of_boreholes"]* + input_data["GHP"]["ghpghx_responses"][1]["outputs"]["length_boreholes_ft"]* + inputs.s.ghp_option_list[1].installed_cost_ghx_per_ft) + + (inputs.s.ghp_option_list[1].installed_cost_heatpump_per_ton* + input_data["GHP"]["ghpghx_responses"][1]["outputs"]["peak_combined_heatpump_thermal_ton"]* + inputs.s.ghp_option_list[1].heatpump_capacity_sizing_factor_on_peak_load) + + (inputs.s.ghp_option_list[1].building_sqft* + inputs.s.ghp_option_list[1].installed_cost_building_hydronic_loop_per_sqft)) - @test results["Financial"]["initial_capital_costs"] ≈ calculated_ghp_capital_costs atol=0.1 - - calculated_om_costs = inputs.s.ghp_option_list[1].building_sqft* - inputs.s.ghp_option_list[1].om_cost_per_sqft_year * inputs.third_party_factor * inputs.pwf_om + @test results["Financial"]["initial_capital_costs"] ≈ calculated_ghp_capital_costs atol=0.1 + + calculated_om_costs = inputs.s.ghp_option_list[1].building_sqft* + inputs.s.ghp_option_list[1].om_cost_per_sqft_year * inputs.third_party_factor * inputs.pwf_om - @test results["Financial"]["lifecycle_om_costs_before_tax"] ≈ calculated_om_costs atol=0.1 + @test results["Financial"]["lifecycle_om_costs_before_tax"] ≈ calculated_om_costs atol=0.1 - calc_om_cost_after_tax = calculated_om_costs*(1-inputs.s.financial.owner_tax_rate_fraction) - @test results["Financial"]["lifecycle_om_costs_after_tax"] - calc_om_cost_after_tax < 0.0001 + calc_om_cost_after_tax = calculated_om_costs*(1-inputs.s.financial.owner_tax_rate_fraction) + @test results["Financial"]["lifecycle_om_costs_after_tax"] - calc_om_cost_after_tax < 0.0001 - @test abs(results["Financial"]["lifecycle_capital_costs_plus_om_after_tax"] - (calc_om_cost_after_tax + 0.7*results["Financial"]["initial_capital_costs"])) < 150.0 + @test abs(results["Financial"]["lifecycle_capital_costs_plus_om_after_tax"] - (calc_om_cost_after_tax + 0.7*results["Financial"]["initial_capital_costs"])) < 150.0 - @test abs(results["Financial"]["lifecycle_capital_costs"] - 0.7*results["Financial"]["initial_capital_costs"]) < 150.0 + @test abs(results["Financial"]["lifecycle_capital_costs"] - 0.7*results["Financial"]["initial_capital_costs"]) < 150.0 - @test abs(results["Financial"]["npv"] - 840621) < 1.0 - @test results["Financial"]["simple_payback_years"] - 5.09 < 0.1 - @test results["Financial"]["internal_rate_of_return"] - 0.18 < 0.01 + @test abs(results["Financial"]["npv"] - 840621) < 1.0 + @test results["Financial"]["simple_payback_years"] - 5.09 < 0.1 + @test results["Financial"]["internal_rate_of_return"] - 0.18 < 0.01 - @test haskey(results["ExistingBoiler"], "year_one_fuel_cost_before_tax_bau") + @test haskey(results["ExistingBoiler"], "year_one_fuel_cost_before_tax_bau") - ## Hybrid - input_data["GHP"]["ghpghx_responses"] = [JSON.parsefile("scenarios/ghpghx_hybrid_results.json")] - input_data["GHP"]["avoided_capex_by_ghp_present_value"] = 1.0e6 - input_data["GHP"]["ghx_useful_life_years"] = 35 + ## Hybrid + input_data["GHP"]["ghpghx_responses"] = [JSON.parsefile("scenarios/ghpghx_hybrid_results.json")] + input_data["GHP"]["avoided_capex_by_ghp_present_value"] = 1.0e6 + input_data["GHP"]["ghx_useful_life_years"] = 35 - inputs = REoptInputs(input_data) + inputs = REoptInputs(input_data) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt([m1,m2], inputs) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt([m1,m2], inputs) - pop!(input_data["GHP"], "ghpghx_inputs", nothing) - pop!(input_data["GHP"], "ghpghx_responses", nothing) - ghp_obj = REopt.GHP(JSON.parsefile("scenarios/ghpghx_hybrid_results.json"), input_data["GHP"]) + pop!(input_data["GHP"], "ghpghx_inputs", nothing) + pop!(input_data["GHP"], "ghpghx_responses", nothing) + ghp_obj = REopt.GHP(JSON.parsefile("scenarios/ghpghx_hybrid_results.json"), input_data["GHP"]) - calculated_ghx_residual_value = ghp_obj.ghx_only_capital_cost* - ( - (ghp_obj.ghx_useful_life_years - inputs.s.financial.analysis_years)/ghp_obj.ghx_useful_life_years - )/( - (1 + inputs.s.financial.offtaker_discount_rate_fraction)^inputs.s.financial.analysis_years - ) - - @test results["GHP"]["ghx_residual_value_present_value"] ≈ calculated_ghx_residual_value atol=0.1 - @test inputs.s.ghp_option_list[1].is_ghx_hybrid = true + calculated_ghx_residual_value = ghp_obj.ghx_only_capital_cost* + ( + (ghp_obj.ghx_useful_life_years - inputs.s.financial.analysis_years)/ghp_obj.ghx_useful_life_years + )/( + (1 + inputs.s.financial.offtaker_discount_rate_fraction)^inputs.s.financial.analysis_years + ) + + @test results["GHP"]["ghx_residual_value_present_value"] ≈ calculated_ghx_residual_value atol=0.1 + @test inputs.s.ghp_option_list[1].is_ghx_hybrid = true - # Test centralized GHP cost calculations - input_data_wwhp = JSON.parsefile("scenarios/ghp_inputs_wwhp.json") - response_wwhp = JSON.parsefile("scenarios/ghpghx_response_wwhp.json") - input_data_wwhp["GHP"]["ghpghx_responses"] = [response_wwhp] + # Test centralized GHP cost calculations + input_data_wwhp = JSON.parsefile("scenarios/ghp_inputs_wwhp.json") + response_wwhp = JSON.parsefile("scenarios/ghpghx_response_wwhp.json") + input_data_wwhp["GHP"]["ghpghx_responses"] = [response_wwhp] - s_wwhp = Scenario(input_data_wwhp) - inputs_wwhp = REoptInputs(s_wwhp) - m3 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results_wwhp = run_reopt(m3, inputs_wwhp) + s_wwhp = Scenario(input_data_wwhp) + inputs_wwhp = REoptInputs(s_wwhp) + m3 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results_wwhp = run_reopt(m3, inputs_wwhp) - heating_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_heating_pump_per_ton"] * - input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_heating_heatpump_thermal_ton"] + heating_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_heating_pump_per_ton"] * + input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_heating_heatpump_thermal_ton"] - cooling_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_cooling_pump_per_ton"] * - input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_cooling_heatpump_thermal_ton"] + cooling_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_cooling_pump_per_ton"] * + input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_cooling_heatpump_thermal_ton"] - ghx_cost = input_data_wwhp["GHP"]["installed_cost_ghx_per_ft"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["length_boreholes_ft"] + ghx_cost = input_data_wwhp["GHP"]["installed_cost_ghx_per_ft"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["length_boreholes_ft"] - # CAPEX reduction factor for 30% ITC, 5-year MACRS, assuming 26% tax rate and 8.3% discount - capex_reduction_factor = 0.455005797 + # CAPEX reduction factor for 30% ITC, 5-year MACRS, assuming 26% tax rate and 8.3% discount + capex_reduction_factor = 0.455005797 - calculated_ghp_capex = (heating_hp_cost + cooling_hp_cost + ghx_cost) * (1 - capex_reduction_factor) + calculated_ghp_capex = (heating_hp_cost + cooling_hp_cost + ghx_cost) * (1 - capex_reduction_factor) - reopt_ghp_capex = results_wwhp["Financial"]["lifecycle_capital_costs"] - @test calculated_ghp_capex ≈ reopt_ghp_capex atol=300 - end + reopt_ghp_capex = results_wwhp["Financial"]["lifecycle_capital_costs"] + @test calculated_ghp_capex ≈ reopt_ghp_capex atol=300 + end - @testset "Cambium Emissions" begin - """ - 1) Location in contiguous US - - Correct data from Cambium (returned location and values) - - Adjusted for load year vs. Cambium year (which starts on Sunday) vs. AVERT year (2022 currently) - - co2 pct increase should be zero - 2) HI and AK locations - - Should use AVERT data and give an "info" message - - Adjust for load year vs. AVERT year - - co2 pct increase should be the default value unless user provided value - 3) International - - all emissions should be zero unless provided - """ - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - - post_name = "cambium.json" - post = JSON.parsefile("./scenarios/$post_name") - - cities = Dict( - "Denver" => (39.7413753050447, -104.99965032911328), - "Fairbanks" => (64.84053664406181, -147.71913656313163), - "Santiago" => (-33.44485437650408, -70.69031905547853) - ) - - # 1) Location in contiguous US - city = "Denver" - post["Site"]["latitude"] = cities[city][1] - post["Site"]["longitude"] = cities[city][2] - post["ElectricLoad"]["loads_kw"] = [20 for i in range(1,8760)] - post["ElectricLoad"]["year"] = 2021 # 2021 First day is Fri - scen = Scenario(post) - - @test scen.electric_utility.avert_emissions_region == "Rocky Mountains" - @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 - @test scen.electric_utility.cambium_emissions_region == "RMPAc" - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 0.394608 rtol=1e-3 - @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[1] ≈ 0.677942 rtol=1e-4 # Should start on Friday - @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[8760] ≈ 0.6598207198 rtol=1e-5 # Should end on Friday - @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) / 8760 ≈ 0.00061165 rtol=1e-5 # check avg from AVERT data for RM region - @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ 0 atol=1e-5 # should be 0 with Cambium data - @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data - @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] - @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] - - # 2) AK location - city = "Fairbanks" - post["Site"]["latitude"] = cities[city][1] - post["Site"]["longitude"] = cities[city][2] - scen = Scenario(post) - - @test scen.electric_utility.avert_emissions_region == "Alaska" - @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 - @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 1.29199999 rtol=1e-3 # check that data from eGRID (AVERT data file) is used - @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["CO2e"] # should get updated to this value - @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data - @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] - @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] - - # 3) International location - city = "Santiago" - post["Site"]["latitude"] = cities[city][1] - post["Site"]["longitude"] = cities[city][2] - scen = Scenario(post) + @testset "Cambium Emissions" begin + """ + 1) Location in contiguous US + - Correct data from Cambium (returned location and values) + - Adjusted for load year vs. Cambium year (which starts on Sunday) vs. AVERT year (2022 currently) + - co2 pct increase should be zero + 2) HI and AK locations + - Should use AVERT data and give an "info" message + - Adjust for load year vs. AVERT year + - co2 pct increase should be the default value unless user provided value + 3) International + - all emissions should be zero unless provided + """ + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - @test scen.electric_utility.avert_emissions_region == "" - @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 5.521032136418236e6 atol=1.0 - @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_NOx_per_kwh) ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_PM25_per_kwh) ≈ 0 - - end - - @testset "Emissions and Renewable Energy Percent" begin - #renewable energy and emissions reduction targets - include_exported_RE_in_total = [true,false,true] - include_exported_ER_in_total = [true,false,true] - RE_target = [0.8,nothing,nothing] - ER_target = [nothing,0.8,nothing] - with_outage = [true,false,false] - - for i in range(1, stop=3) - if i == 3 - inputs = JSON.parsefile("./scenarios/re_emissions_with_thermal.json") - else - inputs = JSON.parsefile("./scenarios/re_emissions_elec_only.json") - end - if i == 1 - inputs["Site"]["latitude"] = 37.746 - inputs["Site"]["longitude"] = -122.448 - # inputs["ElectricUtility"]["emissions_region"] = "California" - end - inputs["Site"]["include_exported_renewable_electricity_in_total"] = include_exported_RE_in_total[i] - inputs["Site"]["include_exported_elec_emissions_in_total"] = include_exported_ER_in_total[i] - inputs["Site"]["renewable_electricity_min_fraction"] = if isnothing(RE_target[i]) 0.0 else RE_target[i] end - inputs["Site"]["renewable_electricity_max_fraction"] = RE_target[i] - inputs["Site"]["CO2_emissions_reduction_min_fraction"] = ER_target[i] - inputs["Site"]["CO2_emissions_reduction_max_fraction"] = ER_target[i] - if with_outage[i] - outage_start_hour = 4032 - outage_duration = 2000 #hrs - inputs["ElectricUtility"]["outage_start_time_step"] = outage_start_hour + 1 - inputs["ElectricUtility"]["outage_end_time_step"] = outage_start_hour + 1 + outage_duration - inputs["Generator"]["max_kw"] = 20 - inputs["Generator"]["existing_kw"] = 2 - inputs["Generator"]["fuel_avail_gal"] = 1000 - end + post_name = "cambium.json" + post = JSON.parsefile("./scenarios/$post_name") + + cities = Dict( + "Denver" => (39.7413753050447, -104.99965032911328), + "Fairbanks" => (64.84053664406181, -147.71913656313163), + "Santiago" => (-33.44485437650408, -70.69031905547853) + ) + + # 1) Location in contiguous US + city = "Denver" + post["Site"]["latitude"] = cities[city][1] + post["Site"]["longitude"] = cities[city][2] + post["ElectricLoad"]["loads_kw"] = [20 for i in range(1,8760)] + post["ElectricLoad"]["year"] = 2021 # 2021 First day is Fri + scen = Scenario(post) + + @test scen.electric_utility.avert_emissions_region == "Rocky Mountains" + @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 + @test scen.electric_utility.cambium_emissions_region == "RMPAc" + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 0.394608 rtol=1e-3 + @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[1] ≈ 0.677942 rtol=1e-4 # Should start on Friday + @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[8760] ≈ 0.6598207198 rtol=1e-5 # Should end on Friday + @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) / 8760 ≈ 0.00061165 rtol=1e-5 # check avg from AVERT data for RM region + @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ 0 atol=1e-5 # should be 0 with Cambium data + @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data + @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] + @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] + + # 2) AK location + city = "Fairbanks" + post["Site"]["latitude"] = cities[city][1] + post["Site"]["longitude"] = cities[city][2] + scen = Scenario(post) + + @test scen.electric_utility.avert_emissions_region == "Alaska" + @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 + @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 1.29199999 rtol=1e-3 # check that data from eGRID (AVERT data file) is used + @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["CO2e"] # should get updated to this value + @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data + @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] + @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] + + # 3) International location + city = "Santiago" + post["Site"]["latitude"] = cities[city][1] + post["Site"]["longitude"] = cities[city][2] + scen = Scenario(post) + + @test scen.electric_utility.avert_emissions_region == "" + @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 5.521032136418236e6 atol=1.0 + @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_NOx_per_kwh) ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_PM25_per_kwh) ≈ 0 + + end - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt([m1, m2], inputs) - - if !isnothing(ER_target[i]) - ER_fraction_out = results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] - @test ER_target[i] ≈ ER_fraction_out atol=1e-3 - lifecycle_emissions_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2"] - lifecycle_emissions_bau_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] - ER_fraction_calced_out = (lifecycle_emissions_bau_tonnes_CO2_out-lifecycle_emissions_tonnes_CO2_out)/lifecycle_emissions_bau_tonnes_CO2_out - ER_fraction_diff = abs(ER_fraction_calced_out-ER_fraction_out) - @test ER_fraction_diff ≈ 0.0 atol=1e-2 - end + @testset "Emissions and Renewable Energy Percent" begin + #renewable energy and emissions reduction targets + include_exported_RE_in_total = [true,false,true] + include_exported_ER_in_total = [true,false,true] + RE_target = [0.8,nothing,nothing] + ER_target = [nothing,0.8,nothing] + with_outage = [true,false,false] + + for i in range(1, stop=3) + if i == 3 + inputs = JSON.parsefile("./scenarios/re_emissions_with_thermal.json") + else + inputs = JSON.parsefile("./scenarios/re_emissions_elec_only.json") + end + if i == 1 + inputs["Site"]["latitude"] = 37.746 + inputs["Site"]["longitude"] = -122.448 + # inputs["ElectricUtility"]["emissions_region"] = "California" + end + inputs["Site"]["include_exported_renewable_electricity_in_total"] = include_exported_RE_in_total[i] + inputs["Site"]["include_exported_elec_emissions_in_total"] = include_exported_ER_in_total[i] + inputs["Site"]["renewable_electricity_min_fraction"] = if isnothing(RE_target[i]) 0.0 else RE_target[i] end + inputs["Site"]["renewable_electricity_max_fraction"] = RE_target[i] + inputs["Site"]["CO2_emissions_reduction_min_fraction"] = ER_target[i] + inputs["Site"]["CO2_emissions_reduction_max_fraction"] = ER_target[i] + if with_outage[i] + outage_start_hour = 4032 + outage_duration = 2000 #hrs + inputs["ElectricUtility"]["outage_start_time_step"] = outage_start_hour + 1 + inputs["ElectricUtility"]["outage_end_time_step"] = outage_start_hour + 1 + outage_duration + inputs["Generator"]["max_kw"] = 20 + inputs["Generator"]["existing_kw"] = 2 + inputs["Generator"]["fuel_avail_gal"] = 1000 + end - annual_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_tonnes_CO2"] - yr1_fuel_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] - yr1_grid_emissions_tonnes_CO2_out = results["ElectricUtility"]["annual_emissions_tonnes_CO2"] - yr1_total_emissions_calced_tonnes_CO2 = yr1_fuel_emissions_tonnes_CO2_out + yr1_grid_emissions_tonnes_CO2_out - @test annual_emissions_tonnes_CO2_out ≈ yr1_total_emissions_calced_tonnes_CO2 atol=1e-1 - if haskey(results["Financial"],"breakeven_cost_of_emissions_reduction_per_tonne_CO2") - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] >= 0.0 - end - - if i == 1 - @test results["PV"]["size_kw"] ≈ 59.7222 atol=1e-1 - @test results["ElectricStorage"]["size_kw"] ≈ 0.0 atol=1e-1 - @test results["ElectricStorage"]["size_kwh"] ≈ 0.0 atol=1e-1 - @test results["Generator"]["size_kw"] ≈ 9.13 atol=1e-1 - @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.148375 atol=1e-4 - @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.57403012 atol=1e-4 - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 332.4 atol=1 - @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.85 atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 7.427 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 - @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8459.45 atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 236.95 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 148.54 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 - @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 27.813 atol=1e-1 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 556.26 - elseif i == 2 - #commented out values are results using same levelization factor as API - @test results["PV"]["size_kw"] ≈ 106.13 atol=1 - @test results["ElectricStorage"]["size_kw"] ≈ 20.09 atol=1 # 20.29 - @test results["ElectricStorage"]["size_kwh"] ≈ 170.94 atol=1 - @test !haskey(results, "Generator") - # Renewable energy - @test results["Site"]["renewable_electricity_fraction"] ≈ 0.78586 atol=1e-3 - @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 - @test results["Site"]["annual_renewable_electricity_kwh_bau"] ≈ 13308.5 atol=10 # 13542.62 atol=10 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 - # CO2 emissions - totals ≈ from grid, from fuelburn, ER, $/tCO2 breakeven - @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.8 atol=1e-3 # 0.8 - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 491.5 atol=1e-1 - @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.662 atol=1 - @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 - @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8397.85 atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1166.19 atol=1 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 atol=1 # 0.0 - @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 233.24 atol=1 - - - #also test CO2 breakeven cost - inputs["PV"]["min_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] - inputs["PV"]["max_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] - inputs["ElectricStorage"]["min_kw"] = results["ElectricStorage"]["size_kw"] - inputs["ElectricStorage"]["max_kw"] = results["ElectricStorage"]["size_kw"] - inputs["ElectricStorage"]["min_kwh"] = results["ElectricStorage"]["size_kwh"] - inputs["ElectricStorage"]["max_kwh"] = results["ElectricStorage"]["size_kwh"] - inputs["Financial"]["CO2_cost_per_tonne"] = results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] - inputs["Settings"]["include_climate_in_objective"] = true m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) results = run_reopt([m1, m2], inputs) - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ inputs["Financial"]["CO2_cost_per_tonne"] atol=1e-1 - elseif i == 3 - @test results["PV"]["size_kw"] ≈ 20.0 atol=1e-1 - @test !haskey(results, "Wind") - @test !haskey(results, "ElectricStorage") - @test !haskey(results, "Generator") - @test results["CHP"]["size_kw"] ≈ 200.0 atol=1e-1 - @test results["AbsorptionChiller"]["size_ton"] ≈ 400.0 atol=1e-1 - @test results["HotThermalStorage"]["size_gal"] ≈ 50000 atol=1e1 - @test results["ColdThermalStorage"]["size_gal"] ≈ 30000 atol=1e1 - yr1_nat_gas_mmbtu = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] + results["CHP"]["annual_fuel_consumption_mmbtu"] - nat_gas_emissions_lb_per_mmbtu = Dict("CO2"=>117.03, "NOx"=>0.09139, "SO2"=>0.000578592, "PM25"=>0.007328833) - TONNE_PER_LB = 1/2204.62 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ nat_gas_emissions_lb_per_mmbtu["CO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_NOx"] ≈ nat_gas_emissions_lb_per_mmbtu["NOx"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_SO2"] ≈ nat_gas_emissions_lb_per_mmbtu["SO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_PM25"] ≈ nat_gas_emissions_lb_per_mmbtu["PM25"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 - @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_NOx"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_NOx"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_NOx"] atol=0.1 - @test results["Site"]["lifecycle_emissions_tonnes_SO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_SO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_SO2"] atol=1e-2 - @test results["Site"]["lifecycle_emissions_tonnes_PM25"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_PM25"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_PM25"] atol=1.5e-2 - @test results["Site"]["annual_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 - @test results["Site"]["renewable_electricity_fraction"] ≈ results["Site"]["annual_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 - KWH_PER_MMBTU = 293.07107 - annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_renewable_electricity_kwh"] - annual_heat_kwh = (results["CHP"]["annual_thermal_production_mmbtu"] + results["ExistingBoiler"]["annual_thermal_production_mmbtu"]) * KWH_PER_MMBTU - @test results["Site"]["total_renewable_energy_fraction"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 + + if !isnothing(ER_target[i]) + ER_fraction_out = results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] + @test ER_target[i] ≈ ER_fraction_out atol=1e-3 + lifecycle_emissions_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2"] + lifecycle_emissions_bau_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] + ER_fraction_calced_out = (lifecycle_emissions_bau_tonnes_CO2_out-lifecycle_emissions_tonnes_CO2_out)/lifecycle_emissions_bau_tonnes_CO2_out + ER_fraction_diff = abs(ER_fraction_calced_out-ER_fraction_out) + @test ER_fraction_diff ≈ 0.0 atol=1e-2 + end + + annual_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_tonnes_CO2"] + yr1_fuel_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] + yr1_grid_emissions_tonnes_CO2_out = results["ElectricUtility"]["annual_emissions_tonnes_CO2"] + yr1_total_emissions_calced_tonnes_CO2 = yr1_fuel_emissions_tonnes_CO2_out + yr1_grid_emissions_tonnes_CO2_out + @test annual_emissions_tonnes_CO2_out ≈ yr1_total_emissions_calced_tonnes_CO2 atol=1e-1 + if haskey(results["Financial"],"breakeven_cost_of_emissions_reduction_per_tonne_CO2") + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] >= 0.0 + end + + if i == 1 + @test results["PV"]["size_kw"] ≈ 59.7222 atol=1e-1 + @test results["ElectricStorage"]["size_kw"] ≈ 0.0 atol=1e-1 + @test results["ElectricStorage"]["size_kwh"] ≈ 0.0 atol=1e-1 + @test results["Generator"]["size_kw"] ≈ 9.13 atol=1e-1 + @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 + @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.148375 atol=1e-4 + @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.57403012 atol=1e-4 + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 332.4 atol=1 + @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.85 atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 7.427 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 + @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8459.45 atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 236.95 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 148.54 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 + @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 27.813 atol=1e-1 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 556.26 + elseif i == 2 + #commented out values are results using same levelization factor as API + @test results["PV"]["size_kw"] ≈ 106.13 atol=1 + @test results["ElectricStorage"]["size_kw"] ≈ 20.09 atol=1 # 20.29 + @test results["ElectricStorage"]["size_kwh"] ≈ 170.94 atol=1 + @test !haskey(results, "Generator") + # Renewable energy + @test results["Site"]["renewable_electricity_fraction"] ≈ 0.78586 atol=1e-3 + @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 + @test results["Site"]["annual_renewable_electricity_kwh_bau"] ≈ 13308.5 atol=10 # 13542.62 atol=10 + @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 + # CO2 emissions - totals ≈ from grid, from fuelburn, ER, $/tCO2 breakeven + @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.8 atol=1e-3 # 0.8 + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 491.5 atol=1e-1 + @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.662 atol=1 + @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 + @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8397.85 atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1166.19 atol=1 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 atol=1 # 0.0 + @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 233.24 atol=1 + + + #also test CO2 breakeven cost + inputs["PV"]["min_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] + inputs["PV"]["max_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] + inputs["ElectricStorage"]["min_kw"] = results["ElectricStorage"]["size_kw"] + inputs["ElectricStorage"]["max_kw"] = results["ElectricStorage"]["size_kw"] + inputs["ElectricStorage"]["min_kwh"] = results["ElectricStorage"]["size_kwh"] + inputs["ElectricStorage"]["max_kwh"] = results["ElectricStorage"]["size_kwh"] + inputs["Financial"]["CO2_cost_per_tonne"] = results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] + inputs["Settings"]["include_climate_in_objective"] = true + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt([m1, m2], inputs) + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ inputs["Financial"]["CO2_cost_per_tonne"] atol=1e-1 + elseif i == 3 + @test results["PV"]["size_kw"] ≈ 20.0 atol=1e-1 + @test !haskey(results, "Wind") + @test !haskey(results, "ElectricStorage") + @test !haskey(results, "Generator") + @test results["CHP"]["size_kw"] ≈ 200.0 atol=1e-1 + @test results["AbsorptionChiller"]["size_ton"] ≈ 400.0 atol=1e-1 + @test results["HotThermalStorage"]["size_gal"] ≈ 50000 atol=1e1 + @test results["ColdThermalStorage"]["size_gal"] ≈ 30000 atol=1e1 + yr1_nat_gas_mmbtu = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] + results["CHP"]["annual_fuel_consumption_mmbtu"] + nat_gas_emissions_lb_per_mmbtu = Dict("CO2"=>117.03, "NOx"=>0.09139, "SO2"=>0.000578592, "PM25"=>0.007328833) + TONNE_PER_LB = 1/2204.62 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ nat_gas_emissions_lb_per_mmbtu["CO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_NOx"] ≈ nat_gas_emissions_lb_per_mmbtu["NOx"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_SO2"] ≈ nat_gas_emissions_lb_per_mmbtu["SO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_PM25"] ≈ nat_gas_emissions_lb_per_mmbtu["PM25"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 + @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_NOx"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_NOx"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_NOx"] atol=0.1 + @test results["Site"]["lifecycle_emissions_tonnes_SO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_SO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_SO2"] atol=1e-2 + @test results["Site"]["lifecycle_emissions_tonnes_PM25"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_PM25"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_PM25"] atol=1.5e-2 + @test results["Site"]["annual_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 + @test results["Site"]["renewable_electricity_fraction"] ≈ results["Site"]["annual_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 + KWH_PER_MMBTU = 293.07107 + annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_renewable_electricity_kwh"] + annual_heat_kwh = (results["CHP"]["annual_thermal_production_mmbtu"] + results["ExistingBoiler"]["annual_thermal_production_mmbtu"]) * KWH_PER_MMBTU + @test results["Site"]["total_renewable_energy_fraction"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 + end end end - end - @testset "Back pressure steam turbine" begin - """ - Validation to ensure that: - 1) ExistingBoiler provides the thermal energy (steam) to a backpressure SteamTurbine for CHP application - 2) SteamTurbine serves the heating load with the condensing steam + @testset "Back pressure steam turbine" begin + """ + Validation to ensure that: + 1) ExistingBoiler provides the thermal energy (steam) to a backpressure SteamTurbine for CHP application + 2) SteamTurbine serves the heating load with the condensing steam - """ - # Setup inputs, make heating load large to entice SteamTurbine - input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") - latitude = input_data["Site"]["latitude"] - longitude = input_data["Site"]["longitude"] - building = "Hospital" - elec_load_multiplier = 5.0 - heat_load_multiplier = 100.0 - input_data["ElectricLoad"]["doe_reference_name"] = building - input_data["SpaceHeatingLoad"]["doe_reference_name"] = building - input_data["DomesticHotWaterLoad"]["doe_reference_name"] = building - elec_load = REopt.ElectricLoad(latitude=latitude, longitude=longitude, doe_reference_name=building) - input_data["ElectricLoad"]["annual_kwh"] = elec_load_multiplier * sum(elec_load.loads_kw) - space_load = REopt.SpaceHeatingLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = heat_load_multiplier * space_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] - dhw_load = REopt.DomesticHotWaterLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = heat_load_multiplier * dhw_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], inputs) - - # The expected values below were directly copied from the REopt_API V2 expected values - @test results["Financial"]["lcc"] ≈ 189359280.0 rtol=0.001 - @test results["Financial"]["npv"] ≈ 8085233.0 rtol=0.01 - @test results["SteamTurbine"]["size_kw"] ≈ 2616.418 atol=1.0 - @test results["SteamTurbine"]["annual_thermal_consumption_mmbtu"] ≈ 1000557.6 rtol=0.001 - @test results["SteamTurbine"]["annual_electric_production_kwh"] ≈ 18970374.6 rtol=0.001 - @test results["SteamTurbine"]["annual_thermal_production_mmbtu"] ≈ 924045.1 rtol=0.001 - - # BAU boiler loads - load_boiler_fuel = (s.space_heating_load.loads_kw + s.dhw_load.loads_kw) ./ REopt.KWH_PER_MMBTU ./ s.existing_boiler.efficiency - load_boiler_thermal = load_boiler_fuel * s.existing_boiler.efficiency - - # ExistingBoiler and SteamTurbine production - boiler_to_load = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] - boiler_to_st = results["ExistingBoiler"]["thermal_to_steamturbine_series_mmbtu_per_hour"] - boiler_total = boiler_to_load + boiler_to_st - st_to_load = results["SteamTurbine"]["thermal_to_load_series_mmbtu_per_hour"] - - # Fuel/thermal **consumption** - boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] - steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] - - # Check that all thermal supply to load meets the BAU load - thermal_to_load = sum(boiler_to_load) + sum(st_to_load) - @test thermal_to_load ≈ sum(load_boiler_thermal) atol=1.0 - - # Check the net electric efficiency of Boiler->SteamTurbine (electric out/fuel in) with the expected value from the Fact Sheet - steamturbine_electric = results["SteamTurbine"]["electric_production_series_kw"] - net_electric_efficiency = sum(steamturbine_electric) / (sum(boiler_fuel) * REopt.KWH_PER_MMBTU) - @test net_electric_efficiency ≈ 0.052 atol=0.005 - - # Check that the max production of the boiler is still less than peak heating load times thermal factor - factor = input_data["ExistingBoiler"]["max_thermal_factor_on_peak_load"] - boiler_capacity = maximum(load_boiler_thermal) * factor - @test maximum(boiler_total) <= boiler_capacity - end + """ + # Setup inputs, make heating load large to entice SteamTurbine + input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") + latitude = input_data["Site"]["latitude"] + longitude = input_data["Site"]["longitude"] + building = "Hospital" + elec_load_multiplier = 5.0 + heat_load_multiplier = 100.0 + input_data["ElectricLoad"]["doe_reference_name"] = building + input_data["SpaceHeatingLoad"]["doe_reference_name"] = building + input_data["DomesticHotWaterLoad"]["doe_reference_name"] = building + elec_load = REopt.ElectricLoad(latitude=latitude, longitude=longitude, doe_reference_name=building) + input_data["ElectricLoad"]["annual_kwh"] = elec_load_multiplier * sum(elec_load.loads_kw) + space_load = REopt.SpaceHeatingLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = heat_load_multiplier * space_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] + dhw_load = REopt.DomesticHotWaterLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = heat_load_multiplier * dhw_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], inputs) + + # The expected values below were directly copied from the REopt_API V2 expected values + @test results["Financial"]["lcc"] ≈ 189359280.0 rtol=0.001 + @test results["Financial"]["npv"] ≈ 8085233.0 rtol=0.01 + @test results["SteamTurbine"]["size_kw"] ≈ 2616.418 atol=1.0 + @test results["SteamTurbine"]["annual_thermal_consumption_mmbtu"] ≈ 1000557.6 rtol=0.001 + @test results["SteamTurbine"]["annual_electric_production_kwh"] ≈ 18970374.6 rtol=0.001 + @test results["SteamTurbine"]["annual_thermal_production_mmbtu"] ≈ 924045.1 rtol=0.001 + + # BAU boiler loads + load_boiler_fuel = (s.space_heating_load.loads_kw + s.dhw_load.loads_kw) ./ REopt.KWH_PER_MMBTU ./ s.existing_boiler.efficiency + load_boiler_thermal = load_boiler_fuel * s.existing_boiler.efficiency + + # ExistingBoiler and SteamTurbine production + boiler_to_load = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] + boiler_to_st = results["ExistingBoiler"]["thermal_to_steamturbine_series_mmbtu_per_hour"] + boiler_total = boiler_to_load + boiler_to_st + st_to_load = results["SteamTurbine"]["thermal_to_load_series_mmbtu_per_hour"] + + # Fuel/thermal **consumption** + boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] + steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] + + # Check that all thermal supply to load meets the BAU load + thermal_to_load = sum(boiler_to_load) + sum(st_to_load) + @test thermal_to_load ≈ sum(load_boiler_thermal) atol=1.0 + + # Check the net electric efficiency of Boiler->SteamTurbine (electric out/fuel in) with the expected value from the Fact Sheet + steamturbine_electric = results["SteamTurbine"]["electric_production_series_kw"] + net_electric_efficiency = sum(steamturbine_electric) / (sum(boiler_fuel) * REopt.KWH_PER_MMBTU) + @test net_electric_efficiency ≈ 0.052 atol=0.005 + + # Check that the max production of the boiler is still less than peak heating load times thermal factor + factor = input_data["ExistingBoiler"]["max_thermal_factor_on_peak_load"] + boiler_capacity = maximum(load_boiler_thermal) * factor + @test maximum(boiler_total) <= boiler_capacity + end - @testset "All heating supply/demand/storage energy balance" begin - """ - Validation to ensure that: - 1) Heat balance is correct with SteamTurbine (backpressure), CHP, HotTES, and AbsorptionChiller included - 2) The sum of a all thermal from techs supplying SteamTurbine is equal to SteamTurbine thermal consumption - 3) Techs are not supplying SteamTurbine with thermal if can_supply_steam_turbine = False - - :return: - """ - - # Start with steam turbine inputs, but adding a bunch below - input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") - input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" - # Add SpaceHeatingLoad building for heating loads, ignore DomesticHotWaterLoad for simplicity of energy balance checks - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" - delete!(input_data, "DomesticHotWaterLoad") - - # Fix size of SteamTurbine, even if smaller than practical, because we're just looking at energy balances - input_data["SteamTurbine"]["min_kw"] = 30.0 - input_data["SteamTurbine"]["max_kw"] = 30.0 - - # Add CHP - input_data["CHP"] = Dict{Any, Any}([ - ("prime_mover", "recip_engine"), - ("size_class", 4), - ("min_kw", 250.0), - ("min_allowable_kw", 0.0), - ("max_kw", 250.0), - ("can_supply_steam_turbine", false), - ("fuel_cost_per_mmbtu", 8.0), - ("cooling_thermal_factor", 1.0) - ]) - - input_data["Financial"]["chp_fuel_cost_escalation_rate_fraction"] = 0.034 - - # Add CoolingLoad and AbsorptionChiller so we can test the energy balance on AbsorptionChiller too (thermal consumption) - input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital") - input_data["AbsorptionChiller"] = Dict{Any, Any}([ - ("min_ton", 600.0), - ("max_ton", 600.0), - ("cop_thermal", 0.7), - ("installed_cost_per_ton", 500.0), - ("om_cost_per_ton", 0.5), - ("heating_load_input", "SpaceHeating") - ]) - - # Add Hot TES - input_data["HotThermalStorage"] = Dict{Any, Any}([ - ("min_gal", 50000.0), - ("max_gal", 50000.0) + @testset "All heating supply/demand/storage energy balance" begin + """ + Validation to ensure that: + 1) Heat balance is correct with SteamTurbine (backpressure), CHP, HotTES, and AbsorptionChiller included + 2) The sum of a all thermal from techs supplying SteamTurbine is equal to SteamTurbine thermal consumption + 3) Techs are not supplying SteamTurbine with thermal if can_supply_steam_turbine = False + + :return: + """ + + # Start with steam turbine inputs, but adding a bunch below + input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") + input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" + # Add SpaceHeatingLoad building for heating loads, ignore DomesticHotWaterLoad for simplicity of energy balance checks + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" + delete!(input_data, "DomesticHotWaterLoad") + + # Fix size of SteamTurbine, even if smaller than practical, because we're just looking at energy balances + input_data["SteamTurbine"]["min_kw"] = 30.0 + input_data["SteamTurbine"]["max_kw"] = 30.0 + + # Add CHP + input_data["CHP"] = Dict{Any, Any}([ + ("prime_mover", "recip_engine"), + ("size_class", 4), + ("min_kw", 250.0), + ("min_allowable_kw", 0.0), + ("max_kw", 250.0), + ("can_supply_steam_turbine", false), + ("fuel_cost_per_mmbtu", 8.0), + ("cooling_thermal_factor", 1.0) ]) - - s = Scenario(input_data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - thermal_techs = ["ExistingBoiler", "CHP", "SteamTurbine"] - thermal_loads = ["load", "storage", "steamturbine", "waste"] # We don't track AbsorptionChiller thermal consumption by tech - tech_to_thermal_load = Dict{Any, Any}() - for tech in thermal_techs - tech_to_thermal_load[tech] = Dict{Any, Any}() - for load in thermal_loads - if (tech == "SteamTurbine" && load == "steamturbine") || (load == "waste" && tech != "CHP") - tech_to_thermal_load[tech][load] = [0.0] * 8760 - else - if load == "waste" - tech_to_thermal_load[tech][load] = results[tech]["thermal_curtailed_series_mmbtu_per_hour"] + + input_data["Financial"]["chp_fuel_cost_escalation_rate_fraction"] = 0.034 + + # Add CoolingLoad and AbsorptionChiller so we can test the energy balance on AbsorptionChiller too (thermal consumption) + input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital") + input_data["AbsorptionChiller"] = Dict{Any, Any}([ + ("min_ton", 600.0), + ("max_ton", 600.0), + ("cop_thermal", 0.7), + ("installed_cost_per_ton", 500.0), + ("om_cost_per_ton", 0.5), + ("heating_load_input", "SpaceHeating") + ]) + + # Add Hot TES + input_data["HotThermalStorage"] = Dict{Any, Any}([ + ("min_gal", 50000.0), + ("max_gal", 50000.0) + ]) + + s = Scenario(input_data) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) + + thermal_techs = ["ExistingBoiler", "CHP", "SteamTurbine"] + thermal_loads = ["load", "storage", "steamturbine", "waste"] # We don't track AbsorptionChiller thermal consumption by tech + tech_to_thermal_load = Dict{Any, Any}() + for tech in thermal_techs + tech_to_thermal_load[tech] = Dict{Any, Any}() + for load in thermal_loads + if (tech == "SteamTurbine" && load == "steamturbine") || (load == "waste" && tech != "CHP") + tech_to_thermal_load[tech][load] = [0.0] * 8760 else - tech_to_thermal_load[tech][load] = results[tech]["thermal_to_"*load*"_series_mmbtu_per_hour"] + if load == "waste" + tech_to_thermal_load[tech][load] = results[tech]["thermal_curtailed_series_mmbtu_per_hour"] + else + tech_to_thermal_load[tech][load] = results[tech]["thermal_to_"*load*"_series_mmbtu_per_hour"] + end end end end - end - # Hot TES is the other thermal supply - hottes_to_load = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] - - # BAU boiler loads - load_boiler_fuel = s.space_heating_load.loads_kw / input_data["ExistingBoiler"]["efficiency"] ./ REopt.KWH_PER_MMBTU - load_boiler_thermal = load_boiler_fuel .* input_data["ExistingBoiler"]["efficiency"] - - # Fuel/thermal **consumption** - boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] - chp_fuel_total = results["CHP"]["annual_fuel_consumption_mmbtu"] - steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] - absorptionchiller_thermal_in = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] - - # Check that all thermal supply to load meets the BAU load plus AbsorptionChiller load which is not explicitly tracked - alltechs_thermal_to_load_total = sum([sum(tech_to_thermal_load[tech]["load"]) for tech in thermal_techs]) + sum(hottes_to_load) - thermal_load_total = sum(load_boiler_thermal) + sum(absorptionchiller_thermal_in) - @test alltechs_thermal_to_load_total ≈ thermal_load_total rtol=1e-5 - - # Check that all thermal to steam turbine is equal to steam turbine thermal consumption - alltechs_thermal_to_steamturbine_total = sum([sum(tech_to_thermal_load[tech]["steamturbine"]) for tech in ["ExistingBoiler", "CHP"]]) - @test alltechs_thermal_to_steamturbine_total ≈ sum(steamturbine_thermal_in) atol=3 - - # Check that "thermal_to_steamturbine" is zero for each tech which has input of can_supply_steam_turbine as False - for tech in ["ExistingBoiler", "CHP"] - if !(tech in inputs.techs.can_supply_steam_turbine) - @test sum(tech_to_thermal_load[tech]["steamturbine"]) == 0.0 + # Hot TES is the other thermal supply + hottes_to_load = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] + + # BAU boiler loads + load_boiler_fuel = s.space_heating_load.loads_kw / input_data["ExistingBoiler"]["efficiency"] ./ REopt.KWH_PER_MMBTU + load_boiler_thermal = load_boiler_fuel .* input_data["ExistingBoiler"]["efficiency"] + + # Fuel/thermal **consumption** + boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] + chp_fuel_total = results["CHP"]["annual_fuel_consumption_mmbtu"] + steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] + absorptionchiller_thermal_in = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] + + # Check that all thermal supply to load meets the BAU load plus AbsorptionChiller load which is not explicitly tracked + alltechs_thermal_to_load_total = sum([sum(tech_to_thermal_load[tech]["load"]) for tech in thermal_techs]) + sum(hottes_to_load) + thermal_load_total = sum(load_boiler_thermal) + sum(absorptionchiller_thermal_in) + @test alltechs_thermal_to_load_total ≈ thermal_load_total rtol=1e-5 + + # Check that all thermal to steam turbine is equal to steam turbine thermal consumption + alltechs_thermal_to_steamturbine_total = sum([sum(tech_to_thermal_load[tech]["steamturbine"]) for tech in ["ExistingBoiler", "CHP"]]) + @test alltechs_thermal_to_steamturbine_total ≈ sum(steamturbine_thermal_in) atol=3 + + # Check that "thermal_to_steamturbine" is zero for each tech which has input of can_supply_steam_turbine as False + for tech in ["ExistingBoiler", "CHP"] + if !(tech in inputs.techs.can_supply_steam_turbine) + @test sum(tech_to_thermal_load[tech]["steamturbine"]) == 0.0 + end end end - end - @testset "Electric Heater" begin - d = JSON.parsefile("./scenarios/electric_heater.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.4 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.4 * 8760 - d["ProcessHeatLoad"]["annual_mmbtu"] = 0.2 * 8760 - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - - #first run: Boiler produces the required heat instead of the electric heater - electric heater should not be purchased - @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 - @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0 - d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_electric_heater_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP - annual_energy_supplied = 87600 + annual_electric_heater_consumption - - #Second run: ElectricHeater produces the required heat with free electricity - @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 - @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ annual_electric_heater_consumption rtol=1e-4 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + @testset "Electric Heater" begin + d = JSON.parsefile("./scenarios/electric_heater.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.4 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.4 * 8760 + d["ProcessHeatLoad"]["annual_mmbtu"] = 0.2 * 8760 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) - end + #first run: Boiler produces the required heat instead of the electric heater - electric heater should not be purchased + @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) - @testset "Process Heat Load" begin - d = JSON.parsefile("./scenarios/process_heat.json") - - # Test set 1: Boiler has free fuel, no emissions, and serves all heating load. - d["Boiler"]["fuel_cost_per_mmbtu"] = 0.0 - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0.0 atol=0.1 - - #Test set 2: Boiler only serves process heat - d["Boiler"]["can_serve_dhw"] = false - d["Boiler"]["can_serve_space_heating"] = false - d["Boiler"]["can_serve_process_heat"] = true - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 8.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - - #Test set 3: Boiler cannot serve process heat but serves DHW, space heating - d["Boiler"]["can_serve_dhw"] = true - d["Boiler"]["can_serve_space_heating"] = true - d["Boiler"]["can_serve_process_heat"] = false - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 16.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - - #Test set 4: Fuel expensive, but ExistingBoiler is retired - d["Boiler"]["can_serve_dhw"] = true - d["Boiler"]["can_serve_space_heating"] = true - d["Boiler"]["can_serve_process_heat"] = true - d["Boiler"]["fuel_cost_per_mmbtu"] = 30.0 - d["ExistingBoiler"]["retire_in_optimal"] = true - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - - #Test set 5: Fuel expensive, ExistingBoiler not retired - d["ExistingBoiler"]["retire_in_optimal"] = false - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - - # Test 6: reduce emissions by half, get half the new boiler size - d["Site"]["CO2_emissions_reduction_min_fraction"] = 0.50 - s = Scenario(d) - p = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 12.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 - end + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_electric_heater_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP + annual_energy_supplied = 87600 + annual_electric_heater_consumption + + #Second run: ElectricHeater produces the required heat with free electricity + @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 + @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ annual_electric_heater_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 + + end - @testset "Custom REopt logger" begin + @testset "Process Heat Load" begin + d = JSON.parsefile("./scenarios/process_heat.json") - # Throw a handled error - d = JSON.parsefile("./scenarios/logger.json") - - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - @test r["Messages"]["has_stacktrace"] == false - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - # Type is dict when errors, otherwise type REoptInputs - @test isa(REoptInputs(d), Dict) - - # Using filepath - n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([n1,n2], "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(n, "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - # Throw an unhandled error: Bad URDB rate -> stack gets returned for debugging - d["ElectricLoad"]["doe_reference_name"] = "MidriseApartment" - d["ElectricTariff"]["urdb_label"] = "62c70a6c40a0c425535d387x" - - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - # Type is dict when errors, otherwise type REoptInputs - @test isa(REoptInputs(d), Dict) - - # Using filepath - n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([n1,n2], "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(n, "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 + # Test set 1: Boiler has free fuel, no emissions, and serves all heating load. + d["Boiler"]["fuel_cost_per_mmbtu"] = 0.0 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0.0 atol=0.1 + + #Test set 2: Boiler only serves process heat + d["Boiler"]["can_serve_dhw"] = false + d["Boiler"]["can_serve_space_heating"] = false + d["Boiler"]["can_serve_process_heat"] = true + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 8.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + + #Test set 3: Boiler cannot serve process heat but serves DHW, space heating + d["Boiler"]["can_serve_dhw"] = true + d["Boiler"]["can_serve_space_heating"] = true + d["Boiler"]["can_serve_process_heat"] = false + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 16.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + + #Test set 4: Fuel expensive, but ExistingBoiler is retired + d["Boiler"]["can_serve_dhw"] = true + d["Boiler"]["can_serve_space_heating"] = true + d["Boiler"]["can_serve_process_heat"] = true + d["Boiler"]["fuel_cost_per_mmbtu"] = 30.0 + d["ExistingBoiler"]["retire_in_optimal"] = true + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + + #Test set 5: Fuel expensive, ExistingBoiler not retired + d["ExistingBoiler"]["retire_in_optimal"] = false + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + + # Test 6: reduce emissions by half, get half the new boiler size + d["Site"]["CO2_emissions_reduction_min_fraction"] = 0.50 + s = Scenario(d) + p = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 12.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 + end + + @testset "Custom REopt logger" begin + + # Throw a handled error + d = JSON.parsefile("./scenarios/logger.json") + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + @test r["Messages"]["has_stacktrace"] == false + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Type is dict when errors, otherwise type REoptInputs + @test isa(REoptInputs(d), Dict) + + # Using filepath + n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([n1,n2], "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(n, "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Throw an unhandled error: Bad URDB rate -> stack gets returned for debugging + d["ElectricLoad"]["doe_reference_name"] = "MidriseApartment" + d["ElectricTariff"]["urdb_label"] = "62c70a6c40a0c425535d387x" + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Type is dict when errors, otherwise type REoptInputs + @test isa(REoptInputs(d), Dict) + + # Using filepath + n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([n1,n2], "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(n, "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + end end end end \ No newline at end of file From e0dbac9ad7a46989f5e28111218989c63f4e970a Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Fri, 9 Aug 2024 22:29:44 -0600 Subject: [PATCH 168/266] Fix wrapper @testset call --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 5094cfc07..e0c51f19e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -23,7 +23,7 @@ elseif "CPLEX" in ARGS end else # run HiGHS tests - @testset verbose=true "REopt test set using HiGHS solver" + @testset verbose=true "REopt test set using HiGHS solver" begin @testset "Inputs" begin @testset "hybrid profile" begin electric_load = REopt.ElectricLoad(; From a8986379142edfe7c4d7994632938145d0d25156 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 12 Aug 2024 21:48:30 -0600 Subject: [PATCH 169/266] Add more verbose=true to @testset supersets And remove an unnecessary superset "inputs" which did not include a majority of input-related tests --- test/runtests.jl | 3804 +++++++++++++++++++++++----------------------- 1 file changed, 1900 insertions(+), 1904 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e0c51f19e..4aee1d809 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -24,46 +24,44 @@ elseif "CPLEX" in ARGS else # run HiGHS tests @testset verbose=true "REopt test set using HiGHS solver" begin - @testset "Inputs" begin - @testset "hybrid profile" begin - electric_load = REopt.ElectricLoad(; - blended_doe_reference_percents = [0.2, 0.2, 0.2, 0.2, 0.2], - blended_doe_reference_names = ["RetailStore", "LargeOffice", "MediumOffice", "SmallOffice", "Warehouse"], - annual_kwh = 50000.0, - year = 2017, - city = "Atlanta", - latitude = 35.2468, - longitude = -91.7337 - ) - @test sum(electric_load.loads_kw) ≈ 50000.0 - end - @testset "Solar dataset" begin - - # 1. Dallas TX - latitude, longitude = 32.775212075983646, -96.78105623767185 - radius = 0 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "nsrdb" - - # 2. Merefa, Ukraine - latitude, longitude = 49.80670544975866, 36.05418033509974 - radius = 0 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "nsrdb" - - # 3. Younde, Cameroon - latitude, longitude = 3.8603988398663125, 11.528880303663136 - radius = 0 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "intl" - - # 4. Fairbanks, AK - site = "Fairbanks" - latitude, longitude = 64.84112047064114, -147.71570239058084 - radius = 20 - dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "tmy3" - end + @testset "hybrid profile" begin + electric_load = REopt.ElectricLoad(; + blended_doe_reference_percents = [0.2, 0.2, 0.2, 0.2, 0.2], + blended_doe_reference_names = ["RetailStore", "LargeOffice", "MediumOffice", "SmallOffice", "Warehouse"], + annual_kwh = 50000.0, + year = 2017, + city = "Atlanta", + latitude = 35.2468, + longitude = -91.7337 + ) + @test sum(electric_load.loads_kw) ≈ 50000.0 + end + @testset "Solar dataset" begin + + # 1. Dallas TX + latitude, longitude = 32.775212075983646, -96.78105623767185 + radius = 0 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "nsrdb" + + # 2. Merefa, Ukraine + latitude, longitude = 49.80670544975866, 36.05418033509974 + radius = 0 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "nsrdb" + + # 3. Younde, Cameroon + latitude, longitude = 3.8603988398663125, 11.528880303663136 + radius = 0 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "intl" + + # 4. Fairbanks, AK + site = "Fairbanks" + latitude, longitude = 64.84112047064114, -147.71570239058084 + radius = 20 + dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) + @test dataset ≈ "tmy3" end @testset "January Export Rates" begin @@ -330,7 +328,7 @@ else # run HiGHS tests @test sim_cooling_ton ≈ s.cooling_load.loads_kw_thermal ./ REopt.KWH_THERMAL_PER_TONHOUR atol=0.1 end - @testset "Backup Generator Reliability" begin + @testset verbose=true "Backup Generator Reliability" begin @testset "Compare backup_reliability and simulate_outages" begin # Tests ensure `backup_reliability()` consistent with `simulate_outages()` @@ -558,7 +556,7 @@ else # run HiGHS tests @test reliability_results["mean_cumulative_survival_final_time_step"] ≈ 0.817586 atol=0.001 end - @testset "Disaggregated Heating Loads" begin + @testset verbose=true "Disaggregated Heating Loads" begin @testset "Process Heat Load Inputs" begin d = JSON.parsefile("./scenarios/electric_heater.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 @@ -590,7 +588,7 @@ else # run HiGHS tests end end - @testset "Net Metering" begin + @testset verbose=true "Net Metering" begin @testset "Net Metering Limit and Wholesale" begin #case 1: net metering limit is met by PV d = JSON.parsefile("./scenarios/net_metering.json") @@ -608,2021 +606,2019 @@ else # run HiGHS tests end end - @testset "Imported Xpress Test Suite" begin - @testset "Heating loads and addressable load fraction" begin - # Default LargeOffice CRB with SpaceHeatingLoad and DomesticHotWaterLoad are served by ExistingBoiler - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, "./scenarios/thermal_load.json") + @testset "Heating loads and addressable load fraction" begin + # Default LargeOffice CRB with SpaceHeatingLoad and DomesticHotWaterLoad are served by ExistingBoiler + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, "./scenarios/thermal_load.json") + + @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 12904 - @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 12904 - - # Hourly fuel load inputs with addressable_load_fraction are served as expected - data = JSON.parsefile("./scenarios/thermal_load.json") + # Hourly fuel load inputs with addressable_load_fraction are served as expected + data = JSON.parsefile("./scenarios/thermal_load.json") - data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) - data["DomesticHotWaterLoad"]["addressable_load_fraction"] = 0.6 - data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) - data["SpaceHeatingLoad"]["addressable_load_fraction"] = 0.8 - data["ProcessHeatLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.3], 8760) - data["ProcessHeatLoad"]["addressable_load_fraction"] = 0.7 + data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) + data["DomesticHotWaterLoad"]["addressable_load_fraction"] = 0.6 + data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.5], 8760) + data["SpaceHeatingLoad"]["addressable_load_fraction"] = 0.8 + data["ProcessHeatLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([0.3], 8760) + data["ProcessHeatLoad"]["addressable_load_fraction"] = 0.7 - s = Scenario(data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, inputs) - @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 8760 * (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) atol = 1.0 - - # Monthly fuel load input with addressable_load_fraction is processed to expected thermal load - data = JSON.parsefile("./scenarios/thermal_load.json") - data["DomesticHotWaterLoad"]["monthly_mmbtu"] = repeat([100], 12) - data["DomesticHotWaterLoad"]["addressable_load_fraction"] = repeat([0.6], 12) - data["SpaceHeatingLoad"]["monthly_mmbtu"] = repeat([200], 12) - data["SpaceHeatingLoad"]["addressable_load_fraction"] = repeat([0.8], 12) - data["ProcessHeatLoad"]["monthly_mmbtu"] = repeat([150], 12) - data["ProcessHeatLoad"]["addressable_load_fraction"] = repeat([0.7], 12) - - # Assuming Scenario and REoptInputs are defined functions/classes in your code - s = Scenario(data) - inputs = REoptInputs(s) + s = Scenario(data) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, inputs) + @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 8760 * (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) atol = 1.0 + + # Monthly fuel load input with addressable_load_fraction is processed to expected thermal load + data = JSON.parsefile("./scenarios/thermal_load.json") + data["DomesticHotWaterLoad"]["monthly_mmbtu"] = repeat([100], 12) + data["DomesticHotWaterLoad"]["addressable_load_fraction"] = repeat([0.6], 12) + data["SpaceHeatingLoad"]["monthly_mmbtu"] = repeat([200], 12) + data["SpaceHeatingLoad"]["addressable_load_fraction"] = repeat([0.8], 12) + data["ProcessHeatLoad"]["monthly_mmbtu"] = repeat([150], 12) + data["ProcessHeatLoad"]["addressable_load_fraction"] = repeat([0.7], 12) + + # Assuming Scenario and REoptInputs are defined functions/classes in your code + s = Scenario(data) + inputs = REoptInputs(s) - dhw_thermal_load_expected = sum(data["DomesticHotWaterLoad"]["monthly_mmbtu"] .* data["DomesticHotWaterLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency - space_thermal_load_expected = sum(data["SpaceHeatingLoad"]["monthly_mmbtu"] .* data["SpaceHeatingLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency - process_thermal_load_expected = sum(data["ProcessHeatLoad"]["monthly_mmbtu"] .* data["ProcessHeatLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + dhw_thermal_load_expected = sum(data["DomesticHotWaterLoad"]["monthly_mmbtu"] .* data["DomesticHotWaterLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + space_thermal_load_expected = sum(data["SpaceHeatingLoad"]["monthly_mmbtu"] .* data["SpaceHeatingLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency + process_thermal_load_expected = sum(data["ProcessHeatLoad"]["monthly_mmbtu"] .* data["ProcessHeatLoad"]["addressable_load_fraction"]) * s.existing_boiler.efficiency - @test round(sum(s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(dhw_thermal_load_expected) - @test round(sum(s.space_heating_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(space_thermal_load_expected) - @test round(sum(s.process_heat_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(process_thermal_load_expected) + @test round(sum(s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(dhw_thermal_load_expected) + @test round(sum(s.space_heating_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(space_thermal_load_expected) + @test round(sum(s.process_heat_load.loads_kw) / REopt.KWH_PER_MMBTU) ≈ sum(process_thermal_load_expected) + end + + @testset verbose=true "CHP" begin + @testset "CHP Sizing" begin + # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods + data_sizing = JSON.parsefile("./scenarios/chp_sizing.json") + s = Scenario(data_sizing) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt(m, inputs) + + @test round(results["CHP"]["size_kw"], digits=0) ≈ 400.0 atol=50.0 + @test round(results["Financial"]["lcc"], digits=0) ≈ 1.3476e7 rtol=1.0e-2 end + + @testset "CHP Cost Curve and Min Allowable Size" begin + # Fixed size CHP with cost curve, no unavailability_periods + data_cost_curve = JSON.parsefile("./scenarios/chp_sizing.json") + data_cost_curve["CHP"] = Dict() + data_cost_curve["CHP"]["prime_mover"] = "recip_engine" + data_cost_curve["CHP"]["size_class"] = 1 + data_cost_curve["CHP"]["fuel_cost_per_mmbtu"] = 8.0 + data_cost_curve["CHP"]["min_kw"] = 0 + data_cost_curve["CHP"]["min_allowable_kw"] = 555.5 + data_cost_curve["CHP"]["max_kw"] = 555.51 + data_cost_curve["CHP"]["installed_cost_per_kw"] = 1800.0 + data_cost_curve["CHP"]["installed_cost_per_kw"] = [2300.0, 1800.0, 1500.0] + data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] = [100.0, 300.0, 1140.0] - @testset "CHP" begin - @testset "CHP Sizing" begin - # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods - data_sizing = JSON.parsefile("./scenarios/chp_sizing.json") - s = Scenario(data_sizing) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - results = run_reopt(m, inputs) - - @test round(results["CHP"]["size_kw"], digits=0) ≈ 400.0 atol=50.0 - @test round(results["Financial"]["lcc"], digits=0) ≈ 1.3476e7 rtol=1.0e-2 - end + data_cost_curve["CHP"]["federal_itc_fraction"] = 0.1 + data_cost_curve["CHP"]["macrs_option_years"] = 0 + data_cost_curve["CHP"]["macrs_bonus_fraction"] = 0.0 + data_cost_curve["CHP"]["macrs_itc_reduction"] = 0.0 - @testset "CHP Cost Curve and Min Allowable Size" begin - # Fixed size CHP with cost curve, no unavailability_periods - data_cost_curve = JSON.parsefile("./scenarios/chp_sizing.json") - data_cost_curve["CHP"] = Dict() - data_cost_curve["CHP"]["prime_mover"] = "recip_engine" - data_cost_curve["CHP"]["size_class"] = 1 - data_cost_curve["CHP"]["fuel_cost_per_mmbtu"] = 8.0 - data_cost_curve["CHP"]["min_kw"] = 0 - data_cost_curve["CHP"]["min_allowable_kw"] = 555.5 - data_cost_curve["CHP"]["max_kw"] = 555.51 - data_cost_curve["CHP"]["installed_cost_per_kw"] = 1800.0 - data_cost_curve["CHP"]["installed_cost_per_kw"] = [2300.0, 1800.0, 1500.0] - data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] = [100.0, 300.0, 1140.0] - - data_cost_curve["CHP"]["federal_itc_fraction"] = 0.1 - data_cost_curve["CHP"]["macrs_option_years"] = 0 - data_cost_curve["CHP"]["macrs_bonus_fraction"] = 0.0 - data_cost_curve["CHP"]["macrs_itc_reduction"] = 0.0 - - expected_x = data_cost_curve["CHP"]["min_allowable_kw"] - cap_cost_y = data_cost_curve["CHP"]["installed_cost_per_kw"] - cap_cost_x = data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] - slope = (cap_cost_x[3] * cap_cost_y[3] - cap_cost_x[2] * cap_cost_y[2]) / (cap_cost_x[3] - cap_cost_x[2]) - init_capex_chp_expected = cap_cost_x[2] * cap_cost_y[2] + (expected_x - cap_cost_x[2]) * slope - lifecycle_capex_chp_expected = init_capex_chp_expected - - REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], - [0, init_capex_chp_expected * data_cost_curve["CHP"]["federal_itc_fraction"]]) - - #PV - data_cost_curve["PV"] = Dict() - data_cost_curve["PV"]["min_kw"] = 1500 - data_cost_curve["PV"]["max_kw"] = 1500 - data_cost_curve["PV"]["installed_cost_per_kw"] = 1600 - data_cost_curve["PV"]["federal_itc_fraction"] = 0.26 - data_cost_curve["PV"]["macrs_option_years"] = 0 - data_cost_curve["PV"]["macrs_bonus_fraction"] = 0.0 - data_cost_curve["PV"]["macrs_itc_reduction"] = 0.0 - - init_capex_pv_expected = data_cost_curve["PV"]["max_kw"] * data_cost_curve["PV"]["installed_cost_per_kw"] - lifecycle_capex_pv_expected = init_capex_pv_expected - - REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], - [0, init_capex_pv_expected * data_cost_curve["PV"]["federal_itc_fraction"]]) - - s = Scenario(data_cost_curve) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - init_capex_total_expected = init_capex_chp_expected + init_capex_pv_expected - lifecycle_capex_total_expected = lifecycle_capex_chp_expected + lifecycle_capex_pv_expected - - init_capex_total = results["Financial"]["initial_capital_costs"] - lifecycle_capex_total = results["Financial"]["initial_capital_costs_after_incentives"] - - - # Check initial CapEx (pre-incentive/tax) and life cycle CapEx (post-incentive/tax) cost with expect - @test init_capex_total_expected ≈ init_capex_total atol=0.0001*init_capex_total_expected - @test lifecycle_capex_total_expected ≈ lifecycle_capex_total atol=0.0001*lifecycle_capex_total_expected - - # Test CHP.min_allowable_kw - the size would otherwise be ~100 kW less by setting min_allowable_kw to zero - @test results["CHP"]["size_kw"] ≈ data_cost_curve["CHP"]["min_allowable_kw"] atol=0.1 - end + expected_x = data_cost_curve["CHP"]["min_allowable_kw"] + cap_cost_y = data_cost_curve["CHP"]["installed_cost_per_kw"] + cap_cost_x = data_cost_curve["CHP"]["tech_sizes_for_cost_curve"] + slope = (cap_cost_x[3] * cap_cost_y[3] - cap_cost_x[2] * cap_cost_y[2]) / (cap_cost_x[3] - cap_cost_x[2]) + init_capex_chp_expected = cap_cost_x[2] * cap_cost_y[2] + (expected_x - cap_cost_x[2]) * slope + lifecycle_capex_chp_expected = init_capex_chp_expected - + REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], + [0, init_capex_chp_expected * data_cost_curve["CHP"]["federal_itc_fraction"]]) - @testset "CHP Unavailability and Outage" begin - """ - Validation to ensure that: - 1) CHP meets load during outage without exporting - 2) CHP never exports if chp.can_wholesale and chp.can_net_meter inputs are False (default) - 3) CHP does not "curtail", i.e. send power to a load bank when chp.can_curtail is False (default) - 4) CHP min_turn_down_fraction is ignored during an outage - 5) Cooling tech production gets zeroed out during the outage period because we ignore the cooling load balance for outage - 6) Unavailability intervals that intersect with grid-outages get ignored - 7) Unavailability intervals that do not intersect with grid-outages result in no CHP production - """ - # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods - data = JSON.parsefile("./scenarios/chp_unavailability_outage.json") - - # Add unavailability periods that 1) intersect (ignored) and 2) don't intersect with outage period - data["CHP"]["unavailability_periods"] = [Dict([("month", 1), ("start_week_of_month", 2), - ("start_day_of_week", 1), ("start_hour", 1), ("duration_hours", 8)]), - Dict([("month", 1), ("start_week_of_month", 2), - ("start_day_of_week", 3), ("start_hour", 9), ("duration_hours", 8)])] - - # Manually doing the math from the unavailability defined above - unavail_1_start = 24 + 1 - unavail_1_end = unavail_1_start + 8 - 1 - unavail_2_start = 24*3 + 9 - unavail_2_end = unavail_2_start + 8 - 1 - - # Specify the CHP.min_turn_down_fraction which is NOT used during an outage - data["CHP"]["min_turn_down_fraction"] = 0.5 - # Specify outage period; outage time_steps are 1-indexed - outage_start = unavail_1_start - data["ElectricUtility"]["outage_start_time_step"] = outage_start - outage_end = unavail_1_end - data["ElectricUtility"]["outage_end_time_step"] = outage_end - data["ElectricLoad"]["critical_load_fraction"] = 0.25 - - s = Scenario(data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - tot_elec_load = results["ElectricLoad"]["load_series_kw"] - chp_total_elec_prod = results["CHP"]["electric_production_series_kw"] - chp_to_load = results["CHP"]["electric_to_load_series_kw"] - chp_export = results["CHP"]["electric_to_grid_series_kw"] - cooling_elec_consumption = results["ExistingChiller"]["electric_consumption_series_kw"] - - # The values compared to the expected values - @test sum([(chp_to_load[i] - tot_elec_load[i]*data["ElectricLoad"]["critical_load_fraction"]) for i in outage_start:outage_end]) ≈ 0.0 atol=0.001 - critical_load = tot_elec_load[outage_start:outage_end] * data["ElectricLoad"]["critical_load_fraction"] - @test sum(chp_to_load[outage_start:outage_end]) ≈ sum(critical_load) atol=0.1 - @test sum(chp_export) == 0.0 - @test sum(chp_total_elec_prod) ≈ sum(chp_to_load) atol=1.0e-5*sum(chp_total_elec_prod) - @test sum(cooling_elec_consumption[outage_start:outage_end]) == 0.0 - @test sum(chp_total_elec_prod[unavail_2_start:unavail_2_end]) == 0.0 - end + #PV + data_cost_curve["PV"] = Dict() + data_cost_curve["PV"]["min_kw"] = 1500 + data_cost_curve["PV"]["max_kw"] = 1500 + data_cost_curve["PV"]["installed_cost_per_kw"] = 1600 + data_cost_curve["PV"]["federal_itc_fraction"] = 0.26 + data_cost_curve["PV"]["macrs_option_years"] = 0 + data_cost_curve["PV"]["macrs_bonus_fraction"] = 0.0 + data_cost_curve["PV"]["macrs_itc_reduction"] = 0.0 - @testset "CHP Supplementary firing and standby" begin - """ - Test to ensure that supplementary firing and standby charges work as intended. The thermal and - electrical loads are constant, and the CHP system size is fixed; the supplementary firing has a - similar cost to the boiler and is purcahsed and used when the boiler efficiency is set to a lower - value than that of the supplementary firing. The test also ensures that demand charges are - correctly calculated when CHP is and is not allowed to reduce demand charges. - """ - data = JSON.parsefile("./scenarios/chp_supplementary_firing.json") - data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10000 - data["ElectricLoad"]["loads_kw"] = repeat([800.0], 8760) - data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) - data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) - #part 1: supplementary firing not used when less efficient than the boiler and expensive - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - s = Scenario(data) - inputs = REoptInputs(s) - results = run_reopt(m1, inputs) - @test results["CHP"]["size_kw"] == 800 - @test results["CHP"]["size_supplemental_firing_kw"] == 0 - @test results["CHP"]["annual_electric_production_kwh"] ≈ 800*8760 rtol=1e-5 - @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 800*(0.4418/0.3573)*8760/293.07107 rtol=1e-5 - @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] == 0 - @test results["HeatingLoad"]["annual_calculated_total_heating_thermal_load_mmbtu"] == 12.0 * 8760 * data["ExistingBoiler"]["efficiency"] - @test results["HeatingLoad"]["annual_calculated_dhw_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] - @test results["HeatingLoad"]["annual_calculated_space_heating_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] - - #part 2: supplementary firing used when more efficient than the boiler and low-cost; demand charges not reduced by CHP - data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10 - data["CHP"]["reduces_demand_charges"] = false - data["ExistingBoiler"]["efficiency"] = 0.85 - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - s = Scenario(data) - inputs = REoptInputs(s) - results = run_reopt(m2, inputs) - @test results["CHP"]["size_supplemental_firing_kw"] ≈ 321.71 atol=0.1 - @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 149136.6 rtol=1e-5 - @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] ≈ 5212.7 rtol=1e-5 - end - - @testset "CHP to Waste Heat" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d = JSON.parsefile("./scenarios/chp_waste.json") - results = run_reopt(m, d) - @test sum(results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 4174.455 atol=1e-3 - end - end + init_capex_pv_expected = data_cost_curve["PV"]["max_kw"] * data_cost_curve["PV"]["installed_cost_per_kw"] + lifecycle_capex_pv_expected = init_capex_pv_expected - + REopt.npv(data_cost_curve["Financial"]["offtaker_discount_rate_fraction"], + [0, init_capex_pv_expected * data_cost_curve["PV"]["federal_itc_fraction"]]) - @testset "FlexibleHVAC" begin - - @testset "Single RC Model heating only" begin - #= - Single RC model: - 1 state/control node - 2 inputs: Ta and Qheat - A = [1/(RC)], B = [1/(RC) 1/C], u = [Ta; Q] - NOTE exogenous_inputs (u) allows for parasitic heat, but it is input as zeros here - - We start with no technologies except ExistingBoiler and ExistingChiller. - FlexibleHVAC is only worth purchasing if its cost is neglible (i.e. below the lcc_bau * MIPTOL) - or if there is a time-varying fuel and/or electricity cost - (and the FlexibleHVAC installed_cost is less than the achievable savings). - =# - - # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC - pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431); - R = 0.00025 # K/kW - C = 1e5 # kJ/K - # the starting scenario has flat fuel and electricty costs - d = JSON.parsefile("./scenarios/thermal_load.json"); - A = reshape([-1/(R*C)], 1,1) - B = [1/(R*C) 1/C] - u = [tamb zeros(8760)]'; - d["FlexibleHVAC"] = Dict( - "control_node" => 1, - "initial_temperatures" => [21], - "temperature_upper_bound_degC" => 22.0, - "temperature_lower_bound_degC" => 19.8, - "installed_cost" => 300.0, # NOTE cost must be more then the MIPTOL * LCC 5e-5 * 5.79661e6 ≈ 290 to make FlexibleHVAC not worth it - "system_matrix" => A, - "input_matrix" => B, - "exogenous_inputs" => u - ) - - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false - # @test r["Financial"]["npv"] == 0 - - # put in a time varying fuel cost, which should make purchasing the FlexibleHVAC system economical - # with flat ElectricTariff the ExistingChiller does not benefit from FlexibleHVAC - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = rand(Float64, (8760))*(50-5).+5; - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # all of the savings are from the ExistingBoiler fuel costs - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === true - # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] - # @test fuel_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 - - # now increase the FlexibleHVAC installed_cost to the fuel costs savings + 100 and expect that the FlexibleHVAC is not purchased - # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + 100 - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false - # @test r["Financial"]["npv"] == 0 - - # add TOU ElectricTariff and expect to benefit from using ExistingChiller intelligently - d["ElectricTariff"] = Dict("urdb_label" => "5ed6c1a15457a3367add15ae") - - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - - # elec_cost_savings = r["ElectricTariff"]["lifecycle_demand_cost_after_tax_bau"] + - # r["ElectricTariff"]["lifecycle_energy_cost_after_tax_bau"] - - # r["ElectricTariff"]["lifecycle_demand_cost_after_tax"] - - # r["ElectricTariff"]["lifecycle_energy_cost_after_tax"] - - # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] - # @test fuel_cost_savings + elec_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 - - # now increase the FlexibleHVAC installed_cost to the fuel costs savings + elec_cost_savings - # + 100 and expect that the FlexibleHVAC is not purchased - # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + elec_cost_savings + 100 - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) - # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false - # @test r["Financial"]["npv"] == 0 + s = Scenario(data_cost_curve) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) - end + init_capex_total_expected = init_capex_chp_expected + init_capex_pv_expected + lifecycle_capex_total_expected = lifecycle_capex_chp_expected + lifecycle_capex_pv_expected + + init_capex_total = results["Financial"]["initial_capital_costs"] + lifecycle_capex_total = results["Financial"]["initial_capital_costs_after_incentives"] + + + # Check initial CapEx (pre-incentive/tax) and life cycle CapEx (post-incentive/tax) cost with expect + @test init_capex_total_expected ≈ init_capex_total atol=0.0001*init_capex_total_expected + @test lifecycle_capex_total_expected ≈ lifecycle_capex_total atol=0.0001*lifecycle_capex_total_expected + + # Test CHP.min_allowable_kw - the size would otherwise be ~100 kW less by setting min_allowable_kw to zero + @test results["CHP"]["size_kw"] ≈ data_cost_curve["CHP"]["min_allowable_kw"] atol=0.1 end - - #= - add a time-of-export rate that is greater than retail rate for the month of January, - check to make sure that PV does NOT export unless the site load is met first for the month of January. - =# - @testset "Do not allow_simultaneous_export_import" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - data = JSON.parsefile("./scenarios/monthly_rate.json") - - # create wholesale_rate with compensation in January > retail rate - jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] - data["ElectricTariff"]["wholesale_rate"] = - append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) - data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) - data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false) - + + @testset "CHP Unavailability and Outage" begin + """ + Validation to ensure that: + 1) CHP meets load during outage without exporting + 2) CHP never exports if chp.can_wholesale and chp.can_net_meter inputs are False (default) + 3) CHP does not "curtail", i.e. send power to a load bank when chp.can_curtail is False (default) + 4) CHP min_turn_down_fraction is ignored during an outage + 5) Cooling tech production gets zeroed out during the outage period because we ignore the cooling load balance for outage + 6) Unavailability intervals that intersect with grid-outages get ignored + 7) Unavailability intervals that do not intersect with grid-outages result in no CHP production + """ + # Sizing CHP with non-constant efficiency, no cost curve, no unavailability_periods + data = JSON.parsefile("./scenarios/chp_unavailability_outage.json") + + # Add unavailability periods that 1) intersect (ignored) and 2) don't intersect with outage period + data["CHP"]["unavailability_periods"] = [Dict([("month", 1), ("start_week_of_month", 2), + ("start_day_of_week", 1), ("start_hour", 1), ("duration_hours", 8)]), + Dict([("month", 1), ("start_week_of_month", 2), + ("start_day_of_week", 3), ("start_hour", 9), ("duration_hours", 8)])] + + # Manually doing the math from the unavailability defined above + unavail_1_start = 24 + 1 + unavail_1_end = unavail_1_start + 8 - 1 + unavail_2_start = 24*3 + 9 + unavail_2_end = unavail_2_start + 8 - 1 + + # Specify the CHP.min_turn_down_fraction which is NOT used during an outage + data["CHP"]["min_turn_down_fraction"] = 0.5 + # Specify outage period; outage time_steps are 1-indexed + outage_start = unavail_1_start + data["ElectricUtility"]["outage_start_time_step"] = outage_start + outage_end = unavail_1_end + data["ElectricUtility"]["outage_end_time_step"] = outage_end + data["ElectricLoad"]["critical_load_fraction"] = 0.25 + s = Scenario(data) inputs = REoptInputs(s) - results = run_reopt(model, inputs) - - @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"][1:744]) - if results["PV"]["electric_to_grid_series_kw"][i] > 0) - end - - @testset "Solar and ElectricStorage w/BAU and degradation" begin - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - d = JSON.parsefile("scenarios/pv_storage.json"); - d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) - results = run_reopt([m1,m2], d) - - @test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01 - @test results["PV"]["lcoe_per_kwh"] ≈ 0.0468 atol = 0.001 - @test results["Financial"]["lcc"] ≈ 1.239179e7 rtol=1e-5 - @test results["Financial"]["lcc_bau"] ≈ 12766397 rtol=1e-5 - @test results["ElectricStorage"]["size_kw"] ≈ 49.02 atol=0.1 - @test results["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 - proforma_npv = REopt.npv(results["Financial"]["offtaker_annual_free_cashflows"] - - results["Financial"]["offtaker_annual_free_cashflows_bau"], 0.081) - @test results["Financial"]["npv"] ≈ proforma_npv rtol=0.0001 - - # compare avg soc with and without degradation, - # using default augmentation battery maintenance strategy - avg_soc_no_degr = sum(results["ElectricStorage"]["soc_series_fraction"]) / 8760 - d["ElectricStorage"]["model_degradation"] = true - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r_degr = run_reopt(m, d) - avg_soc_degr = sum(r_degr["ElectricStorage"]["soc_series_fraction"]) / 8760 - @test avg_soc_no_degr > avg_soc_degr - - # test the replacement strategy - d["ElectricStorage"]["degradation"] = Dict("maintenance_strategy" => "replacement") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - set_optimizer_attribute(m, "mip_rel_gap", 0.01) - r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) - # #optimal SOH at end of horizon is 80\% to prevent any replacement - # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 - # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 - # # the maintenance_cost comes out to 3004.39 on Actions, so we test the LCC since it should match - # @test r["Financial"]["lcc"] ≈ 1.240096e7 rtol=0.01 - # @test last(value.(m[:SOH])) ≈ 66.633 rtol=0.01 - # @test r["ElectricStorage"]["size_kwh"] ≈ 83.29 rtol=0.01 - - # test minimum_avg_soc_fraction - d["ElectricStorage"]["minimum_avg_soc_fraction"] = 0.72 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - set_optimizer_attribute(m, "mip_rel_gap", 0.01) - r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) - # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) + + tot_elec_load = results["ElectricLoad"]["load_series_kw"] + chp_total_elec_prod = results["CHP"]["electric_production_series_kw"] + chp_to_load = results["CHP"]["electric_to_load_series_kw"] + chp_export = results["CHP"]["electric_to_grid_series_kw"] + cooling_elec_consumption = results["ExistingChiller"]["electric_consumption_series_kw"] + + # The values compared to the expected values + @test sum([(chp_to_load[i] - tot_elec_load[i]*data["ElectricLoad"]["critical_load_fraction"]) for i in outage_start:outage_end]) ≈ 0.0 atol=0.001 + critical_load = tot_elec_load[outage_start:outage_end] * data["ElectricLoad"]["critical_load_fraction"] + @test sum(chp_to_load[outage_start:outage_end]) ≈ sum(critical_load) atol=0.1 + @test sum(chp_export) == 0.0 + @test sum(chp_total_elec_prod) ≈ sum(chp_to_load) atol=1.0e-5*sum(chp_total_elec_prod) + @test sum(cooling_elec_consumption[outage_start:outage_end]) == 0.0 + @test sum(chp_total_elec_prod[unavail_2_start:unavail_2_end]) == 0.0 end - - @testset "Outage with Generator, outage simulator, BAU critical load outputs" begin + + @testset "CHP Supplementary firing and standby" begin + """ + Test to ensure that supplementary firing and standby charges work as intended. The thermal and + electrical loads are constant, and the CHP system size is fixed; the supplementary firing has a + similar cost to the boiler and is purcahsed and used when the boiler efficiency is set to a lower + value than that of the supplementary firing. The test also ensures that demand charges are + correctly calculated when CHP is and is not allowed to reduce demand charges. + """ + data = JSON.parsefile("./scenarios/chp_supplementary_firing.json") + data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10000 + data["ElectricLoad"]["loads_kw"] = repeat([800.0], 8760) + data["DomesticHotWaterLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) + data["SpaceHeatingLoad"]["fuel_loads_mmbtu_per_hour"] = repeat([6.0], 8760) + #part 1: supplementary firing not used when less efficient than the boiler and expensive m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(m1, inputs) + @test results["CHP"]["size_kw"] == 800 + @test results["CHP"]["size_supplemental_firing_kw"] == 0 + @test results["CHP"]["annual_electric_production_kwh"] ≈ 800*8760 rtol=1e-5 + @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 800*(0.4418/0.3573)*8760/293.07107 rtol=1e-5 + @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] == 0 + @test results["HeatingLoad"]["annual_calculated_total_heating_thermal_load_mmbtu"] == 12.0 * 8760 * data["ExistingBoiler"]["efficiency"] + @test results["HeatingLoad"]["annual_calculated_dhw_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] + @test results["HeatingLoad"]["annual_calculated_space_heating_thermal_load_mmbtu"] == 6.0 * 8760 * data["ExistingBoiler"]["efficiency"] + + #part 2: supplementary firing used when more efficient than the boiler and low-cost; demand charges not reduced by CHP + data["CHP"]["supplementary_firing_capital_cost_per_kw"] = 10 + data["CHP"]["reduces_demand_charges"] = false + data["ExistingBoiler"]["efficiency"] = 0.85 m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - p = REoptInputs("./scenarios/generator.json") - results = run_reopt([m1,m2], p) - @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 - @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + - sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 - @test results["ElectricLoad"]["bau_critical_load_met"] == false - @test results["ElectricLoad"]["bau_critical_load_met_time_steps"] == 0 - - simresults = simulate_outages(results, p) - @test simresults["resilience_hours_max"] == 11 + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(m2, inputs) + @test results["CHP"]["size_supplemental_firing_kw"] ≈ 321.71 atol=0.1 + @test results["CHP"]["annual_thermal_production_mmbtu"] ≈ 149136.6 rtol=1e-5 + @test results["ElectricTariff"]["lifecycle_demand_cost_after_tax"] ≈ 5212.7 rtol=1e-5 end - @testset "Minimize Unserved Load" begin - d = JSON.parsefile("./scenarios/outage.json") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - results = run_reopt(m, d) - - @test results["Outages"]["expected_outage_cost"] ≈ 0 atol=0.1 - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 0 atol=0.1 - @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 - @test value(m[:binMGTechUsed]["CHP"]) ≈ 1 - @test value(m[:binMGTechUsed]["PV"]) ≈ 1 - @test value(m[:binMGStorageUsed]) ≈ 1 - - # Increase cost of microgrid upgrade and PV Size, PV not used and some load not met - d["Financial"]["microgrid_upgrade_cost_fraction"] = 0.3 - d["PV"]["min_kw"] = 200.0 - d["PV"]["max_kw"] = 200.0 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + @testset "CHP to Waste Heat" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d = JSON.parsefile("./scenarios/chp_waste.json") results = run_reopt(m, d) - @test value(m[:binMGTechUsed]["PV"]) ≈ 0 - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 24.16 atol=0.1 - + @test sum(results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 4174.455 atol=1e-3 + end + end + + @testset verbose=true "FlexibleHVAC" begin + + @testset "Single RC Model heating only" begin #= - Scenario with $0.001/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 - - should meet 168 kWh in each outage such that the total unserved load is 12 kWh + Single RC model: + 1 state/control node + 2 inputs: Ta and Qheat + A = [1/(RC)], B = [1/(RC) 1/C], u = [Ta; Q] + NOTE exogenous_inputs (u) allows for parasitic heat, but it is input as zeros here + + We start with no technologies except ExistingBoiler and ExistingChiller. + FlexibleHVAC is only worth purchasing if its cost is neglible (i.e. below the lcc_bau * MIPTOL) + or if there is a time-varying fuel and/or electricity cost + (and the FlexibleHVAC installed_cost is less than the achievable savings). =# - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/nogridcost_minresilhours.json") - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 12 - - # testing dvUnserved load, which would output 100 kWh for this scenario before output fix - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/nogridcost_multiscenario.json") - @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 60 - @test results["Outages"]["expected_outage_cost"] ≈ 485.43270 atol=1.0e-5 #avg duration (3h) * load per time step (10) * present worth factor (16.18109) - @test results["Outages"]["max_outage_cost_per_outage_duration"][1] ≈ 161.8109 atol=1.0e-5 - - # Scenario with generator, PV, electric storage - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/outages_gen_pv_stor.json") - @test results["Outages"]["expected_outage_cost"] ≈ 3.54476923e6 atol=10 - @test results["Financial"]["lcc"] ≈ 8.6413594727e7 rtol=0.001 - - # Scenario with generator, PV, wind, electric storage - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt(m, "./scenarios/outages_gen_pv_wind_stor.json") - @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 - @test value(m[:binMGTechUsed]["PV"]) ≈ 1 - @test value(m[:binMGTechUsed]["Wind"]) ≈ 1 - @test results["Outages"]["expected_outage_cost"] ≈ 1.296319791276051e6 atol=1.0 - @test results["Financial"]["lcc"] ≈ 4.8046446434e6 rtol=0.001 + + # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC + pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431); + R = 0.00025 # K/kW + C = 1e5 # kJ/K + # the starting scenario has flat fuel and electricty costs + d = JSON.parsefile("./scenarios/thermal_load.json"); + A = reshape([-1/(R*C)], 1,1) + B = [1/(R*C) 1/C] + u = [tamb zeros(8760)]'; + d["FlexibleHVAC"] = Dict( + "control_node" => 1, + "initial_temperatures" => [21], + "temperature_upper_bound_degC" => 22.0, + "temperature_lower_bound_degC" => 19.8, + "installed_cost" => 300.0, # NOTE cost must be more then the MIPTOL * LCC 5e-5 * 5.79661e6 ≈ 290 to make FlexibleHVAC not worth it + "system_matrix" => A, + "input_matrix" => B, + "exogenous_inputs" => u + ) + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false + # @test r["Financial"]["npv"] == 0 + + # put in a time varying fuel cost, which should make purchasing the FlexibleHVAC system economical + # with flat ElectricTariff the ExistingChiller does not benefit from FlexibleHVAC + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = rand(Float64, (8760))*(50-5).+5; + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # all of the savings are from the ExistingBoiler fuel costs + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === true + # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] + # @test fuel_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 + + # now increase the FlexibleHVAC installed_cost to the fuel costs savings + 100 and expect that the FlexibleHVAC is not purchased + # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + 100 + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false + # @test r["Financial"]["npv"] == 0 + + # add TOU ElectricTariff and expect to benefit from using ExistingChiller intelligently + d["ElectricTariff"] = Dict("urdb_label" => "5ed6c1a15457a3367add15ae") + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # elec_cost_savings = r["ElectricTariff"]["lifecycle_demand_cost_after_tax_bau"] + + # r["ElectricTariff"]["lifecycle_energy_cost_after_tax_bau"] - + # r["ElectricTariff"]["lifecycle_demand_cost_after_tax"] - + # r["ElectricTariff"]["lifecycle_energy_cost_after_tax"] + + # fuel_cost_savings = r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax_bau"] - r["ExistingBoiler"]["lifecycle_fuel_cost_after_tax"] + # @test fuel_cost_savings + elec_cost_savings - d["FlexibleHVAC"]["installed_cost"] ≈ r["Financial"]["npv"] atol=0.1 + + # now increase the FlexibleHVAC installed_cost to the fuel costs savings + elec_cost_savings + # + 100 and expect that the FlexibleHVAC is not purchased + # d["FlexibleHVAC"]["installed_cost"] = fuel_cost_savings + elec_cost_savings + 100 + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test (occursin("not supported by the solver", string(r["Messages"]["errors"])) || occursin("REopt scenarios solved either with errors or non-optimal solutions", string(r["Messages"]["errors"]))) + # @test Meta.parse(r["FlexibleHVAC"]["purchased"]) === false + # @test r["Financial"]["npv"] == 0 + end + end - @testset "Outages with Wind and supply-to-load no greater than critical load" begin - input_data = JSON.parsefile("./scenarios/wind_outages.json") - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - results = run_reopt([m1,m2], inputs) - - # Check that supply-to-load is equal to critical load during outages, including wind - supply_to_load = results["Outages"]["storage_discharge_series_kw"] .+ results["Outages"]["wind_to_load_series_kw"] - supply_to_load = [supply_to_load[:,:,i][1] for i in eachindex(supply_to_load)] - critical_load = results["Outages"]["critical_loads_per_outage_series_kw"][1,1,:] - check = .≈(supply_to_load, critical_load, atol=0.001) - @test !(0 in check) - - # Check that the soc_series_fraction is the same length as the storage_discharge_series_kw - @test size(results["Outages"]["soc_series_fraction"]) == size(results["Outages"]["storage_discharge_series_kw"]) - end + #= + add a time-of-export rate that is greater than retail rate for the month of January, + check to make sure that PV does NOT export unless the site load is met first for the month of January. + =# + @testset "Do not allow_simultaneous_export_import" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + data = JSON.parsefile("./scenarios/monthly_rate.json") - @testset "Multiple Sites" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - ps = [ - REoptInputs("./scenarios/pv_storage.json"), - REoptInputs("./scenarios/monthly_rate.json"), - ]; - results = run_reopt(m, ps) - @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.2830872235e7 rtol=1e-5 - end + # create wholesale_rate with compensation in January > retail rate + jan_rate = data["ElectricTariff"]["monthly_energy_rates"][1] + data["ElectricTariff"]["wholesale_rate"] = + append!(repeat([jan_rate + 0.1], 31 * 24), repeat([0.0], 8760 - 31*24)) + data["ElectricTariff"]["monthly_demand_rates"] = repeat([0], 12) + data["ElectricUtility"] = Dict("allow_simultaneous_export_import" => false) - @testset "MPC" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_mpc(model, "./scenarios/mpc.json") - @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0 - @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0 - @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0 - grid_draw = r["ElectricUtility"]["to_load_series_kw"] .+ r["ElectricUtility"]["to_battery_series_kw"] - # the grid draw limit in the 10th time step is set to 90 - # without the 90 limit the grid draw is 98 in the 10th time step - @test grid_draw[10] <= 90 - end + s = Scenario(data) + inputs = REoptInputs(s) + results = run_reopt(model, inputs) + + @test all(x == 0.0 for (i,x) in enumerate(results["ElectricUtility"]["electric_to_load_series_kw"][1:744]) + if results["PV"]["electric_to_grid_series_kw"][i] > 0) + end - @testset "Complex Incentives" begin - """ - This test was compared against the API test: - reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives - when using the hardcoded levelization_factor in this package's REoptInputs function. - The two LCC's matched within 0.00005%. (The Julia pkg LCC is 1.0971991e7) - """ - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, "./scenarios/incentives.json") - @test results["Financial"]["lcc"] ≈ 1.094596365e7 atol=5e4 - end + @testset "Solar and ElectricStorage w/BAU and degradation" begin + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + d = JSON.parsefile("scenarios/pv_storage.json"); + d["Settings"] = Dict{Any,Any}("add_soc_incentive" => false) + results = run_reopt([m1,m2], d) + + @test results["PV"]["size_kw"] ≈ 216.6667 atol=0.01 + @test results["PV"]["lcoe_per_kwh"] ≈ 0.0468 atol = 0.001 + @test results["Financial"]["lcc"] ≈ 1.239179e7 rtol=1e-5 + @test results["Financial"]["lcc_bau"] ≈ 12766397 rtol=1e-5 + @test results["ElectricStorage"]["size_kw"] ≈ 49.02 atol=0.1 + @test results["ElectricStorage"]["size_kwh"] ≈ 83.3 atol=0.1 + proforma_npv = REopt.npv(results["Financial"]["offtaker_annual_free_cashflows"] - + results["Financial"]["offtaker_annual_free_cashflows_bau"], 0.081) + @test results["Financial"]["npv"] ≈ proforma_npv rtol=0.0001 + + # compare avg soc with and without degradation, + # using default augmentation battery maintenance strategy + avg_soc_no_degr = sum(results["ElectricStorage"]["soc_series_fraction"]) / 8760 + d["ElectricStorage"]["model_degradation"] = true + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r_degr = run_reopt(m, d) + avg_soc_degr = sum(r_degr["ElectricStorage"]["soc_series_fraction"]) / 8760 + @test avg_soc_no_degr > avg_soc_degr + + # test the replacement strategy + d["ElectricStorage"]["degradation"] = Dict("maintenance_strategy" => "replacement") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + set_optimizer_attribute(m, "mip_rel_gap", 0.01) + r = run_reopt(m, d) + @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + # #optimal SOH at end of horizon is 80\% to prevent any replacement + # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 + # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 + # # the maintenance_cost comes out to 3004.39 on Actions, so we test the LCC since it should match + # @test r["Financial"]["lcc"] ≈ 1.240096e7 rtol=0.01 + # @test last(value.(m[:SOH])) ≈ 66.633 rtol=0.01 + # @test r["ElectricStorage"]["size_kwh"] ≈ 83.29 rtol=0.01 + + # test minimum_avg_soc_fraction + d["ElectricStorage"]["minimum_avg_soc_fraction"] = 0.72 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + set_optimizer_attribute(m, "mip_rel_gap", 0.01) + r = run_reopt(m, d) + @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 + end - @testset verbose = true "Rate Structures" begin + @testset "Outage with Generator, outage simulator, BAU critical load outputs" begin + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + p = REoptInputs("./scenarios/generator.json") + results = run_reopt([m1,m2], p) + @test results["Generator"]["size_kw"] ≈ 9.55 atol=0.01 + @test (sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 1:9) + + sum(results["Generator"]["electric_to_load_series_kw"][i] for i in 13:8760)) == 0 + @test results["ElectricLoad"]["bau_critical_load_met"] == false + @test results["ElectricLoad"]["bau_critical_load_met_time_steps"] == 0 + + simresults = simulate_outages(results, p) + @test simresults["resilience_hours_max"] == 11 + end - @testset "Tiered Energy" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, "./scenarios/tiered_energy_rate.json") - @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 2342.88 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 24000.0 atol=0.1 - @test results["ElectricLoad"]["annual_calculated_kwh"] ≈ 24000.0 atol=0.1 - end + @testset "Minimize Unserved Load" begin + d = JSON.parsefile("./scenarios/outage.json") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt(m, d) + + @test results["Outages"]["expected_outage_cost"] ≈ 0 atol=0.1 + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 0 atol=0.1 + @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 + @test value(m[:binMGTechUsed]["CHP"]) ≈ 1 + @test value(m[:binMGTechUsed]["PV"]) ≈ 1 + @test value(m[:binMGStorageUsed]) ≈ 1 + + # Increase cost of microgrid upgrade and PV Size, PV not used and some load not met + d["Financial"]["microgrid_upgrade_cost_fraction"] = 0.3 + d["PV"]["min_kw"] = 200.0 + d["PV"]["max_kw"] = 200.0 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt(m, d) + @test value(m[:binMGTechUsed]["PV"]) ≈ 0 + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 24.16 atol=0.1 + + #= + Scenario with $0.001/kWh value_of_lost_load_per_kwh, 12x169 hour outages, 1kW load/hour, and min_resil_time_steps = 168 + - should meet 168 kWh in each outage such that the total unserved load is 12 kWh + =# + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/nogridcost_minresilhours.json") + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 12 + + # testing dvUnserved load, which would output 100 kWh for this scenario before output fix + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/nogridcost_multiscenario.json") + @test sum(results["Outages"]["unserved_load_per_outage_kwh"]) ≈ 60 + @test results["Outages"]["expected_outage_cost"] ≈ 485.43270 atol=1.0e-5 #avg duration (3h) * load per time step (10) * present worth factor (16.18109) + @test results["Outages"]["max_outage_cost_per_outage_duration"][1] ≈ 161.8109 atol=1.0e-5 + + # Scenario with generator, PV, electric storage + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/outages_gen_pv_stor.json") + @test results["Outages"]["expected_outage_cost"] ≈ 3.54476923e6 atol=10 + @test results["Financial"]["lcc"] ≈ 8.6413594727e7 rtol=0.001 + + # Scenario with generator, PV, wind, electric storage + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt(m, "./scenarios/outages_gen_pv_wind_stor.json") + @test value(m[:binMGTechUsed]["Generator"]) ≈ 1 + @test value(m[:binMGTechUsed]["PV"]) ≈ 1 + @test value(m[:binMGTechUsed]["Wind"]) ≈ 1 + @test results["Outages"]["expected_outage_cost"] ≈ 1.296319791276051e6 atol=1.0 + @test results["Financial"]["lcc"] ≈ 4.8046446434e6 rtol=0.001 + + end - @testset "Lookback Demand Charges" begin - # 1. Testing rate from URDB - data = JSON.parsefile("./scenarios/lookback_rate.json") - # urdb_label used https://apps.openei.org/IURDB/rate/view/539f6a23ec4f024411ec8bf9#2__Demand - # has a demand charge lookback of 35% for all months with 2 different demand charges based on which month - data["ElectricLoad"]["loads_kw"] = ones(8760) - data["ElectricLoad"]["loads_kw"][8] = 100.0 - inputs = REoptInputs(Scenario(data)) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, inputs) - # Expected result is 100 kW demand for January, 35% of that for all other months and - # with 5x other $10.5/kW cold months and 6x $11.5/kW warm months - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 100 * (10.5 + 0.35*10.5*5 + 0.35*11.5*6) - - # 2. Testing custom rate from user with demand_lookback_months - d = JSON.parsefile("./scenarios/lookback_rate.json") - d["ElectricTariff"] = Dict() - d["ElectricTariff"]["demand_lookback_percent"] = 0.75 - d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] - d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak - d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) - d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) - d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak - d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] - d["ElectricTariff"]["demand_lookback_months"] = [1,0,0,1,0,0,0,0,0,0,0,1] # Jan, April, Dec - d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, REoptInputs(Scenario(d))) - - monthly_peaks = [300,300,300,400,300,500,300,300,300,300,300,300] # 300 = 400*0.75. Sets peak in all months excpet April and June - expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) - @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost - - # 3. Testing custom rate from user with demand_lookback_range - d = JSON.parsefile("./scenarios/lookback_rate.json") - d["ElectricTariff"] = Dict() - d["ElectricTariff"]["demand_lookback_percent"] = 0.75 - d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] - d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak - d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) - d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) - d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak - d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] - d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 - d["ElectricTariff"]["demand_lookback_range"] = 6 - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, REoptInputs(Scenario(d))) - - monthly_peaks = [225, 225, 225, 400, 300, 500, 375, 375, 375, 375, 375, 375] - expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) - @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + @testset "Outages with Wind and supply-to-load no greater than critical load" begin + input_data = JSON.parsefile("./scenarios/wind_outages.json") + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + results = run_reopt([m1,m2], inputs) + + # Check that supply-to-load is equal to critical load during outages, including wind + supply_to_load = results["Outages"]["storage_discharge_series_kw"] .+ results["Outages"]["wind_to_load_series_kw"] + supply_to_load = [supply_to_load[:,:,i][1] for i in eachindex(supply_to_load)] + critical_load = results["Outages"]["critical_loads_per_outage_series_kw"][1,1,:] + check = .≈(supply_to_load, critical_load, atol=0.001) + @test !(0 in check) + + # Check that the soc_series_fraction is the same length as the storage_discharge_series_kw + @test size(results["Outages"]["soc_series_fraction"]) == size(results["Outages"]["storage_discharge_series_kw"]) + end - end + @testset "Multiple Sites" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + ps = [ + REoptInputs("./scenarios/pv_storage.json"), + REoptInputs("./scenarios/monthly_rate.json"), + ]; + results = run_reopt(m, ps) + @test results[3]["Financial"]["lcc"] + results[10]["Financial"]["lcc"] ≈ 1.2830872235e7 rtol=1e-5 + end - @testset "Blended tariff" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/no_techs.json") - @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 1000.0 - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 136.99 - end + @testset "MPC" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_mpc(model, "./scenarios/mpc.json") + @test maximum(r["ElectricUtility"]["to_load_series_kw"][1:15]) <= 98.0 + @test maximum(r["ElectricUtility"]["to_load_series_kw"][16:24]) <= 97.0 + @test sum(r["PV"]["to_grid_series_kw"]) ≈ 0 + grid_draw = r["ElectricUtility"]["to_load_series_kw"] .+ r["ElectricUtility"]["to_battery_series_kw"] + # the grid draw limit in the 10th time step is set to 90 + # without the 90 limit the grid draw is 98 in the 10th time step + @test grid_draw[10] <= 90 + end - @testset "Coincident Peak Charges" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, "./scenarios/coincident_peak.json") - @test results["ElectricTariff"]["year_one_coincident_peak_cost_before_tax"] ≈ 15.0 - end + @testset "Complex Incentives" begin + """ + This test was compared against the API test: + reo.tests.test_reopt_url.EntryResourceTest.test_complex_incentives + when using the hardcoded levelization_factor in this package's REoptInputs function. + The two LCC's matched within 0.00005%. (The Julia pkg LCC is 1.0971991e7) + """ + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, "./scenarios/incentives.json") + @test results["Financial"]["lcc"] ≈ 1.094596365e7 atol=5e4 + end - @testset "URDB sell rate" begin - #= The URDB contains at least one "Customer generation" tariff that only has a "sell" key in the energyratestructure (the tariff tested here) - =# - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - p = REoptInputs("./scenarios/URDB_customer_generation.json") - results = run_reopt(model, p) - @test results["PV"]["size_kw"] ≈ p.max_sizes["PV"] - end + @testset verbose=true "Rate Structures" begin - @testset "Custom URDB with Sub-Hourly" begin - # Avoid excessive JuMP warning messages about += with Expressions - logger = SimpleLogger() - with_logger(logger) do - # Testing a 15-min post with a urdb_response with multiple n_energy_tiers - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) - p = REoptInputs("./scenarios/subhourly_with_urdb.json") - results = run_reopt(model, p) - @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 - @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw - end - end + @testset "Tiered Energy" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, "./scenarios/tiered_energy_rate.json") + @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 2342.88 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 24000.0 atol=0.1 + @test results["ElectricLoad"]["annual_calculated_kwh"] ≈ 24000.0 atol=0.1 + end - @testset "Multi-tier demand and energy rates" begin - #This test ensures that when multiple energy or demand regimes are included, that the tier limits load appropriately - d = JSON.parsefile("./scenarios/no_techs.json") - d["ElectricTariff"] = Dict() - d["ElectricTariff"]["urdb_response"] = JSON.parsefile("./scenarios/multi_tier_urdb_response.json") - s = Scenario(d) - p = REoptInputs(s) - @test p.s.electric_tariff.tou_demand_tier_limits[1, 1] ≈ 1.0e8 atol=1.0 - @test p.s.electric_tariff.tou_demand_tier_limits[1, 2] ≈ 1.0e8 atol=1.0 - @test p.s.electric_tariff.tou_demand_tier_limits[2, 1] ≈ 100.0 atol=1.0 - @test p.s.electric_tariff.tou_demand_tier_limits[2, 2] ≈ 1.0e8 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[1, 1] ≈ 1.0e10 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[1, 2] ≈ 1.0e10 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[6, 1] ≈ 20000.0 atol=1.0 - @test p.s.electric_tariff.energy_tier_limits[6, 2] ≈ 1.0e10 atol=1.0 - end + @testset "Lookback Demand Charges" begin + # 1. Testing rate from URDB + data = JSON.parsefile("./scenarios/lookback_rate.json") + # urdb_label used https://apps.openei.org/IURDB/rate/view/539f6a23ec4f024411ec8bf9#2__Demand + # has a demand charge lookback of 35% for all months with 2 different demand charges based on which month + data["ElectricLoad"]["loads_kw"] = ones(8760) + data["ElectricLoad"]["loads_kw"][8] = 100.0 + inputs = REoptInputs(Scenario(data)) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, inputs) + # Expected result is 100 kW demand for January, 35% of that for all other months and + # with 5x other $10.5/kW cold months and 6x $11.5/kW warm months + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 100 * (10.5 + 0.35*10.5*5 + 0.35*11.5*6) + + # 2. Testing custom rate from user with demand_lookback_months + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["demand_lookback_months"] = [1,0,0,1,0,0,0,0,0,0,0,1] # Jan, April, Dec + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 - @testset "Tiered TOU Demand" begin - data = JSON.parsefile("./scenarios/tiered_tou_demand.json") - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(model, data) - max_demand = data["ElectricLoad"]["annual_kwh"] / 8760 - tier1_max = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["max"] - tier1_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["rate"] - tier2_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][2]["rate"] - expected_demand_charges = 12 * (tier1_max * tier1_rate + (max_demand - tier1_max) * tier2_rate) - @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_charges atol=1 - end + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [300,300,300,400,300,500,300,300,300,300,300,300] # 300 = 400*0.75. Sets peak in all months excpet April and June + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost + + # 3. Testing custom rate from user with demand_lookback_range + d = JSON.parsefile("./scenarios/lookback_rate.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["demand_lookback_percent"] = 0.75 + d["ElectricLoad"]["loads_kw"] = [100 for i in range(1,8760)] + d["ElectricLoad"]["loads_kw"][22] = 200 # Jan peak + d["ElectricLoad"]["loads_kw"][2403] = 400 # April peak (Should set dvPeakDemandLookback) + d["ElectricLoad"]["loads_kw"][4088] = 500 # June peak (not in peak month lookback) + d["ElectricLoad"]["loads_kw"][8333] = 300 # Dec peak + d["ElectricTariff"]["monthly_demand_rates"] = [10,10,20,50,20,10,20,20,20,20,20,5] + d["ElectricTariff"]["blended_annual_energy_rate"] = 0.01 + d["ElectricTariff"]["demand_lookback_range"] = 6 - # # tiered monthly demand rate TODO: expected results? - # m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - # data = JSON.parsefile("./scenarios/tiered_energy_rate.json") - # data["ElectricTariff"]["urdb_label"] = "59bc22705457a3372642da67" - # s = Scenario(data) - # inputs = REoptInputs(s) - # results = run_reopt(m, inputs) - - @testset "Non-Standard Units for Energy Rates" begin - d = JSON.parsefile("./scenarios/no_techs.json") - d["ElectricTariff"] = Dict( - "urdb_label" => "6272e4ae7eb76766c247d469" - ) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) - @test occursin("URDB energy tiers have non-standard units of", string(results["Messages"])) - end + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, REoptInputs(Scenario(d))) + + monthly_peaks = [225, 225, 225, 400, 300, 500, 375, 375, 375, 375, 375, 375] + expected_demand_cost = sum(monthly_peaks.*d["ElectricTariff"]["monthly_demand_rates"]) + @test r["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_cost end - @testset "EASIUR" begin - d = JSON.parsefile("./scenarios/pv.json") - d["Site"]["latitude"] = 30.2672 - d["Site"]["longitude"] = -97.7431 - scen = Scenario(d) - @test scen.financial.NOx_grid_cost_per_tonne ≈ 5510.61 atol=0.1 + @testset "Blended tariff" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/no_techs.json") + @test results["ElectricTariff"]["year_one_energy_cost_before_tax"] ≈ 1000.0 + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ 136.99 end - @testset "Wind" begin - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d = JSON.parsefile("./scenarios/wind.json") - results = run_reopt(m, d) - @test results["Wind"]["size_kw"] ≈ 3752 atol=0.1 - @test results["Financial"]["lcc"] ≈ 8.591017e6 rtol=1e-5 - #= - 0.5% higher LCC in this package as compared to API ? 8,591,017 vs 8,551,172 - - both have zero curtailment - - same energy to grid: 5,839,317 vs 5,839,322 - - same energy to load: 4,160,683 vs 4,160,677 - - same city: Boulder - - same total wind prod factor - - REopt.jl has: - - bigger turbine: 3752 vs 3735 - - net_capital_costs_plus_om: 8,576,590 vs. 8,537,480 + @testset "Coincident Peak Charges" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, "./scenarios/coincident_peak.json") + @test results["ElectricTariff"]["year_one_coincident_peak_cost_before_tax"] ≈ 15.0 + end - TODO: will these discrepancies be addressed once NMIL binaries are added? + @testset "URDB sell rate" begin + #= The URDB contains at least one "Customer generation" tariff that only has a "sell" key in the energyratestructure (the tariff tested here) =# - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d["Site"]["land_acres"] = 60 # = 2 MW (with 0.03 acres/kW) - results = run_reopt(m, d) - @test results["Wind"]["size_kw"] == 2000.0 # Wind should be constrained by land_acres - - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - d["Wind"]["min_kw"] = 2001 # min_kw greater than land-constrained max should error - results = run_reopt(m, d) - @test "errors" ∈ keys(results["Messages"]) - @test length(results["Messages"]["errors"]) > 0 + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + p = REoptInputs("./scenarios/URDB_customer_generation.json") + results = run_reopt(model, p) + @test results["PV"]["size_kw"] ≈ p.max_sizes["PV"] end - @testset "Multiple PVs" begin + @testset "Custom URDB with Sub-Hourly" begin + # Avoid excessive JuMP warning messages about += with Expressions logger = SimpleLogger() with_logger(logger) do - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json") - - ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])] - roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])] - roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])] - - @test ground_pv["size_kw"] ≈ 15 atol=0.1 - @test roof_west["size_kw"] ≈ 7 atol=0.1 - @test roof_east["size_kw"] ≈ 4 atol=0.1 - @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 - @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 - @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 - @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 - @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 - @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 + # Testing a 15-min post with a urdb_response with multiple n_energy_tiers + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.1)) + p = REoptInputs("./scenarios/subhourly_with_urdb.json") + results = run_reopt(model, p) + @test length(p.s.electric_tariff.export_rates[:WHL]) ≈ 8760*4 + @test results["PV"]["size_kw"] ≈ p.s.pvs[1].existing_kw end end - @testset "Thermal Energy Storage + Absorption Chiller" begin - model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - data = JSON.parsefile("./scenarios/thermal_storage.json") - s = Scenario(data) + @testset "Multi-tier demand and energy rates" begin + #This test ensures that when multiple energy or demand regimes are included, that the tier limits load appropriately + d = JSON.parsefile("./scenarios/no_techs.json") + d["ElectricTariff"] = Dict() + d["ElectricTariff"]["urdb_response"] = JSON.parsefile("./scenarios/multi_tier_urdb_response.json") + s = Scenario(d) p = REoptInputs(s) - - #test for get_absorption_chiller_defaults consistency with inputs data and Scenario s. - htf_defaults_response = get_absorption_chiller_defaults(; - thermal_consumption_hot_water_or_steam=get(data["AbsorptionChiller"], "thermal_consumption_hot_water_or_steam", nothing), - boiler_type=get(data["ExistingBoiler"], "production_type", nothing), - load_max_tons=maximum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) - ) - - expected_installed_cost_per_ton = htf_defaults_response["default_inputs"]["installed_cost_per_ton"] - expected_om_cost_per_ton = htf_defaults_response["default_inputs"]["om_cost_per_ton"] - - @test p.s.absorption_chiller.installed_cost_per_kw ≈ expected_installed_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 - @test p.s.absorption_chiller.om_cost_per_kw ≈ expected_om_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 - @test p.s.absorption_chiller.cop_thermal ≈ htf_defaults_response["default_inputs"]["cop_thermal"] atol=0.001 - - #load test values - p.s.absorption_chiller.installed_cost_per_kw = 500.0 / REopt.KWH_THERMAL_PER_TONHOUR - p.s.absorption_chiller.om_cost_per_kw = 0.5 / REopt.KWH_THERMAL_PER_TONHOUR - p.s.absorption_chiller.cop_thermal = 0.7 - - #Make every other hour zero fuel and electric cost; storage should charge and discharge in each period - for ts in p.time_steps - #heating and cooling loads only - if ts % 2 == 0 #in even periods, there is a nonzero load and energy is higher cost, and storage should discharge - p.s.electric_load.loads_kw[ts] = 10 - p.s.dhw_load.loads_kw[ts] = 5 - p.s.space_heating_load.loads_kw[ts] = 5 - p.s.cooling_load.loads_kw_thermal[ts] = 10 - p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 100 - for tier in 1:p.s.electric_tariff.n_energy_tiers - p.s.electric_tariff.energy_rates[ts, tier] = 100 - end - else #in odd periods, there is no load and energy is cheaper - storage should charge - p.s.electric_load.loads_kw[ts] = 0 - p.s.dhw_load.loads_kw[ts] = 0 - p.s.space_heating_load.loads_kw[ts] = 0 - p.s.cooling_load.loads_kw_thermal[ts] = 0 - p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 1 - for tier in 1:p.s.electric_tariff.n_energy_tiers - p.s.electric_tariff.energy_rates[ts, tier] = 50 - end - end - end - - r = run_reopt(model, p) - - #dispatch to load should be 10kW every other period = 4,380 * 10 - @test sum(r["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"]) ≈ 149.45 atol=0.1 - @test sum(r["ColdThermalStorage"]["storage_to_load_series_ton"]) ≈ 12454.33 atol=0.1 - #size should be just over 10kW in gallons, accounting for efficiency losses and min SOC - @test r["HotThermalStorage"]["size_gal"] ≈ 233.0 atol=0.1 - @test r["ColdThermalStorage"]["size_gal"] ≈ 378.0 atol=0.1 - #No production from existing chiller, only absorption chiller, which is sized at ~5kW to manage electric demand charge & capital cost. - @test r["ExistingChiller"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=0.1 - @test r["AbsorptionChiller"]["annual_thermal_production_tonhour"] ≈ 12464.15 atol=0.1 - @test r["AbsorptionChiller"]["size_ton"] ≈ 2.846 atol=0.01 + @test p.s.electric_tariff.tou_demand_tier_limits[1, 1] ≈ 1.0e8 atol=1.0 + @test p.s.electric_tariff.tou_demand_tier_limits[1, 2] ≈ 1.0e8 atol=1.0 + @test p.s.electric_tariff.tou_demand_tier_limits[2, 1] ≈ 100.0 atol=1.0 + @test p.s.electric_tariff.tou_demand_tier_limits[2, 2] ≈ 1.0e8 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[1, 1] ≈ 1.0e10 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[1, 2] ≈ 1.0e10 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[6, 1] ≈ 20000.0 atol=1.0 + @test p.s.electric_tariff.energy_tier_limits[6, 2] ≈ 1.0e10 atol=1.0 end - @testset "Heat and cool energy balance" begin - """ + @testset "Tiered TOU Demand" begin + data = JSON.parsefile("./scenarios/tiered_tou_demand.json") + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(model, data) + max_demand = data["ElectricLoad"]["annual_kwh"] / 8760 + tier1_max = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["max"] + tier1_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][1]["rate"] + tier2_rate = data["ElectricTariff"]["urdb_response"]["demandratestructure"][1][2]["rate"] + expected_demand_charges = 12 * (tier1_max * tier1_rate + (max_demand - tier1_max) * tier2_rate) + @test results["ElectricTariff"]["year_one_demand_cost_before_tax"] ≈ expected_demand_charges atol=1 + end - This is an "energy balance" type of test which tests the model formulation/math as opposed - to a specific scenario. This test is robust to changes in the model "MIPRELSTOP" or "MAXTIME" setting + # # tiered monthly demand rate TODO: expected results? + # m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + # data = JSON.parsefile("./scenarios/tiered_energy_rate.json") + # data["ElectricTariff"]["urdb_label"] = "59bc22705457a3372642da67" + # s = Scenario(data) + # inputs = REoptInputs(s) + # results = run_reopt(m, inputs) + + @testset "Non-Standard Units for Energy Rates" begin + d = JSON.parsefile("./scenarios/no_techs.json") + d["ElectricTariff"] = Dict( + "urdb_label" => "6272e4ae7eb76766c247d469" + ) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, d) + @test occursin("URDB energy tiers have non-standard units of", string(results["Messages"])) + end - Validation to ensure that: - 1) The electric and absorption chillers are supplying 100% of the cooling thermal load plus losses from ColdThermalStorage - 2) The boiler and CHP are supplying the heating load plus additional absorption chiller thermal load - 3) The Cold and Hot TES efficiency (charge loss and thermal decay) are being tracked properly + end - """ - input_data = JSON.parsefile("./scenarios/heat_cool_energy_balance_inputs.json") - s = Scenario(input_data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) + @testset "EASIUR" begin + d = JSON.parsefile("./scenarios/pv.json") + d["Site"]["latitude"] = 30.2672 + d["Site"]["longitude"] = -97.7431 + scen = Scenario(d) + @test scen.financial.NOx_grid_cost_per_tonne ≈ 5510.61 atol=0.1 + end + + @testset "Wind" begin + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d = JSON.parsefile("./scenarios/wind.json") + results = run_reopt(m, d) + @test results["Wind"]["size_kw"] ≈ 3752 atol=0.1 + @test results["Financial"]["lcc"] ≈ 8.591017e6 rtol=1e-5 + #= + 0.5% higher LCC in this package as compared to API ? 8,591,017 vs 8,551,172 + - both have zero curtailment + - same energy to grid: 5,839,317 vs 5,839,322 + - same energy to load: 4,160,683 vs 4,160,677 + - same city: Boulder + - same total wind prod factor + + REopt.jl has: + - bigger turbine: 3752 vs 3735 + - net_capital_costs_plus_om: 8,576,590 vs. 8,537,480 + + TODO: will these discrepancies be addressed once NMIL binaries are added? + =# - # Annual cooling **thermal** energy load of CRB is based on annual cooling electric energy (from CRB models) and a conditional COP depending on the peak cooling thermal load - # 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, - 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 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d["Site"]["land_acres"] = 60 # = 2 MW (with 0.03 acres/kW) + results = run_reopt(m, d) + @test results["Wind"]["size_kw"] == 2000.0 # Wind should be constrained by land_acres - #Test cooling load results - @test round(cooling_thermal_load_tonhour_total, digits=1) ≈ results["CoolingLoad"]["annual_calculated_tonhour"] atol=1.0 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + d["Wind"]["min_kw"] = 2001 # min_kw greater than land-constrained max should error + results = run_reopt(m, d) + @test "errors" ∈ keys(results["Messages"]) + @test length(results["Messages"]["errors"]) > 0 + end + + @testset "Multiple PVs" begin + logger = SimpleLogger() + with_logger(logger) do + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], "./scenarios/multiple_pvs.json") + + ground_pv = results["PV"][findfirst(pv -> pv["name"] == "ground", results["PV"])] + roof_west = results["PV"][findfirst(pv -> pv["name"] == "roof_west", results["PV"])] + roof_east = results["PV"][findfirst(pv -> pv["name"] == "roof_east", results["PV"])] + + @test ground_pv["size_kw"] ≈ 15 atol=0.1 + @test roof_west["size_kw"] ≈ 7 atol=0.1 + @test roof_east["size_kw"] ≈ 4 atol=0.1 + @test ground_pv["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 + @test roof_west["lifecycle_om_cost_after_tax_bau"] ≈ 782.0 atol=0.1 + @test ground_pv["annual_energy_produced_kwh_bau"] ≈ 8933.09 atol=0.1 + @test roof_west["annual_energy_produced_kwh_bau"] ≈ 7656.11 atol=0.1 + @test ground_pv["annual_energy_produced_kwh"] ≈ 26799.26 atol=0.1 + @test roof_west["annual_energy_produced_kwh"] ≈ 10719.51 atol=0.1 + @test roof_east["annual_energy_produced_kwh"] ≈ 6685.95 atol=0.1 + end + end + + @testset "Thermal Energy Storage + Absorption Chiller" begin + model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + data = JSON.parsefile("./scenarios/thermal_storage.json") + s = Scenario(data) + p = REoptInputs(s) - # Convert fuel input to thermal using user input boiler efficiency - boiler_thermal_load_mmbtu_total = (671.40531 + 11570.9155) * input_data["ExistingBoiler"]["efficiency"] # From CRB models, in heating_cooling_loads.jl, BuiltInDomesticHotWaterLoad + BuiltInSpaceHeatingLoad data for location (SanFrancisco Hospital) - boiler_fuel_consumption_total_mod_efficiency = boiler_thermal_load_mmbtu_total / inputs.s.existing_boiler.efficiency - - # Cooling outputs - cooling_elecchl_tons_to_load_series = results["ExistingChiller"]["thermal_to_load_series_ton"] - cooling_elecchl_tons_to_tes_series = results["ExistingChiller"]["thermal_to_storage_series_ton"] - cooling_absorpchl_tons_to_load_series = results["AbsorptionChiller"]["thermal_to_load_series_ton"] - cooling_absorpchl_tons_to_tes_series = results["AbsorptionChiller"]["thermal_to_storage_series_ton"] - cooling_tonhour_to_load_tech_total = sum(cooling_elecchl_tons_to_load_series) + sum(cooling_absorpchl_tons_to_load_series) - cooling_tonhour_to_tes_total = sum(cooling_elecchl_tons_to_tes_series) + sum(cooling_absorpchl_tons_to_tes_series) - cooling_tes_tons_to_load_series = results["ColdThermalStorage"]["storage_to_load_series_ton"] - cooling_extra_from_tes_losses = cooling_tonhour_to_tes_total - sum(cooling_tes_tons_to_load_series) - tes_effic_with_decay = sum(cooling_tes_tons_to_load_series) / cooling_tonhour_to_tes_total - cooling_total_prod_from_techs = cooling_tonhour_to_load_tech_total + cooling_tonhour_to_tes_total - cooling_load_plus_tes_losses = cooling_thermal_load_tonhour_total + cooling_extra_from_tes_losses - - # Absorption Chiller electric consumption addition - absorpchl_total_cooling_produced_series_ton = cooling_absorpchl_tons_to_load_series .+ cooling_absorpchl_tons_to_tes_series - absorpchl_total_cooling_produced_ton_hour = sum(absorpchl_total_cooling_produced_series_ton) - absorpchl_electric_consumption_total_kwh = results["AbsorptionChiller"]["annual_electric_consumption_kwh"] - absorpchl_cop_elec = s.absorption_chiller.cop_electric - - # Check if sum of electric and absorption chillers equals cooling thermal total - @test tes_effic_with_decay < 0.97 - @test round(cooling_total_prod_from_techs, digits=0) ≈ cooling_load_plus_tes_losses atol=5.0 - @test round(absorpchl_electric_consumption_total_kwh, digits=0) ≈ absorpchl_total_cooling_produced_ton_hour * REopt.KWH_THERMAL_PER_TONHOUR / absorpchl_cop_elec atol=1.0 - - # Heating outputs - boiler_fuel_consumption_calculated = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] - boiler_thermal_series = results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"] - boiler_to_load_series = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] - boiler_thermal_to_tes_series = results["ExistingBoiler"]["thermal_to_storage_series_mmbtu_per_hour"] - chp_thermal_to_load_series = results["CHP"]["thermal_to_load_series_mmbtu_per_hour"] - chp_thermal_to_tes_series = results["CHP"]["thermal_to_storage_series_mmbtu_per_hour"] - chp_thermal_to_waste_series = results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"] - absorpchl_thermal_series = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] - hot_tes_mmbtu_per_hour_to_load_series = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] - tes_inflows = sum(chp_thermal_to_tes_series) + sum(boiler_thermal_to_tes_series) - total_chp_production = sum(chp_thermal_to_load_series) + sum(chp_thermal_to_waste_series) + sum(chp_thermal_to_tes_series) - tes_outflows = sum(hot_tes_mmbtu_per_hour_to_load_series) - total_thermal_expected = boiler_thermal_load_mmbtu_total + sum(chp_thermal_to_waste_series) + tes_inflows + sum(absorpchl_thermal_series) - boiler_fuel_expected = (total_thermal_expected - total_chp_production - tes_outflows) / inputs.s.existing_boiler.efficiency - total_thermal_mmbtu_calculated = sum(boiler_thermal_series) + total_chp_production + tes_outflows - - @test round(boiler_fuel_consumption_calculated, digits=0) ≈ boiler_fuel_expected atol=8.0 - @test round(total_thermal_mmbtu_calculated, digits=0) ≈ total_thermal_expected atol=8.0 - - # Test CHP["cooling_thermal_factor"] = 0.8, AbsorptionChiller["cop_thermal"] = 0.7 (from inputs .json) - absorpchl_heat_in_kwh = results["AbsorptionChiller"]["annual_thermal_consumption_mmbtu"] * REopt.KWH_PER_MMBTU - absorpchl_cool_out_kwh = results["AbsorptionChiller"]["annual_thermal_production_tonhour"] * REopt.KWH_THERMAL_PER_TONHOUR - absorpchl_cop = absorpchl_cool_out_kwh / absorpchl_heat_in_kwh - - @test round(absorpchl_cop, digits=5) ≈ 0.8*0.7 rtol=1e-4 + #test for get_absorption_chiller_defaults consistency with inputs data and Scenario s. + htf_defaults_response = get_absorption_chiller_defaults(; + thermal_consumption_hot_water_or_steam=get(data["AbsorptionChiller"], "thermal_consumption_hot_water_or_steam", nothing), + boiler_type=get(data["ExistingBoiler"], "production_type", nothing), + load_max_tons=maximum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) + ) + + expected_installed_cost_per_ton = htf_defaults_response["default_inputs"]["installed_cost_per_ton"] + expected_om_cost_per_ton = htf_defaults_response["default_inputs"]["om_cost_per_ton"] + + @test p.s.absorption_chiller.installed_cost_per_kw ≈ expected_installed_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 + @test p.s.absorption_chiller.om_cost_per_kw ≈ expected_om_cost_per_ton / REopt.KWH_THERMAL_PER_TONHOUR atol=0.001 + @test p.s.absorption_chiller.cop_thermal ≈ htf_defaults_response["default_inputs"]["cop_thermal"] atol=0.001 + + #load test values + p.s.absorption_chiller.installed_cost_per_kw = 500.0 / REopt.KWH_THERMAL_PER_TONHOUR + p.s.absorption_chiller.om_cost_per_kw = 0.5 / REopt.KWH_THERMAL_PER_TONHOUR + p.s.absorption_chiller.cop_thermal = 0.7 + + #Make every other hour zero fuel and electric cost; storage should charge and discharge in each period + for ts in p.time_steps + #heating and cooling loads only + if ts % 2 == 0 #in even periods, there is a nonzero load and energy is higher cost, and storage should discharge + p.s.electric_load.loads_kw[ts] = 10 + p.s.dhw_load.loads_kw[ts] = 5 + p.s.space_heating_load.loads_kw[ts] = 5 + p.s.cooling_load.loads_kw_thermal[ts] = 10 + p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 100 + for tier in 1:p.s.electric_tariff.n_energy_tiers + p.s.electric_tariff.energy_rates[ts, tier] = 100 + end + else #in odd periods, there is no load and energy is cheaper - storage should charge + p.s.electric_load.loads_kw[ts] = 0 + p.s.dhw_load.loads_kw[ts] = 0 + p.s.space_heating_load.loads_kw[ts] = 0 + p.s.cooling_load.loads_kw_thermal[ts] = 0 + p.fuel_cost_per_kwh["ExistingBoiler"][ts] = 1 + for tier in 1:p.s.electric_tariff.n_energy_tiers + p.s.electric_tariff.energy_rates[ts, tier] = 50 + end + end end + + r = run_reopt(model, p) + + #dispatch to load should be 10kW every other period = 4,380 * 10 + @test sum(r["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"]) ≈ 149.45 atol=0.1 + @test sum(r["ColdThermalStorage"]["storage_to_load_series_ton"]) ≈ 12454.33 atol=0.1 + #size should be just over 10kW in gallons, accounting for efficiency losses and min SOC + @test r["HotThermalStorage"]["size_gal"] ≈ 233.0 atol=0.1 + @test r["ColdThermalStorage"]["size_gal"] ≈ 378.0 atol=0.1 + #No production from existing chiller, only absorption chiller, which is sized at ~5kW to manage electric demand charge & capital cost. + @test r["ExistingChiller"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=0.1 + @test r["AbsorptionChiller"]["annual_thermal_production_tonhour"] ≈ 12464.15 atol=0.1 + @test r["AbsorptionChiller"]["size_ton"] ≈ 2.846 atol=0.01 + end - @testset "Heating and cooling inputs + CHP defaults" begin - """ + @testset "Heat and cool energy balance" begin + """ - This tests the various ways to input heating and cooling loads to make sure they are processed correctly. - There are no "new" technologies in this test, so heating is served by ExistingBoiler, and - cooling is served by ExistingCooler. Since this is just inputs processing tests, no optimization is needed. + This is an "energy balance" type of test which tests the model formulation/math as opposed + to a specific scenario. This test is robust to changes in the model "MIPRELSTOP" or "MAXTIME" setting - """ - input_data = JSON.parsefile("./scenarios/heating_cooling_load_inputs.json") - s = Scenario(input_data) - inputs = REoptInputs(s) + Validation to ensure that: + 1) The electric and absorption chillers are supplying 100% of the cooling thermal load plus losses from ColdThermalStorage + 2) The boiler and CHP are supplying the heating load plus additional absorption chiller thermal load + 3) The Cold and Hot TES efficiency (charge loss and thermal decay) are being tracked properly - # Heating load is input as **fuel**, not thermal - # If boiler efficiency is not input, we use REopt.EXISTING_BOILER_EFFICIENCY to convert fuel to thermal - expected_fuel = input_data["SpaceHeatingLoad"]["annual_mmbtu"] + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] - total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU - @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY atol=1.0 - total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency - @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY / inputs.s.existing_boiler.efficiency atol=1.0 - # If boiler efficiency is input, use that with annual or monthly mmbtu input to convert fuel to thermal - input_data["ExistingBoiler"]["efficiency"] = 0.72 - s = Scenario(input_data) - inputs = REoptInputs(s) - total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU - @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] atol=1.0 - total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency - @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] / inputs.s.existing_boiler.efficiency atol=1.0 - - # The expected cooling load is based on the default **fraction of total electric** profile for the doe_reference_name when annual_tonhour is NOT input - # the 320540.0 kWh number is from the default LargeOffice fraction of total electric profile applied to the Hospital default total electric profile - total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.existing_chiller.cop - @test round(total_chiller_electric_consumption, digits=0) ≈ 320544.0 atol=1.0 # loads_kw is **electric**, loads_kw_thermal is **thermal** - - #Test CHP defaults use average fuel load, size class 2 for recip_engine - @test inputs.s.chp.min_allowable_kw ≈ 50.0 atol=0.01 - @test inputs.s.chp.om_cost_per_kwh ≈ 0.0235 atol=0.0001 - - delete!(input_data, "SpaceHeatingLoad") - delete!(input_data, "DomesticHotWaterLoad") - annual_fraction_of_electric_load_input = 0.5 - input_data["CoolingLoad"] = Dict{Any, Any}("annual_fraction_of_electric_load" => annual_fraction_of_electric_load_input) - - s = Scenario(input_data) - inputs = REoptInputs(s) + """ + input_data = JSON.parsefile("./scenarios/heat_cool_energy_balance_inputs.json") + s = Scenario(input_data) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) + + # Annual cooling **thermal** energy load of CRB is based on annual cooling electric energy (from CRB models) and a conditional COP depending on the peak cooling thermal load + # 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, + 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 + + #Test cooling load results + @test round(cooling_thermal_load_tonhour_total, digits=1) ≈ results["CoolingLoad"]["annual_calculated_tonhour"] atol=1.0 + + # Convert fuel input to thermal using user input boiler efficiency + boiler_thermal_load_mmbtu_total = (671.40531 + 11570.9155) * input_data["ExistingBoiler"]["efficiency"] # From CRB models, in heating_cooling_loads.jl, BuiltInDomesticHotWaterLoad + BuiltInSpaceHeatingLoad data for location (SanFrancisco Hospital) + boiler_fuel_consumption_total_mod_efficiency = boiler_thermal_load_mmbtu_total / inputs.s.existing_boiler.efficiency + + # Cooling outputs + cooling_elecchl_tons_to_load_series = results["ExistingChiller"]["thermal_to_load_series_ton"] + cooling_elecchl_tons_to_tes_series = results["ExistingChiller"]["thermal_to_storage_series_ton"] + cooling_absorpchl_tons_to_load_series = results["AbsorptionChiller"]["thermal_to_load_series_ton"] + cooling_absorpchl_tons_to_tes_series = results["AbsorptionChiller"]["thermal_to_storage_series_ton"] + cooling_tonhour_to_load_tech_total = sum(cooling_elecchl_tons_to_load_series) + sum(cooling_absorpchl_tons_to_load_series) + cooling_tonhour_to_tes_total = sum(cooling_elecchl_tons_to_tes_series) + sum(cooling_absorpchl_tons_to_tes_series) + cooling_tes_tons_to_load_series = results["ColdThermalStorage"]["storage_to_load_series_ton"] + cooling_extra_from_tes_losses = cooling_tonhour_to_tes_total - sum(cooling_tes_tons_to_load_series) + tes_effic_with_decay = sum(cooling_tes_tons_to_load_series) / cooling_tonhour_to_tes_total + cooling_total_prod_from_techs = cooling_tonhour_to_load_tech_total + cooling_tonhour_to_tes_total + cooling_load_plus_tes_losses = cooling_thermal_load_tonhour_total + cooling_extra_from_tes_losses + + # Absorption Chiller electric consumption addition + absorpchl_total_cooling_produced_series_ton = cooling_absorpchl_tons_to_load_series .+ cooling_absorpchl_tons_to_tes_series + absorpchl_total_cooling_produced_ton_hour = sum(absorpchl_total_cooling_produced_series_ton) + absorpchl_electric_consumption_total_kwh = results["AbsorptionChiller"]["annual_electric_consumption_kwh"] + absorpchl_cop_elec = s.absorption_chiller.cop_electric + + # Check if sum of electric and absorption chillers equals cooling thermal total + @test tes_effic_with_decay < 0.97 + @test round(cooling_total_prod_from_techs, digits=0) ≈ cooling_load_plus_tes_losses atol=5.0 + @test round(absorpchl_electric_consumption_total_kwh, digits=0) ≈ absorpchl_total_cooling_produced_ton_hour * REopt.KWH_THERMAL_PER_TONHOUR / absorpchl_cop_elec atol=1.0 + + # Heating outputs + boiler_fuel_consumption_calculated = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] + boiler_thermal_series = results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"] + boiler_to_load_series = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] + boiler_thermal_to_tes_series = results["ExistingBoiler"]["thermal_to_storage_series_mmbtu_per_hour"] + chp_thermal_to_load_series = results["CHP"]["thermal_to_load_series_mmbtu_per_hour"] + chp_thermal_to_tes_series = results["CHP"]["thermal_to_storage_series_mmbtu_per_hour"] + chp_thermal_to_waste_series = results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"] + absorpchl_thermal_series = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] + hot_tes_mmbtu_per_hour_to_load_series = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] + tes_inflows = sum(chp_thermal_to_tes_series) + sum(boiler_thermal_to_tes_series) + total_chp_production = sum(chp_thermal_to_load_series) + sum(chp_thermal_to_waste_series) + sum(chp_thermal_to_tes_series) + tes_outflows = sum(hot_tes_mmbtu_per_hour_to_load_series) + total_thermal_expected = boiler_thermal_load_mmbtu_total + sum(chp_thermal_to_waste_series) + tes_inflows + sum(absorpchl_thermal_series) + boiler_fuel_expected = (total_thermal_expected - total_chp_production - tes_outflows) / inputs.s.existing_boiler.efficiency + total_thermal_mmbtu_calculated = sum(boiler_thermal_series) + total_chp_production + tes_outflows + + @test round(boiler_fuel_consumption_calculated, digits=0) ≈ boiler_fuel_expected atol=8.0 + @test round(total_thermal_mmbtu_calculated, digits=0) ≈ total_thermal_expected atol=8.0 + + # Test CHP["cooling_thermal_factor"] = 0.8, AbsorptionChiller["cop_thermal"] = 0.7 (from inputs .json) + absorpchl_heat_in_kwh = results["AbsorptionChiller"]["annual_thermal_consumption_mmbtu"] * REopt.KWH_PER_MMBTU + absorpchl_cool_out_kwh = results["AbsorptionChiller"]["annual_thermal_production_tonhour"] * REopt.KWH_THERMAL_PER_TONHOUR + absorpchl_cop = absorpchl_cool_out_kwh / absorpchl_heat_in_kwh + + @test round(absorpchl_cop, digits=5) ≈ 0.8*0.7 rtol=1e-4 + end - expected_cooling_electricity = sum(inputs.s.electric_load.loads_kw) * annual_fraction_of_electric_load_input - total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop - @test round(total_chiller_electric_consumption, digits=0) ≈ round(expected_cooling_electricity) atol=1.0 - @test round(total_chiller_electric_consumption, digits=0) ≈ 3876410 atol=1.0 + @testset "Heating and cooling inputs + CHP defaults" begin + """ - # Check that without heating load or max_kw input, CHP.max_kw gets set based on peak electric load - @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.01 + This tests the various ways to input heating and cooling loads to make sure they are processed correctly. + There are no "new" technologies in this test, so heating is served by ExistingBoiler, and + cooling is served by ExistingCooler. Since this is just inputs processing tests, no optimization is needed. - input_data["SpaceHeatingLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) - input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) - input_data["CoolingLoad"] = Dict{Any, Any}("monthly_fractions_of_electric_load" => repeat([0.1], 12)) + """ + input_data = JSON.parsefile("./scenarios/heating_cooling_load_inputs.json") + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + # Heating load is input as **fuel**, not thermal + # If boiler efficiency is not input, we use REopt.EXISTING_BOILER_EFFICIENCY to convert fuel to thermal + expected_fuel = input_data["SpaceHeatingLoad"]["annual_mmbtu"] + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] + total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU + @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY atol=1.0 + total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency + @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * REopt.EXISTING_BOILER_EFFICIENCY / inputs.s.existing_boiler.efficiency atol=1.0 + # If boiler efficiency is input, use that with annual or monthly mmbtu input to convert fuel to thermal + input_data["ExistingBoiler"]["efficiency"] = 0.72 + s = Scenario(input_data) + inputs = REoptInputs(s) + total_boiler_heating_thermal_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + sum(inputs.s.dhw_load.loads_kw)) / REopt.KWH_PER_MMBTU + @test round(total_boiler_heating_thermal_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] atol=1.0 + total_boiler_heating_fuel_load_mmbtu = total_boiler_heating_thermal_load_mmbtu / inputs.s.existing_boiler.efficiency + @test round(total_boiler_heating_fuel_load_mmbtu, digits=0) ≈ expected_fuel * input_data["ExistingBoiler"]["efficiency"] / inputs.s.existing_boiler.efficiency atol=1.0 - #Test CHP defaults use average fuel load, size class changes to 3 - @test inputs.s.chp.min_allowable_kw ≈ 125.0 atol=0.1 - @test inputs.s.chp.om_cost_per_kwh ≈ 0.021 atol=0.0001 - #Update CHP prime_mover and test new defaults - input_data["CHP"]["prime_mover"] = "combustion_turbine" - input_data["CHP"]["size_class"] = 1 - # Set max_kw higher than peak electric load so min_allowable_kw doesn't get assigned to max_kw - input_data["CHP"]["max_kw"] = 2500.0 + # The expected cooling load is based on the default **fraction of total electric** profile for the doe_reference_name when annual_tonhour is NOT input + # the 320540.0 kWh number is from the default LargeOffice fraction of total electric profile applied to the Hospital default total electric profile + total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.existing_chiller.cop + @test round(total_chiller_electric_consumption, digits=0) ≈ 320544.0 atol=1.0 # loads_kw is **electric**, loads_kw_thermal is **thermal** - s = Scenario(input_data) - inputs = REoptInputs(s) + #Test CHP defaults use average fuel load, size class 2 for recip_engine + @test inputs.s.chp.min_allowable_kw ≈ 50.0 atol=0.01 + @test inputs.s.chp.om_cost_per_kwh ≈ 0.0235 atol=0.0001 - @test inputs.s.chp.min_allowable_kw ≈ 2000.0 atol=0.1 - @test inputs.s.chp.om_cost_per_kwh ≈ 0.014499999999999999 atol=0.0001 + delete!(input_data, "SpaceHeatingLoad") + delete!(input_data, "DomesticHotWaterLoad") + annual_fraction_of_electric_load_input = 0.5 + input_data["CoolingLoad"] = Dict{Any, Any}("annual_fraction_of_electric_load" => annual_fraction_of_electric_load_input) - total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + - sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU - @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 24000 atol=1.0 - total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop - @test round(total_chiller_electric_consumption, digits=0) ≈ 775282 atol=1.0 + s = Scenario(input_data) + inputs = REoptInputs(s) - input_data["SpaceHeatingLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) - input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) - input_data["CoolingLoad"] = Dict{Any, Any}("per_time_step_fractions_of_electric_load" => repeat([0.01], 8760)) + expected_cooling_electricity = sum(inputs.s.electric_load.loads_kw) * annual_fraction_of_electric_load_input + total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop + @test round(total_chiller_electric_consumption, digits=0) ≈ round(expected_cooling_electricity) atol=1.0 + @test round(total_chiller_electric_consumption, digits=0) ≈ 3876410 atol=1.0 - s = Scenario(input_data) - inputs = REoptInputs(s) + # Check that without heating load or max_kw input, CHP.max_kw gets set based on peak electric load + @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.01 - total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + - sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU - @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 8760 atol=0.1 - @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop, digits=0) ≈ 77528.0 atol=1.0 + input_data["SpaceHeatingLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) + input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("monthly_mmbtu" => repeat([1000.0], 12)) + input_data["CoolingLoad"] = Dict{Any, Any}("monthly_fractions_of_electric_load" => repeat([0.1], 12)) - # Make sure annual_tonhour is preserved with conditional existing_chiller_default logic, where guess-and-correct method is applied - input_data["SpaceHeatingLoad"] = Dict{Any, Any}() - input_data["DomesticHotWaterLoad"] = Dict{Any, Any}() - annual_tonhour = 25000.0 - input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital", - "annual_tonhour" => annual_tonhour) - input_data["ExistingChiller"] = Dict{Any, Any}() + s = Scenario(input_data) + inputs = REoptInputs(s) - s = Scenario(input_data) - inputs = REoptInputs(s) + #Test CHP defaults use average fuel load, size class changes to 3 + @test inputs.s.chp.min_allowable_kw ≈ 125.0 atol=0.1 + @test inputs.s.chp.om_cost_per_kwh ≈ 0.021 atol=0.0001 + #Update CHP prime_mover and test new defaults + input_data["CHP"]["prime_mover"] = "combustion_turbine" + input_data["CHP"]["size_class"] = 1 + # Set max_kw higher than peak electric load so min_allowable_kw doesn't get assigned to max_kw + input_data["CHP"]["max_kw"] = 2500.0 - @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / REopt.KWH_THERMAL_PER_TONHOUR, digits=0) ≈ annual_tonhour atol=1.0 - - # Test for prime generator CHP inputs (electric only) - # First get CHP cost to compare later with prime generator - input_data["ElectricLoad"] = Dict("doe_reference_name" => "FlatLoad", - "annual_kwh" => 876000) - input_data["ElectricTariff"] = Dict("blended_annual_energy_rate" => 0.06, - "blended_annual_demand_rate" => 0.0 ) - s_chp = Scenario(input_data) - inputs_chp = REoptInputs(s) - installed_cost_chp = s_chp.chp.installed_cost_per_kw - - # Now get prime generator (electric only) - input_data["CHP"]["is_electric_only"] = true - delete!(input_data["CHP"], "max_kw") - s = Scenario(input_data) - inputs = REoptInputs(s) - # Costs are 75% of CHP - @test inputs.s.chp.installed_cost_per_kw ≈ (0.75*installed_cost_chp) atol=1.0 - @test inputs.s.chp.om_cost_per_kwh ≈ (0.75*0.0145) atol=0.0001 - @test inputs.s.chp.federal_itc_fraction ≈ 0.0 atol=0.0001 - # Thermal efficiency set to zero - @test inputs.s.chp.thermal_efficiency_full_load == 0 - @test inputs.s.chp.thermal_efficiency_half_load == 0 - # Max size based on electric load, not heating load - @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.001 - end + s = Scenario(input_data) + inputs = REoptInputs(s) - @testset "Hybrid/blended heating and cooling loads" begin - """ + @test inputs.s.chp.min_allowable_kw ≈ 2000.0 atol=0.1 + @test inputs.s.chp.om_cost_per_kwh ≈ 0.014499999999999999 atol=0.0001 - This tests the hybrid/campus loads for heating and cooling, where a blended_doe_reference_names - and blended_doe_reference_percents are given and blended to create an aggregate load profile + total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + + sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU + @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 24000 atol=1.0 + total_chiller_electric_consumption = sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop + @test round(total_chiller_electric_consumption, digits=0) ≈ 775282 atol=1.0 - """ - input_data = JSON.parsefile("./scenarios/hybrid_loads_heating_cooling_inputs.json") + input_data["SpaceHeatingLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) + input_data["DomesticHotWaterLoad"] = Dict{Any, Any}("fuel_loads_mmbtu_per_hour" => repeat([0.5], 8760)) + input_data["CoolingLoad"] = Dict{Any, Any}("per_time_step_fractions_of_electric_load" => repeat([0.01], 8760)) - hospital_fraction = 0.75 - hotel_fraction = 1.0 - hospital_fraction + s = Scenario(input_data) + inputs = REoptInputs(s) - # Hospital only - input_data["ElectricLoad"]["annual_kwh"] = hospital_fraction * 100 - input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hospital_fraction * 100 - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hospital_fraction * 100 - input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "Hospital" - input_data["CoolingLoad"]["doe_reference_name"] = "Hospital" + total_heating_fuel_load_mmbtu = (sum(inputs.s.space_heating_load.loads_kw) + + sum(inputs.s.dhw_load.loads_kw)) / input_data["ExistingBoiler"]["efficiency"] / REopt.KWH_PER_MMBTU + @test round(total_heating_fuel_load_mmbtu, digits=0) ≈ 8760 atol=0.1 + @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / inputs.s.cooling_load.existing_chiller_cop, digits=0) ≈ 77528.0 atol=1.0 - s = Scenario(input_data) - inputs = REoptInputs(s) + # Make sure annual_tonhour is preserved with conditional existing_chiller_default logic, where guess-and-correct method is applied + input_data["SpaceHeatingLoad"] = Dict{Any, Any}() + input_data["DomesticHotWaterLoad"] = Dict{Any, Any}() + annual_tonhour = 25000.0 + input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital", + "annual_tonhour" => annual_tonhour) + input_data["ExistingChiller"] = Dict{Any, Any}() - elec_hospital = inputs.s.electric_load.loads_kw - space_hospital = inputs.s.space_heating_load.loads_kw # thermal - dhw_hospital = inputs.s.dhw_load.loads_kw # thermal - cooling_hospital = inputs.s.cooling_load.loads_kw_thermal # thermal - cooling_elec_frac_of_total_hospital = cooling_hospital / inputs.s.cooling_load.existing_chiller_cop ./ elec_hospital - - # Hotel only - input_data["ElectricLoad"]["annual_kwh"] = hotel_fraction * 100 - input_data["ElectricLoad"]["doe_reference_name"] = "LargeHotel" - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hotel_fraction * 100 - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "LargeHotel" - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hotel_fraction * 100 - input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "LargeHotel" - input_data["CoolingLoad"]["doe_reference_name"] = "LargeHotel" - - s = Scenario(input_data) - inputs = REoptInputs(s) + s = Scenario(input_data) + inputs = REoptInputs(s) - elec_hotel = inputs.s.electric_load.loads_kw - space_hotel = inputs.s.space_heating_load.loads_kw # thermal - dhw_hotel = inputs.s.dhw_load.loads_kw # thermal - cooling_hotel = inputs.s.cooling_load.loads_kw_thermal # thermal - cooling_elec_frac_of_total_hotel = cooling_hotel / inputs.s.cooling_load.existing_chiller_cop ./ elec_hotel + @test round(sum(inputs.s.cooling_load.loads_kw_thermal) / REopt.KWH_THERMAL_PER_TONHOUR, digits=0) ≈ annual_tonhour atol=1.0 + + # Test for prime generator CHP inputs (electric only) + # First get CHP cost to compare later with prime generator + input_data["ElectricLoad"] = Dict("doe_reference_name" => "FlatLoad", + "annual_kwh" => 876000) + input_data["ElectricTariff"] = Dict("blended_annual_energy_rate" => 0.06, + "blended_annual_demand_rate" => 0.0 ) + s_chp = Scenario(input_data) + inputs_chp = REoptInputs(s) + installed_cost_chp = s_chp.chp.installed_cost_per_kw + + # Now get prime generator (electric only) + input_data["CHP"]["is_electric_only"] = true + delete!(input_data["CHP"], "max_kw") + s = Scenario(input_data) + inputs = REoptInputs(s) + # Costs are 75% of CHP + @test inputs.s.chp.installed_cost_per_kw ≈ (0.75*installed_cost_chp) atol=1.0 + @test inputs.s.chp.om_cost_per_kwh ≈ (0.75*0.0145) atol=0.0001 + @test inputs.s.chp.federal_itc_fraction ≈ 0.0 atol=0.0001 + # Thermal efficiency set to zero + @test inputs.s.chp.thermal_efficiency_full_load == 0 + @test inputs.s.chp.thermal_efficiency_half_load == 0 + # Max size based on electric load, not heating load + @test inputs.s.chp.max_kw ≈ maximum(inputs.s.electric_load.loads_kw) atol=0.001 + end - # Hybrid mix of hospital and hotel - # Remove previous assignment of doe_reference_name - for load in ["ElectricLoad", "SpaceHeatingLoad", "DomesticHotWaterLoad", "CoolingLoad"] - delete!(input_data[load], "doe_reference_name") - end - annual_energy = (hospital_fraction + hotel_fraction) * 100 - building_list = ["Hospital", "LargeHotel"] - percent_share_list = [hospital_fraction, hotel_fraction] - input_data["ElectricLoad"]["annual_kwh"] = annual_energy - input_data["ElectricLoad"]["blended_doe_reference_names"] = building_list - input_data["ElectricLoad"]["blended_doe_reference_percents"] = percent_share_list - - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = annual_energy - input_data["SpaceHeatingLoad"]["blended_doe_reference_names"] = building_list - input_data["SpaceHeatingLoad"]["blended_doe_reference_percents"] = percent_share_list - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = annual_energy - input_data["DomesticHotWaterLoad"]["blended_doe_reference_names"] = building_list - input_data["DomesticHotWaterLoad"]["blended_doe_reference_percents"] = percent_share_list - - # CoolingLoad now use a weighted fraction of total electric profile if no annual_tonhour is provided - input_data["CoolingLoad"]["blended_doe_reference_names"] = building_list - input_data["CoolingLoad"]["blended_doe_reference_percents"] = percent_share_list - - s = Scenario(input_data) - inputs = REoptInputs(s) + @testset "Hybrid/blended heating and cooling loads" begin + """ - elec_hybrid = inputs.s.electric_load.loads_kw - space_hybrid = inputs.s.space_heating_load.loads_kw # thermal - dhw_hybrid = inputs.s.dhw_load.loads_kw # thermal - cooling_hybrid = inputs.s.cooling_load.loads_kw_thermal # thermal - cooling_elec_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop # electric - cooling_elec_frac_of_total_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop ./ elec_hybrid - - # Check that the combined/hybrid load is the same as the sum of the individual loads in each time_step - - @test round(sum(elec_hybrid .- (elec_hospital .+ elec_hotel)), digits=1) ≈ 0.0 atol=0.1 - @test round(sum(space_hybrid .- (space_hospital .+ space_hotel)), digits=1) ≈ 0.0 atol=0.1 - @test round(sum(dhw_hybrid .- (dhw_hospital .+ dhw_hotel)), digits=1) ≈ 0.0 atol=0.1 - # Check that the cooling load is the weighted average of the default CRB fraction of total electric profiles - cooling_electric_hybrid_expected = elec_hybrid .* (cooling_elec_frac_of_total_hospital * hospital_fraction .+ - cooling_elec_frac_of_total_hotel * hotel_fraction) - @test round(sum(cooling_electric_hybrid_expected .- cooling_elec_hybrid), digits=1) ≈ 0.0 atol=0.1 - end + This tests the hybrid/campus loads for heating and cooling, where a blended_doe_reference_names + and blended_doe_reference_percents are given and blended to create an aggregate load profile - @testset "Boiler (new) test" begin - input_data = JSON.parsefile("scenarios/boiler_new_inputs.json") - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], inputs) - - # BAU boiler loads - load_thermal_mmbtu_bau = sum(s.space_heating_load.loads_kw + s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU - existing_boiler_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) - boiler_thermal_mmbtu = sum(results["Boiler"]["thermal_production_series_mmbtu_per_hour"]) - - # Used monthly fuel cost for ExistingBoiler and Boiler, where ExistingBoiler has lower fuel cost only - # in February (28 days), so expect ExistingBoiler to serve the flat/constant load 28 days of the year - @test existing_boiler_mmbtu ≈ load_thermal_mmbtu_bau * 28 / 365 atol=0.00001 - @test boiler_thermal_mmbtu ≈ load_thermal_mmbtu_bau - existing_boiler_mmbtu atol=0.00001 - end + """ + input_data = JSON.parsefile("./scenarios/hybrid_loads_heating_cooling_inputs.json") - @testset "OffGrid" begin - ## Scenario 1: Solar, Storage, Fixed Generator - post_name = "off_grid.json" - post = JSON.parsefile("./scenarios/$post_name") - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, post) - scen = Scenario(post) - - # Test default values - @test scen.electric_utility.outage_start_time_step ≈ 1 - @test scen.electric_utility.outage_end_time_step ≈ 8760 * scen.settings.time_steps_per_hour - @test scen.storage.attr["ElectricStorage"].soc_init_fraction ≈ 1 - @test scen.storage.attr["ElectricStorage"].can_grid_charge ≈ false - @test scen.generator.fuel_avail_gal ≈ 1.0e9 - @test scen.generator.min_turn_down_fraction ≈ 0.15 - @test sum(scen.electric_load.loads_kw) - sum(scen.electric_load.critical_loads_kw) ≈ 0 # critical loads should equal loads_kw - @test scen.financial.microgrid_upgrade_cost_fraction ≈ 0 - - # Test outputs - @test r["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0 # no interaction with grid - @test r["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ 2617.092 atol=0.01 # Check straight line depreciation calc - @test sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) # OR provided >= required - @test r["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction - @test r["PV"]["size_kw"] ≈ 5050.0 - f = r["Financial"] - @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + - f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + - f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + - f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - - f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 - - ## Scenario 2: Fixed Generator only - post["ElectricLoad"]["annual_kwh"] = 100.0 - post["PV"]["max_kw"] = 0.0 - post["ElectricStorage"]["max_kw"] = 0.0 - post["Generator"]["min_turn_down_fraction"] = 0.0 + hospital_fraction = 0.75 + hotel_fraction = 1.0 - hospital_fraction - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, post) - - # Test generator outputs - @test r["Generator"]["annual_fuel_consumption_gal"] ≈ 7.52 # 99 kWh * 0.076 gal/kWh - @test r["Generator"]["annual_energy_produced_kwh"] ≈ 99.0 - @test r["Generator"]["year_one_fuel_cost_before_tax"] ≈ 22.57 - @test r["Generator"]["lifecycle_fuel_cost_after_tax"] ≈ 205.35 - @test r["Financial"]["initial_capital_costs"] ≈ 100*(700) - @test r["Financial"]["lifecycle_capital_costs"] ≈ 100*(700+324.235442*(1-0.26)) atol=0.1 # replacement in yr 10 is considered tax deductible - @test r["Financial"]["initial_capital_costs_after_incentives"] ≈ 700*100 atol=0.1 - @test r["Financial"]["replacements_future_cost_after_tax"] ≈ 700*100 - @test r["Financial"]["replacements_present_cost_after_tax"] ≈ 100*(324.235442*(1-0.26)) atol=0.1 - - ## Scenario 3: Fixed Generator that can meet load, but cannot meet load operating reserve requirement - ## This test ensures the load operating reserve requirement is being enforced - post["ElectricLoad"]["doe_reference_name"] = "FlatLoad" - post["ElectricLoad"]["annual_kwh"] = 876000.0 # requires 100 kW gen - post["ElectricLoad"]["min_load_met_annual_fraction"] = 1.0 # requires additional generator capacity - post["PV"]["max_kw"] = 0.0 - post["ElectricStorage"]["max_kw"] = 0.0 - post["Generator"]["min_turn_down_fraction"] = 0.0 + # Hospital only + input_data["ElectricLoad"]["annual_kwh"] = hospital_fraction * 100 + input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hospital_fraction * 100 + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hospital_fraction * 100 + input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "Hospital" + input_data["CoolingLoad"]["doe_reference_name"] = "Hospital" - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, post) + s = Scenario(input_data) + inputs = REoptInputs(s) - # Test generator outputs - @test typeof(r) == Model # this is true when the model is infeasible + elec_hospital = inputs.s.electric_load.loads_kw + space_hospital = inputs.s.space_heating_load.loads_kw # thermal + dhw_hospital = inputs.s.dhw_load.loads_kw # thermal + cooling_hospital = inputs.s.cooling_load.loads_kw_thermal # thermal + cooling_elec_frac_of_total_hospital = cooling_hospital / inputs.s.cooling_load.existing_chiller_cop ./ elec_hospital + + # Hotel only + input_data["ElectricLoad"]["annual_kwh"] = hotel_fraction * 100 + input_data["ElectricLoad"]["doe_reference_name"] = "LargeHotel" + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = hotel_fraction * 100 + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "LargeHotel" + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = hotel_fraction * 100 + input_data["DomesticHotWaterLoad"]["doe_reference_name"] = "LargeHotel" + input_data["CoolingLoad"]["doe_reference_name"] = "LargeHotel" - ### Scenario 3: Indonesia. Wind (custom prod) and Generator only - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) - post_name = "wind_intl_offgrid.json" - post = JSON.parsefile("./scenarios/$post_name") - post["ElectricLoad"]["loads_kw"] = [10.0 for i in range(1,8760)] - scen = Scenario(post) - post["Wind"]["production_factor_series"] = reduce(vcat, readdlm("./data/example_wind_prod_factor_kw.csv", '\n', header=true)[1]) + s = Scenario(input_data) + inputs = REoptInputs(s) - results = run_reopt(m, post) - - @test results["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction - f = results["Financial"] - @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + - f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + - f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + - f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - - f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 - - windOR = sum(results["Wind"]["electric_to_load_series_kw"] * post["Wind"]["operating_reserve_required_fraction"]) - loadOR = sum(post["ElectricLoad"]["loads_kw"] * scen.electric_load.operating_reserve_required_fraction) - @test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) ≈ loadOR + windOR atol=1.0 + elec_hotel = inputs.s.electric_load.loads_kw + space_hotel = inputs.s.space_heating_load.loads_kw # thermal + dhw_hotel = inputs.s.dhw_load.loads_kw # thermal + cooling_hotel = inputs.s.cooling_load.loads_kw_thermal # thermal + cooling_elec_frac_of_total_hotel = cooling_hotel / inputs.s.cooling_load.existing_chiller_cop ./ elec_hotel + # Hybrid mix of hospital and hotel + # Remove previous assignment of doe_reference_name + for load in ["ElectricLoad", "SpaceHeatingLoad", "DomesticHotWaterLoad", "CoolingLoad"] + delete!(input_data[load], "doe_reference_name") end + annual_energy = (hospital_fraction + hotel_fraction) * 100 + building_list = ["Hospital", "LargeHotel"] + percent_share_list = [hospital_fraction, hotel_fraction] + input_data["ElectricLoad"]["annual_kwh"] = annual_energy + input_data["ElectricLoad"]["blended_doe_reference_names"] = building_list + input_data["ElectricLoad"]["blended_doe_reference_percents"] = percent_share_list + + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = annual_energy + input_data["SpaceHeatingLoad"]["blended_doe_reference_names"] = building_list + input_data["SpaceHeatingLoad"]["blended_doe_reference_percents"] = percent_share_list + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = annual_energy + input_data["DomesticHotWaterLoad"]["blended_doe_reference_names"] = building_list + input_data["DomesticHotWaterLoad"]["blended_doe_reference_percents"] = percent_share_list + + # CoolingLoad now use a weighted fraction of total electric profile if no annual_tonhour is provided + input_data["CoolingLoad"]["blended_doe_reference_names"] = building_list + input_data["CoolingLoad"]["blended_doe_reference_percents"] = percent_share_list - @testset "GHP" begin - """ + s = Scenario(input_data) + inputs = REoptInputs(s) - This tests multiple unique aspects of GHP: - 1. REopt takes the output data of GhpGhx, creates multiple GHP options, and chooses the expected one - 2. GHP with heating and cooling "..efficiency_thermal_factors" reduces the net thermal load - 3. GHP serves only the SpaceHeatingLoad by default unless it is allowed to serve DHW - 4. GHP serves all the Cooling load - 5. Input of a custom COP map for GHP and check the GHP performance to make sure it's using it correctly - 6. Hybrid GHP capability functions as expected + elec_hybrid = inputs.s.electric_load.loads_kw + space_hybrid = inputs.s.space_heating_load.loads_kw # thermal + dhw_hybrid = inputs.s.dhw_load.loads_kw # thermal + cooling_hybrid = inputs.s.cooling_load.loads_kw_thermal # thermal + cooling_elec_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop # electric + cooling_elec_frac_of_total_hybrid = cooling_hybrid / inputs.s.cooling_load.existing_chiller_cop ./ elec_hybrid + + # Check that the combined/hybrid load is the same as the sum of the individual loads in each time_step + + @test round(sum(elec_hybrid .- (elec_hospital .+ elec_hotel)), digits=1) ≈ 0.0 atol=0.1 + @test round(sum(space_hybrid .- (space_hospital .+ space_hotel)), digits=1) ≈ 0.0 atol=0.1 + @test round(sum(dhw_hybrid .- (dhw_hospital .+ dhw_hotel)), digits=1) ≈ 0.0 atol=0.1 + # Check that the cooling load is the weighted average of the default CRB fraction of total electric profiles + cooling_electric_hybrid_expected = elec_hybrid .* (cooling_elec_frac_of_total_hospital * hospital_fraction .+ + cooling_elec_frac_of_total_hotel * hotel_fraction) + @test round(sum(cooling_electric_hybrid_expected .- cooling_elec_hybrid), digits=1) ≈ 0.0 atol=0.1 + end - """ - # Load base inputs - input_data = JSON.parsefile("scenarios/ghp_inputs.json") - - # Modify ["GHP"]["ghpghx_inputs"] for running GhpGhx.jl - # Heat pump performance maps - cop_map_mat_header = readdlm("scenarios/ghp_cop_map_custom.csv", ',', header=true) - data = cop_map_mat_header[1] - headers = cop_map_mat_header[2] - # Generate a "records" style dictionary from the - cop_map_list = [] - for i in axes(data,1) - dict_record = Dict(name=>data[i, col] for (col, name) in enumerate(headers)) - push!(cop_map_list, dict_record) - end - input_data["GHP"]["ghpghx_inputs"][1]["cop_map_eft_heating_cooling"] = cop_map_list - - # Due to GhpGhx not being a registered package (no OSI-approved license), - # the registered REopt package cannot have GhpGhx as a "normal" dependency; - # Therefore, we only use a "ghpghx_response" (the output of GhpGhx) as an - # input to REopt to avoid GhpGhx module calls - response_1 = JSON.parsefile("scenarios/ghpghx_response.json") - response_2 = deepcopy(response_1) - # Reduce the electric consumption of response 2 which should then be the chosen system - response_2["outputs"]["yearly_total_electric_consumption_series_kw"] *= 0.5 - input_data["GHP"]["ghpghx_responses"] = [response_1, response_2] - - # Heating load - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" - input_data["SpaceHeatingLoad"]["monthly_mmbtu"] = fill(1000.0, 12) - input_data["SpaceHeatingLoad"]["monthly_mmbtu"][1] = 500.0 - input_data["SpaceHeatingLoad"]["monthly_mmbtu"][end] = 1500.0 - - # Call REopt - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt([m1,m2], inputs) - - ghp_option_chosen = results["GHP"]["ghp_option_chosen"] - @test ghp_option_chosen == 2 - - # Test GHP heating and cooling load reduced - hot_load_reduced_mmbtu = sum(results["GHP"]["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"]) - cold_load_reduced_tonhour = sum(results["GHP"]["cooling_thermal_load_reduction_with_ghp_ton"]) - @test hot_load_reduced_mmbtu ≈ 1440.00 atol=0.1 - @test cold_load_reduced_tonhour ≈ 761382.78 atol=0.1 - - # Test GHP serving space heating with VAV thermal efficiency improvements - heating_served_mmbtu = sum(s.ghp_option_list[ghp_option_chosen].heating_thermal_kw / REopt.KWH_PER_MMBTU) - expected_heating_served_mmbtu = 12000 * 0.8 * 0.85 # (fuel_mmbtu * boiler_effic * space_heating_efficiency_thermal_factor) - @test round(heating_served_mmbtu, digits=1) ≈ expected_heating_served_mmbtu atol=1.0 - - # Boiler serves all of the DHW load, no DHW thermal reduction due to GHP retrofit - boiler_served_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) - expected_boiler_served_mmbtu = 3000 * 0.8 # (fuel_mmbtu * boiler_effic) - @test round(boiler_served_mmbtu, digits=1) ≈ expected_boiler_served_mmbtu atol=1.0 - - # LoadProfileChillerThermal cooling thermal is 1/cooling_efficiency_thermal_factor of GHP cooling thermal production - bau_chiller_thermal_tonhour = sum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) - ghp_cooling_thermal_tonhour = sum(inputs.ghp_cooling_thermal_load_served_kw[1,:] / REopt.KWH_THERMAL_PER_TONHOUR) - @test round(bau_chiller_thermal_tonhour) ≈ ghp_cooling_thermal_tonhour/0.6 atol=1.0 - - # Custom heat pump COP map is used properly - ghp_option_chosen = results["GHP"]["ghp_option_chosen"] - heating_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["heating_cop_avg"] - cooling_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["cooling_cop_avg"] - # Average COP which includes pump power should be lower than Heat Pump only COP specified by the map - @test heating_cop_avg <= 4.0 - @test cooling_cop_avg <= 8.0 + @testset "Boiler (new) test" begin + input_data = JSON.parsefile("scenarios/boiler_new_inputs.json") + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], inputs) + + # BAU boiler loads + load_thermal_mmbtu_bau = sum(s.space_heating_load.loads_kw + s.dhw_load.loads_kw) / REopt.KWH_PER_MMBTU + existing_boiler_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) + boiler_thermal_mmbtu = sum(results["Boiler"]["thermal_production_series_mmbtu_per_hour"]) + + # Used monthly fuel cost for ExistingBoiler and Boiler, where ExistingBoiler has lower fuel cost only + # in February (28 days), so expect ExistingBoiler to serve the flat/constant load 28 days of the year + @test existing_boiler_mmbtu ≈ load_thermal_mmbtu_bau * 28 / 365 atol=0.00001 + @test boiler_thermal_mmbtu ≈ load_thermal_mmbtu_bau - existing_boiler_mmbtu atol=0.00001 + end + + @testset "OffGrid" begin + ## Scenario 1: Solar, Storage, Fixed Generator + post_name = "off_grid.json" + post = JSON.parsefile("./scenarios/$post_name") + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, post) + scen = Scenario(post) + + # Test default values + @test scen.electric_utility.outage_start_time_step ≈ 1 + @test scen.electric_utility.outage_end_time_step ≈ 8760 * scen.settings.time_steps_per_hour + @test scen.storage.attr["ElectricStorage"].soc_init_fraction ≈ 1 + @test scen.storage.attr["ElectricStorage"].can_grid_charge ≈ false + @test scen.generator.fuel_avail_gal ≈ 1.0e9 + @test scen.generator.min_turn_down_fraction ≈ 0.15 + @test sum(scen.electric_load.loads_kw) - sum(scen.electric_load.critical_loads_kw) ≈ 0 # critical loads should equal loads_kw + @test scen.financial.microgrid_upgrade_cost_fraction ≈ 0 + + # Test outputs + @test r["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0 # no interaction with grid + @test r["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ 2617.092 atol=0.01 # Check straight line depreciation calc + @test sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) # OR provided >= required + @test r["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction + @test r["PV"]["size_kw"] ≈ 5050.0 + f = r["Financial"] + @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + + f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + + f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + + f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - + f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 + + ## Scenario 2: Fixed Generator only + post["ElectricLoad"]["annual_kwh"] = 100.0 + post["PV"]["max_kw"] = 0.0 + post["ElectricStorage"]["max_kw"] = 0.0 + post["Generator"]["min_turn_down_fraction"] = 0.0 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, post) + + # Test generator outputs + @test r["Generator"]["annual_fuel_consumption_gal"] ≈ 7.52 # 99 kWh * 0.076 gal/kWh + @test r["Generator"]["annual_energy_produced_kwh"] ≈ 99.0 + @test r["Generator"]["year_one_fuel_cost_before_tax"] ≈ 22.57 + @test r["Generator"]["lifecycle_fuel_cost_after_tax"] ≈ 205.35 + @test r["Financial"]["initial_capital_costs"] ≈ 100*(700) + @test r["Financial"]["lifecycle_capital_costs"] ≈ 100*(700+324.235442*(1-0.26)) atol=0.1 # replacement in yr 10 is considered tax deductible + @test r["Financial"]["initial_capital_costs_after_incentives"] ≈ 700*100 atol=0.1 + @test r["Financial"]["replacements_future_cost_after_tax"] ≈ 700*100 + @test r["Financial"]["replacements_present_cost_after_tax"] ≈ 100*(324.235442*(1-0.26)) atol=0.1 + + ## Scenario 3: Fixed Generator that can meet load, but cannot meet load operating reserve requirement + ## This test ensures the load operating reserve requirement is being enforced + post["ElectricLoad"]["doe_reference_name"] = "FlatLoad" + post["ElectricLoad"]["annual_kwh"] = 876000.0 # requires 100 kW gen + post["ElectricLoad"]["min_load_met_annual_fraction"] = 1.0 # requires additional generator capacity + post["PV"]["max_kw"] = 0.0 + post["ElectricStorage"]["max_kw"] = 0.0 + post["Generator"]["min_turn_down_fraction"] = 0.0 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, post) + + # Test generator outputs + @test typeof(r) == Model # this is true when the model is infeasible + + ### Scenario 3: Indonesia. Wind (custom prod) and Generator only + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) + post_name = "wind_intl_offgrid.json" + post = JSON.parsefile("./scenarios/$post_name") + post["ElectricLoad"]["loads_kw"] = [10.0 for i in range(1,8760)] + scen = Scenario(post) + post["Wind"]["production_factor_series"] = reduce(vcat, readdlm("./data/example_wind_prod_factor_kw.csv", '\n', header=true)[1]) + + results = run_reopt(m, post) + + @test results["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction + f = results["Financial"] + @test f["lifecycle_generation_tech_capital_costs"] + f["lifecycle_storage_capital_costs"] + f["lifecycle_om_costs_after_tax"] + + f["lifecycle_fuel_costs_after_tax"] + f["lifecycle_chp_standby_cost_after_tax"] + f["lifecycle_elecbill_after_tax"] + + f["lifecycle_offgrid_other_annual_costs_after_tax"] + f["lifecycle_offgrid_other_capital_costs"] + + f["lifecycle_outage_cost"] + f["lifecycle_MG_upgrade_and_fuel_cost"] - + f["lifecycle_production_incentive_after_tax"] ≈ f["lcc"] atol=1.0 + + windOR = sum(results["Wind"]["electric_to_load_series_kw"] * post["Wind"]["operating_reserve_required_fraction"]) + loadOR = sum(post["ElectricLoad"]["loads_kw"] * scen.electric_load.operating_reserve_required_fraction) + @test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) ≈ loadOR + windOR atol=1.0 + + end + + @testset "GHP" begin + """ + + This tests multiple unique aspects of GHP: + 1. REopt takes the output data of GhpGhx, creates multiple GHP options, and chooses the expected one + 2. GHP with heating and cooling "..efficiency_thermal_factors" reduces the net thermal load + 3. GHP serves only the SpaceHeatingLoad by default unless it is allowed to serve DHW + 4. GHP serves all the Cooling load + 5. Input of a custom COP map for GHP and check the GHP performance to make sure it's using it correctly + 6. Hybrid GHP capability functions as expected + + """ + # Load base inputs + input_data = JSON.parsefile("scenarios/ghp_inputs.json") + + # Modify ["GHP"]["ghpghx_inputs"] for running GhpGhx.jl + # Heat pump performance maps + cop_map_mat_header = readdlm("scenarios/ghp_cop_map_custom.csv", ',', header=true) + data = cop_map_mat_header[1] + headers = cop_map_mat_header[2] + # Generate a "records" style dictionary from the + cop_map_list = [] + for i in axes(data,1) + dict_record = Dict(name=>data[i, col] for (col, name) in enumerate(headers)) + push!(cop_map_list, dict_record) end + input_data["GHP"]["ghpghx_inputs"][1]["cop_map_eft_heating_cooling"] = cop_map_list + + # Due to GhpGhx not being a registered package (no OSI-approved license), + # the registered REopt package cannot have GhpGhx as a "normal" dependency; + # Therefore, we only use a "ghpghx_response" (the output of GhpGhx) as an + # input to REopt to avoid GhpGhx module calls + response_1 = JSON.parsefile("scenarios/ghpghx_response.json") + response_2 = deepcopy(response_1) + # Reduce the electric consumption of response 2 which should then be the chosen system + response_2["outputs"]["yearly_total_electric_consumption_series_kw"] *= 0.5 + input_data["GHP"]["ghpghx_responses"] = [response_1, response_2] + + # Heating load + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" + input_data["SpaceHeatingLoad"]["monthly_mmbtu"] = fill(1000.0, 12) + input_data["SpaceHeatingLoad"]["monthly_mmbtu"][1] = 500.0 + input_data["SpaceHeatingLoad"]["monthly_mmbtu"][end] = 1500.0 + + # Call REopt + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt([m1,m2], inputs) + + ghp_option_chosen = results["GHP"]["ghp_option_chosen"] + @test ghp_option_chosen == 2 + + # Test GHP heating and cooling load reduced + hot_load_reduced_mmbtu = sum(results["GHP"]["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"]) + cold_load_reduced_tonhour = sum(results["GHP"]["cooling_thermal_load_reduction_with_ghp_ton"]) + @test hot_load_reduced_mmbtu ≈ 1440.00 atol=0.1 + @test cold_load_reduced_tonhour ≈ 761382.78 atol=0.1 + + # Test GHP serving space heating with VAV thermal efficiency improvements + heating_served_mmbtu = sum(s.ghp_option_list[ghp_option_chosen].heating_thermal_kw / REopt.KWH_PER_MMBTU) + expected_heating_served_mmbtu = 12000 * 0.8 * 0.85 # (fuel_mmbtu * boiler_effic * space_heating_efficiency_thermal_factor) + @test round(heating_served_mmbtu, digits=1) ≈ expected_heating_served_mmbtu atol=1.0 + + # Boiler serves all of the DHW load, no DHW thermal reduction due to GHP retrofit + boiler_served_mmbtu = sum(results["ExistingBoiler"]["thermal_production_series_mmbtu_per_hour"]) + expected_boiler_served_mmbtu = 3000 * 0.8 # (fuel_mmbtu * boiler_effic) + @test round(boiler_served_mmbtu, digits=1) ≈ expected_boiler_served_mmbtu atol=1.0 + + # LoadProfileChillerThermal cooling thermal is 1/cooling_efficiency_thermal_factor of GHP cooling thermal production + bau_chiller_thermal_tonhour = sum(s.cooling_load.loads_kw_thermal / REopt.KWH_THERMAL_PER_TONHOUR) + ghp_cooling_thermal_tonhour = sum(inputs.ghp_cooling_thermal_load_served_kw[1,:] / REopt.KWH_THERMAL_PER_TONHOUR) + @test round(bau_chiller_thermal_tonhour) ≈ ghp_cooling_thermal_tonhour/0.6 atol=1.0 + + # Custom heat pump COP map is used properly + ghp_option_chosen = results["GHP"]["ghp_option_chosen"] + heating_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["heating_cop_avg"] + cooling_cop_avg = s.ghp_option_list[ghp_option_chosen].ghpghx_response["outputs"]["cooling_cop_avg"] + # Average COP which includes pump power should be lower than Heat Pump only COP specified by the map + @test heating_cop_avg <= 4.0 + @test cooling_cop_avg <= 8.0 + end - @testset "Hybrid GHX and GHP calculated costs validation" begin - ## Hybrid GHP validation. - # Load base inputs - input_data = JSON.parsefile("scenarios/ghp_financial_hybrid.json") + @testset "Hybrid GHX and GHP calculated costs validation" begin + ## Hybrid GHP validation. + # Load base inputs + input_data = JSON.parsefile("scenarios/ghp_financial_hybrid.json") - inputs = REoptInputs(input_data) + inputs = REoptInputs(input_data) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt([m1,m2], inputs) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt([m1,m2], inputs) - calculated_ghp_capital_costs = ((input_data["GHP"]["ghpghx_responses"][1]["outputs"]["number_of_boreholes"]* - input_data["GHP"]["ghpghx_responses"][1]["outputs"]["length_boreholes_ft"]* - inputs.s.ghp_option_list[1].installed_cost_ghx_per_ft) + - (inputs.s.ghp_option_list[1].installed_cost_heatpump_per_ton* - input_data["GHP"]["ghpghx_responses"][1]["outputs"]["peak_combined_heatpump_thermal_ton"]* - inputs.s.ghp_option_list[1].heatpump_capacity_sizing_factor_on_peak_load) + - (inputs.s.ghp_option_list[1].building_sqft* - inputs.s.ghp_option_list[1].installed_cost_building_hydronic_loop_per_sqft)) + calculated_ghp_capital_costs = ((input_data["GHP"]["ghpghx_responses"][1]["outputs"]["number_of_boreholes"]* + input_data["GHP"]["ghpghx_responses"][1]["outputs"]["length_boreholes_ft"]* + inputs.s.ghp_option_list[1].installed_cost_ghx_per_ft) + + (inputs.s.ghp_option_list[1].installed_cost_heatpump_per_ton* + input_data["GHP"]["ghpghx_responses"][1]["outputs"]["peak_combined_heatpump_thermal_ton"]* + inputs.s.ghp_option_list[1].heatpump_capacity_sizing_factor_on_peak_load) + + (inputs.s.ghp_option_list[1].building_sqft* + inputs.s.ghp_option_list[1].installed_cost_building_hydronic_loop_per_sqft)) - @test results["Financial"]["initial_capital_costs"] ≈ calculated_ghp_capital_costs atol=0.1 - - calculated_om_costs = inputs.s.ghp_option_list[1].building_sqft* - inputs.s.ghp_option_list[1].om_cost_per_sqft_year * inputs.third_party_factor * inputs.pwf_om + @test results["Financial"]["initial_capital_costs"] ≈ calculated_ghp_capital_costs atol=0.1 + + calculated_om_costs = inputs.s.ghp_option_list[1].building_sqft* + inputs.s.ghp_option_list[1].om_cost_per_sqft_year * inputs.third_party_factor * inputs.pwf_om - @test results["Financial"]["lifecycle_om_costs_before_tax"] ≈ calculated_om_costs atol=0.1 + @test results["Financial"]["lifecycle_om_costs_before_tax"] ≈ calculated_om_costs atol=0.1 - calc_om_cost_after_tax = calculated_om_costs*(1-inputs.s.financial.owner_tax_rate_fraction) - @test results["Financial"]["lifecycle_om_costs_after_tax"] - calc_om_cost_after_tax < 0.0001 + calc_om_cost_after_tax = calculated_om_costs*(1-inputs.s.financial.owner_tax_rate_fraction) + @test results["Financial"]["lifecycle_om_costs_after_tax"] - calc_om_cost_after_tax < 0.0001 - @test abs(results["Financial"]["lifecycle_capital_costs_plus_om_after_tax"] - (calc_om_cost_after_tax + 0.7*results["Financial"]["initial_capital_costs"])) < 150.0 + @test abs(results["Financial"]["lifecycle_capital_costs_plus_om_after_tax"] - (calc_om_cost_after_tax + 0.7*results["Financial"]["initial_capital_costs"])) < 150.0 - @test abs(results["Financial"]["lifecycle_capital_costs"] - 0.7*results["Financial"]["initial_capital_costs"]) < 150.0 + @test abs(results["Financial"]["lifecycle_capital_costs"] - 0.7*results["Financial"]["initial_capital_costs"]) < 150.0 - @test abs(results["Financial"]["npv"] - 840621) < 1.0 - @test results["Financial"]["simple_payback_years"] - 5.09 < 0.1 - @test results["Financial"]["internal_rate_of_return"] - 0.18 < 0.01 + @test abs(results["Financial"]["npv"] - 840621) < 1.0 + @test results["Financial"]["simple_payback_years"] - 5.09 < 0.1 + @test results["Financial"]["internal_rate_of_return"] - 0.18 < 0.01 - @test haskey(results["ExistingBoiler"], "year_one_fuel_cost_before_tax_bau") + @test haskey(results["ExistingBoiler"], "year_one_fuel_cost_before_tax_bau") - ## Hybrid - input_data["GHP"]["ghpghx_responses"] = [JSON.parsefile("scenarios/ghpghx_hybrid_results.json")] - input_data["GHP"]["avoided_capex_by_ghp_present_value"] = 1.0e6 - input_data["GHP"]["ghx_useful_life_years"] = 35 + ## Hybrid + input_data["GHP"]["ghpghx_responses"] = [JSON.parsefile("scenarios/ghpghx_hybrid_results.json")] + input_data["GHP"]["avoided_capex_by_ghp_present_value"] = 1.0e6 + input_data["GHP"]["ghx_useful_life_years"] = 35 - inputs = REoptInputs(input_data) + inputs = REoptInputs(input_data) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt([m1,m2], inputs) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt([m1,m2], inputs) - pop!(input_data["GHP"], "ghpghx_inputs", nothing) - pop!(input_data["GHP"], "ghpghx_responses", nothing) - ghp_obj = REopt.GHP(JSON.parsefile("scenarios/ghpghx_hybrid_results.json"), input_data["GHP"]) + pop!(input_data["GHP"], "ghpghx_inputs", nothing) + pop!(input_data["GHP"], "ghpghx_responses", nothing) + ghp_obj = REopt.GHP(JSON.parsefile("scenarios/ghpghx_hybrid_results.json"), input_data["GHP"]) - calculated_ghx_residual_value = ghp_obj.ghx_only_capital_cost* - ( - (ghp_obj.ghx_useful_life_years - inputs.s.financial.analysis_years)/ghp_obj.ghx_useful_life_years - )/( - (1 + inputs.s.financial.offtaker_discount_rate_fraction)^inputs.s.financial.analysis_years - ) - - @test results["GHP"]["ghx_residual_value_present_value"] ≈ calculated_ghx_residual_value atol=0.1 - @test inputs.s.ghp_option_list[1].is_ghx_hybrid = true + calculated_ghx_residual_value = ghp_obj.ghx_only_capital_cost* + ( + (ghp_obj.ghx_useful_life_years - inputs.s.financial.analysis_years)/ghp_obj.ghx_useful_life_years + )/( + (1 + inputs.s.financial.offtaker_discount_rate_fraction)^inputs.s.financial.analysis_years + ) + + @test results["GHP"]["ghx_residual_value_present_value"] ≈ calculated_ghx_residual_value atol=0.1 + @test inputs.s.ghp_option_list[1].is_ghx_hybrid = true - # Test centralized GHP cost calculations - input_data_wwhp = JSON.parsefile("scenarios/ghp_inputs_wwhp.json") - response_wwhp = JSON.parsefile("scenarios/ghpghx_response_wwhp.json") - input_data_wwhp["GHP"]["ghpghx_responses"] = [response_wwhp] + # Test centralized GHP cost calculations + input_data_wwhp = JSON.parsefile("scenarios/ghp_inputs_wwhp.json") + response_wwhp = JSON.parsefile("scenarios/ghpghx_response_wwhp.json") + input_data_wwhp["GHP"]["ghpghx_responses"] = [response_wwhp] - s_wwhp = Scenario(input_data_wwhp) - inputs_wwhp = REoptInputs(s_wwhp) - m3 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results_wwhp = run_reopt(m3, inputs_wwhp) + s_wwhp = Scenario(input_data_wwhp) + inputs_wwhp = REoptInputs(s_wwhp) + m3 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results_wwhp = run_reopt(m3, inputs_wwhp) - heating_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_heating_pump_per_ton"] * - input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_heating_heatpump_thermal_ton"] + heating_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_heating_pump_per_ton"] * + input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_heating_heatpump_thermal_ton"] - cooling_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_cooling_pump_per_ton"] * - input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_cooling_heatpump_thermal_ton"] + cooling_hp_cost = input_data_wwhp["GHP"]["installed_cost_wwhp_cooling_pump_per_ton"] * + input_data_wwhp["GHP"]["heatpump_capacity_sizing_factor_on_peak_load"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["peak_cooling_heatpump_thermal_ton"] - ghx_cost = input_data_wwhp["GHP"]["installed_cost_ghx_per_ft"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"] * - results_wwhp["GHP"]["ghpghx_chosen_outputs"]["length_boreholes_ft"] + ghx_cost = input_data_wwhp["GHP"]["installed_cost_ghx_per_ft"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["number_of_boreholes"] * + results_wwhp["GHP"]["ghpghx_chosen_outputs"]["length_boreholes_ft"] - # CAPEX reduction factor for 30% ITC, 5-year MACRS, assuming 26% tax rate and 8.3% discount - capex_reduction_factor = 0.455005797 + # CAPEX reduction factor for 30% ITC, 5-year MACRS, assuming 26% tax rate and 8.3% discount + capex_reduction_factor = 0.455005797 - calculated_ghp_capex = (heating_hp_cost + cooling_hp_cost + ghx_cost) * (1 - capex_reduction_factor) + calculated_ghp_capex = (heating_hp_cost + cooling_hp_cost + ghx_cost) * (1 - capex_reduction_factor) - reopt_ghp_capex = results_wwhp["Financial"]["lifecycle_capital_costs"] - @test calculated_ghp_capex ≈ reopt_ghp_capex atol=300 - end + reopt_ghp_capex = results_wwhp["Financial"]["lifecycle_capital_costs"] + @test calculated_ghp_capex ≈ reopt_ghp_capex atol=300 + end - @testset "Cambium Emissions" begin - """ - 1) Location in contiguous US - - Correct data from Cambium (returned location and values) - - Adjusted for load year vs. Cambium year (which starts on Sunday) vs. AVERT year (2022 currently) - - co2 pct increase should be zero - 2) HI and AK locations - - Should use AVERT data and give an "info" message - - Adjust for load year vs. AVERT year - - co2 pct increase should be the default value unless user provided value - 3) International - - all emissions should be zero unless provided - """ - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - - post_name = "cambium.json" - post = JSON.parsefile("./scenarios/$post_name") - - cities = Dict( - "Denver" => (39.7413753050447, -104.99965032911328), - "Fairbanks" => (64.84053664406181, -147.71913656313163), - "Santiago" => (-33.44485437650408, -70.69031905547853) - ) - - # 1) Location in contiguous US - city = "Denver" - post["Site"]["latitude"] = cities[city][1] - post["Site"]["longitude"] = cities[city][2] - post["ElectricLoad"]["loads_kw"] = [20 for i in range(1,8760)] - post["ElectricLoad"]["year"] = 2021 # 2021 First day is Fri - scen = Scenario(post) - - @test scen.electric_utility.avert_emissions_region == "Rocky Mountains" - @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 - @test scen.electric_utility.cambium_emissions_region == "RMPAc" - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 0.394608 rtol=1e-3 - @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[1] ≈ 0.677942 rtol=1e-4 # Should start on Friday - @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[8760] ≈ 0.6598207198 rtol=1e-5 # Should end on Friday - @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) / 8760 ≈ 0.00061165 rtol=1e-5 # check avg from AVERT data for RM region - @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ 0 atol=1e-5 # should be 0 with Cambium data - @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data - @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] - @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] - - # 2) AK location - city = "Fairbanks" - post["Site"]["latitude"] = cities[city][1] - post["Site"]["longitude"] = cities[city][2] - scen = Scenario(post) - - @test scen.electric_utility.avert_emissions_region == "Alaska" - @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 - @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 1.29199999 rtol=1e-3 # check that data from eGRID (AVERT data file) is used - @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["CO2e"] # should get updated to this value - @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data - @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] - @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] - - # 3) International location - city = "Santiago" - post["Site"]["latitude"] = cities[city][1] - post["Site"]["longitude"] = cities[city][2] - scen = Scenario(post) - - @test scen.electric_utility.avert_emissions_region == "" - @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 5.521032136418236e6 atol=1.0 - @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" - @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_NOx_per_kwh) ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) ≈ 0 - @test sum(scen.electric_utility.emissions_factor_series_lb_PM25_per_kwh) ≈ 0 + @testset "Cambium Emissions" begin + """ + 1) Location in contiguous US + - Correct data from Cambium (returned location and values) + - Adjusted for load year vs. Cambium year (which starts on Sunday) vs. AVERT year (2022 currently) + - co2 pct increase should be zero + 2) HI and AK locations + - Should use AVERT data and give an "info" message + - Adjust for load year vs. AVERT year + - co2 pct increase should be the default value unless user provided value + 3) International + - all emissions should be zero unless provided + """ + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + + post_name = "cambium.json" + post = JSON.parsefile("./scenarios/$post_name") + + cities = Dict( + "Denver" => (39.7413753050447, -104.99965032911328), + "Fairbanks" => (64.84053664406181, -147.71913656313163), + "Santiago" => (-33.44485437650408, -70.69031905547853) + ) + + # 1) Location in contiguous US + city = "Denver" + post["Site"]["latitude"] = cities[city][1] + post["Site"]["longitude"] = cities[city][2] + post["ElectricLoad"]["loads_kw"] = [20 for i in range(1,8760)] + post["ElectricLoad"]["year"] = 2021 # 2021 First day is Fri + scen = Scenario(post) + + @test scen.electric_utility.avert_emissions_region == "Rocky Mountains" + @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 + @test scen.electric_utility.cambium_emissions_region == "RMPAc" + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 0.394608 rtol=1e-3 + @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[1] ≈ 0.677942 rtol=1e-4 # Should start on Friday + @test scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh[8760] ≈ 0.6598207198 rtol=1e-5 # Should end on Friday + @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) / 8760 ≈ 0.00061165 rtol=1e-5 # check avg from AVERT data for RM region + @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ 0 atol=1e-5 # should be 0 with Cambium data + @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data + @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] + @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] + + # 2) AK location + city = "Fairbanks" + post["Site"]["latitude"] = cities[city][1] + post["Site"]["longitude"] = cities[city][2] + scen = Scenario(post) + + @test scen.electric_utility.avert_emissions_region == "Alaska" + @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 0 atol=1e-5 + @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) / 8760 ≈ 1.29199999 rtol=1e-3 # check that data from eGRID (AVERT data file) is used + @test scen.electric_utility.emissions_factor_CO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["CO2e"] # should get updated to this value + @test scen.electric_utility.emissions_factor_SO2_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["SO2"] # should be 2.163% for AVERT data + @test scen.electric_utility.emissions_factor_NOx_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["NOx"] + @test scen.electric_utility.emissions_factor_PM25_decrease_fraction ≈ REopt.EMISSIONS_DECREASE_DEFAULTS["PM25"] + + # 3) International location + city = "Santiago" + post["Site"]["latitude"] = cities[city][1] + post["Site"]["longitude"] = cities[city][2] + scen = Scenario(post) - end + @test scen.electric_utility.avert_emissions_region == "" + @test scen.electric_utility.distance_to_avert_emissions_region_meters ≈ 5.521032136418236e6 atol=1.0 + @test scen.electric_utility.cambium_emissions_region == "NA - Cambium data not used for climate emissions" + @test sum(scen.electric_utility.emissions_factor_series_lb_CO2_per_kwh) ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_NOx_per_kwh) ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_SO2_per_kwh) ≈ 0 + @test sum(scen.electric_utility.emissions_factor_series_lb_PM25_per_kwh) ≈ 0 + + end - @testset "Emissions and Renewable Energy Percent" begin - #renewable energy and emissions reduction targets - include_exported_RE_in_total = [true,false,true] - include_exported_ER_in_total = [true,false,true] - RE_target = [0.8,nothing,nothing] - ER_target = [nothing,0.8,nothing] - with_outage = [true,false,false] - - for i in range(1, stop=3) - if i == 3 - inputs = JSON.parsefile("./scenarios/re_emissions_with_thermal.json") - else - inputs = JSON.parsefile("./scenarios/re_emissions_elec_only.json") - end - if i == 1 - inputs["Site"]["latitude"] = 37.746 - inputs["Site"]["longitude"] = -122.448 - # inputs["ElectricUtility"]["emissions_region"] = "California" - end - inputs["Site"]["include_exported_renewable_electricity_in_total"] = include_exported_RE_in_total[i] - inputs["Site"]["include_exported_elec_emissions_in_total"] = include_exported_ER_in_total[i] - inputs["Site"]["renewable_electricity_min_fraction"] = if isnothing(RE_target[i]) 0.0 else RE_target[i] end - inputs["Site"]["renewable_electricity_max_fraction"] = RE_target[i] - inputs["Site"]["CO2_emissions_reduction_min_fraction"] = ER_target[i] - inputs["Site"]["CO2_emissions_reduction_max_fraction"] = ER_target[i] - if with_outage[i] - outage_start_hour = 4032 - outage_duration = 2000 #hrs - inputs["ElectricUtility"]["outage_start_time_step"] = outage_start_hour + 1 - inputs["ElectricUtility"]["outage_end_time_step"] = outage_start_hour + 1 + outage_duration - inputs["Generator"]["max_kw"] = 20 - inputs["Generator"]["existing_kw"] = 2 - inputs["Generator"]["fuel_avail_gal"] = 1000 - end + @testset "Emissions and Renewable Energy Percent" begin + #renewable energy and emissions reduction targets + include_exported_RE_in_total = [true,false,true] + include_exported_ER_in_total = [true,false,true] + RE_target = [0.8,nothing,nothing] + ER_target = [nothing,0.8,nothing] + with_outage = [true,false,false] + + for i in range(1, stop=3) + if i == 3 + inputs = JSON.parsefile("./scenarios/re_emissions_with_thermal.json") + else + inputs = JSON.parsefile("./scenarios/re_emissions_elec_only.json") + end + if i == 1 + inputs["Site"]["latitude"] = 37.746 + inputs["Site"]["longitude"] = -122.448 + # inputs["ElectricUtility"]["emissions_region"] = "California" + end + inputs["Site"]["include_exported_renewable_electricity_in_total"] = include_exported_RE_in_total[i] + inputs["Site"]["include_exported_elec_emissions_in_total"] = include_exported_ER_in_total[i] + inputs["Site"]["renewable_electricity_min_fraction"] = if isnothing(RE_target[i]) 0.0 else RE_target[i] end + inputs["Site"]["renewable_electricity_max_fraction"] = RE_target[i] + inputs["Site"]["CO2_emissions_reduction_min_fraction"] = ER_target[i] + inputs["Site"]["CO2_emissions_reduction_max_fraction"] = ER_target[i] + if with_outage[i] + outage_start_hour = 4032 + outage_duration = 2000 #hrs + inputs["ElectricUtility"]["outage_start_time_step"] = outage_start_hour + 1 + inputs["ElectricUtility"]["outage_end_time_step"] = outage_start_hour + 1 + outage_duration + inputs["Generator"]["max_kw"] = 20 + inputs["Generator"]["existing_kw"] = 2 + inputs["Generator"]["fuel_avail_gal"] = 1000 + end + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) + results = run_reopt([m1, m2], inputs) + + if !isnothing(ER_target[i]) + ER_fraction_out = results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] + @test ER_target[i] ≈ ER_fraction_out atol=1e-3 + lifecycle_emissions_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2"] + lifecycle_emissions_bau_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] + ER_fraction_calced_out = (lifecycle_emissions_bau_tonnes_CO2_out-lifecycle_emissions_tonnes_CO2_out)/lifecycle_emissions_bau_tonnes_CO2_out + ER_fraction_diff = abs(ER_fraction_calced_out-ER_fraction_out) + @test ER_fraction_diff ≈ 0.0 atol=1e-2 + end + annual_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_tonnes_CO2"] + yr1_fuel_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] + yr1_grid_emissions_tonnes_CO2_out = results["ElectricUtility"]["annual_emissions_tonnes_CO2"] + yr1_total_emissions_calced_tonnes_CO2 = yr1_fuel_emissions_tonnes_CO2_out + yr1_grid_emissions_tonnes_CO2_out + @test annual_emissions_tonnes_CO2_out ≈ yr1_total_emissions_calced_tonnes_CO2 atol=1e-1 + if haskey(results["Financial"],"breakeven_cost_of_emissions_reduction_per_tonne_CO2") + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] >= 0.0 + end + + if i == 1 + @test results["PV"]["size_kw"] ≈ 59.7222 atol=1e-1 + @test results["ElectricStorage"]["size_kw"] ≈ 0.0 atol=1e-1 + @test results["ElectricStorage"]["size_kwh"] ≈ 0.0 atol=1e-1 + @test results["Generator"]["size_kw"] ≈ 9.13 atol=1e-1 + @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 + @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.148375 atol=1e-4 + @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.57403012 atol=1e-4 + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 332.4 atol=1 + @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.85 atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 7.427 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 + @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8459.45 atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 236.95 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 148.54 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 + @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 27.813 atol=1e-1 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 556.26 + elseif i == 2 + #commented out values are results using same levelization factor as API + @test results["PV"]["size_kw"] ≈ 106.13 atol=1 + @test results["ElectricStorage"]["size_kw"] ≈ 20.09 atol=1 # 20.29 + @test results["ElectricStorage"]["size_kwh"] ≈ 170.94 atol=1 + @test !haskey(results, "Generator") + # Renewable energy + @test results["Site"]["renewable_electricity_fraction"] ≈ 0.78586 atol=1e-3 + @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 + @test results["Site"]["annual_renewable_electricity_kwh_bau"] ≈ 13308.5 atol=10 # 13542.62 atol=10 + @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 + # CO2 emissions - totals ≈ from grid, from fuelburn, ER, $/tCO2 breakeven + @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.8 atol=1e-3 # 0.8 + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 491.5 atol=1e-1 + @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.662 atol=1 + @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 + @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8397.85 atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1166.19 atol=1 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 + @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 atol=1 # 0.0 + @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 + @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 233.24 atol=1 + + + #also test CO2 breakeven cost + inputs["PV"]["min_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] + inputs["PV"]["max_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] + inputs["ElectricStorage"]["min_kw"] = results["ElectricStorage"]["size_kw"] + inputs["ElectricStorage"]["max_kw"] = results["ElectricStorage"]["size_kw"] + inputs["ElectricStorage"]["min_kwh"] = results["ElectricStorage"]["size_kwh"] + inputs["ElectricStorage"]["max_kwh"] = results["ElectricStorage"]["size_kwh"] + inputs["Financial"]["CO2_cost_per_tonne"] = results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] + inputs["Settings"]["include_climate_in_objective"] = true m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) results = run_reopt([m1, m2], inputs) - - if !isnothing(ER_target[i]) - ER_fraction_out = results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] - @test ER_target[i] ≈ ER_fraction_out atol=1e-3 - lifecycle_emissions_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2"] - lifecycle_emissions_bau_tonnes_CO2_out = results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] - ER_fraction_calced_out = (lifecycle_emissions_bau_tonnes_CO2_out-lifecycle_emissions_tonnes_CO2_out)/lifecycle_emissions_bau_tonnes_CO2_out - ER_fraction_diff = abs(ER_fraction_calced_out-ER_fraction_out) - @test ER_fraction_diff ≈ 0.0 atol=1e-2 - end - - annual_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_tonnes_CO2"] - yr1_fuel_emissions_tonnes_CO2_out = results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] - yr1_grid_emissions_tonnes_CO2_out = results["ElectricUtility"]["annual_emissions_tonnes_CO2"] - yr1_total_emissions_calced_tonnes_CO2 = yr1_fuel_emissions_tonnes_CO2_out + yr1_grid_emissions_tonnes_CO2_out - @test annual_emissions_tonnes_CO2_out ≈ yr1_total_emissions_calced_tonnes_CO2 atol=1e-1 - if haskey(results["Financial"],"breakeven_cost_of_emissions_reduction_per_tonne_CO2") - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] >= 0.0 - end - - if i == 1 - @test results["PV"]["size_kw"] ≈ 59.7222 atol=1e-1 - @test results["ElectricStorage"]["size_kw"] ≈ 0.0 atol=1e-1 - @test results["ElectricStorage"]["size_kwh"] ≈ 0.0 atol=1e-1 - @test results["Generator"]["size_kw"] ≈ 9.13 atol=1e-1 - @test results["Site"]["total_renewable_energy_fraction"] ≈ 0.8 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.148375 atol=1e-4 - @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.57403012 atol=1e-4 - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 332.4 atol=1 - @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.85 atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 7.427 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 - @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8459.45 atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ 236.95 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 148.54 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 - @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 27.813 atol=1e-1 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 556.26 - elseif i == 2 - #commented out values are results using same levelization factor as API - @test results["PV"]["size_kw"] ≈ 106.13 atol=1 - @test results["ElectricStorage"]["size_kw"] ≈ 20.09 atol=1 # 20.29 - @test results["ElectricStorage"]["size_kwh"] ≈ 170.94 atol=1 - @test !haskey(results, "Generator") - # Renewable energy - @test results["Site"]["renewable_electricity_fraction"] ≈ 0.78586 atol=1e-3 - @test results["Site"]["renewable_electricity_fraction_bau"] ≈ 0.132118 atol=1e-3 #0.1354 atol=1e-3 - @test results["Site"]["annual_renewable_electricity_kwh_bau"] ≈ 13308.5 atol=10 # 13542.62 atol=10 - @test results["Site"]["total_renewable_energy_fraction_bau"] ≈ 0.132118 atol=1e-3 # 0.1354 atol=1e-3 - # CO2 emissions - totals ≈ from grid, from fuelburn, ER, $/tCO2 breakeven - @test results["Site"]["lifecycle_emissions_reduction_CO2_fraction"] ≈ 0.8 atol=1e-3 # 0.8 - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ 491.5 atol=1e-1 - @test results["Site"]["annual_emissions_tonnes_CO2"] ≈ 11.662 atol=1 - @test results["Site"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 - @test results["Financial"]["lifecycle_emissions_cost_climate"] ≈ 8397.85 atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_CO2_bau"] ≈ 1166.19 atol=1 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] ≈ 0.0 atol=1 # 0.0 - @test results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2_bau"] ≈ 0.0 atol=1 # 0.0 - @test results["ElectricUtility"]["annual_emissions_tonnes_CO2_bau"] ≈ 58.3095 atol=1 - @test results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] ≈ 233.24 atol=1 - - - #also test CO2 breakeven cost - inputs["PV"]["min_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] - inputs["PV"]["max_kw"] = results["PV"]["size_kw"] - inputs["PV"]["existing_kw"] - inputs["ElectricStorage"]["min_kw"] = results["ElectricStorage"]["size_kw"] - inputs["ElectricStorage"]["max_kw"] = results["ElectricStorage"]["size_kw"] - inputs["ElectricStorage"]["min_kwh"] = results["ElectricStorage"]["size_kwh"] - inputs["ElectricStorage"]["max_kwh"] = results["ElectricStorage"]["size_kwh"] - inputs["Financial"]["CO2_cost_per_tonne"] = results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] - inputs["Settings"]["include_climate_in_objective"] = true - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "presolve" => "on")) - results = run_reopt([m1, m2], inputs) - @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ inputs["Financial"]["CO2_cost_per_tonne"] atol=1e-1 - elseif i == 3 - @test results["PV"]["size_kw"] ≈ 20.0 atol=1e-1 - @test !haskey(results, "Wind") - @test !haskey(results, "ElectricStorage") - @test !haskey(results, "Generator") - @test results["CHP"]["size_kw"] ≈ 200.0 atol=1e-1 - @test results["AbsorptionChiller"]["size_ton"] ≈ 400.0 atol=1e-1 - @test results["HotThermalStorage"]["size_gal"] ≈ 50000 atol=1e1 - @test results["ColdThermalStorage"]["size_gal"] ≈ 30000 atol=1e1 - yr1_nat_gas_mmbtu = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] + results["CHP"]["annual_fuel_consumption_mmbtu"] - nat_gas_emissions_lb_per_mmbtu = Dict("CO2"=>117.03, "NOx"=>0.09139, "SO2"=>0.000578592, "PM25"=>0.007328833) - TONNE_PER_LB = 1/2204.62 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ nat_gas_emissions_lb_per_mmbtu["CO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_NOx"] ≈ nat_gas_emissions_lb_per_mmbtu["NOx"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_SO2"] ≈ nat_gas_emissions_lb_per_mmbtu["SO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 - @test results["Site"]["annual_emissions_from_fuelburn_tonnes_PM25"] ≈ nat_gas_emissions_lb_per_mmbtu["PM25"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 - @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] atol=1 - @test results["Site"]["lifecycle_emissions_tonnes_NOx"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_NOx"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_NOx"] atol=0.1 - @test results["Site"]["lifecycle_emissions_tonnes_SO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_SO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_SO2"] atol=1e-2 - @test results["Site"]["lifecycle_emissions_tonnes_PM25"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_PM25"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_PM25"] atol=1.5e-2 - @test results["Site"]["annual_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 - @test results["Site"]["renewable_electricity_fraction"] ≈ results["Site"]["annual_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 - KWH_PER_MMBTU = 293.07107 - annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_renewable_electricity_kwh"] - annual_heat_kwh = (results["CHP"]["annual_thermal_production_mmbtu"] + results["ExistingBoiler"]["annual_thermal_production_mmbtu"]) * KWH_PER_MMBTU - @test results["Site"]["total_renewable_energy_fraction"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 - end + @test results["Financial"]["breakeven_cost_of_emissions_reduction_per_tonne_CO2"] ≈ inputs["Financial"]["CO2_cost_per_tonne"] atol=1e-1 + elseif i == 3 + @test results["PV"]["size_kw"] ≈ 20.0 atol=1e-1 + @test !haskey(results, "Wind") + @test !haskey(results, "ElectricStorage") + @test !haskey(results, "Generator") + @test results["CHP"]["size_kw"] ≈ 200.0 atol=1e-1 + @test results["AbsorptionChiller"]["size_ton"] ≈ 400.0 atol=1e-1 + @test results["HotThermalStorage"]["size_gal"] ≈ 50000 atol=1e1 + @test results["ColdThermalStorage"]["size_gal"] ≈ 30000 atol=1e1 + yr1_nat_gas_mmbtu = results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"] + results["CHP"]["annual_fuel_consumption_mmbtu"] + nat_gas_emissions_lb_per_mmbtu = Dict("CO2"=>117.03, "NOx"=>0.09139, "SO2"=>0.000578592, "PM25"=>0.007328833) + TONNE_PER_LB = 1/2204.62 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] ≈ nat_gas_emissions_lb_per_mmbtu["CO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_NOx"] ≈ nat_gas_emissions_lb_per_mmbtu["NOx"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_SO2"] ≈ nat_gas_emissions_lb_per_mmbtu["SO2"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 + @test results["Site"]["annual_emissions_from_fuelburn_tonnes_PM25"] ≈ nat_gas_emissions_lb_per_mmbtu["PM25"] * yr1_nat_gas_mmbtu * TONNE_PER_LB atol=1e-2 + @test results["Site"]["lifecycle_emissions_tonnes_CO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_CO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_CO2"] atol=1 + @test results["Site"]["lifecycle_emissions_tonnes_NOx"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_NOx"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_NOx"] atol=0.1 + @test results["Site"]["lifecycle_emissions_tonnes_SO2"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_SO2"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_SO2"] atol=1e-2 + @test results["Site"]["lifecycle_emissions_tonnes_PM25"] ≈ results["Site"]["lifecycle_emissions_from_fuelburn_tonnes_PM25"] + results["ElectricUtility"]["lifecycle_emissions_tonnes_PM25"] atol=1.5e-2 + @test results["Site"]["annual_renewable_electricity_kwh"] ≈ results["PV"]["annual_energy_produced_kwh"] + inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_electric_production_kwh"] atol=1 + @test results["Site"]["renewable_electricity_fraction"] ≈ results["Site"]["annual_renewable_electricity_kwh"] / results["ElectricLoad"]["annual_calculated_kwh"] atol=1e-6#0.044285 atol=1e-4 + KWH_PER_MMBTU = 293.07107 + annual_RE_kwh = inputs["CHP"]["fuel_renewable_energy_fraction"] * results["CHP"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + results["Site"]["annual_renewable_electricity_kwh"] + annual_heat_kwh = (results["CHP"]["annual_thermal_production_mmbtu"] + results["ExistingBoiler"]["annual_thermal_production_mmbtu"]) * KWH_PER_MMBTU + @test results["Site"]["total_renewable_energy_fraction"] ≈ annual_RE_kwh / (annual_heat_kwh + results["ElectricLoad"]["annual_calculated_kwh"]) atol=1e-6 end end + end - @testset "Back pressure steam turbine" begin - """ - Validation to ensure that: - 1) ExistingBoiler provides the thermal energy (steam) to a backpressure SteamTurbine for CHP application - 2) SteamTurbine serves the heating load with the condensing steam + @testset "Back pressure steam turbine" begin + """ + Validation to ensure that: + 1) ExistingBoiler provides the thermal energy (steam) to a backpressure SteamTurbine for CHP application + 2) SteamTurbine serves the heating load with the condensing steam - """ - # Setup inputs, make heating load large to entice SteamTurbine - input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") - latitude = input_data["Site"]["latitude"] - longitude = input_data["Site"]["longitude"] - building = "Hospital" - elec_load_multiplier = 5.0 - heat_load_multiplier = 100.0 - input_data["ElectricLoad"]["doe_reference_name"] = building - input_data["SpaceHeatingLoad"]["doe_reference_name"] = building - input_data["DomesticHotWaterLoad"]["doe_reference_name"] = building - elec_load = REopt.ElectricLoad(latitude=latitude, longitude=longitude, doe_reference_name=building) - input_data["ElectricLoad"]["annual_kwh"] = elec_load_multiplier * sum(elec_load.loads_kw) - space_load = REopt.SpaceHeatingLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) - input_data["SpaceHeatingLoad"]["annual_mmbtu"] = heat_load_multiplier * space_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] - dhw_load = REopt.DomesticHotWaterLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) - input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = heat_load_multiplier * dhw_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] - s = Scenario(input_data) - inputs = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], inputs) - - # The expected values below were directly copied from the REopt_API V2 expected values - @test results["Financial"]["lcc"] ≈ 189359280.0 rtol=0.001 - @test results["Financial"]["npv"] ≈ 8085233.0 rtol=0.01 - @test results["SteamTurbine"]["size_kw"] ≈ 2616.418 atol=1.0 - @test results["SteamTurbine"]["annual_thermal_consumption_mmbtu"] ≈ 1000557.6 rtol=0.001 - @test results["SteamTurbine"]["annual_electric_production_kwh"] ≈ 18970374.6 rtol=0.001 - @test results["SteamTurbine"]["annual_thermal_production_mmbtu"] ≈ 924045.1 rtol=0.001 - - # BAU boiler loads - load_boiler_fuel = (s.space_heating_load.loads_kw + s.dhw_load.loads_kw) ./ REopt.KWH_PER_MMBTU ./ s.existing_boiler.efficiency - load_boiler_thermal = load_boiler_fuel * s.existing_boiler.efficiency - - # ExistingBoiler and SteamTurbine production - boiler_to_load = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] - boiler_to_st = results["ExistingBoiler"]["thermal_to_steamturbine_series_mmbtu_per_hour"] - boiler_total = boiler_to_load + boiler_to_st - st_to_load = results["SteamTurbine"]["thermal_to_load_series_mmbtu_per_hour"] - - # Fuel/thermal **consumption** - boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] - steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] - - # Check that all thermal supply to load meets the BAU load - thermal_to_load = sum(boiler_to_load) + sum(st_to_load) - @test thermal_to_load ≈ sum(load_boiler_thermal) atol=1.0 - - # Check the net electric efficiency of Boiler->SteamTurbine (electric out/fuel in) with the expected value from the Fact Sheet - steamturbine_electric = results["SteamTurbine"]["electric_production_series_kw"] - net_electric_efficiency = sum(steamturbine_electric) / (sum(boiler_fuel) * REopt.KWH_PER_MMBTU) - @test net_electric_efficiency ≈ 0.052 atol=0.005 - - # Check that the max production of the boiler is still less than peak heating load times thermal factor - factor = input_data["ExistingBoiler"]["max_thermal_factor_on_peak_load"] - boiler_capacity = maximum(load_boiler_thermal) * factor - @test maximum(boiler_total) <= boiler_capacity - end + """ + # Setup inputs, make heating load large to entice SteamTurbine + input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") + latitude = input_data["Site"]["latitude"] + longitude = input_data["Site"]["longitude"] + building = "Hospital" + elec_load_multiplier = 5.0 + heat_load_multiplier = 100.0 + input_data["ElectricLoad"]["doe_reference_name"] = building + input_data["SpaceHeatingLoad"]["doe_reference_name"] = building + input_data["DomesticHotWaterLoad"]["doe_reference_name"] = building + elec_load = REopt.ElectricLoad(latitude=latitude, longitude=longitude, doe_reference_name=building) + input_data["ElectricLoad"]["annual_kwh"] = elec_load_multiplier * sum(elec_load.loads_kw) + space_load = REopt.SpaceHeatingLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) + input_data["SpaceHeatingLoad"]["annual_mmbtu"] = heat_load_multiplier * space_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] + dhw_load = REopt.DomesticHotWaterLoad(latitude=latitude, longitude=longitude, doe_reference_name=building, existing_boiler_efficiency=input_data["ExistingBoiler"]["efficiency"]) + input_data["DomesticHotWaterLoad"]["annual_mmbtu"] = heat_load_multiplier * dhw_load.annual_mmbtu / input_data["ExistingBoiler"]["efficiency"] + s = Scenario(input_data) + inputs = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], inputs) + + # The expected values below were directly copied from the REopt_API V2 expected values + @test results["Financial"]["lcc"] ≈ 189359280.0 rtol=0.001 + @test results["Financial"]["npv"] ≈ 8085233.0 rtol=0.01 + @test results["SteamTurbine"]["size_kw"] ≈ 2616.418 atol=1.0 + @test results["SteamTurbine"]["annual_thermal_consumption_mmbtu"] ≈ 1000557.6 rtol=0.001 + @test results["SteamTurbine"]["annual_electric_production_kwh"] ≈ 18970374.6 rtol=0.001 + @test results["SteamTurbine"]["annual_thermal_production_mmbtu"] ≈ 924045.1 rtol=0.001 + + # BAU boiler loads + load_boiler_fuel = (s.space_heating_load.loads_kw + s.dhw_load.loads_kw) ./ REopt.KWH_PER_MMBTU ./ s.existing_boiler.efficiency + load_boiler_thermal = load_boiler_fuel * s.existing_boiler.efficiency + + # ExistingBoiler and SteamTurbine production + boiler_to_load = results["ExistingBoiler"]["thermal_to_load_series_mmbtu_per_hour"] + boiler_to_st = results["ExistingBoiler"]["thermal_to_steamturbine_series_mmbtu_per_hour"] + boiler_total = boiler_to_load + boiler_to_st + st_to_load = results["SteamTurbine"]["thermal_to_load_series_mmbtu_per_hour"] + + # Fuel/thermal **consumption** + boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] + steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] + + # Check that all thermal supply to load meets the BAU load + thermal_to_load = sum(boiler_to_load) + sum(st_to_load) + @test thermal_to_load ≈ sum(load_boiler_thermal) atol=1.0 + + # Check the net electric efficiency of Boiler->SteamTurbine (electric out/fuel in) with the expected value from the Fact Sheet + steamturbine_electric = results["SteamTurbine"]["electric_production_series_kw"] + net_electric_efficiency = sum(steamturbine_electric) / (sum(boiler_fuel) * REopt.KWH_PER_MMBTU) + @test net_electric_efficiency ≈ 0.052 atol=0.005 + + # Check that the max production of the boiler is still less than peak heating load times thermal factor + factor = input_data["ExistingBoiler"]["max_thermal_factor_on_peak_load"] + boiler_capacity = maximum(load_boiler_thermal) * factor + @test maximum(boiler_total) <= boiler_capacity + end - @testset "All heating supply/demand/storage energy balance" begin - """ - Validation to ensure that: - 1) Heat balance is correct with SteamTurbine (backpressure), CHP, HotTES, and AbsorptionChiller included - 2) The sum of a all thermal from techs supplying SteamTurbine is equal to SteamTurbine thermal consumption - 3) Techs are not supplying SteamTurbine with thermal if can_supply_steam_turbine = False - - :return: - """ - - # Start with steam turbine inputs, but adding a bunch below - input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") - input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" - # Add SpaceHeatingLoad building for heating loads, ignore DomesticHotWaterLoad for simplicity of energy balance checks - input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" - delete!(input_data, "DomesticHotWaterLoad") - - # Fix size of SteamTurbine, even if smaller than practical, because we're just looking at energy balances - input_data["SteamTurbine"]["min_kw"] = 30.0 - input_data["SteamTurbine"]["max_kw"] = 30.0 - - # Add CHP - input_data["CHP"] = Dict{Any, Any}([ - ("prime_mover", "recip_engine"), - ("size_class", 4), - ("min_kw", 250.0), - ("min_allowable_kw", 0.0), - ("max_kw", 250.0), - ("can_supply_steam_turbine", false), - ("fuel_cost_per_mmbtu", 8.0), - ("cooling_thermal_factor", 1.0) + @testset "All heating supply/demand/storage energy balance" begin + """ + Validation to ensure that: + 1) Heat balance is correct with SteamTurbine (backpressure), CHP, HotTES, and AbsorptionChiller included + 2) The sum of a all thermal from techs supplying SteamTurbine is equal to SteamTurbine thermal consumption + 3) Techs are not supplying SteamTurbine with thermal if can_supply_steam_turbine = False + + :return: + """ + + # Start with steam turbine inputs, but adding a bunch below + input_data = JSON.parsefile("scenarios/backpressure_steamturbine_inputs.json") + input_data["ElectricLoad"]["doe_reference_name"] = "Hospital" + # Add SpaceHeatingLoad building for heating loads, ignore DomesticHotWaterLoad for simplicity of energy balance checks + input_data["SpaceHeatingLoad"]["doe_reference_name"] = "Hospital" + delete!(input_data, "DomesticHotWaterLoad") + + # Fix size of SteamTurbine, even if smaller than practical, because we're just looking at energy balances + input_data["SteamTurbine"]["min_kw"] = 30.0 + input_data["SteamTurbine"]["max_kw"] = 30.0 + + # Add CHP + input_data["CHP"] = Dict{Any, Any}([ + ("prime_mover", "recip_engine"), + ("size_class", 4), + ("min_kw", 250.0), + ("min_allowable_kw", 0.0), + ("max_kw", 250.0), + ("can_supply_steam_turbine", false), + ("fuel_cost_per_mmbtu", 8.0), + ("cooling_thermal_factor", 1.0) + ]) + + input_data["Financial"]["chp_fuel_cost_escalation_rate_fraction"] = 0.034 + + # Add CoolingLoad and AbsorptionChiller so we can test the energy balance on AbsorptionChiller too (thermal consumption) + input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital") + input_data["AbsorptionChiller"] = Dict{Any, Any}([ + ("min_ton", 600.0), + ("max_ton", 600.0), + ("cop_thermal", 0.7), + ("installed_cost_per_ton", 500.0), + ("om_cost_per_ton", 0.5), + ("heating_load_input", "SpaceHeating") + ]) + + # Add Hot TES + input_data["HotThermalStorage"] = Dict{Any, Any}([ + ("min_gal", 50000.0), + ("max_gal", 50000.0) ]) - - input_data["Financial"]["chp_fuel_cost_escalation_rate_fraction"] = 0.034 - - # Add CoolingLoad and AbsorptionChiller so we can test the energy balance on AbsorptionChiller too (thermal consumption) - input_data["CoolingLoad"] = Dict{Any, Any}("doe_reference_name" => "Hospital") - input_data["AbsorptionChiller"] = Dict{Any, Any}([ - ("min_ton", 600.0), - ("max_ton", 600.0), - ("cop_thermal", 0.7), - ("installed_cost_per_ton", 500.0), - ("om_cost_per_ton", 0.5), - ("heating_load_input", "SpaceHeating") - ]) - - # Add Hot TES - input_data["HotThermalStorage"] = Dict{Any, Any}([ - ("min_gal", 50000.0), - ("max_gal", 50000.0) - ]) - - s = Scenario(input_data) - inputs = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) - results = run_reopt(m, inputs) - - thermal_techs = ["ExistingBoiler", "CHP", "SteamTurbine"] - thermal_loads = ["load", "storage", "steamturbine", "waste"] # We don't track AbsorptionChiller thermal consumption by tech - tech_to_thermal_load = Dict{Any, Any}() - for tech in thermal_techs - tech_to_thermal_load[tech] = Dict{Any, Any}() - for load in thermal_loads - if (tech == "SteamTurbine" && load == "steamturbine") || (load == "waste" && tech != "CHP") - tech_to_thermal_load[tech][load] = [0.0] * 8760 + + s = Scenario(input_data) + inputs = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01)) + results = run_reopt(m, inputs) + + thermal_techs = ["ExistingBoiler", "CHP", "SteamTurbine"] + thermal_loads = ["load", "storage", "steamturbine", "waste"] # We don't track AbsorptionChiller thermal consumption by tech + tech_to_thermal_load = Dict{Any, Any}() + for tech in thermal_techs + tech_to_thermal_load[tech] = Dict{Any, Any}() + for load in thermal_loads + if (tech == "SteamTurbine" && load == "steamturbine") || (load == "waste" && tech != "CHP") + tech_to_thermal_load[tech][load] = [0.0] * 8760 + else + if load == "waste" + tech_to_thermal_load[tech][load] = results[tech]["thermal_curtailed_series_mmbtu_per_hour"] else - if load == "waste" - tech_to_thermal_load[tech][load] = results[tech]["thermal_curtailed_series_mmbtu_per_hour"] - else - tech_to_thermal_load[tech][load] = results[tech]["thermal_to_"*load*"_series_mmbtu_per_hour"] - end + tech_to_thermal_load[tech][load] = results[tech]["thermal_to_"*load*"_series_mmbtu_per_hour"] end end end - # Hot TES is the other thermal supply - hottes_to_load = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] - - # BAU boiler loads - load_boiler_fuel = s.space_heating_load.loads_kw / input_data["ExistingBoiler"]["efficiency"] ./ REopt.KWH_PER_MMBTU - load_boiler_thermal = load_boiler_fuel .* input_data["ExistingBoiler"]["efficiency"] - - # Fuel/thermal **consumption** - boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] - chp_fuel_total = results["CHP"]["annual_fuel_consumption_mmbtu"] - steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] - absorptionchiller_thermal_in = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] - - # Check that all thermal supply to load meets the BAU load plus AbsorptionChiller load which is not explicitly tracked - alltechs_thermal_to_load_total = sum([sum(tech_to_thermal_load[tech]["load"]) for tech in thermal_techs]) + sum(hottes_to_load) - thermal_load_total = sum(load_boiler_thermal) + sum(absorptionchiller_thermal_in) - @test alltechs_thermal_to_load_total ≈ thermal_load_total rtol=1e-5 - - # Check that all thermal to steam turbine is equal to steam turbine thermal consumption - alltechs_thermal_to_steamturbine_total = sum([sum(tech_to_thermal_load[tech]["steamturbine"]) for tech in ["ExistingBoiler", "CHP"]]) - @test alltechs_thermal_to_steamturbine_total ≈ sum(steamturbine_thermal_in) atol=3 - - # Check that "thermal_to_steamturbine" is zero for each tech which has input of can_supply_steam_turbine as False - for tech in ["ExistingBoiler", "CHP"] - if !(tech in inputs.techs.can_supply_steam_turbine) - @test sum(tech_to_thermal_load[tech]["steamturbine"]) == 0.0 - end - end - end - - @testset "Electric Heater" begin - d = JSON.parsefile("./scenarios/electric_heater.json") - d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.4 * 8760 - d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.4 * 8760 - d["ProcessHeatLoad"]["annual_mmbtu"] = 0.2 * 8760 - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - - #first run: Boiler produces the required heat instead of the electric heater - electric heater should not be purchased - @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 - @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 - - d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0 - d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - - annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_electric_heater_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP - annual_energy_supplied = 87600 + annual_electric_heater_consumption - - #Second run: ElectricHeater produces the required heat with free electricity - @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 - @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ annual_electric_heater_consumption rtol=1e-4 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - end - - @testset "Process Heat Load" begin - d = JSON.parsefile("./scenarios/process_heat.json") + # Hot TES is the other thermal supply + hottes_to_load = results["HotThermalStorage"]["storage_to_load_series_mmbtu_per_hour"] - # Test set 1: Boiler has free fuel, no emissions, and serves all heating load. - d["Boiler"]["fuel_cost_per_mmbtu"] = 0.0 - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0.0 atol=0.1 - - #Test set 2: Boiler only serves process heat - d["Boiler"]["can_serve_dhw"] = false - d["Boiler"]["can_serve_space_heating"] = false - d["Boiler"]["can_serve_process_heat"] = true - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 8.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - - #Test set 3: Boiler cannot serve process heat but serves DHW, space heating - d["Boiler"]["can_serve_dhw"] = true - d["Boiler"]["can_serve_space_heating"] = true - d["Boiler"]["can_serve_process_heat"] = false - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 16.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - - #Test set 4: Fuel expensive, but ExistingBoiler is retired - d["Boiler"]["can_serve_dhw"] = true - d["Boiler"]["can_serve_space_heating"] = true - d["Boiler"]["can_serve_process_heat"] = true - d["Boiler"]["fuel_cost_per_mmbtu"] = 30.0 - d["ExistingBoiler"]["retire_in_optimal"] = true - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - - #Test set 5: Fuel expensive, ExistingBoiler not retired - d["ExistingBoiler"]["retire_in_optimal"] = false - s = Scenario(d) - p = REoptInputs(s) - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 - - # Test 6: reduce emissions by half, get half the new boiler size - d["Site"]["CO2_emissions_reduction_min_fraction"] = 0.50 - s = Scenario(d) - p = REoptInputs(s) - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt([m1,m2], p) - @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 12.0 atol=0.1 - @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 - @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 + # BAU boiler loads + load_boiler_fuel = s.space_heating_load.loads_kw / input_data["ExistingBoiler"]["efficiency"] ./ REopt.KWH_PER_MMBTU + load_boiler_thermal = load_boiler_fuel .* input_data["ExistingBoiler"]["efficiency"] + + # Fuel/thermal **consumption** + boiler_fuel = results["ExistingBoiler"]["fuel_consumption_series_mmbtu_per_hour"] + chp_fuel_total = results["CHP"]["annual_fuel_consumption_mmbtu"] + steamturbine_thermal_in = results["SteamTurbine"]["thermal_consumption_series_mmbtu_per_hour"] + absorptionchiller_thermal_in = results["AbsorptionChiller"]["thermal_consumption_series_mmbtu_per_hour"] + + # Check that all thermal supply to load meets the BAU load plus AbsorptionChiller load which is not explicitly tracked + alltechs_thermal_to_load_total = sum([sum(tech_to_thermal_load[tech]["load"]) for tech in thermal_techs]) + sum(hottes_to_load) + thermal_load_total = sum(load_boiler_thermal) + sum(absorptionchiller_thermal_in) + @test alltechs_thermal_to_load_total ≈ thermal_load_total rtol=1e-5 + + # Check that all thermal to steam turbine is equal to steam turbine thermal consumption + alltechs_thermal_to_steamturbine_total = sum([sum(tech_to_thermal_load[tech]["steamturbine"]) for tech in ["ExistingBoiler", "CHP"]]) + @test alltechs_thermal_to_steamturbine_total ≈ sum(steamturbine_thermal_in) atol=3 + + # Check that "thermal_to_steamturbine" is zero for each tech which has input of can_supply_steam_turbine as False + for tech in ["ExistingBoiler", "CHP"] + if !(tech in inputs.techs.can_supply_steam_turbine) + @test sum(tech_to_thermal_load[tech]["steamturbine"]) == 0.0 + end end + end - @testset "Custom REopt logger" begin - - # Throw a handled error - d = JSON.parsefile("./scenarios/logger.json") - - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - @test r["Messages"]["has_stacktrace"] == false + @testset "Electric Heater" begin + d = JSON.parsefile("./scenarios/electric_heater.json") + d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.4 * 8760 + d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.4 * 8760 + d["ProcessHeatLoad"]["annual_mmbtu"] = 0.2 * 8760 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + #first run: Boiler produces the required heat instead of the electric heater - electric heater should not be purchased + @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 + + d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 + d["ElectricHeater"]["installed_cost_per_mmbtu_per_hour"] = 1.0 + d["ElectricTariff"]["monthly_energy_rates"] = [0,0,0,0,0,0,0,0,0,0,0,0] + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + + annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour + annual_electric_heater_consumption = annual_thermal_prod * REopt.KWH_PER_MMBTU #1.0 COP + annual_energy_supplied = 87600 + annual_electric_heater_consumption + + #Second run: ElectricHeater produces the required heat with free electricity + @test results["ElectricHeater"]["size_mmbtu_per_hour"] ≈ 0.8 atol=0.1 + @test results["ElectricHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ElectricHeater"]["annual_electric_consumption_kwh"] ≈ annual_electric_heater_consumption rtol=1e-4 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - # Type is dict when errors, otherwise type REoptInputs - @test isa(REoptInputs(d), Dict) - - # Using filepath - n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([n1,n2], "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(n, "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - # Throw an unhandled error: Bad URDB rate -> stack gets returned for debugging - d["ElectricLoad"]["doe_reference_name"] = "MidriseApartment" - d["ElectricTariff"]["urdb_label"] = "62c70a6c40a0c425535d387x" + end - m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([m1,m2], d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 + @testset "Process Heat Load" begin + d = JSON.parsefile("./scenarios/process_heat.json") + + # Test set 1: Boiler has free fuel, no emissions, and serves all heating load. + d["Boiler"]["fuel_cost_per_mmbtu"] = 0.0 + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0.0 atol=0.1 + + #Test set 2: Boiler only serves process heat + d["Boiler"]["can_serve_dhw"] = false + d["Boiler"]["can_serve_space_heating"] = false + d["Boiler"]["can_serve_process_heat"] = true + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 8.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + + #Test set 3: Boiler cannot serve process heat but serves DHW, space heating + d["Boiler"]["can_serve_dhw"] = true + d["Boiler"]["can_serve_space_heating"] = true + d["Boiler"]["can_serve_process_heat"] = false + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 16.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 140160.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + + #Test set 4: Fuel expensive, but ExistingBoiler is retired + d["Boiler"]["can_serve_dhw"] = true + d["Boiler"]["can_serve_space_heating"] = true + d["Boiler"]["can_serve_process_heat"] = true + d["Boiler"]["fuel_cost_per_mmbtu"] = 30.0 + d["ExistingBoiler"]["retire_in_optimal"] = true + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 24.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + + #Test set 5: Fuel expensive, ExistingBoiler not retired + d["ExistingBoiler"]["retire_in_optimal"] = false + s = Scenario(d) + p = REoptInputs(s) + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt(m, p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 0.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test sum(results["Boiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 0.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 210240.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_dhw_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_space_heating_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + @test sum(results["ExistingBoiler"]["thermal_to_process_heat_load_series_mmbtu_per_hour"]) ≈ 70080.0 atol=0.1 + + # Test 6: reduce emissions by half, get half the new boiler size + d["Site"]["CO2_emissions_reduction_min_fraction"] = 0.50 + s = Scenario(d) + p = REoptInputs(s) + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], p) + @test results["Boiler"]["size_mmbtu_per_hour"] ≈ 12.0 atol=0.1 + @test results["Boiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 + @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 105120.0 atol=0.1 + end - m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(m, d) - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - # Type is dict when errors, otherwise type REoptInputs - @test isa(REoptInputs(d), Dict) - - # Using filepath - n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt([n1,n2], "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - - n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - r = run_reopt(n, "./scenarios/logger.json") - @test r["status"] == "error" - @test "Messages" ∈ keys(r) - @test "errors" ∈ keys(r["Messages"]) - @test "warnings" ∈ keys(r["Messages"]) - @test length(r["Messages"]["errors"]) > 0 - @test length(r["Messages"]["warnings"]) > 0 - end + @testset "Custom REopt logger" begin + + # Throw a handled error + d = JSON.parsefile("./scenarios/logger.json") + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + @test r["Messages"]["has_stacktrace"] == false + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Type is dict when errors, otherwise type REoptInputs + @test isa(REoptInputs(d), Dict) + + # Using filepath + n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([n1,n2], "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(n, "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Throw an unhandled error: Bad URDB rate -> stack gets returned for debugging + d["ElectricLoad"]["doe_reference_name"] = "MidriseApartment" + d["ElectricTariff"]["urdb_label"] = "62c70a6c40a0c425535d387x" + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([m1,m2], d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(m, d) + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + # Type is dict when errors, otherwise type REoptInputs + @test isa(REoptInputs(d), Dict) + + # Using filepath + n1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + n2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt([n1,n2], "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 + + n = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + r = run_reopt(n, "./scenarios/logger.json") + @test r["status"] == "error" + @test "Messages" ∈ keys(r) + @test "errors" ∈ keys(r["Messages"]) + @test "warnings" ∈ keys(r["Messages"]) + @test length(r["Messages"]["errors"]) > 0 + @test length(r["Messages"]["warnings"]) > 0 end end end \ No newline at end of file From de8d85b3c2831a0b67ca6d1679e555d98dd64b13 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 12 Aug 2024 22:13:29 -0600 Subject: [PATCH 170/266] Fix proforma calcs for CHP, ExistingBoiler, and other thermal techs --- src/results/existing_boiler.jl | 2 +- src/results/financial.jl | 126 +++++++++++++++++++++----------- src/results/proforma.jl | 130 ++++++++++++++++++++++----------- 3 files changed, 173 insertions(+), 85 deletions(-) diff --git a/src/results/existing_boiler.jl b/src/results/existing_boiler.jl index 32dfa01ba..2b81a9e4d 100644 --- a/src/results/existing_boiler.jl +++ b/src/results/existing_boiler.jl @@ -18,7 +18,7 @@ """ function add_existing_boiler_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - + r["size_mmbtu_per_hour"] = round(value(m[Symbol("dvSize"*_n)]["ExistingBoiler"]) / KWH_PER_MMBTU, digits=3) r["fuel_consumption_series_mmbtu_per_hour"] = round.(value.(m[:dvFuelUsage]["ExistingBoiler", ts] for ts in p.time_steps) ./ KWH_PER_MMBTU, digits=5) r["annual_fuel_consumption_mmbtu"] = round(sum(r["fuel_consumption_series_mmbtu_per_hour"]), digits=5) diff --git a/src/results/financial.jl b/src/results/financial.jl index 6d17db100..490657466 100644 --- a/src/results/financial.jl +++ b/src/results/financial.jl @@ -161,34 +161,17 @@ function initial_capex(m::JuMP.AbstractModel, p::REoptInputs; _n="") end if "CHP" in p.techs.all - # CHP.installed_cost_per_kw is now a list with potentially > 1 elements - cost_list = p.s.chp.installed_cost_per_kw - size_list = p.s.chp.tech_sizes_for_cost_curve - chp_size = value.(m[Symbol("dvPurchaseSize"*_n)])["CHP"] - if typeof(cost_list) == Vector{Float64} - if chp_size <= size_list[1] - initial_capex += chp_size * cost_list[1] # Currently not handling non-zero cost ($) for 0 kW size input - elseif chp_size > size_list[end] - initial_capex += chp_size * cost_list[end] - else - for s in 2:length(size_list) - if (chp_size > size_list[s-1]) && (chp_size <= size_list[s]) - slope = (cost_list[s] * size_list[s] - cost_list[s-1] * size_list[s-1]) / - (size_list[s] - size_list[s-1]) - initial_capex += cost_list[s-1] * size_list[s-1] + (chp_size - size_list[s-1]) * slope - end - end - end - else - initial_capex += cost_list * chp_size - #Add supplementary firing capital cost - # chp_supp_firing_size = self.nested_outputs["Scenario"]["Site"][tech].get("size_supplementary_firing_kw") - # chp_supp_firing_cost = self.inputs[tech].get("supplementary_firing_capital_cost_per_kw") or 0 - # initial_capex += chp_supp_firing_size * chp_supp_firing_cost - end + chp_size_kw = value.(m[Symbol("dvPurchaseSize"*_n)])["CHP"] + initial_capex += get_chp_initial_capex(p, chp_size_kw) end - # TODO thermal tech costs + if "Boiler" in p.techs.all + initial_capex += p.s.boiler.installed_cost_per_kw * value.(m[Symbol("dvPurchaseSize"*_n)])["Boiler"] + end + + if "AbsorptionChiller" in p.techs.all + initial_capex += p.s.absorption_chiller.installed_cost_per_kw * value.(m[Symbol("dvPurchaseSize"*_n)])["AbsorptionChiller"] + end if !isempty(p.s.ghp_option_list) @@ -325,21 +308,10 @@ function calculate_lcoe(p::REoptInputs, tech_results::Dict, tech::AbstractTech) federal_itc_amount = tech.federal_itc_fraction * federal_itc_basis npv_federal_itc = federal_itc_amount * (1.0/(1.0+discount_rate_fraction)) - depreciation_schedule = zeros(years) - if tech.macrs_option_years in [5,7] - if tech.macrs_option_years == 5 - schedule = p.s.financial.macrs_five_year - elseif tech.macrs_option_years == 7 - schedule = p.s.financial.macrs_seven_year - end - macrs_bonus_basis = federal_itc_basis - (federal_itc_basis * tech.federal_itc_fraction * tech.macrs_itc_reduction) - macrs_basis = macrs_bonus_basis * (1 - tech.macrs_bonus_fraction) - for (i,r) in enumerate(schedule) - if i-1 < length(depreciation_schedule) - depreciation_schedule[i] = macrs_basis * r - end - end - depreciation_schedule[1] += tech.macrs_bonus_fraction * macrs_bonus_basis + if tech.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, tech, federal_itc_basis) + else + depreciation_schedule = zeros(years) end tax_deductions = (om_series + depreciation_schedule) * federal_tax_rate_fraction @@ -354,4 +326,76 @@ function calculate_lcoe(p::REoptInputs, tech_results::Dict, tech::AbstractTech) lcoe = (capital_costs + npv_om - npv_pbi - cbi - ibi - npv_federal_itc - npv_tax_deductions ) / npv_annual_energy return round(lcoe, digits=4) +end + +""" + get_depreciation_schedule(p::REoptInputs, tech::AbstractTech, federal_itc_basis::Float64=0.0) + +Get the depreciation schedule for MACRS. First check if tech.macrs_option_years in [5 ,7], then call function to return depreciation schedule +Used in results/financial.jl and results/proformal.jl multiple times +""" +function get_depreciation_schedule(p::REoptInputs, tech::AbstractTech, federal_itc_basis::Float64=0.0) + schedule = [] + if tech.macrs_option_years == 5 + schedule = p.s.financial.macrs_five_year + elseif tech.macrs_option_years == 7 + schedule = p.s.financial.macrs_seven_year + end + + federal_itc_fraction = 0.0 + try + federal_itc_fraction = tech.federal_itc_fraction + catch + @warn "did not find tech.federal_itc_fraction so using 0.0" + end + + macrs_bonus_basis = federal_itc_basis - federal_itc_basis * federal_itc_fraction * tech.macrs_itc_reduction + macrs_basis = macrs_bonus_basis * (1 - tech.macrs_bonus_fraction) + + depreciation_schedule = zeros(p.s.financial.analysis_years) + for (i, r) in enumerate(schedule) + if i < length(depreciation_schedule) + depreciation_schedule[i] = macrs_basis * r + end + end + depreciation_schedule[1] += (tech.macrs_bonus_fraction * macrs_bonus_basis) + + return depreciation_schedule +end + + +""" + get_chp_initial_capex(p::REoptInputs, size_kw::Float64) + +CHP has a cost-curve input option, so calculating the initial CapEx requires more logic than typical tech CapEx calcs +""" +function get_chp_initial_capex(p::REoptInputs, size_kw::Float64) + # CHP.installed_cost_per_kw is now a list with potentially > 1 elements + cost_list = p.s.chp.installed_cost_per_kw + size_list = p.s.chp.tech_sizes_for_cost_curve + chp_size = size_kw + initial_capex = 0.0 + if typeof(cost_list) == Vector{Float64} + if chp_size <= size_list[1] + initial_capex = chp_size * cost_list[1] # Currently not handling non-zero cost ($) for 0 kW size input + elseif chp_size > size_list[end] + initial_capex = chp_size * cost_list[end] + else + for s in 2:length(size_list) + if (chp_size > size_list[s-1]) && (chp_size <= size_list[s]) + slope = (cost_list[s] * size_list[s] - cost_list[s-1] * size_list[s-1]) / + (size_list[s] - size_list[s-1]) + initial_capex = cost_list[s-1] * size_list[s-1] + (chp_size - size_list[s-1]) * slope + end + end + end + else + initial_capex = cost_list * chp_size + #Add supplementary firing capital cost + # chp_supp_firing_size = self.nested_outputs["Scenario"]["Site"][tech].get("size_supplementary_firing_kw") + # chp_supp_firing_cost = self.inputs[tech].get("supplementary_firing_capital_cost_per_kw") or 0 + # initial_capex += chp_supp_firing_size * chp_supp_firing_cost + end + + return initial_capex end \ No newline at end of file diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 9e403862e..bcaf78658 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -48,6 +48,7 @@ function proforma_results(p::REoptInputs, d::Dict) years = p.s.financial.analysis_years escalate_elec(val) = [-1 * val * (1 + p.s.financial.elec_cost_escalation_rate_fraction)^yr for yr in 1:years] escalate_om(val) = [val * (1 + p.s.financial.om_cost_escalation_rate_fraction)^yr for yr in 1:years] + escalate_fuel(val, esc_rate) = [val * (1 + esc_rate)^yr for yr in 1:years] third_party = p.s.financial.third_party_ownership # Create placeholder variables to store summed totals across all relevant techs @@ -111,6 +112,7 @@ function proforma_results(p::REoptInputs, d::Dict) # In the two party case the developer does not include the fuel cost in their costs # It is assumed that the offtaker will pay for this at a rate that is not marked up # to cover developer profits + # TODO escalate fuel cost with p.s.financial.generator_fuel_cost_escalation_rate_fraction fixed_and_var_om = d["Generator"]["year_one_fixed_om_cost_before_tax"] + d["Generator"]["year_one_variable_om_cost_before_tax"] fixed_and_var_om_bau = 0.0 year_one_fuel_cost_bau = 0.0 @@ -133,6 +135,64 @@ function proforma_results(p::REoptInputs, d::Dict) m.om_series_bau += escalate_om(annual_om_bau) end + # calculate CHP o+m costs, incentives, and depreciation + if "CHP" in keys(d) && d["CHP"]["size_kw"] > 0 + update_metrics(m, p, p.s.chp, "CHP", d, third_party) + end + + # calculate (new) Boiler o+m costs (just fuel, no non-fuel operating costs currently) + # the optional installed_cost inputs assume net present cost so no option for MACRS or incentives + if "ExistingBoiler" in keys(d) && d["ExistingBoiler"]["size_mmbtu_per_hour"] > 0 + fuel_cost = d["ExistingBoiler"]["year_one_fuel_cost_before_tax"] + m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.existing_boiler_fuel_cost_escalation_rate_fraction) + var_om = 0.0 + fixed_om = 0.0 + annual_om = -1 * (var_om + fixed_om) + m.om_series += escalate_om(annual_om) + + # BAU ExistingBoiler + fuel_cost_bau = d["ExistingBoiler"]["year_one_fuel_cost_before_tax_bau"] + m.om_series_bau += escalate_fuel(-1 * fuel_cost_bau, p.s.financial.existing_boiler_fuel_cost_escalation_rate_fraction) + var_om_bau = 0.0 + fixed_om_bau = 0.0 + annual_om_bau = -1 * (var_om_bau + fixed_om_bau) + m.om_series_bau += escalate_om(annual_om_bau) + + + end + + # calculate (new) Boiler o+m costs and depreciation (no incentives currently) + if "Boiler" in keys(d) && d["Boiler"]["size_mmbtu_per_hour"] > 0 + fuel_cost = d["Boiler"]["year_one_fuel_cost_before_tax"] + m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.boiler_fuel_cost_escalation_rate_fraction) + var_om = tech.om_cost_per_kwh * d["Boiler"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + fixed_om = tech.om_cost_per_kw * d["Boiler"]["size_mmbtu_per_hour"] * KWH_PER_MMBTU + annual_om = -1 * (var_om + fixed_om) + m.om_series += escalate_om(annual_om) + + # Depreciation + if tech.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, tech) + m.total_depreciation += depreciation_schedule + end + end + + # calculate Absorption Chiller o+m costs and depreciation (no incentives currently) + if "AbsorptionChiller" in keys(d) && d["AbsorptionChiller"]["size_ton"] > 0 + # Some thermal techs (e.g. Boiler) only have struct fields for O&M "per_kw" (converted from e.g. per_mmbtu_per_hour or per_ton) + # but Absorption Chiller also has the input-style "per_ton" O&M, so no need to convert like for Boiler + fixed_om = tech.om_cost_per_ton * d["Absorption"]["size_ton"] + + annual_om = -1 * (fixed_om) + m.om_series += escalate_om(annual_om) + + # Depreciation + if tech.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, tech) + m.total_depreciation += depreciation_schedule + end + end + # calculate GHP incentives, and depreciation if "GHP" in keys(d) && d["GHP"]["ghp_option_chosen"] > 0 update_ghp_metrics(m, p, p.s.ghp_option_list[d["GHP"]["ghp_option_chosen"]], "GHP", d, third_party) @@ -284,10 +344,20 @@ function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_nam total_kw = results[tech_name]["size_kw"] existing_kw = :existing_kw in fieldnames(typeof(tech)) ? tech.existing_kw : 0 new_kw = total_kw - existing_kw - capital_cost = new_kw * tech.installed_cost_per_kw + if tech_name == "CHP" + capital_cost = get_chp_initial_capex(p, results["CHP"]["size_kw"]) + else + capital_cost = new_kw * tech.installed_cost_per_kw + end - # owner is responsible for both new and existing PV maintenance in optimal case - if third_party + # owner is responsible for both new and existing PV (or Generator) maintenance in optimal case + # CHP doesn't have existing CHP, and it has different O&M cost parameters + if tech_name == "CHP" + hours_operating = sum(results["CHP"]["electric_production_series_kw"] .> 0.0) / (8760 * p.s.settings.time_steps_per_hour) + annual_om = -1 * (results["CHP"]["annual_electric_production_kwh"] * tech.om_cost_per_kwh + + new_kw * tech.om_cost_per_kw + + new_kw * tech.om_cost_per_hr_per_kw_rated * hours_operating) + elseif third_party annual_om = -1 * new_kw * tech.om_cost_per_kw else annual_om = -1 * total_kw * tech.om_cost_per_kw @@ -297,6 +367,12 @@ function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_nam m.om_series += escalate_om(annual_om) m.om_series_bau += escalate_om(-1 * existing_kw * tech.om_cost_per_kw) + if tech_name == "CHP" + escalate_fuel(val, esc_rate) = [val * (1 + esc_rate)^yr for yr in 1:years] + fuel_cost = results["CHP"]["year_one_fuel_cost_before_tax"] + m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.chp_fuel_cost_escalation_rate_fraction) + end + # incentive calculations, in the spreadsheet utility incentives are applied first utility_ibi = minimum([capital_cost * tech.utility_ibi_fraction, tech.utility_ibi_max]) utility_cbi = minimum([new_kw * tech.utility_rebate_per_kw, tech.utility_rebate_max]) @@ -311,7 +387,11 @@ function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_nam pbi_series = Float64[] pbi_series_bau = Float64[] existing_energy_bau = third_party ? get(results[tech_name], "year_one_energy_produced_kwh_bau", 0) : 0 - year_one_energy = "year_one_energy_produced_kwh" in keys(results[tech_name]) ? results[tech_name]["year_one_energy_produced_kwh"] : results[tech_name]["annual_energy_produced_kwh"] + if tech_name == "CHP" + year_one_energy = results[tech_name]["annual_electric_production_kwh"] + else + year_one_energy = "year_one_energy_produced_kwh" in keys(results[tech_name]) ? results[tech_name]["year_one_energy_produced_kwh"] : results[tech_name]["annual_energy_produced_kwh"] + end for yr in range(0, stop=years-1) if yr < tech.production_incentive_years degradation_fraction = :degradation_fraction in fieldnames(typeof(tech)) ? (1 - tech.degradation_fraction)^yr : 1.0 @@ -334,31 +414,13 @@ function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_nam m.total_pbi_bau += pbi_series_bau # Federal ITC - # NOTE: bug in v1 has the ITC within the `if tech.macrs_option_years in [5 ,7]` block. - # NOTE: bug in v1 reduces the federal_itc_basis with the federal_cbi, which is incorrect federal_itc_basis = capital_cost - state_ibi - utility_ibi - state_cbi - utility_cbi federal_itc_amount = tech.federal_itc_fraction * federal_itc_basis m.federal_itc += federal_itc_amount # Depreciation if tech.macrs_option_years in [5 ,7] - schedule = [] - if tech.macrs_option_years == 5 - schedule = p.s.financial.macrs_five_year - elseif tech.macrs_option_years == 7 - schedule = p.s.financial.macrs_seven_year - end - - macrs_bonus_basis = federal_itc_basis - federal_itc_basis * tech.federal_itc_fraction * tech.macrs_itc_reduction - macrs_basis = macrs_bonus_basis * (1 - tech.macrs_bonus_fraction) - - depreciation_schedule = zeros(years) - for (i, r) in enumerate(schedule) - if i < length(depreciation_schedule) - depreciation_schedule[i] = macrs_basis * r - end - end - depreciation_schedule[1] += (tech.macrs_bonus_fraction * macrs_bonus_basis) + depreciation_schedule = get_depreciation_schedule(p, tech, federal_itc_basis) m.total_depreciation += depreciation_schedule end nothing @@ -407,31 +469,13 @@ function update_ghp_metrics(m::REopt.Metrics, p::REoptInputs, tech::REopt.Abstra m.total_pbi_bau += pbi_series_bau # Federal ITC - # NOTE: bug in v1 has the ITC within the `if tech.macrs_option_years in [5 ,7]` block. - # NOTE: bug in v1 reduces the federal_itc_basis with the federal_cbi, which is incorrect federal_itc_basis = capital_cost - state_ibi - utility_ibi - state_cbi - utility_cbi federal_itc_amount = tech.federal_itc_fraction * federal_itc_basis m.federal_itc += federal_itc_amount # Depreciation if tech.macrs_option_years in [5 ,7] - schedule = [] - if tech.macrs_option_years == 5 - schedule = p.s.financial.macrs_five_year - elseif tech.macrs_option_years == 7 - schedule = p.s.financial.macrs_seven_year - end - - macrs_bonus_basis = federal_itc_basis - federal_itc_basis * tech.federal_itc_fraction * tech.macrs_itc_reduction - macrs_basis = macrs_bonus_basis * (1 - tech.macrs_bonus_fraction) - - depreciation_schedule = zeros(years) - for (i, r) in enumerate(schedule) - if i < length(depreciation_schedule) - depreciation_schedule[i] = macrs_basis * r - end - end - depreciation_schedule[1] += (tech.macrs_bonus_fraction * macrs_bonus_basis) + depreciation_schedule = get_depreciation_schedule(p, tech, federal_itc_basis) m.total_depreciation += depreciation_schedule end nothing @@ -452,6 +496,6 @@ function irr(cashflows::AbstractArray{<:Real, 1}) try rate = fzero(f, [0.0, 0.99]) finally - return round(rate, digits=2) + return round(rate, digits=3) end end From b5e820f96542f515965f070d6f7f44b156b1ec66 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 12 Aug 2024 22:44:43 -0600 Subject: [PATCH 171/266] Apply depreciation function to Storage too --- src/results/financial.jl | 2 +- src/results/proforma.jl | 21 ++------------------- 2 files changed, 3 insertions(+), 20 deletions(-) diff --git a/src/results/financial.jl b/src/results/financial.jl index 490657466..14290fa88 100644 --- a/src/results/financial.jl +++ b/src/results/financial.jl @@ -334,7 +334,7 @@ end Get the depreciation schedule for MACRS. First check if tech.macrs_option_years in [5 ,7], then call function to return depreciation schedule Used in results/financial.jl and results/proformal.jl multiple times """ -function get_depreciation_schedule(p::REoptInputs, tech::AbstractTech, federal_itc_basis::Float64=0.0) +function get_depreciation_schedule(p::REoptInputs, tech::Union{AbstractTech,AbstractStorage}, federal_itc_basis::Float64=0.0) schedule = [] if tech.macrs_option_years == 5 schedule = p.s.financial.macrs_five_year diff --git a/src/results/proforma.jl b/src/results/proforma.jl index bcaf78658..2a6434443 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -86,23 +86,8 @@ function proforma_results(p::REoptInputs, d::Dict) m.federal_itc += federal_itc_amount # Depreciation - if storage.macrs_option_years in [5, 7] - schedule = [] - if storage.macrs_option_years == 5 - schedule = p.s.financial.macrs_five_year - elseif storage.macrs_option_years == 7 - schedule = p.s.financial.macrs_seven_year - end - macrs_bonus_basis = federal_itc_basis * (1 - storage.total_itc_fraction * storage.macrs_itc_reduction) - macrs_basis = macrs_bonus_basis * (1 - storage.macrs_bonus_fraction) - - depreciation_schedule = zeros(years) - for (i, r) in enumerate(schedule) - if i < length(depreciation_schedule) - depreciation_schedule[i] = macrs_basis * r - end - end - depreciation_schedule[1] += storage.macrs_bonus_fraction * macrs_bonus_basis + if storage.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, storage) m.total_depreciation += depreciation_schedule end end @@ -157,8 +142,6 @@ function proforma_results(p::REoptInputs, d::Dict) fixed_om_bau = 0.0 annual_om_bau = -1 * (var_om_bau + fixed_om_bau) m.om_series_bau += escalate_om(annual_om_bau) - - end # calculate (new) Boiler o+m costs and depreciation (no incentives currently) From 4409a11f44f9820f02aa1950a17d911caafa9d0f Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 12 Aug 2024 22:46:04 -0600 Subject: [PATCH 172/266] Add CHP proforma test with known value from external proforma spreadsheet model --- test/runtests.jl | 13 +++++ test/scenarios/chp_payback.json | 86 +++++++++++++++++++++++++++++++++ 2 files changed, 99 insertions(+) create mode 100644 test/scenarios/chp_payback.json diff --git a/test/runtests.jl b/test/runtests.jl index e3c176641..464770bfa 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -832,6 +832,19 @@ else # run HiGHS tests results = run_reopt(m, d) @test sum(results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 4174.455 atol=1e-3 end + + @testset "CHP Proforma Metrics" begin + # This test compares the resulting simple payback period (years) for CHP to a proforma spreadsheet model which has been verified + # All financial parameters which influence this calc have been input to avoid breaking with changing defaults + input_data = JSON.parsefile("./scenarios/chp_payback.json") + s = Scenario(input_data) + inputs = REoptInputs(s) + + m1 = Model(optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.01, "output_flag" => false, "log_to_console" => false)) + m2 = Model(optimizer_with_attributes(HiGHS.Optimizer, "mip_rel_gap" => 0.01, "output_flag" => false, "log_to_console" => false)) + results = run_reopt([m1,m2], inputs) + @test abs(results["Financial"]["simple_payback_years"] - 8.12) <= 0.02 + end end @testset "FlexibleHVAC" begin diff --git a/test/scenarios/chp_payback.json b/test/scenarios/chp_payback.json new file mode 100644 index 000000000..ae8e05353 --- /dev/null +++ b/test/scenarios/chp_payback.json @@ -0,0 +1,86 @@ +{ + "Financial": { + "offtaker_tax_rate_fraction": 0.26, + "elec_cost_escalation_rate_fraction": 0.017, + "chp_fuel_cost_escalation_rate_fraction": 0.015, + "existing_boiler_fuel_cost_escalation_rate_fraction": 0.015, + "om_cost_escalation_rate_fraction": 0.025 + }, + "Settings": { + "solver_name": "HiGHS", + "off_grid_flag": false, + "include_climate_in_objective": false, + "include_health_in_objective": false + }, + "Site": { + "latitude": 41.8809434, + "longitude": -72.2600655, + "include_exported_renewable_electricity_in_total": true, + "include_exported_elec_emissions_in_total": true, + "land_acres": 1000000.0, + "roof_squarefeet": 0 + }, + "ElectricLoad": { + "monthly_totals_kwh": [ + 921984.0, + 884352.0, + 912576.0, + 921984.0, + 950208.0, + 903168.0, + 1025472.0, + 978432.0, + 1072512.0, + 1044288.0, + 987840.0, + 950208.0 + ], + "critical_load_fraction": 0.8, + "doe_reference_name": "FlatLoad" + }, + "ElectricTariff": { + "blended_annual_demand_rate": 21.487, + "blended_annual_energy_rate": 0.0788 + }, + "ElectricUtility": { + "net_metering_limit_kw": 0.0, + "avert_emissions_region": "New England", + "cambium_location_type": "GEA Regions", + "cambium_levelization_years": 1, + "cambium_metric_col": "lrmer_co2e", + "cambium_scenario": "Mid-case", + "cambium_grid_level": "enduse" + }, + "Generator": { + "max_kw": 0, + "existing_kw": 0 + }, + "CHP": { + "can_net_meter": false, + "prime_mover": "recip_engine", + "size_class": 5, + "min_kw": 1104.0, + "min_allowable_kw": 1104.0, + "max_kw": 1104.0, + "fuel_type": "natural_gas", + "can_wholesale": false, + "fuel_cost_per_mmbtu": 10.55, + "federal_itc_fraction": 0.3, + "macrs_option_years": 5, + "macrs_bonus_fraction": 0.8, + "macrs_itc_reduction": 0.5 + }, + "DomesticHotWaterLoad": { + "annual_mmbtu": 6795.0, + "doe_reference_name": "FlatLoad" + }, + "SpaceHeatingLoad": { + "annual_mmbtu": 36205.0, + "doe_reference_name": "FlatLoad" + }, + "ExistingBoiler": { + "fuel_type": "natural_gas", + "production_type": "hot_water", + "fuel_cost_per_mmbtu": 10.55 + } +} \ No newline at end of file From 14eb79df1901e8aff913b72023b4a87e54dbfa11 Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Mon, 12 Aug 2024 22:50:16 -0600 Subject: [PATCH 173/266] Update CHANGELOG.md --- CHANGELOG.md | 1 + 1 file changed, 1 insertion(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index d61265cb3..299463000 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,7 @@ Classify the change according to the following categories: ## Develop 08-09-2024 ### Changed +- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage - Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. . - Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings - Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov. From f5b2c5f14e048458789f5dfd6efe0ad3496479a8 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 21:30:06 -0600 Subject: [PATCH 174/266] Update GHP proforma tests after adding ExistingBoiler operating/fuel costs, to expected improvement in metrics --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 8d3d946be..9e3317bb2 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1947,8 +1947,8 @@ else # run HiGHS tests @test abs(results["Financial"]["lifecycle_capital_costs"] - 0.7*results["Financial"]["initial_capital_costs"]) < 150.0 @test abs(results["Financial"]["npv"] - 840621) < 1.0 - @test results["Financial"]["simple_payback_years"] - 5.09 < 0.1 - @test results["Financial"]["internal_rate_of_return"] - 0.18 < 0.01 + @test abs(results["Financial"]["simple_payback_years"] - 3.59) < 0.1 + @test abs(results["Financial"]["internal_rate_of_return"] - 0.258) < 0.01 @test haskey(results["ExistingBoiler"], "year_one_fuel_cost_before_tax_bau") From aa918f1704a931846ce3391e3315d8958b2c19c9 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 21:30:43 -0600 Subject: [PATCH 175/266] Fix issue with (new) Boiler proforma metrics --- src/core/reopt_inputs.jl | 10 ++++++---- 1 file changed, 6 insertions(+), 4 deletions(-) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index fa0969da2..09b80a377 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -396,8 +396,8 @@ function setup_tech_inputs(s::AbstractScenario) end if "Boiler" in techs.all - setup_boiler_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, - boiler_efficiency, production_factor, fuel_cost_per_kwh) + setup_boiler_inputs(s, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, + om_cost_per_kw, production_factor, fuel_cost_per_kwh) end if "CHP" in techs.all @@ -693,14 +693,16 @@ end """ function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, - production_factor, fuel_cost_per_kwh) + om_cost_per_kw, production_factor, fuel_cost_per_kwh) Update tech-indexed data arrays necessary to build the JuMP model with the values for (new) boiler. This version of this function, used in BAUInputs(), doesn't update renewable energy and emissions arrays. """ -function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, boiler_efficiency, production_factor, fuel_cost_per_kwh) +function setup_boiler_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, boiler_efficiency, + om_cost_per_kw, production_factor, fuel_cost_per_kwh) max_sizes["Boiler"] = s.boiler.max_kw min_sizes["Boiler"] = s.boiler.min_kw + existing_sizes["Boiler"] = 0.0 boiler_efficiency["Boiler"] = s.boiler.efficiency # The Boiler only has a MACRS benefit, no ITC etc. From ded5562eb2af3c3294fbd74a69088c791d081762 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 21:31:43 -0600 Subject: [PATCH 176/266] Replace generic "tech" reference with specific tech object --- src/results/proforma.jl | 28 +++++++++++++++++++++------- 1 file changed, 21 insertions(+), 7 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 2a6434443..d92be48d9 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -87,7 +87,7 @@ function proforma_results(p::REoptInputs, d::Dict) # Depreciation if storage.macrs_option_years in [5 ,7] - depreciation_schedule = get_depreciation_schedule(p, storage) + depreciation_schedule = get_depreciation_schedule(p, storage, federal_itc_basis) m.total_depreciation += depreciation_schedule end end @@ -148,18 +148,32 @@ function proforma_results(p::REoptInputs, d::Dict) if "Boiler" in keys(d) && d["Boiler"]["size_mmbtu_per_hour"] > 0 fuel_cost = d["Boiler"]["year_one_fuel_cost_before_tax"] m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.boiler_fuel_cost_escalation_rate_fraction) - var_om = tech.om_cost_per_kwh * d["Boiler"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU - fixed_om = tech.om_cost_per_kw * d["Boiler"]["size_mmbtu_per_hour"] * KWH_PER_MMBTU + var_om = p.s.boiler.om_cost_per_kwh * d["Boiler"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU + fixed_om = p.s.boiler.om_cost_per_kw * d["Boiler"]["size_mmbtu_per_hour"] * KWH_PER_MMBTU annual_om = -1 * (var_om + fixed_om) m.om_series += escalate_om(annual_om) # Depreciation - if tech.macrs_option_years in [5 ,7] - depreciation_schedule = get_depreciation_schedule(p, tech) + if p.s.boiler.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, p.s.boiler) m.total_depreciation += depreciation_schedule end end + # calculate Steam Turbine o+m costs and depreciation (no incentives currently) + if "SteamTurbine" in keys(d) && get(d["SteamTurbine"], "size_kw", 0) > 0 + fixed_om = tech.om_cost_per_kw * d["SteamTurbine"]["size_kw"] + var_om = tech.om_cost_per_kwh * d["SteamTurbine"]["annual_electric_production_kwh"] + annual_om = -1 * (fixed_om + var_om) + m.om_series += escalate_om(annual_om) + + # Depreciation + if p.s.steam_turbine.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, p.s.steam_turbine) + m.total_depreciation += depreciation_schedule + end + end + # calculate Absorption Chiller o+m costs and depreciation (no incentives currently) if "AbsorptionChiller" in keys(d) && d["AbsorptionChiller"]["size_ton"] > 0 # Some thermal techs (e.g. Boiler) only have struct fields for O&M "per_kw" (converted from e.g. per_mmbtu_per_hour or per_ton) @@ -170,8 +184,8 @@ function proforma_results(p::REoptInputs, d::Dict) m.om_series += escalate_om(annual_om) # Depreciation - if tech.macrs_option_years in [5 ,7] - depreciation_schedule = get_depreciation_schedule(p, tech) + if p.s.absorption_chiller.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, p.s.absorption_chiller) m.total_depreciation += depreciation_schedule end end From 900a3a60e62a2eb32c33cf5e9d938e7191396082 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 21:32:11 -0600 Subject: [PATCH 177/266] Add SteamTurbine to initial_capex --- src/results/financial.jl | 4 ++++ 1 file changed, 4 insertions(+) diff --git a/src/results/financial.jl b/src/results/financial.jl index 14290fa88..e19e71c5c 100644 --- a/src/results/financial.jl +++ b/src/results/financial.jl @@ -165,6 +165,10 @@ function initial_capex(m::JuMP.AbstractModel, p::REoptInputs; _n="") initial_capex += get_chp_initial_capex(p, chp_size_kw) end + if "SteamTurbine" in p.techs.all + initial_capex += p.s.steam_turbine.installed_cost_per_kw * value.(m[Symbol("dvPurchaseSize"*_n)])["SteamTurbine"] + end + if "Boiler" in p.techs.all initial_capex += p.s.boiler.installed_cost_per_kw * value.(m[Symbol("dvPurchaseSize"*_n)])["Boiler"] end From e402a3ed8882e176ab01b34e85a3ac2d26d63215 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 22:35:46 -0600 Subject: [PATCH 178/266] More proforma tech/name reference fixes --- src/results/proforma.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index d92be48d9..0359958c5 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -162,8 +162,8 @@ function proforma_results(p::REoptInputs, d::Dict) # calculate Steam Turbine o+m costs and depreciation (no incentives currently) if "SteamTurbine" in keys(d) && get(d["SteamTurbine"], "size_kw", 0) > 0 - fixed_om = tech.om_cost_per_kw * d["SteamTurbine"]["size_kw"] - var_om = tech.om_cost_per_kwh * d["SteamTurbine"]["annual_electric_production_kwh"] + fixed_om = p.s.steam_turbine.om_cost_per_kw * d["SteamTurbine"]["size_kw"] + var_om = p.s.steam_turbine.om_cost_per_kwh * d["SteamTurbine"]["annual_electric_production_kwh"] annual_om = -1 * (fixed_om + var_om) m.om_series += escalate_om(annual_om) @@ -178,7 +178,7 @@ function proforma_results(p::REoptInputs, d::Dict) if "AbsorptionChiller" in keys(d) && d["AbsorptionChiller"]["size_ton"] > 0 # Some thermal techs (e.g. Boiler) only have struct fields for O&M "per_kw" (converted from e.g. per_mmbtu_per_hour or per_ton) # but Absorption Chiller also has the input-style "per_ton" O&M, so no need to convert like for Boiler - fixed_om = tech.om_cost_per_ton * d["Absorption"]["size_ton"] + fixed_om = p.s.absorption_chiller.om_cost_per_ton * d["AbsorptionChiller"]["size_ton"] annual_om = -1 * (fixed_om) m.om_series += escalate_om(annual_om) From f6524bdf1fc9dc9461f62a73a16a707842af92ca Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 22:48:29 -0600 Subject: [PATCH 179/266] Update expected indicator constraint error message --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 9e3317bb2..e8cead5a6 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -991,7 +991,7 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) set_optimizer_attribute(m, "mip_rel_gap", 0.01) r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + @test occursin("Unable to use IndicatorToMILPBridge", string(r["Messages"]["errors"])) # #optimal SOH at end of horizon is 80\% to prevent any replacement # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 @@ -1005,7 +1005,7 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) set_optimizer_attribute(m, "mip_rel_gap", 0.01) r = run_reopt(m, d) - @test occursin("not supported by the solver", string(r["Messages"]["errors"])) + @test occursin("Unable to use IndicatorToMILPBridge", string(r["Messages"]["errors"])) # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 end From 09292b116d077eced34226933bda821896e212d1 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 13 Aug 2024 23:06:04 -0600 Subject: [PATCH 180/266] Simplify CHP sizing test to speed up --- test/runtests.jl | 4 ++-- test/scenarios/chp_sizing.json | 11 ++++++----- 2 files changed, 8 insertions(+), 7 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index e8cead5a6..5ccff9949 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -660,8 +660,8 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false, "mip_rel_gap" => 0.01, "presolve" => "on")) results = run_reopt(m, inputs) - @test round(results["CHP"]["size_kw"], digits=0) ≈ 400.0 atol=50.0 - @test round(results["Financial"]["lcc"], digits=0) ≈ 1.3476e7 rtol=1.0e-2 + @test round(results["CHP"]["size_kw"], digits=0) ≈ 263.0 atol=50.0 + @test round(results["Financial"]["lcc"], digits=0) ≈ 1.11e7 rtol=0.05 end @testset "CHP Cost Curve and Min Allowable Size" begin diff --git a/test/scenarios/chp_sizing.json b/test/scenarios/chp_sizing.json index eefb65416..657a9a8c4 100644 --- a/test/scenarios/chp_sizing.json +++ b/test/scenarios/chp_sizing.json @@ -17,7 +17,8 @@ "doe_reference_name": "Hospital" }, "ElectricTariff": { - "urdb_label": "5e1676e95457a3f87673e3b0" + "blended_annual_energy_rate": 0.12, + "blended_annual_demand_rate": 10.0 }, "SpaceHeatingLoad": { "doe_reference_name": "Hospital" @@ -40,11 +41,11 @@ "om_cost_per_kw": 149.8, "om_cost_per_kwh": 0.0, "om_cost_per_hr_per_kw_rated": 0.0, - "electric_efficiency_full_load": 0.3573, - "electric_efficiency_half_load": 0.3216, + "electric_efficiency_full_load": 0.34, + "electric_efficiency_half_load": 0.34, "min_turn_down_fraction": 0.5, - "thermal_efficiency_full_load": 0.4418, - "thermal_efficiency_half_load": 0.4664, + "thermal_efficiency_full_load": 0.45, + "thermal_efficiency_half_load": 0.45, "macrs_option_years": 0, "macrs_bonus_fraction": 0.0, "macrs_itc_reduction": 0.0, From e319a3c890696728f1e9f32dc1efb4624bc5d7ce Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 14 Aug 2024 02:07:05 -0600 Subject: [PATCH 181/266] added avoided HVAC upgrade costs for ASHP --- src/constraints/thermal_tech_constraints.jl | 6 ++++++ src/core/ashp.jl | 10 +++++++-- src/core/bau_inputs.jl | 4 +++- src/core/reopt.jl | 10 ++++++++- src/core/reopt_inputs.jl | 24 ++++++++++++--------- 5 files changed, 40 insertions(+), 14 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 11b9c4bc8..c663264e7 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -126,6 +126,12 @@ function add_ashp_force_in_constraints(m, p; _n="") end end +function avoided_capex_by_ashp(m, p; _n="") + m[:AvoidedCapexByASHP] = @expression(m, + sum(p.avoided_capex_by_ashp_present_value[t] for t in p.techs.ashp) + ) +end + function no_existing_boiler_production(m, p; _n="") for ts in p.time_steps for q in p.heating_loads diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 09aa2f799..fece79ece 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -44,6 +44,7 @@ struct ASHP <: AbstractThermalTech can_serve_cooling::Bool force_into_system::Bool back_up_temp_threshold_degF::Real + avoided_capex_by_ashp_present_value::Real end @@ -64,6 +65,7 @@ function ASHP_SpaceHeater(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true + avoided_capex_by_ashp_present_value::Real = 0.0 #The following inputs are used to create the attributes heating_cop and heating cf: heating_cop_reference::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) @@ -89,6 +91,7 @@ function ASHP_SpaceHeater(; om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, + avoided_capex_by_ashp_present_value::Real = 0.0, can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, heating_cop_reference::Array{Float64,1} = Float64[], @@ -193,7 +196,8 @@ function ASHP_SpaceHeater(; can_serve_process_heat, can_serve_cooling, force_into_system, - back_up_temp_threshold_degF + back_up_temp_threshold_degF, + avoided_capex_by_ashp_present_value ) end @@ -229,6 +233,7 @@ function ASHP_WaterHeater(; om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, + avoided_capex_by_ashp_present_value::Real = 0.0, force_into_system::Union{Bool, Nothing} = nothing, heating_cop_reference::Array{Float64,1} = Float64[], heating_cf_reference::Array{Float64,1} = Float64[], @@ -312,7 +317,8 @@ function ASHP_WaterHeater(; can_serve_process_heat, can_serve_cooling, force_into_system, - back_up_temp_threshold_degF + back_up_temp_threshold_degF, + avoided_capex_by_ashp_present_value ) end diff --git a/src/core/bau_inputs.jl b/src/core/bau_inputs.jl index a4777d319..c8b7661b5 100644 --- a/src/core/bau_inputs.jl +++ b/src/core/bau_inputs.jl @@ -30,6 +30,7 @@ function BAUInputs(p::REoptInputs) cooling_cop = Dict{String, Array{Float64,1}}() heating_cf = Dict{String, Array{Float64,1}}() cooling_cf = Dict{String, Array{Float64,1}}() + avoided_capex_by_ashp_present_value = Dict(t => 0.0 for t in techs.all) production_factor = DenseAxisArray{Float64}(undef, techs.all, p.time_steps) tech_renewable_energy_fraction = Dict(t => 0.0 for t in techs.all) # !!! note: tech_emissions_factors are in lb / kWh of fuel burned (gets multiplied by kWh of fuel burned, not kWh electricity consumption, ergo the use of the HHV instead of fuel slope) @@ -231,7 +232,8 @@ function BAUInputs(p::REoptInputs) heating_loads_kw, heating_loads_served_by_tes, unavailability, - absorption_chillers_using_heating_load + absorption_chillers_using_heating_load, + avoided_capex_by_ashp_present_value ) end diff --git a/src/core/reopt.jl b/src/core/reopt.jl index 1b8a5ea86..e5e1d11ef 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -263,6 +263,7 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:GHPCapCosts] = 0.0 m[:GHPOMCosts] = 0.0 m[:AvoidedCapexByGHP] = 0.0 + m[:AvoidedCapexByASHP] = 0.0 m[:ResidualGHXCapCost] = 0.0 m[:ObjectivePenalties] = 0.0 @@ -324,6 +325,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) if !isempty(p.techs.ashp) add_ashp_force_in_constraints(m, p) end + + if !isempty(p.avoided_capex_by_ashp_present_value) && !isempty(p.techs.ashp) + avoided_capex_by_ashp(m, p) + end if !isempty(p.techs.thermal) add_thermal_load_constraints(m, p) # split into heating and cooling constraints? @@ -491,7 +496,10 @@ function build_reopt!(m::JuMP.AbstractModel, p::REoptInputs) m[:OffgridOtherCapexAfterDepr] - # Subtract capital expenditures avoided by inclusion of GHP and residual present value of GHX. - m[:AvoidedCapexByGHP] - m[:ResidualGHXCapCost] + m[:AvoidedCapexByGHP] - m[:ResidualGHXCapCost] - + + # Subtract capital expenditures avoided by inclusion of ASHP + m[:AvoidedCapexByASHP] ); if !isempty(p.s.electric_utility.outage_durations) diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 4f4f815b6..3720bfe04 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -119,7 +119,7 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs ghp_electric_consumption_kw::Array{Float64,2} # Array of electric load profiles consumed by GHP ghp_installed_cost::Array{Float64,1} # Array of installed cost for GHP options ghp_om_cost_year_one::Array{Float64,1} # Array of O&M cost for GHP options - avoided_capex_by_ghp_present_value::Array{Float64,1} # HVAC upgrade costs avoided + avoided_capex_by_ghp_present_value::Array{Float64,1} # HVAC upgrade costs avoided (GHP) ghx_useful_life_years::Array{Float64,1} # GHX useful life years ghx_residual_value::Array{Float64,1} # Residual value of each GHX options tech_renewable_energy_fraction::Dict{String, <:Real} # (techs) @@ -137,6 +137,7 @@ struct REoptInputs{ScenarioType <: AbstractScenario} <: AbstractInputs heating_loads_served_by_tes::Dict{String, Array{String,1}} # ("HotThermalStorage" or empty) unavailability::Dict{String, Array{Float64,1}} # (techs.elec) absorption_chillers_using_heating_load::Dict{String,Array{String,1}} # ("AbsorptionChiller" or empty) + avoided_capex_by_ashp_present_value::Dict{String, <:Real} # HVAC upgrade costs avoided (ASHP) end @@ -172,7 +173,7 @@ function REoptInputs(s::AbstractScenario) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - heating_cop, cooling_cop, heating_cf, cooling_cf = setup_tech_inputs(s,time_steps) + heating_cop, cooling_cop, heating_cf, cooling_cf, avoided_capex_by_ashp_present_value = setup_tech_inputs(s,time_steps) pbi_pwf, pbi_max_benefit, pbi_max_kw, pbi_benefit_per_kwh = setup_pbi_inputs(s, techs) @@ -326,7 +327,8 @@ function REoptInputs(s::AbstractScenario) heating_loads_kw, heating_loads_served_by_tes, unavailability, - absorption_chillers_using_heating_load + absorption_chillers_using_heating_load, + avoided_capex_by_ashp_present_value ) end @@ -363,6 +365,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) heating_cf = Dict(t => zeros(length(time_steps)) for t in union(techs.heating, techs.chp)) cooling_cf = Dict(t => zeros(length(time_steps)) for t in techs.cooling) cooling_cop = Dict(t => zeros(length(time_steps)) for t in techs.cooling) + avoided_capex_by_ashp_present_value = Dict(t => 0.0 for t in techs.all) # export related inputs techs_by_exportbin = Dict{Symbol, AbstractArray}(k => [] for k in s.electric_tariff.export_bins) @@ -445,7 +448,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) if "ASHP_SpaceHeater" in techs.all setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, - techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) + techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) else heating_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) cooling_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) @@ -455,7 +458,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) if "ASHP_WaterHeater" in techs.all setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, - techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) + techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) else heating_cop["ASHP_WaterHeater"] = ones(length(time_steps)) heating_cf["ASHP_WaterHeater"] = zeros(length(time_steps)) @@ -482,7 +485,7 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, export_bins_by_tech, boiler_efficiency, tech_renewable_energy_fraction, tech_emissions_factors_CO2, tech_emissions_factors_NOx, tech_emissions_factors_SO2, tech_emissions_factors_PM25, techs_operating_reserve_req_fraction, thermal_cop, fuel_cost_per_kwh, - heating_cop, cooling_cop, heating_cf, cooling_cf + heating_cop, cooling_cop, heating_cf, cooling_cf, avoided_capex_by_ashp_present_value end @@ -937,7 +940,7 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, - segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) + segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) max_sizes["ASHP_SpaceHeater"] = s.ashp.max_kw min_sizes["ASHP_SpaceHeater"] = s.ashp.min_kw om_cost_per_kw["ASHP_SpaceHeater"] = s.ashp.om_cost_per_kw @@ -971,11 +974,12 @@ function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, else cap_cost_slope["ASHP_SpaceHeater"] = s.ashp.installed_cost_per_kw end - + + avoided_capex_by_ashp_present_value["ASHP_SpaceHeater"] = s.ashp.avoided_capex_by_ashp_present_value end function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, - segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint) + segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) max_sizes["ASHP_WaterHeater"] = s.ashp_wh.max_kw min_sizes["ASHP_WaterHeater"] = s.ashp_wh.min_kw om_cost_per_kw["ASHP_WaterHeater"] = s.ashp_wh.om_cost_per_kw @@ -1007,7 +1011,7 @@ function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, else cap_cost_slope["ASHP_WaterHeater"] = s.ashp_wh.installed_cost_per_kw end - + avoided_capex_by_ashp_present_value["ASHP_WaterHeater"] = s.ashp_wh.avoided_capex_by_ashp_present_value end From aa7a868f4ea8748fd9b2079840dfb28c41e135c3 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 14 Aug 2024 10:38:06 -0600 Subject: [PATCH 182/266] Handle different versions of JuMP/HiGHS with different indicator error messages --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 5ccff9949..2e5077abb 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -991,7 +991,7 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) set_optimizer_attribute(m, "mip_rel_gap", 0.01) r = run_reopt(m, d) - @test occursin("Unable to use IndicatorToMILPBridge", string(r["Messages"]["errors"])) + @test occursin("are not supported by the solver", string(r["Messages"]["errors"])) || occursin("Unable to use IndicatorToMILPBridge", string(r["Messages"]["errors"])) # #optimal SOH at end of horizon is 80\% to prevent any replacement # @test sum(value.(m[:bmth_BkWh])) ≈ 0 atol=0.1 # # @test r["ElectricStorage"]["maintenance_cost"] ≈ 2972.66 atol=0.01 @@ -1005,7 +1005,7 @@ else # run HiGHS tests m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) set_optimizer_attribute(m, "mip_rel_gap", 0.01) r = run_reopt(m, d) - @test occursin("Unable to use IndicatorToMILPBridge", string(r["Messages"]["errors"])) + @test occursin("are not supported by the solver", string(r["Messages"]["errors"])) || occursin("Unable to use IndicatorToMILPBridge", string(r["Messages"]["errors"])) # @test round(sum(r["ElectricStorage"]["soc_series_fraction"]), digits=2) / 8760 >= 0.7199 end From 2174ecc312cdd9c3c147c92167135576b636e55b Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Wed, 14 Aug 2024 16:48:19 -0600 Subject: [PATCH 183/266] Update CHANGELOG.md --- CHANGELOG.md | 14 +++++++++++--- 1 file changed, 11 insertions(+), 3 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 299463000..546b09f94 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,10 +25,18 @@ Classify the change according to the following categories: ## Develop 08-09-2024 ### Changed -- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage -- Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. . -- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings +- Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage. +- Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. +- Suppress JuMP warning messages from 15-minute and multiple PVs test scenarios to avoid flooding the test logs with those warnings. - Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov. +- Improves DRY coding by replacing multiple instances of the same chunks of code for MACRS deprecation and CHP capital cost into functions that are now in financial.jl. +- Simplifies the CHP sizing test to avoid a ~30 minute solve time, by avoiding the fuel burn y-intercept binaries which come with differences between full-load and part-load efficiency. +### Fixed +- Proforma calcs including "simple" payback and IRR for thermal techs/scenarios. + - The operating costs of fuel and O&M were missing for all thermal techs such as ExistingBoiler, CHP, and others; this adds those sections of code to properly calculate the operating costs. +- Added a test to validate the simple payback calculation with CHP (and ExistingBoiler) and checks the REopt result value against a spreadsheet proforma calculation (see Bill's spreadsheet). +- Added a couple of missing techs for the initial capital cost calculation in financial.jl. +- An issue with setup_boiler_inputs in reopt_inputs.jl. ## v0.47.2 ### Fixed From 6e16b03e1b1f523540b9f3c1311cf45c78d1ba78 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 19 Aug 2024 10:51:25 -0600 Subject: [PATCH 184/266] docstrings for default COP, CF functions --- src/core/ashp.jl | 9 +++++++++ 1 file changed, 9 insertions(+) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index fece79ece..ad6ee22fb 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -387,6 +387,11 @@ end """ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) + +Obtains the default ASHP heating COP and CF profiles. + +ambient_temp_degF::Vector{Float64} -- time series ambient temperature in degrees Fahrenheit +back_up_temp_threshold::Float64 -- temperature threshold at which resistive backup heater turns on """ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) heating_cop = round.(0.0462 .* ambient_temp_degF .+ 1.351, digits=3) @@ -398,6 +403,10 @@ end """ function get_default_ashp_cooling(ambient_temp_degF) + +Obtains the default ASHP cooling COP and CF profiles. + +ambient_temp_degF::Vector{Float64} -- time series ambient temperature in degrees Fahrenheit """ function get_default_ashp_cooling(ambient_temp_degF) cooling_cop = round.(-0.044 .* ambient_temp_degF .+ 6.822, digits=3) From f3e5549129f84ac0e7bea0f9396df39f6d506dcc Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 19 Aug 2024 15:27:31 -0600 Subject: [PATCH 185/266] add function get_ashp_default_min_allowable_size --- src/core/ashp.jl | 74 ++++++++++++++++++++++++++++++++++---------- src/core/scenario.jl | 9 ++---- 2 files changed, 61 insertions(+), 22 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index ad6ee22fb..7f8b7fe63 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -80,6 +80,8 @@ function ASHP_SpaceHeater(; #The following input is taken from the Site object: ambient_temp_degF::Array{Float64,1} #time series of ambient temperature + heating_load::Array{Float64,1} # time series of site space heating load + cooling_load::Union{Array{Float64,1}, Nothing} # time series of site cooling load ) ``` """ @@ -101,7 +103,9 @@ function ASHP_SpaceHeater(; cooling_cop_reference::Array{Float64,1} = Float64[], cooling_cf_reference::Array{Float64,1} = Float64[], cooling_reference_temps::Array{Float64,1} = Float64[], - ambient_temp_degF::Array{Float64,1} = Float64[] + ambient_temp_degF::Array{Float64,1} = Float64[], + heating_load::Array{Float64,1} = Float64[], + cooling_load::Array{Float64,1} = Float64[] ) defaults = get_ashp_defaults("SpaceHeating") @@ -140,11 +144,7 @@ function ASHP_SpaceHeater(; # Convert max sizes, cost factors from mmbtu_per_hour to kw min_kw = min_ton * KWH_THERMAL_PER_TONHOUR max_kw = max_ton * KWH_THERMAL_PER_TONHOUR - if !isnothing(min_allowable_ton) - min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR - else - min_allowable_kw = 0.0 - end + installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR @@ -178,6 +178,13 @@ function ASHP_SpaceHeater(; cooling_cf = Float64[] end + if !isnothing(min_allowable_ton) + min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR + @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") + else + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) + end + ASHP( min_kw, max_kw, @@ -222,6 +229,8 @@ function ASHP_WaterHeater(; heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true back_up_temp_threshold_degF::Real = 10 + ambient_temp_degF::Array{Float64,1} = Float64[] + heating_load::Array{Float64,1} # time series of site space heating load ) ``` """ @@ -239,7 +248,8 @@ function ASHP_WaterHeater(; heating_cf_reference::Array{Float64,1} = Float64[], heating_reference_temps::Array{Float64,1} = Float64[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, - ambient_temp_degF::Array{Float64,1} = Float64[] + ambient_temp_degF::Array{Float64,1} = Float64[], + heating_load::Array{Float64,1} = Float64[] ) defaults = get_ashp_defaults("DomesticHotWater") @@ -266,12 +276,6 @@ function ASHP_WaterHeater(; max_ton = defaults["max_ton"] end - if !isnothing(min_allowable_ton) - min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR - else - min_allowable_kw = 0.0 - end - #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] can_serve_space_heating = defaults["can_serve_space_heating"] @@ -299,6 +303,13 @@ function ASHP_WaterHeater(; heating_cf[heating_cop .== 1] .= 1 + if !isnothing(min_allowable_ton) + min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR + @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") + else + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Float64[], Float64[]) + end + ASHP( min_kw, max_kw, @@ -390,7 +401,7 @@ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF Obtains the default ASHP heating COP and CF profiles. -ambient_temp_degF::Vector{Float64} -- time series ambient temperature in degrees Fahrenheit +ambient_temp_degF::Array{Float64,1} -- time series ambient temperature in degrees Fahrenheit back_up_temp_threshold::Float64 -- temperature threshold at which resistive backup heater turns on """ function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) @@ -406,10 +417,41 @@ function get_default_ashp_cooling(ambient_temp_degF) Obtains the default ASHP cooling COP and CF profiles. -ambient_temp_degF::Vector{Float64} -- time series ambient temperature in degrees Fahrenheit +ambient_temp_degF::Array{Float64,1} -- time series ambient temperature in degrees Fahrenheit """ function get_default_ashp_cooling(ambient_temp_degF) cooling_cop = round.(-0.044 .* ambient_temp_degF .+ 6.822, digits=3) cooling_cf = round.(-0.0056 .* ambient_temp_degF .+ 1.4778, digits=3) return cooling_cop, cooling_cf -end \ No newline at end of file +end + +""" +function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, # time series of heating load + heating_cf::Array{Float64,1}, # time series of capacity factor for heating + cooling_load::Array{Float64,1} = Float64[], # # time series of capacity factor for heating + cooling_cf::Array{Float64,1} = Float64[], # time series of capacity factor for heating + peak_load_thermal_factor::Float64 = 0.5 # peak load multiplier for minimum allowable size + ) + +Obtains the default minimum allowable size for ASHP system. This is calculated as half of the peak site thermal load(s) addressed by the system, including the capacity factor +""" +function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, + heating_cf::Array{Float64,1}, + cooling_load::Array{Float64,1} = Float64[], + cooling_cf::Array{Float64,1} = Float64[], + peak_load_thermal_factor::Float64 = 0.5 + ) + + println(heating_load) + println(heating_cf) + println(cooling_load) + println(cooling_cf) + if isempty(cooling_cf) + peak_load = maximum(heating_load ./ heating_cf) + else + peak_load = maximum( (heating_load ./ heating_cf) .+ (cooling_load ./ cooling_cf) ) + end + + return peak_load_thermal_factor * peak_load +end + diff --git a/src/core/scenario.jl b/src/core/scenario.jl index a4604f00d..6c9161203 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -656,10 +656,6 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # ASHP ashp = nothing - heating_cop = [] - cooling_cop = [] - heating_cf = [] - cooling_cf = [] if haskey(d, "ASHP_SpaceHeater") if !haskey(d["ASHP_SpaceHeater"], "max_ton") max_ton = get_ashp_defaults("SpaceHeating")["max_ton"] @@ -691,6 +687,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 d["ASHP_SpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHP_SpaceHeater"]["heating_load"] = heating_load + d["ASHP_SpaceHeater"]["cooling_load"] = cooling_load ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) end @@ -698,8 +696,6 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # ASHP Water Heater: ashp_wh = nothing - heating_cop = [] - heating_cf = [] if haskey(d, "ASHP_WaterHeater") if !haskey(d["ASHP_WaterHeater"], "max_ton") @@ -733,6 +729,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 d["ASHP_WaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHP_SpaceHeater"]["heating_load"] = heating_load ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) end From cc5d7aca1d0d86615c2dcf25e81ea216a6144655 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 19 Aug 2024 15:27:45 -0600 Subject: [PATCH 186/266] add test for min allowable size --- test/runtests.jl | 23 +++++++++++++++-------- 1 file changed, 15 insertions(+), 8 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 1d76c842f..554688a01 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -63,8 +63,8 @@ else # run HiGHS tests dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) @test dataset ≈ "tmy3" end - - @testset "ASHP COP and CF Profiles" begin + + @testset "ASHP min allowable size and COP, CF Profiles" begin #Heating profiles heating_reference_temps = [10,20,30] heating_cop_reference = [1,3,4] @@ -73,13 +73,13 @@ else # run HiGHS tests test_temps = [5,15,25,35] test_cops = [1.0,2.0,3.5,4.0] test_cfs = [1.0,1.25,1.4,1.5] - cop, cf = REopt.get_ashp_performance(heating_cop_reference, + heating_cop, heating_cf = REopt.get_ashp_performance(heating_cop_reference, heating_cf_performance, heating_reference_temps, test_temps, back_up_temp_threshold_degF) - @test all(cop .== test_cops) - @test all(cf .== test_cfs) + @test all(heating_cop .== test_cops) + @test all(heating_cf .== test_cfs) #Cooling profiles cooling_reference_temps = [30,20,10] cooling_cop_reference = [1,3,4] @@ -88,13 +88,20 @@ else # run HiGHS tests test_temps = [35,25,15,5] test_cops = [1.0,2.0,3.5,4.0] test_cfs = [1.2,1.25,1.4,1.5] - cop, cf = REopt.get_ashp_performance(cooling_cop_reference, + cooling_cop, cooling_cf = REopt.get_ashp_performance(cooling_cop_reference, cooling_cf_performance, cooling_reference_temps, test_temps, back_up_temp_threshold_degF) - @test all(cop .== test_cops) - @test all(cf .== test_cfs) + @test all(cooling_cop .== test_cops) + @test all(cooling_cf .== test_cfs) + # min allowable size + heating_load = [10.0,10.0,10.0,10.0] + cooling_load = [10.0,10.0,10.0,10.0] + space_heating_min_allowable_size = REopt.get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) + wh_min_allowable_size = REopt.get_ashp_default_min_allowable_size(heating_load, heating_cf) + @test space_heating_min_allowable_size ≈ 9.166666666666666 atol=1e-8 + @test wh_min_allowable_size ≈ 5.0 atol=1e-8 end end From d510dc55aadaff1da63fe8b0142bdaf7d6e235e5 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 20 Aug 2024 09:56:10 -0600 Subject: [PATCH 187/266] update scenario loading in ASHP test suite --- test/runtests.jl | 34 ++++++++++++++++++++++------------ 1 file changed, 22 insertions(+), 12 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 554688a01..b423be36a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2468,9 +2468,10 @@ else # run HiGHS tests #Case 1: Boiler and existing chiller produce the required heat and cooling - ASHP is not purchased d = JSON.parsefile("./scenarios/ashp.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 1.0 * 8760 - + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + results = run_reopt(m, p) @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 0.0 atol=0.1 @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @@ -2483,9 +2484,10 @@ else # run HiGHS tests d["ASHP_SpaceHeater"]["installed_cost_per_ton"] = 300 d["ASHP_SpaceHeater"]["min_allowable_ton"] = 80.0 - p = REoptInputs(d) + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + results = run_reopt(m, p) annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption @@ -2500,7 +2502,8 @@ else # run HiGHS tests d["ExistingChiller"] = Dict("cop" => 0.5) d["ASHP_SpaceHeater"]["can_serve_cooling"] = true - p = REoptInputs(d) + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) @@ -2514,8 +2517,10 @@ else # run HiGHS tests d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) d["ExistingBoiler"]["retire_in_optimal"] = true d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0 + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + results = run_reopt(m, p) @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 @@ -2526,9 +2531,10 @@ else # run HiGHS tests d = JSON.parsefile("./scenarios/ashp_wh.json") d["SpaceHeatingLoad"]["annual_mmbtu"] = 0.5 * 8760 d["DomesticHotWaterLoad"]["annual_mmbtu"] = 0.5 * 8760 - + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) - results = run_reopt(m, d) + results = run_reopt(m, p) @test results["ASHP_WaterHeater"]["size_ton"] ≈ 0.0 atol=0.1 @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @@ -2540,7 +2546,8 @@ else # run HiGHS tests d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 d["ASHP_WaterHeater"]["installed_cost_per_ton"] = 300 - p = REoptInputs(d) + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour @@ -2564,7 +2571,8 @@ else # run HiGHS tests d["ASHP_SpaceHeater"]["force_into_system"] = true d["ASHP_WaterHeater"] = Dict{String,Any}("force_into_system" => true, "max_ton" => 100000) - p = REoptInputs(d) + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) @@ -2573,7 +2581,8 @@ else # run HiGHS tests @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 d["ASHP_SpaceHeater"]["force_into_system"] = false - p = REoptInputs(d) + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) @@ -2583,7 +2592,8 @@ else # run HiGHS tests d["ASHP_SpaceHeater"]["force_into_system"] = true d["ASHP_WaterHeater"]["force_into_system"] = false - p = REoptInputs(d) + s = Scenario(d) + p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) From 77755e3640c9489f345831069cb434393c38afd1 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 20 Aug 2024 09:56:47 -0600 Subject: [PATCH 188/266] fix types and inputs to ASHP systems for min allowable size --- src/core/ashp.jl | 12 ++++++------ src/core/scenario.jl | 6 +++--- src/results/results.jl | 1 + 3 files changed, 10 insertions(+), 9 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 7f8b7fe63..929c52d94 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -104,8 +104,8 @@ function ASHP_SpaceHeater(; cooling_cf_reference::Array{Float64,1} = Float64[], cooling_reference_temps::Array{Float64,1} = Float64[], ambient_temp_degF::Array{Float64,1} = Float64[], - heating_load::Array{Float64,1} = Float64[], - cooling_load::Array{Float64,1} = Float64[] + heating_load::Array{Real,1} = Real[], + cooling_load::Array{Real,1} = Real[] ) defaults = get_ashp_defaults("SpaceHeating") @@ -249,7 +249,7 @@ function ASHP_WaterHeater(; heating_reference_temps::Array{Float64,1} = Float64[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, ambient_temp_degF::Array{Float64,1} = Float64[], - heating_load::Array{Float64,1} = Float64[] + heating_load::Array{Real,1} = Real[] ) defaults = get_ashp_defaults("DomesticHotWater") @@ -307,7 +307,7 @@ function ASHP_WaterHeater(; min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") else - min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Float64[], Float64[]) + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf) end ASHP( @@ -435,9 +435,9 @@ function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, # ti Obtains the default minimum allowable size for ASHP system. This is calculated as half of the peak site thermal load(s) addressed by the system, including the capacity factor """ -function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, +function get_ashp_default_min_allowable_size(heating_load::Array{Real,1}, heating_cf::Array{Float64,1}, - cooling_load::Array{Float64,1} = Float64[], + cooling_load::Array{Real,1} = Real[], cooling_cf::Array{Float64,1} = Float64[], peak_load_thermal_factor::Float64 = 0.5 ) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 6c9161203..5a63f53fb 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -687,8 +687,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 d["ASHP_SpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - d["ASHP_SpaceHeater"]["heating_load"] = heating_load - d["ASHP_SpaceHeater"]["cooling_load"] = cooling_load + d["ASHP_SpaceHeater"]["heating_load"] = space_heating_load.loads_kw + d["ASHP_SpaceHeater"]["cooling_load"] = cooling_load.loads_kw_thermal ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) end @@ -729,7 +729,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 d["ASHP_WaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - d["ASHP_SpaceHeater"]["heating_load"] = heating_load + d["ASHP_WaterHeater"]["heating_load"] = dhw_load.loads_kw ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) end diff --git a/src/results/results.jl b/src/results/results.jl index 3c6914928..61ceab94f 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -107,6 +107,7 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") if "ASHP_SpaceHeater" in p.techs.ashp add_ashp_results(m, p, d; _n) end + if "ASHP_WaterHeater" in p.techs.ashp_wh add_ashp_wh_results(m, p, d; _n) end From b28ca133ac38f43bb02e31f4e4d777ebd34d615b Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 20 Aug 2024 12:43:39 -0600 Subject: [PATCH 189/266] update ashp inputs tests --- test/runtests.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index b423be36a..ed59b61e5 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -96,8 +96,8 @@ else # run HiGHS tests @test all(cooling_cop .== test_cops) @test all(cooling_cf .== test_cfs) # min allowable size - heating_load = [10.0,10.0,10.0,10.0] - cooling_load = [10.0,10.0,10.0,10.0] + heating_load = Array{Real}([10.0,10.0,10.0,10.0]) + cooling_load = Array{Real}([10.0,10.0,10.0,10.0]) space_heating_min_allowable_size = REopt.get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) wh_min_allowable_size = REopt.get_ashp_default_min_allowable_size(heating_load, heating_cf) @test space_heating_min_allowable_size ≈ 9.166666666666666 atol=1e-8 From b4e5f8aafd4d1185cc3aae171fc5c7bc28ee4cfa Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 20 Aug 2024 12:58:27 -0600 Subject: [PATCH 190/266] fix ashp docstrings --- src/core/ashp.jl | 24 ++++++++++++------------ 1 file changed, 12 insertions(+), 12 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 929c52d94..18710521a 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -73,15 +73,15 @@ function ASHP_SpaceHeater(; heating_reference_temps ::Array{Float64,1}, # ASHP's reference temperatures for heating COP and CF back_up_temp_threshold_degF::Real = 10, # Degree in F that system switches from ASHP to resistive heater - #The following inputs are used to create the attributes heating_cop and heating cf: + #The following inputs are used to create the attributes cooling_cop and cooling cf: cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves heating_reference_temps ::Array{Float64,1}, # ASHP's reference temperatures for cooling COP and CF - #The following input is taken from the Site object: + #The following inputs are taken from the Site object: ambient_temp_degF::Array{Float64,1} #time series of ambient temperature - heating_load::Array{Float64,1} # time series of site space heating load - cooling_load::Union{Array{Float64,1}, Nothing} # time series of site cooling load + heating_load::Array{Real,1} # time series of site space heating load + cooling_load::Union{Array{Real,1}, Nothing} # time series of site cooling load ) ``` """ @@ -226,11 +226,15 @@ function ASHP_WaterHeater(; macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production + + #The following inputs are used to create the attributes heating_cop and heating cf: heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true - back_up_temp_threshold_degF::Real = 10 - ambient_temp_degF::Array{Float64,1} = Float64[] - heating_load::Array{Float64,1} # time series of site space heating load + back_up_temp_threshold_degF::Real = 10 # temperature threshold at which backup resistive heater is used + + #The following inputs are taken from the Site object: + ambient_temp_degF::Array{Float64,1} = Float64[] # time series of ambient temperature + heating_load::Array{Float64,1} # time series of site domestic hot water load ) ``` """ @@ -441,11 +445,7 @@ function get_ashp_default_min_allowable_size(heating_load::Array{Real,1}, cooling_cf::Array{Float64,1} = Float64[], peak_load_thermal_factor::Float64 = 0.5 ) - - println(heating_load) - println(heating_cf) - println(cooling_load) - println(cooling_cf) + if isempty(cooling_cf) peak_load = maximum(heating_load ./ heating_cf) else From 20c9f1e0634fd82c156b0c657146d6f17d659092 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 20 Aug 2024 16:31:23 -0600 Subject: [PATCH 191/266] generalize type definitions to Real in ASHP structs and functions --- src/core/ashp.jl | 74 +++++++++++++++++++++++++----------------------- 1 file changed, 38 insertions(+), 36 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 18710521a..a4fc90581 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -16,10 +16,10 @@ ASHP_SpaceHeater has the following attributes: om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS - heating_cop::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) - heating_cf::Array{Float64,1}, # ASHP's heating capacity factor curves - cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves + heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + cooling_cop::Array{<:Real,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) + heating_cf::Array{<:Real,1}, # ASHP's heating capacity factor curves + cooling_cf::Array{<:Real,1}, # ASHP's cooling capacity factor curves can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true back_up_temp_threshold_degF::Real = 10 # Degree in F that system switches from ASHP to resistive heater @@ -34,10 +34,10 @@ struct ASHP <: AbstractThermalTech macrs_option_years::Int macrs_bonus_fraction::Real can_supply_steam_turbine::Bool - heating_cop::Array{Float64,1} - cooling_cop::Array{Float64,1} - heating_cf::Array{Float64,1} - cooling_cf::Array{Float64,1} + heating_cop::Array{<:Real,1} + cooling_cop::Array{<:Real,1} + heating_cf::Array{<:Real,1} + cooling_cf::Array{<:Real,1} can_serve_dhw::Bool can_serve_space_heating::Bool can_serve_process_heat::Bool @@ -68,18 +68,18 @@ function ASHP_SpaceHeater(; avoided_capex_by_ashp_present_value::Real = 0.0 #The following inputs are used to create the attributes heating_cop and heating cf: - heating_cop_reference::Array{Float64,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - heating_cf_reference::Array{Float64,1}, # ASHP's heating capacity factor curves - heating_reference_temps ::Array{Float64,1}, # ASHP's reference temperatures for heating COP and CF + heating_cop_reference::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + heating_cf_reference::Array{<:Real,1}, # ASHP's heating capacity factor curves + heating_reference_temps ::Array{<:Real,1}, # ASHP's reference temperatures for heating COP and CF back_up_temp_threshold_degF::Real = 10, # Degree in F that system switches from ASHP to resistive heater #The following inputs are used to create the attributes cooling_cop and cooling cf: - cooling_cop::Array{Float64,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) - cooling_cf::Array{Float64,1}, # ASHP's cooling capacity factor curves - heating_reference_temps ::Array{Float64,1}, # ASHP's reference temperatures for cooling COP and CF + cooling_cop::Array{<:Real,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) + cooling_cf::Array{<:Real,1}, # ASHP's cooling capacity factor curves + heating_reference_temps ::Array{<:Real,1}, # ASHP's reference temperatures for cooling COP and CF #The following inputs are taken from the Site object: - ambient_temp_degF::Array{Float64,1} #time series of ambient temperature + ambient_temp_degF::Array{<:Real,1} #time series of ambient temperature heating_load::Array{Real,1} # time series of site space heating load cooling_load::Union{Array{Real,1}, Nothing} # time series of site cooling load ) @@ -96,16 +96,18 @@ function ASHP_SpaceHeater(; avoided_capex_by_ashp_present_value::Real = 0.0, can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::Array{Float64,1} = Float64[], - heating_cf_reference::Array{Float64,1} = Float64[], - heating_reference_temps::Array{Float64,1} = Float64[], + heating_cop_reference::Array{<:Real,1} = Float64[], + heating_cf_reference::Array{<:Real,1} = Float64[], + heating_reference_temps::Array{<:Real,1} = Float64[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, - cooling_cop_reference::Array{Float64,1} = Float64[], - cooling_cf_reference::Array{Float64,1} = Float64[], - cooling_reference_temps::Array{Float64,1} = Float64[], + cooling_cop_reference::Array{<:Real,1} = Float64[], + cooling_cf_reference::Array{<:Real,1} = Float64[], + cooling_reference_temps::Array{<:Real,1} = Float64[], ambient_temp_degF::Array{Float64,1} = Float64[], - heating_load::Array{Real,1} = Real[], - cooling_load::Array{Real,1} = Real[] + heating_load::Array{<:Real,1} = Real[], + cooling_load::Array{<:Real,1} = Real[], + includes_heat_recovery::Bool = false, + heat_recovery_cop::Union{Real, Nothing} = nothing ) defaults = get_ashp_defaults("SpaceHeating") @@ -233,8 +235,8 @@ function ASHP_WaterHeater(; back_up_temp_threshold_degF::Real = 10 # temperature threshold at which backup resistive heater is used #The following inputs are taken from the Site object: - ambient_temp_degF::Array{Float64,1} = Float64[] # time series of ambient temperature - heating_load::Array{Float64,1} # time series of site domestic hot water load + ambient_temp_degF::Array{<:Real,1} = Float64[] # time series of ambient temperature + heating_load::Array{<:Real,1} # time series of site domestic hot water load ) ``` """ @@ -248,12 +250,12 @@ function ASHP_WaterHeater(; macrs_bonus_fraction::Real = 0.0, avoided_capex_by_ashp_present_value::Real = 0.0, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::Array{Float64,1} = Float64[], - heating_cf_reference::Array{Float64,1} = Float64[], - heating_reference_temps::Array{Float64,1} = Float64[], + heating_cop_reference::Array{<:Real,1} = Real[], + heating_cf_reference::Array{<:Real,1} = Real[], + heating_reference_temps::Array{<:Real,1} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, - ambient_temp_degF::Array{Float64,1} = Float64[], - heating_load::Array{Real,1} = Real[] + ambient_temp_degF::Array{<:Real,1} = Real[], + heating_load::Array{<:Real,1} = Real[] ) defaults = get_ashp_defaults("DomesticHotWater") @@ -439,13 +441,13 @@ function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, # ti Obtains the default minimum allowable size for ASHP system. This is calculated as half of the peak site thermal load(s) addressed by the system, including the capacity factor """ -function get_ashp_default_min_allowable_size(heating_load::Array{Real,1}, - heating_cf::Array{Float64,1}, - cooling_load::Array{Real,1} = Real[], - cooling_cf::Array{Float64,1} = Float64[], - peak_load_thermal_factor::Float64 = 0.5 +function get_ashp_default_min_allowable_size(heating_load::Array{<:Real,1}, + heating_cf::Array{<:Real,1}, + cooling_load::Array{<:Real,1} = Real[], + cooling_cf::Array{<:Real,1} = Real[], + peak_load_thermal_factor::Real = 0.5 ) - + if isempty(cooling_cf) peak_load = maximum(heating_load ./ heating_cf) else From d623ffcf810ec5a09f4c373c001a0895e31e7b65 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 22 Aug 2024 07:51:12 -0600 Subject: [PATCH 192/266] add cooling- and heating-specific electric consumption in ASHP results --- src/results/ashp.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index b87a02817..fa13bc65b 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -89,6 +89,8 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPColdElectricConsumptionSeries, 0.0) end r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) + r["electric_consumption_for_cooling_series_kw"] = round.(value.(ASHPColdElectricConsumptionSeries), digits=3) + r["electric_consumption_for_heating_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) d["ASHP_SpaceHeater"] = r From a01cf84eb21d1d49dd99f3a2fc0f88d3791aca73 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 22 Aug 2024 08:25:38 -0600 Subject: [PATCH 193/266] add time series COPs to ASHP results --- src/results/ashp.jl | 3 +++ 1 file changed, 3 insertions(+) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index fa13bc65b..c7ee23846 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -82,16 +82,19 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * m[:dvCoolingProduction]["ASHP_SpaceHeater",ts] / p.cooling_cop["ASHP_SpaceHeater"][ts] ) + r["cooling_cop"] = p.cooling_cop["ASHP_SpaceHeater"] else r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) r["annual_thermal_production_tonhour"] = 0.0 @expression(m, ASHPColdElectricConsumptionSeries, 0.0) + r["cooling_cop"] = zeros(length(p.time_steps)) end r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) r["electric_consumption_for_cooling_series_kw"] = round.(value.(ASHPColdElectricConsumptionSeries), digits=3) r["electric_consumption_for_heating_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) + r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] d["ASHP_SpaceHeater"] = r nothing From a98a645cdc703d3faa48f9cebcd5b6da34dd6fe5 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 22 Aug 2024 08:45:11 -0600 Subject: [PATCH 194/266] add CFs to ASHP tech results, COP to water heater --- src/results/ashp.jl | 7 ++++++- 1 file changed, 6 insertions(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index c7ee23846..736ba2f2f 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -83,18 +83,21 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") p.hours_per_time_step * m[:dvCoolingProduction]["ASHP_SpaceHeater",ts] / p.cooling_cop["ASHP_SpaceHeater"][ts] ) r["cooling_cop"] = p.cooling_cop["ASHP_SpaceHeater"] + r["cooling_cf"] = p.cooling_cf["ASHP_SpaceHeater"] else r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) r["annual_thermal_production_tonhour"] = 0.0 @expression(m, ASHPColdElectricConsumptionSeries, 0.0) r["cooling_cop"] = zeros(length(p.time_steps)) + r["cooling_cf"] = zeros(length(p.time_steps)) end r["electric_consumption_series_kw"] = round.(value.(ASHPElectricConsumptionSeries .+ ASHPColdElectricConsumptionSeries), digits=3) r["electric_consumption_for_cooling_series_kw"] = round.(value.(ASHPColdElectricConsumptionSeries), digits=3) r["electric_consumption_for_heating_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] + r["heating_cf"] = p.heating_cf["ASHP_SpaceHeater"] d["ASHP_SpaceHeater"] = r nothing @@ -160,7 +163,9 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= r["electric_consumption_series_kw"] = round.(value.(ASHPWHElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) - + r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] + r["heating_cf"] = p.heating_cf["ASHP_SpaceHeater"] + d["ASHP_WaterHeater"] = r nothing end \ No newline at end of file From 723c55f742ac9ecc61634f730ac15595562f9db0 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 22 Aug 2024 08:52:44 -0600 Subject: [PATCH 195/266] add annual kWh consumed for heating/cooling for ASHP --- src/results/ashp.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 736ba2f2f..0114bf51a 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -96,6 +96,8 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["electric_consumption_for_cooling_series_kw"] = round.(value.(ASHPColdElectricConsumptionSeries), digits=3) r["electric_consumption_for_heating_series_kw"] = round.(value.(ASHPElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) + r["annual_electric_consumption_for_cooling_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_for_cooling_series_kw"]) + r["annual_electric_consumption_for_heating_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_for_heating_series_kw"]) r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] r["heating_cf"] = p.heating_cf["ASHP_SpaceHeater"] @@ -165,7 +167,7 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] r["heating_cf"] = p.heating_cf["ASHP_SpaceHeater"] - + d["ASHP_WaterHeater"] = r nothing end \ No newline at end of file From 6f5a0323495bdaa1eb93075783e840760be74a6c Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Thu, 22 Aug 2024 22:23:18 -0600 Subject: [PATCH 196/266] Fix Generator fuel cost escalation in proforma calcs Before it was using inflation (om_escalation), but should be using it's specific fuel escalation rate --- src/results/proforma.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 0359958c5..e5deff037 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -107,16 +107,22 @@ function proforma_results(p::REoptInputs, d::Dict) year_one_fuel_cost_bau = d["Generator"]["year_one_fuel_cost_before_tax_bau"] end if !third_party - annual_om = -1 * (fixed_and_var_om + d["Generator"]["year_one_fuel_cost_before_tax"]) + annual_fuel = -1 * d["Generator"]["year_one_fuel_cost_before_tax"] + annual_om = -1 * fixed_and_var_om - annual_om_bau = -1 * (fixed_and_var_om_bau + year_one_fuel_cost_bau) + annual_fuel_bau = -1 * year_one_fuel_cost_bau + annual_om_bau = -1 * fixed_and_var_om_bau else + annual_fuel = 0.0 annual_om = -1 * fixed_and_var_om + annual_fuel_bau = 0.0 annual_om_bau = -1 * fixed_and_var_om_bau end + m.om_series += escalate_fuel(annual_fuel, p.s.financial.generator_fuel_cost_escalation_rate_fraction) m.om_series += escalate_om(annual_om) + m.om_series_bau += escalate_fuel(annual_fuel_bau, p.s.financial.generator_fuel_cost_escalation_rate_fraction) m.om_series_bau += escalate_om(annual_om_bau) end From 7f50590558e0bde4b8311a25219ceb08271bb788 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 23 Aug 2024 12:45:50 -0600 Subject: [PATCH 197/266] move comment --- src/results/financial.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/results/financial.jl b/src/results/financial.jl index e19e71c5c..eee14740b 100644 --- a/src/results/financial.jl +++ b/src/results/financial.jl @@ -273,10 +273,10 @@ function calculate_lcoe(p::REoptInputs, tech_results::Dict, tech::AbstractTech) capital_costs = new_kw * tech.installed_cost_per_kw # pre-incentive capital costs - annual_om = new_kw * tech.om_cost_per_kw # NPV of O&M charges escalated over financial life + annual_om = new_kw * tech.om_cost_per_kw om_series = [annual_om * (1+p.s.financial.om_cost_escalation_rate_fraction)^yr for yr in 1:years] - npv_om = sum([om * (1.0/(1.0+discount_rate_fraction))^yr for (yr, om) in enumerate(om_series)]) + npv_om = sum([om * (1.0/(1.0+discount_rate_fraction))^yr for (yr, om) in enumerate(om_series)]) # NPV of O&M charges escalated over financial life #Incentives as calculated in the spreadsheet, note utility incentives are applied before state incentives utility_ibi = min(capital_costs * tech.utility_ibi_fraction, tech.utility_ibi_max) @@ -350,7 +350,7 @@ function get_depreciation_schedule(p::REoptInputs, tech::Union{AbstractTech,Abst try federal_itc_fraction = tech.federal_itc_fraction catch - @warn "did not find tech.federal_itc_fraction so using 0.0" + @warn "Did not find $(tech).federal_itc_fraction so using 0.0 in calculation of depreciation_schedule." end macrs_bonus_basis = federal_itc_basis - federal_itc_basis * federal_itc_fraction * tech.macrs_itc_reduction From 5b003341450a4155ca99a75ee1d8a62919546faf Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 19:30:01 -0600 Subject: [PATCH 198/266] add inputs min_allowable_peak_load_fraction, --- src/core/ashp.jl | 13 ++++++++++++- 1 file changed, 12 insertions(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index a4fc90581..b3cf3357b 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -12,6 +12,7 @@ ASHP_SpaceHeater has the following attributes: min_kw::Real = 0.0, # Minimum thermal power size max_kw::Real = BIG_NUMBER, # Maximum thermal power size min_allowable_kw::Real = 0.0 # Minimum nonzero thermal power size if included + sizing_factor::Real = 1.1 # Size multiplier of system, relative that of the max load given by dispatch profile installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable @@ -29,6 +30,7 @@ struct ASHP <: AbstractThermalTech min_kw::Real max_kw::Real min_allowable_kw::Real + sizing_factor::Real installed_cost_per_kw::Real om_cost_per_kw::Real macrs_option_years::Int @@ -59,7 +61,9 @@ to meet the heating load. function ASHP_SpaceHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size - min_allowable_ton::Real = 0.0 # Minimum nonzero thermal power size if included + min_allowable_ton::Union{Real, Nothing} = nothing, # Minimum nonzero thermal power size if included + min_allowable_peak_load_fraction::Union{Real, Nothing} = nothing, # minimum allowable fraction of peak heating + cooling load + sizing_factor::::Union{Real, Nothing} = nothing, # Size multiplier of system, relative that of the max load given by dispatch profile om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS @@ -89,6 +93,8 @@ function ASHP_SpaceHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, min_allowable_ton::Union{Real, Nothing} = nothing, + min_allowable_peak_load_fraction::Union{Real, Nothing} = nothing, + sizing_factor::Union{Real, Nothing} = nothing, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, @@ -135,6 +141,7 @@ function ASHP_SpaceHeater(; if isnothing(max_ton) max_ton = defaults["max_ton"] end + #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -187,10 +194,14 @@ function ASHP_SpaceHeater(; min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) end + installed_cost_per_kw *= sizing_factor + om_cost_per_kw *= sizing_factor + ASHP( min_kw, max_kw, min_allowable_kw, + sizing_factor, installed_cost_per_kw, om_cost_per_kw, macrs_option_years, From 6b072bda93df4ee57adb2088b8ffdfb49873c422 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 19:38:34 -0600 Subject: [PATCH 199/266] update error handling, use min_allowable_peak_load_fraction if provided --- src/core/ashp.jl | 16 +++++++++++++--- 1 file changed, 13 insertions(+), 3 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index b3cf3357b..b6d249f77 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -187,10 +187,15 @@ function ASHP_SpaceHeater(; cooling_cf = Float64[] end - if !isnothing(min_allowable_ton) + if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_load_fraction) + throw(@error("at most one of min_allowable_ton and min_allowable_peak_load_fraction may be input.")) + elseif !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") else + if isnothing(min_allowable_peak_load_fraction) + min_allowable_peak_load_fraction = 0.5 + end min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) end @@ -320,11 +325,16 @@ function ASHP_WaterHeater(; heating_cf[heating_cop .== 1] .= 1 - if !isnothing(min_allowable_ton) + if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_load_fraction) + throw(@error("at most one of min_allowable_ton and min_allowable_peak_load_fraction may be input.")) + elseif !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") else - min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf) + if isnothing(min_allowable_peak_load_fraction) + min_allowable_peak_load_fraction = 0.5 + end + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) end ASHP( From 6dea77c55ff9783a8b964c4a704a9c3ea173ac4c Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 19:39:26 -0600 Subject: [PATCH 200/266] use ashp sizing factor in results --- src/results/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 0114bf51a..9d8e732b9 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -124,7 +124,7 @@ end function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_WaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + r["size_ton"] = p.s.ashp.sizing_factor * round(value(m[Symbol("dvSize"*_n)]["ASHP_WaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPWHElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp_wh) From eae43c163903f2789432219f25a147b48fad5c34 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 19:41:32 -0600 Subject: [PATCH 201/266] add min_allowable_peak_load_fraction and sizing_factor to ASHP_WaterHeater --- src/core/ashp.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index b6d249f77..b6463518b 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -260,6 +260,8 @@ function ASHP_WaterHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, min_allowable_ton::Union{Real, Nothing} = nothing, + min_allowable_peak_load_fraction::Union{Real, Nothing} = nothing, + sizing_factor::Union{Real, Nothing} = nothing, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, macrs_option_years::Int = 0, @@ -293,7 +295,6 @@ function ASHP_WaterHeater(; if isnothing(back_up_temp_threshold_degF) back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] end - if isnothing(max_ton) max_ton = defaults["max_ton"] end @@ -341,6 +342,7 @@ function ASHP_WaterHeater(; min_kw, max_kw, min_allowable_kw, + sizing_factor, installed_cost_per_kw, om_cost_per_kw, macrs_option_years, From b2b6f33bc37bb2f6a9caa6523c8e4ab123bd46be Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 19:42:19 -0600 Subject: [PATCH 202/266] add default sizing factor --- data/ashp/ashp_defaults.json | 6 ++++-- src/core/ashp.jl | 6 ++++++ 2 files changed, 10 insertions(+), 2 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 1e7d74e9a..afc87e0c6 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -12,7 +12,8 @@ "can_serve_space_heating": true, "can_serve_cooling": true, "force_into_system": false, - "back_up_temp_threshold_degF": 10.0 + "back_up_temp_threshold_degF": 10.0, + "sizing_factor": 1.1 }, "DomesticHotWater": { @@ -27,6 +28,7 @@ "can_serve_space_heating": false, "can_serve_cooling": false, "force_into_system": false, - "back_up_temp_threshold_degF": 10.0 + "back_up_temp_threshold_degF": 10.0, + "sizing_factor": 1.1 } } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index b6463518b..a36da9f3a 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -141,6 +141,9 @@ function ASHP_SpaceHeater(; if isnothing(max_ton) max_ton = defaults["max_ton"] end + if isnothing(sizing_factor) + sizing_factor = defaults["sizing_factor"] + end #pre-set defaults that aren't mutable due to technology specifications @@ -298,6 +301,9 @@ function ASHP_WaterHeater(; if isnothing(max_ton) max_ton = defaults["max_ton"] end + if isnothing(sizing_factor) + sizing_factor = defaults["sizing_factor"] + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] From fe46361487018d43046a0093328fc71b95674141 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 20:22:43 -0600 Subject: [PATCH 203/266] use default references instead of lone default curve --- data/ashp/ashp_defaults.json | 13 +++++- src/core/ashp.jl | 78 ++++++++++++++++++++++-------------- 2 files changed, 58 insertions(+), 33 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index afc87e0c6..fc8176d58 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -13,7 +13,13 @@ "can_serve_cooling": true, "force_into_system": false, "back_up_temp_threshold_degF": 10.0, - "sizing_factor": 1.1 + "sizing_factor": 1.1, + "heating_cop_reference": [0.427, 2.737, 5.047], + "heating_cf_reference": [0.2236, 0.8036, 1.3836], + "heating_reference_temps": [-20, 30, 80], + "cooling_cop_reference": [3.39, 2.466, 1.542], + "cooling_cf_reference": [1.041, 0.9234, 0.8058], + "cooling_reference_temps": [78, 99, 120] }, "DomesticHotWater": { @@ -29,6 +35,9 @@ "can_serve_cooling": false, "force_into_system": false, "back_up_temp_threshold_degF": 10.0, - "sizing_factor": 1.1 + "sizing_factor": 1.1, + "heating_cop_reference": [0.427, 2.737, 5.047], + "heating_cf_reference": [0.2236, 0.8036, 1.3836], + "heating_reference_temps": [-20, 30, 80] } } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index a36da9f3a..7b19270b8 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -144,7 +144,25 @@ function ASHP_SpaceHeater(; if isnothing(sizing_factor) sizing_factor = defaults["sizing_factor"] end - + + if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps) + throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps must all be the same length.")) + else + if length(heating_cop_reference) == 0 + heating_cop_reference = defaults["heating_cop_reference"] + heating_cf_reference = defaults["heating_cf_reference"] + heating_reference_temps = defaults["heating_reference_temps"] + end + end + if length(cooling_cop_reference) != length(cooling_cf_reference) || length(cooling_cf_reference) != length(cooling_reference_temps) + throw(@error("cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps must all be the same length.")) + else + if length(cooling_cop_reference) == 0 && can_serve_cooling + cooling_cop_reference = defaults["cooling_cop_reference"] + cooling_cf_reference = defaults["cooling_cf_reference"] + cooling_reference_temps = defaults["cooling_reference_temps"] + end + end #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] @@ -161,30 +179,22 @@ function ASHP_SpaceHeater(; installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR - if !isempty(heating_reference_temps) - heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, - heating_cf_reference, - heating_reference_temps, - ambient_temp_degF, - back_up_temp_threshold_degF - ) - else - heating_cop, heating_cf = get_default_ashp_heating(ambient_temp_degF,ambient_temp_degF) - end + heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, + heating_cf_reference, + heating_reference_temps, + ambient_temp_degF, + back_up_temp_threshold_degF + ) heating_cf[heating_cop .== 1] .= 1 if can_serve_cooling - if !isempty(cooling_reference_temps) - cooling_cop, cooling_cf = get_ashp_performance(cooling_cop_reference, - cooling_cf_reference, - cooling_reference_temps, - ambient_temp_degF, - -460 - ) - else - cooling_cop, cooling_cf = get_default_ashp_cooling(ambient_temp_degF) - end + cooling_cop, cooling_cf = get_ashp_performance(cooling_cop_reference, + cooling_cf_reference, + cooling_reference_temps, + ambient_temp_degF, + -460 + ) else cooling_cop = Float64[] cooling_cf = Float64[] @@ -305,6 +315,16 @@ function ASHP_WaterHeater(; sizing_factor = defaults["sizing_factor"] end + if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps) + throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps must all be the same length.")) + else + if length(heating_cop_reference) == 0 + heating_cop_reference = defaults["heating_cop_reference"] + heating_cf_reference = defaults["heating_cf_reference"] + heating_reference_temps = defaults["heating_reference_temps"] + end + end + #pre-set defaults that aren't mutable due to technology specifications can_supply_steam_turbine = defaults["can_supply_steam_turbine"] can_serve_space_heating = defaults["can_serve_space_heating"] @@ -319,16 +339,12 @@ function ASHP_WaterHeater(; installed_cost_per_kw = installed_cost_per_ton / KWH_THERMAL_PER_TONHOUR om_cost_per_kw = om_cost_per_ton / KWH_THERMAL_PER_TONHOUR - if !isempty(heating_reference_temps) - heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, - heating_cf_reference, - heating_reference_temps, - ambient_temp_degF, - back_up_temp_threshold_degF - ) - else - heating_cop, heating_cf = get_default_ashp_heating(ambient_temp_degF,back_up_temp_threshold_degF) - end + heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, + heating_cf_reference, + heating_reference_temps, + ambient_temp_degF, + back_up_temp_threshold_degF + ) heating_cf[heating_cop .== 1] .= 1 From cead7f1c3571bf7c3e7e7208c1b8881a404e8ad4 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 20:24:38 -0600 Subject: [PATCH 204/266] ren ASHP_SpaceHeater ASHPSpaceHeater, ren ASHP_WaterHeater ASHPWaterHeater --- src/constraints/thermal_tech_constraints.jl | 12 +-- src/core/ashp.jl | 22 +++--- src/core/reopt_inputs.jl | 84 ++++++++++----------- src/core/scenario.jl | 38 +++++----- src/core/techs.jl | 38 +++++----- src/results/ashp.jl | 58 +++++++------- src/results/results.jl | 4 +- test/runtests.jl | 74 +++++++++--------- test/scenarios/ashp.json | 2 +- test/scenarios/ashp_wh.json | 2 +- 10 files changed, 167 insertions(+), 167 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index c663264e7..b20dd5619 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -99,8 +99,8 @@ end function add_ashp_force_in_constraints(m, p; _n="") - if "ASHP_SpaceHeater" in p.techs.ashp && p.s.ashp.force_into_system - for t in setdiff(p.techs.can_serve_space_heating, ["ASHP_SpaceHeater"]) + if "ASHPSpaceHeater" in p.techs.ashp && p.s.ashp.force_into_system + for t in setdiff(p.techs.can_serve_space_heating, ["ASHPSpaceHeater"]) for ts in p.time_steps fix(m[Symbol("dvHeatingProduction"*_n)][t,"SpaceHeating",ts], 0.0, force=true) fix(m[Symbol("dvProductionToWaste"*_n)][t,"SpaceHeating",ts], 0.0, force=true) @@ -108,16 +108,16 @@ function add_ashp_force_in_constraints(m, p; _n="") end end - if "ASHP_SpaceHeater" in p.techs.cooling && p.s.ashp.force_into_system - for t in setdiff(p.techs.cooling, ["ASHP_SpaceHeater"]) + if "ASHPSpaceHeater" in p.techs.cooling && p.s.ashp.force_into_system + for t in setdiff(p.techs.cooling, ["ASHPSpaceHeater"]) for ts in p.time_steps fix(m[Symbol("dvCoolingProduction"*_n)][t,ts], 0.0, force=true) end end end - if "ASHP_WaterHeater" in p.techs.ashp && p.s.ashp_wh.force_into_system - for t in setdiff(p.techs.can_serve_dhw, ["ASHP_WaterHeater"]) + if "ASHPWaterHeater" in p.techs.ashp && p.s.ashp_wh.force_into_system + for t in setdiff(p.techs.can_serve_dhw, ["ASHPWaterHeater"]) for ts in p.time_steps fix(m[Symbol("dvHeatingProduction"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) fix(m[Symbol("dvProductionToWaste"*_n)][t,"DomesticHotWater",ts], 0.0, force=true) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 7b19270b8..99311b08f 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -1,13 +1,13 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. """ -ASHP_SpaceHeater +ASHPSpaceHeater -If a user provides the `ASHP_SpaceHeater` key then the optimal scenario has the option to purchase +If a user provides the `ASHPSpaceHeater` key then the optimal scenario has the option to purchase this new `ASHP` to meet the heating load in addition to using the `ExistingBoiler` to meet the heating load. -ASHP_SpaceHeater has the following attributes: +ASHPSpaceHeater has the following attributes: ```julia min_kw::Real = 0.0, # Minimum thermal power size max_kw::Real = BIG_NUMBER, # Maximum thermal power size @@ -51,14 +51,14 @@ end """ -ASHP_SpaceHeater +ASHPSpaceHeater -If a user provides the `ASHP_SpaceHeater` key then the optimal scenario has the option to purchase +If a user provides the `ASHPSpaceHeater` key then the optimal scenario has the option to purchase this new `ASHP` to meet the heating load in addition to using the `ExistingBoiler` to meet the heating load. ```julia -function ASHP_SpaceHeater(; +function ASHPSpaceHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size min_allowable_ton::Union{Real, Nothing} = nothing, # Minimum nonzero thermal power size if included @@ -89,7 +89,7 @@ function ASHP_SpaceHeater(; ) ``` """ -function ASHP_SpaceHeater(; +function ASHPSpaceHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, min_allowable_ton::Union{Real, Nothing} = nothing, @@ -243,12 +243,12 @@ end """ ASHP Water_Heater -If a user provides the `ASHP_WaterHeater` key then the optimal scenario has the option to purchase -this new `ASHP_WaterHeater` to meet the domestic hot water load in addition to using the `ExistingBoiler` +If a user provides the `ASHPWaterHeater` key then the optimal scenario has the option to purchase +this new `ASHPWaterHeater` to meet the domestic hot water load in addition to using the `ExistingBoiler` to meet the domestic hot water load. ```julia -function ASHP_WaterHeater(; +function ASHPWaterHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size min_allowable_ton::Real = 0.0 # Minimum nonzero thermal power size if included @@ -269,7 +269,7 @@ function ASHP_WaterHeater(; ) ``` """ -function ASHP_WaterHeater(; +function ASHPWaterHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, min_allowable_ton::Union{Real, Nothing} = nothing, diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 3720bfe04..b7c567a28 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -446,22 +446,22 @@ function setup_tech_inputs(s::AbstractScenario, time_steps) heating_cf["ElectricHeater"] = zeros(length(time_steps)) end - if "ASHP_SpaceHeater" in techs.all - setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, + if "ASHPSpaceHeater" in techs.all + setup_ASHPSpaceHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) else - heating_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) - cooling_cop["ASHP_SpaceHeater"] = ones(length(time_steps)) - heating_cf["ASHP_SpaceHeater"] = zeros(length(time_steps)) - cooling_cf["ASHP_SpaceHeater"] = zeros(length(time_steps)) + heating_cop["ASHPSpaceHeater"] = ones(length(time_steps)) + cooling_cop["ASHPSpaceHeater"] = ones(length(time_steps)) + heating_cf["ASHPSpaceHeater"] = zeros(length(time_steps)) + cooling_cf["ASHPSpaceHeater"] = zeros(length(time_steps)) end - if "ASHP_WaterHeater" in techs.all - setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, + if "ASHPWaterHeater" in techs.all + setup_ASHPWaterHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, techs.segmented, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) else - heating_cop["ASHP_WaterHeater"] = ones(length(time_steps)) - heating_cf["ASHP_WaterHeater"] = zeros(length(time_steps)) + heating_cop["ASHPWaterHeater"] = ones(length(time_steps)) + heating_cf["ASHPWaterHeater"] = zeros(length(time_steps)) end if !isempty(techs.ghp) @@ -939,27 +939,27 @@ function setup_electric_heater_inputs(s, max_sizes, min_sizes, cap_cost_slope, o end -function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, +function setup_ASHPSpaceHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, cooling_cop, heating_cf, cooling_cf, segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) - max_sizes["ASHP_SpaceHeater"] = s.ashp.max_kw - min_sizes["ASHP_SpaceHeater"] = s.ashp.min_kw - om_cost_per_kw["ASHP_SpaceHeater"] = s.ashp.om_cost_per_kw - heating_cop["ASHP_SpaceHeater"] = s.ashp.heating_cop - cooling_cop["ASHP_SpaceHeater"] = s.ashp.cooling_cop - heating_cf["ASHP_SpaceHeater"] = s.ashp.heating_cf - cooling_cf["ASHP_SpaceHeater"] = s.ashp.cooling_cf + max_sizes["ASHPSpaceHeater"] = s.ashp.max_kw + min_sizes["ASHPSpaceHeater"] = s.ashp.min_kw + om_cost_per_kw["ASHPSpaceHeater"] = s.ashp.om_cost_per_kw + heating_cop["ASHPSpaceHeater"] = s.ashp.heating_cop + cooling_cop["ASHPSpaceHeater"] = s.ashp.cooling_cop + heating_cf["ASHPSpaceHeater"] = s.ashp.heating_cf + cooling_cf["ASHPSpaceHeater"] = s.ashp.cooling_cf if s.ashp.min_allowable_kw > 0.0 - cap_cost_slope["ASHP_SpaceHeater"] = s.ashp.installed_cost_per_kw - push!(segmented_techs, "ASHP_SpaceHeater") - seg_max_size["ASHP_SpaceHeater"] = Dict{Int,Float64}(1 => s.ashp.max_kw) - seg_min_size["ASHP_SpaceHeater"] = Dict{Int,Float64}(1 => s.ashp.min_allowable_kw) - n_segs_by_tech["ASHP_SpaceHeater"] = 1 - seg_yint["ASHP_SpaceHeater"] = Dict{Int,Float64}(1 => 0.0) + cap_cost_slope["ASHPSpaceHeater"] = s.ashp.installed_cost_per_kw + push!(segmented_techs, "ASHPSpaceHeater") + seg_max_size["ASHPSpaceHeater"] = Dict{Int,Float64}(1 => s.ashp.max_kw) + seg_min_size["ASHPSpaceHeater"] = Dict{Int,Float64}(1 => s.ashp.min_allowable_kw) + n_segs_by_tech["ASHPSpaceHeater"] = 1 + seg_yint["ASHPSpaceHeater"] = Dict{Int,Float64}(1 => 0.0) end if s.ashp.macrs_option_years in [5, 7] - cap_cost_slope["ASHP_SpaceHeater"] = effective_cost(; + cap_cost_slope["ASHPSpaceHeater"] = effective_cost(; itc_basis = s.ashp.installed_cost_per_kw, replacement_cost = 0.0, replacement_year = s.financial.analysis_years, @@ -972,31 +972,31 @@ function setup_ashp_spaceheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, rebate_per_kw = 0.0 ) else - cap_cost_slope["ASHP_SpaceHeater"] = s.ashp.installed_cost_per_kw + cap_cost_slope["ASHPSpaceHeater"] = s.ashp.installed_cost_per_kw end - avoided_capex_by_ashp_present_value["ASHP_SpaceHeater"] = s.ashp.avoided_capex_by_ashp_present_value + avoided_capex_by_ashp_present_value["ASHPSpaceHeater"] = s.ashp.avoided_capex_by_ashp_present_value end -function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, +function setup_ASHPWaterHeater_inputs(s, max_sizes, min_sizes, cap_cost_slope, om_cost_per_kw, heating_cop, heating_cf, segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, avoided_capex_by_ashp_present_value) - max_sizes["ASHP_WaterHeater"] = s.ashp_wh.max_kw - min_sizes["ASHP_WaterHeater"] = s.ashp_wh.min_kw - om_cost_per_kw["ASHP_WaterHeater"] = s.ashp_wh.om_cost_per_kw - heating_cop["ASHP_WaterHeater"] = s.ashp_wh.heating_cop - heating_cf["ASHP_WaterHeater"] = s.ashp_wh.heating_cf + max_sizes["ASHPWaterHeater"] = s.ashp_wh.max_kw + min_sizes["ASHPWaterHeater"] = s.ashp_wh.min_kw + om_cost_per_kw["ASHPWaterHeater"] = s.ashp_wh.om_cost_per_kw + heating_cop["ASHPWaterHeater"] = s.ashp_wh.heating_cop + heating_cf["ASHPWaterHeater"] = s.ashp_wh.heating_cf if s.ashp_wh.min_allowable_kw > 0.0 - cap_cost_slope["ASHP_WaterHeater"] = s.ashp_wh.installed_cost_per_kw - push!(segmented_techs, "ASHP_WaterHeater") - seg_max_size["ASHP_WaterHeater"] = Dict{Int,Float64}(1 => s.ashp_wh.max_kw) - seg_min_size["ASHP_WaterHeater"] = Dict{Int,Float64}(1 => s.ashp_wh.min_allowable_kw) - n_segs_by_tech["ASHP_WaterHeater"] = 1 - seg_yint["ASHP_WaterHeater"] = Dict{Int,Float64}(1 => 0.0) + cap_cost_slope["ASHPWaterHeater"] = s.ashp_wh.installed_cost_per_kw + push!(segmented_techs, "ASHPWaterHeater") + seg_max_size["ASHPWaterHeater"] = Dict{Int,Float64}(1 => s.ashp_wh.max_kw) + seg_min_size["ASHPWaterHeater"] = Dict{Int,Float64}(1 => s.ashp_wh.min_allowable_kw) + n_segs_by_tech["ASHPWaterHeater"] = 1 + seg_yint["ASHPWaterHeater"] = Dict{Int,Float64}(1 => 0.0) end if s.ashp_wh.macrs_option_years in [5, 7] - cap_cost_slope["ASHP_WaterHeater"] = effective_cost(; + cap_cost_slope["ASHPWaterHeater"] = effective_cost(; itc_basis = s.ashp_wh.installed_cost_per_kw, replacement_cost = 0.0, replacement_year = s.financial.analysis_years, @@ -1009,9 +1009,9 @@ function setup_ashp_waterheater_inputs(s, max_sizes, min_sizes, cap_cost_slope, rebate_per_kw = 0.0 ) else - cap_cost_slope["ASHP_WaterHeater"] = s.ashp_wh.installed_cost_per_kw + cap_cost_slope["ASHPWaterHeater"] = s.ashp_wh.installed_cost_per_kw end - avoided_capex_by_ashp_present_value["ASHP_WaterHeater"] = s.ashp_wh.avoided_capex_by_ashp_present_value + avoided_capex_by_ashp_present_value["ASHPWaterHeater"] = s.ashp_wh.avoided_capex_by_ashp_present_value end diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 5a63f53fb..c67567a11 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -54,8 +54,8 @@ A Scenario struct can contain the following keys: - [GHP](@ref) (optional, can be Array) - [SteamTurbine](@ref) (optional) - [ElectricHeater](@ref) (optional) -- [ASHP_SpaceHeater](@ref) (optional) -- [ASHP_WaterHeater](@ref) (optional) +- [ASHPSpaceHeater](@ref) (optional) +- [ASHPWaterHeater](@ref) (optional) All values of `d` are expected to be `Dicts` except for `PV` and `GHP`, which can be either a `Dict` or `Dict[]` (for multiple PV arrays or GHP options). @@ -656,19 +656,19 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # ASHP ashp = nothing - if haskey(d, "ASHP_SpaceHeater") - if !haskey(d["ASHP_SpaceHeater"], "max_ton") + if haskey(d, "ASHPSpaceHeater") + if !haskey(d["ASHPSpaceHeater"], "max_ton") max_ton = get_ashp_defaults("SpaceHeating")["max_ton"] else - max_ton = d["ASHP_SpaceHeater"]["max_ton"] + max_ton = d["ASHPSpaceHeater"]["max_ton"] end if max_ton > 0 # ASHP Space Heater's temp back_up_temp_threshold_degF - if !haskey(d["ASHP_SpaceHeater"], "back_up_temp_threshold_degF") + if !haskey(d["ASHPSpaceHeater"], "back_up_temp_threshold_degF") ambient_temp_thres_fahrenheit = get_ashp_defaults("SpaceHeating")["back_up_temp_threshold_degF"] else - ambient_temp_thres_fahrenheit = d["ASHP_SpaceHeater"]["back_up_temp_threshold_degF"] + ambient_temp_thres_fahrenheit = d["ASHPSpaceHeater"]["back_up_temp_threshold_degF"] end # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor @@ -686,30 +686,30 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - d["ASHP_SpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - d["ASHP_SpaceHeater"]["heating_load"] = space_heating_load.loads_kw - d["ASHP_SpaceHeater"]["cooling_load"] = cooling_load.loads_kw_thermal + d["ASHPSpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHPSpaceHeater"]["heating_load"] = space_heating_load.loads_kw + d["ASHPSpaceHeater"]["cooling_load"] = cooling_load.loads_kw_thermal - ashp = ASHP_SpaceHeater(;dictkeys_tosymbols(d["ASHP_SpaceHeater"])...) + ashp = ASHPSpaceHeater(;dictkeys_tosymbols(d["ASHPSpaceHeater"])...) end end # ASHP Water Heater: ashp_wh = nothing - if haskey(d, "ASHP_WaterHeater") - if !haskey(d["ASHP_WaterHeater"], "max_ton") + if haskey(d, "ASHPWaterHeater") + if !haskey(d["ASHPWaterHeater"], "max_ton") max_ton = get_ashp_defaults("DomesticHotWater")["max_ton"] else - max_ton = d["ASHP_WaterHeater"]["max_ton"] + max_ton = d["ASHPWaterHeater"]["max_ton"] end if max_ton > 0.0 # ASHP Space Heater's temp back_up_temp_threshold_degF - if !haskey(d["ASHP_WaterHeater"], "back_up_temp_threshold_degF") + if !haskey(d["ASHPWaterHeater"], "back_up_temp_threshold_degF") ambient_temp_thres_fahrenheit = get_ashp_defaults("DomesticHotWater")["back_up_temp_threshold_degF"] else - ambient_temp_thres_fahrenheit = d["ASHP_WaterHeater"]["back_up_temp_threshold_degF"] + ambient_temp_thres_fahrenheit = d["ASHPWaterHeater"]["back_up_temp_threshold_degF"] end # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor @@ -728,10 +728,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - d["ASHP_WaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit - d["ASHP_WaterHeater"]["heating_load"] = dhw_load.loads_kw + d["ASHPWaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHPWaterHeater"]["heating_load"] = dhw_load.loads_kw - ashp_wh = ASHP_WaterHeater(;dictkeys_tosymbols(d["ASHP_WaterHeater"])...) + ashp_wh = ASHPWaterHeater(;dictkeys_tosymbols(d["ASHPWaterHeater"])...) end end diff --git a/src/core/techs.jl b/src/core/techs.jl index b144bda05..b8e82261e 100644 --- a/src/core/techs.jl +++ b/src/core/techs.jl @@ -270,47 +270,47 @@ function Techs(s::Scenario) end if !isnothing(s.ashp) - push!(all_techs, "ASHP_SpaceHeater") - push!(heating_techs, "ASHP_SpaceHeater") - push!(electric_heaters, "ASHP_SpaceHeater") - push!(ashp_techs, "ASHP_SpaceHeater") + push!(all_techs, "ASHPSpaceHeater") + push!(heating_techs, "ASHPSpaceHeater") + push!(electric_heaters, "ASHPSpaceHeater") + push!(ashp_techs, "ASHPSpaceHeater") if s.ashp.can_supply_steam_turbine - push!(techs_can_supply_steam_turbine, "ASHP_SpaceHeater") + push!(techs_can_supply_steam_turbine, "ASHPSpaceHeater") end if s.ashp.can_serve_space_heating - push!(techs_can_serve_space_heating, "ASHP_SpaceHeater") + push!(techs_can_serve_space_heating, "ASHPSpaceHeater") end if s.ashp.can_serve_dhw - push!(techs_can_serve_dhw, "ASHP_SpaceHeater") + push!(techs_can_serve_dhw, "ASHPSpaceHeater") end if s.ashp.can_serve_process_heat - push!(techs_can_serve_process_heat, "ASHP_SpaceHeater") + push!(techs_can_serve_process_heat, "ASHPSpaceHeater") end if s.ashp.can_serve_cooling - push!(cooling_techs, "ASHP_SpaceHeater") + push!(cooling_techs, "ASHPSpaceHeater") end end if !isnothing(s.ashp_wh) - push!(all_techs, "ASHP_WaterHeater") - push!(heating_techs, "ASHP_WaterHeater") - push!(electric_heaters, "ASHP_WaterHeater") - push!(ashp_techs, "ASHP_WaterHeater") - push!(ashp_wh_techs, "ASHP_WaterHeater") + push!(all_techs, "ASHPWaterHeater") + push!(heating_techs, "ASHPWaterHeater") + push!(electric_heaters, "ASHPWaterHeater") + push!(ashp_techs, "ASHPWaterHeater") + push!(ashp_wh_techs, "ASHPWaterHeater") if s.ashp_wh.can_supply_steam_turbine - push!(techs_can_supply_steam_turbine, "ASHP_WaterHeater") + push!(techs_can_supply_steam_turbine, "ASHPWaterHeater") end if s.ashp_wh.can_serve_space_heating - push!(techs_can_serve_space_heating, "ASHP_WaterHeater") + push!(techs_can_serve_space_heating, "ASHPWaterHeater") end if s.ashp_wh.can_serve_dhw - push!(techs_can_serve_dhw, "ASHP_WaterHeater") + push!(techs_can_serve_dhw, "ASHPWaterHeater") end if s.ashp_wh.can_serve_process_heat - push!(techs_can_serve_process_heat, "ASHP_WaterHeater") + push!(techs_can_serve_process_heat, "ASHPWaterHeater") end if s.ashp_wh.can_serve_cooling - push!(cooling_techs, "ASHP_WaterHeater") + push!(cooling_techs, "ASHPWaterHeater") end end diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 9d8e732b9..26092f320 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -1,7 +1,7 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. """ -`ASHP_SpaceHeater` results keys: +`ASHPSpaceHeater` results keys: - `size_ton` # Thermal production capacity size of the ASHP [ton/hr] - `electric_consumption_series_kw` # Fuel consumption series [kW] - `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] @@ -23,24 +23,24 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHP_SpaceHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHPSpaceHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * sum(m[:dvHeatingProduction]["ASHP_SpaceHeater",q,ts] for q in p.heating_loads) - / p.heating_cop["ASHP_SpaceHeater"][ts] + p.hours_per_time_step * sum(m[:dvHeatingProduction]["ASHPSpaceHeater",q,ts] for q in p.heating_loads) + / p.heating_cop["ASHPSpaceHeater"][ts] ) @expression(m, ASHPThermalProductionSeries[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHP_SpaceHeater",q,ts] for q in p.heating_loads)) # TODO add cooling + sum(m[:dvHeatingProduction]["ASHPSpaceHeater",q,ts] for q in p.heating_loads)) # TODO add cooling r["thermal_production_series_mmbtu_per_hour"] = round.(value.(ASHPThermalProductionSeries) / KWH_PER_MMBTU, digits=5) r["annual_thermal_production_mmbtu"] = round(sum(r["thermal_production_series_mmbtu_per_hour"]), digits=3) if !isempty(p.s.storage.types.hot) @expression(m, ASHPToHotTESKW[ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP_SpaceHeater",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) + sum(m[:dvHeatToStorage][b,"ASHPSpaceHeater",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) ) @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP_SpaceHeater",q,ts] for b in p.s.storage.types.hot) + sum(m[:dvHeatToStorage][b,"ASHPSpaceHeater",q,ts] for b in p.s.storage.types.hot) ) else @expression(m, ASHPToHotTESKW[ts in p.time_steps], 0.0) @@ -49,41 +49,41 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPToHotTESKW) / KWH_PER_MMBTU, digits=3) @expression(m, ASHPToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHP_SpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] + sum(m[:dvHeatingProduction]["ASHPSpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_SpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] + m[:dvHeatingProduction]["ASHPSpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] ) else @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], 0.0) end r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = round.(value.(ASHPToSpaceHeatingKW ./ KWH_PER_MMBTU), digits=5) - if "ASHP_SpaceHeater" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 + if "ASHPSpaceHeater" in p.techs.cooling && sum(p.s.cooling_load.loads_kw_thermal) > 0.0 @expression(m, ASHPtoColdTES[ts in p.time_steps], - sum(m[:dvProductionToStorage][b,"ASHP_SpaceHeater",ts] for b in p.s.storage.types.cold) + sum(m[:dvProductionToStorage][b,"ASHPSpaceHeater",ts] for b in p.s.storage.types.cold) ) r["thermal_to_storage_series_ton"] = round.(value.(ASHPtoColdTES ./ KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPtoColdLoad[ts in p.time_steps], - sum(m[:dvCoolingProduction]["ASHP_SpaceHeater", ts]) - ASHPtoColdTES[ts] + sum(m[:dvCoolingProduction]["ASHPSpaceHeater", ts]) - ASHPtoColdTES[ts] ) r["thermal_to_load_series_ton"] = round.(value.(ASHPtoColdLoad ./ KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, Year1ASHPColdThermalProd, - p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHP_SpaceHeater", ts] for ts in p.time_steps) + p.hours_per_time_step * sum(m[:dvCoolingProduction]["ASHPSpaceHeater", ts] for ts in p.time_steps) ) r["annual_thermal_production_tonhour"] = round(value(Year1ASHPColdThermalProd / KWH_THERMAL_PER_TONHOUR), digits=3) @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], - p.hours_per_time_step * m[:dvCoolingProduction]["ASHP_SpaceHeater",ts] / p.cooling_cop["ASHP_SpaceHeater"][ts] + p.hours_per_time_step * m[:dvCoolingProduction]["ASHPSpaceHeater",ts] / p.cooling_cop["ASHPSpaceHeater"][ts] ) - r["cooling_cop"] = p.cooling_cop["ASHP_SpaceHeater"] - r["cooling_cf"] = p.cooling_cf["ASHP_SpaceHeater"] + r["cooling_cop"] = p.cooling_cop["ASHPSpaceHeater"] + r["cooling_cf"] = p.cooling_cf["ASHPSpaceHeater"] else r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) @@ -98,16 +98,16 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) r["annual_electric_consumption_for_cooling_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_for_cooling_series_kw"]) r["annual_electric_consumption_for_heating_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_for_heating_series_kw"]) - r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] - r["heating_cf"] = p.heating_cf["ASHP_SpaceHeater"] + r["heating_cop"] = p.heating_cop["ASHPSpaceHeater"] + r["heating_cf"] = p.heating_cf["ASHPSpaceHeater"] - d["ASHP_SpaceHeater"] = r + d["ASHPSpaceHeater"] = r nothing end """ -`ASHP_WaterHeater` results keys: -- `size_ton` # Thermal production capacity size of the ASHP_WaterHeater [ton/hr] +`ASHPWaterHeater` results keys: +- `size_ton` # Thermal production capacity size of the ASHPWaterHeater [ton/hr] - `electric_consumption_series_kw` # Fuel consumption series [kW] - `annual_electric_consumption_kwh` # Fuel consumed in a year [kWh] - `thermal_production_series_mmbtu_per_hour` # Thermal heating energy production series [MMBtu/hr] @@ -124,7 +124,7 @@ end function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = p.s.ashp.sizing_factor * round(value(m[Symbol("dvSize"*_n)]["ASHP_WaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + r["size_ton"] = p.s.ashp.sizing_factor * round(value(m[Symbol("dvSize"*_n)]["ASHPWaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPWHElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp_wh) @@ -138,10 +138,10 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= if !isempty(p.s.storage.types.hot) @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP_WaterHeater",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) + sum(m[:dvHeatToStorage][b,"ASHPWaterHeater",q,ts] for b in p.s.storage.types.hot, q in p.heating_loads) ) @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], - sum(m[:dvHeatToStorage][b,"ASHP_WaterHeater",q,ts] for b in p.s.storage.types.hot) + sum(m[:dvHeatToStorage][b,"ASHPWaterHeater",q,ts] for b in p.s.storage.types.hot) ) else @expression(m, ASHPWHToHotTESKW[ts in p.time_steps], 0.0) @@ -150,13 +150,13 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPWHToHotTESKW) / KWH_PER_MMBTU, digits=3) @expression(m, ASHPWHToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHP_WaterHeater", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] + sum(m[:dvHeatingProduction]["ASHPWaterHeater", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToLoad) ./ KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp_wh.can_serve_dhw @expression(m, ASHPWHToDHWKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHP_WaterHeater","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] + m[:dvHeatingProduction]["ASHPWaterHeater","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] ) else @expression(m, ASHPWHToDHWKW[ts in p.time_steps], 0.0) @@ -165,9 +165,9 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= r["electric_consumption_series_kw"] = round.(value.(ASHPWHElectricConsumptionSeries), digits=3) r["annual_electric_consumption_kwh"] = p.hours_per_time_step * sum(r["electric_consumption_series_kw"]) - r["heating_cop"] = p.heating_cop["ASHP_SpaceHeater"] - r["heating_cf"] = p.heating_cf["ASHP_SpaceHeater"] + r["heating_cop"] = p.heating_cop["ASHPSpaceHeater"] + r["heating_cf"] = p.heating_cf["ASHPSpaceHeater"] - d["ASHP_WaterHeater"] = r + d["ASHPWaterHeater"] = r nothing end \ No newline at end of file diff --git a/src/results/results.jl b/src/results/results.jl index 61ceab94f..1016ff15e 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -104,11 +104,11 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") add_electric_heater_results(m, p, d; _n) end - if "ASHP_SpaceHeater" in p.techs.ashp + if "ASHPSpaceHeater" in p.techs.ashp add_ashp_results(m, p, d; _n) end - if "ASHP_WaterHeater" in p.techs.ashp_wh + if "ASHPWaterHeater" in p.techs.ashp_wh add_ashp_wh_results(m, p, d; _n) end diff --git a/test/runtests.jl b/test/runtests.jl index ed59b61e5..abab4c342 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2472,46 +2472,46 @@ else # run HiGHS tests p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 0.0 atol=0.1 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 #Case 2: ASHP has temperature-dependent output and serves all heating load d["ExistingChiller"] = Dict("retire_in_optimal" => false) d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP_SpaceHeater"]["installed_cost_per_ton"] = 300 - d["ASHP_SpaceHeater"]["min_allowable_ton"] = 80.0 + d["ASHPSpaceHeater"]["installed_cost_per_ton"] = 300 + d["ASHPSpaceHeater"]["min_allowable_ton"] = 80.0 s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) + annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPSpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 + @test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 0.0 atol=1e-4 #Case 3: ASHP can serve cooling, add cooling load d["CoolingLoad"] = Dict("thermal_loads_ton" => ones(8760)*0.1) d["ExistingChiller"] = Dict("cop" => 0.5) - d["ASHP_SpaceHeater"]["can_serve_cooling"] = true + d["ASHPSpaceHeater"]["can_serve_cooling"] = true s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHP_SpaceHeater"][ts] for ts in p.time_steps) + annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHPSpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHP_SpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 #size increases when cooling load also served - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 #size increases when cooling load also served + @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 #Case 4: ASHP used for everything because the existing boiler and chiller are retired even if efficient or free to operate d["ExistingChiller"] = Dict("retire_in_optimal" => true, "cop" => 100) @@ -2521,8 +2521,8 @@ else # run HiGHS tests p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP_SpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 + @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 atol=1e-4 end @@ -2535,27 +2535,27 @@ else # run HiGHS tests p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP_WaterHeater"]["size_ton"] ≈ 0.0 atol=0.1 - @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 - @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 + @test results["ASHPWaterHeater"]["size_ton"] ≈ 0.0 atol=0.1 + @test results["ASHPWaterHeater"]["annual_thermal_production_mmbtu"] ≈ 0.0 atol=0.1 + @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ 0.0 atol=0.1 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 87600.0 atol=0.1 #Case 2: ASHP_WH has temperature-dependent output and serves all DHW load d["ExistingChiller"] = Dict("retire_in_optimal" => false) d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 100 - d["ASHP_WaterHeater"]["installed_cost_per_ton"] = 300 + d["ASHPWaterHeater"]["installed_cost_per_ton"] = 300 s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour - annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) + annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHP_WaterHeater"]["size_ton"] ≈ 37.495 atol=0.01 - @test results["ASHP_WaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 - @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 + @test results["ASHPWaterHeater"]["size_ton"] ≈ 37.495 atol=0.01 + @test results["ASHPWaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 + @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 end @@ -2567,38 +2567,38 @@ else # run HiGHS tests d["ExistingChiller"] = Dict{String,Any}("retire_in_optimal" => false, "cop" => 100) d["ExistingBoiler"]["retire_in_optimal"] = false d["ExistingBoiler"]["fuel_cost_per_mmbtu"] = 0.001 - d["ASHP_SpaceHeater"]["can_serve_cooling"] = true - d["ASHP_SpaceHeater"]["force_into_system"] = true - d["ASHP_WaterHeater"] = Dict{String,Any}("force_into_system" => true, "max_ton" => 100000) + d["ASHPSpaceHeater"]["can_serve_cooling"] = true + d["ASHPSpaceHeater"]["force_into_system"] = true + d["ASHPWaterHeater"] = Dict{String,Any}("force_into_system" => true, "max_ton" => 100000) s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 - d["ASHP_SpaceHeater"]["force_into_system"] = false + d["ASHPSpaceHeater"]["force_into_system"] = false s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP_WaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHP_WaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 + @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) rtol=1e-4 @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 @test results["ExistingChiller"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 - d["ASHP_SpaceHeater"]["force_into_system"] = true - d["ASHP_WaterHeater"]["force_into_system"] = false + d["ASHPSpaceHeater"]["force_into_system"] = true + d["ASHPWaterHeater"]["force_into_system"] = false s = Scenario(d) p = REoptInputs(s) m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) results = run_reopt(m, p) - @test results["ASHP_SpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 - @test results["ASHP_SpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 + @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 @test results["ExistingBoiler"]["annual_thermal_production_mmbtu"] ≈ 0.4 * 8760 rtol=1e-4 end diff --git a/test/scenarios/ashp.json b/test/scenarios/ashp.json index c4371b244..d28d3b5fc 100644 --- a/test/scenarios/ashp.json +++ b/test/scenarios/ashp.json @@ -9,7 +9,7 @@ "fuel_type": "natural_gas", "fuel_cost_per_mmbtu": 5 }, - "ASHP_SpaceHeater": { + "ASHPSpaceHeater": { "min_ton": 0.0, "max_ton": 100000, "installed_cost_per_ton": 4050, diff --git a/test/scenarios/ashp_wh.json b/test/scenarios/ashp_wh.json index ee6725274..7be52fc6c 100644 --- a/test/scenarios/ashp_wh.json +++ b/test/scenarios/ashp_wh.json @@ -9,7 +9,7 @@ "fuel_type": "natural_gas", "fuel_cost_per_mmbtu": 5 }, - "ASHP_WaterHeater": { + "ASHPWaterHeater": { "min_ton": 0.0, "max_ton": 100000, "installed_cost_per_ton": 4050, From 5ce934ff0b49d5a991d2a6a1b0f4ae9adad31891 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 21:16:09 -0600 Subject: [PATCH 205/266] fix ASHPWaterHeater min allowable size calculation --- src/core/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 99311b08f..81f886891 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -357,7 +357,7 @@ function ASHPWaterHeater(; if isnothing(min_allowable_peak_load_fraction) min_allowable_peak_load_fraction = 0.5 end - min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Real[], Real[], min_allowable_peak_load_fraction) end ASHP( From 0599b645b8d2f7e7d5599cc13bbb5b5b8b339e63 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 27 Aug 2024 21:16:22 -0600 Subject: [PATCH 206/266] update ASHP test values to reflect sizing factor --- src/results/ashp.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 26092f320..b5373be82 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -23,7 +23,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = round(value(m[Symbol("dvSize"*_n)]["ASHPSpaceHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + r["size_ton"] = round(p.s.ashp.sizing_factor * value(m[Symbol("dvSize"*_n)]["ASHPSpaceHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction]["ASHPSpaceHeater",q,ts] for q in p.heating_loads) / p.heating_cop["ASHPSpaceHeater"][ts] @@ -124,7 +124,7 @@ end function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r = Dict{String, Any}() - r["size_ton"] = p.s.ashp.sizing_factor * round(value(m[Symbol("dvSize"*_n)]["ASHPWaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) + r["size_ton"] = round(p.s.ashp_wh.sizing_factor * value(m[Symbol("dvSize"*_n)]["ASHPWaterHeater"]) / KWH_THERMAL_PER_TONHOUR, digits=3) @expression(m, ASHPWHElectricConsumptionSeries[ts in p.time_steps], p.hours_per_time_step * sum(m[:dvHeatingProduction][t,q,ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.ashp_wh) From 9cbefab96d678613c18704ddc21be6c03a527019 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 28 Aug 2024 07:03:31 -0600 Subject: [PATCH 207/266] update ASHP test values to reflect sizing factor --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index abab4c342..7b60d0f10 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2491,7 +2491,7 @@ else # run HiGHS tests annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPSpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHPSpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 88.0 atol=0.01 @test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2509,7 +2509,7 @@ else # run HiGHS tests annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHPSpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHPSpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 #size increases when cooling load also served + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 88.0 atol=0.01 #size increases when cooling load also served @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 @@ -2553,7 +2553,7 @@ else # run HiGHS tests annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHPWaterHeater"]["size_ton"] ≈ 37.495 atol=0.01 + @test results["ASHPWaterHeater"]["size_ton"] ≈ 41.22 atol=0.1 @test results["ASHPWaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 From 68de7ef10d86dfd0e7f12812709f3f70a5b9f91a Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 28 Aug 2024 07:17:37 -0600 Subject: [PATCH 208/266] remove default COP, CF curves (replaced by values for performance function) --- src/core/ashp.jl | 29 ----------------------------- 1 file changed, 29 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 81f886891..c250ad1db 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -447,35 +447,6 @@ function get_ashp_performance(cop_reference, return cop, cf end -""" -function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) - -Obtains the default ASHP heating COP and CF profiles. - -ambient_temp_degF::Array{Float64,1} -- time series ambient temperature in degrees Fahrenheit -back_up_temp_threshold::Float64 -- temperature threshold at which resistive backup heater turns on -""" -function get_default_ashp_heating(ambient_temp_degF, back_up_temp_threshold_degF) - heating_cop = round.(0.0462 .* ambient_temp_degF .+ 1.351, digits=3) - heating_cop[ambient_temp_degF .<= back_up_temp_threshold_degF] .= 1 - heating_cf = round.(0.0116 .* ambient_temp_degF .+ 0.4556, digits=3) - heating_cf[heating_cop .== 1.0] .= 1.0 - return heating_cop, heating_cf -end - -""" -function get_default_ashp_cooling(ambient_temp_degF) - -Obtains the default ASHP cooling COP and CF profiles. - -ambient_temp_degF::Array{Float64,1} -- time series ambient temperature in degrees Fahrenheit -""" -function get_default_ashp_cooling(ambient_temp_degF) - cooling_cop = round.(-0.044 .* ambient_temp_degF .+ 6.822, digits=3) - cooling_cf = round.(-0.0056 .* ambient_temp_degF .+ 1.4778, digits=3) - return cooling_cop, cooling_cf -end - """ function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, # time series of heating load heating_cf::Array{Float64,1}, # time series of capacity factor for heating From 284f9a1cb20be51eb9a1ff546b555ea9bb363f33 Mon Sep 17 00:00:00 2001 From: An Pham Date: Wed, 28 Aug 2024 20:15:46 -0600 Subject: [PATCH 209/266] set sizing_factor = 1 for configs 1 and 3 --- src/core/ashp.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index c250ad1db..715938d92 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -142,7 +142,11 @@ function ASHPSpaceHeater(; max_ton = defaults["max_ton"] end if isnothing(sizing_factor) - sizing_factor = defaults["sizing_factor"] + if force_into_system == true + sizing_factor = defaults["sizing_factor"] + else + sizing_factor = 1 + end end if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps) @@ -312,7 +316,11 @@ function ASHPWaterHeater(; max_ton = defaults["max_ton"] end if isnothing(sizing_factor) - sizing_factor = defaults["sizing_factor"] + if force_into_system == true + sizing_factor = defaults["sizing_factor"] + else + sizing_factor = 1 + end end if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps) From 4a85145d33c3d8695b703dfc89c5437650d3fe00 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 29 Aug 2024 12:31:29 -0600 Subject: [PATCH 210/266] update ashp docstrings --- src/core/ashp.jl | 11 +++++++---- 1 file changed, 7 insertions(+), 4 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 715938d92..0132c8654 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -69,7 +69,7 @@ function ASHPSpaceHeater(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true - avoided_capex_by_ashp_present_value::Real = 0.0 + avoided_capex_by_ashp_present_value::Real = 0.0 # avoided capital expenditure due to presence of ASHP system vs. defaults heating and cooling techs #The following inputs are used to create the attributes heating_cop and heating cf: heating_cop_reference::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) @@ -261,10 +261,13 @@ function ASHPWaterHeater(; macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production - - #The following inputs are used to create the attributes heating_cop and heating cf: - heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + avoided_capex_by_ashp_present_value::Real = 0.0 # avoided capital expenditure due to presence of ASHP system vs. defaults heating and cooling techs force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true + + #The following inputs are used to create the attributes heating_cop and heating cf: + heating_cop_reference::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) + heating_cf_reference::Array{<:Real,1}, # ASHP's heating capacity factor curves + heating_reference_temps ::Array{<:Real,1}, # ASHP's reference temperatures for heating COP and CF back_up_temp_threshold_degF::Real = 10 # temperature threshold at which backup resistive heater is used #The following inputs are taken from the Site object: From 797577fe80830722718904be426343bdc3385196 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 29 Aug 2024 14:52:27 -0600 Subject: [PATCH 211/266] revert test values for sizing_factor=1.0 --- test/runtests.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 7b60d0f10..307b93549 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2491,7 +2491,7 @@ else # run HiGHS tests annual_thermal_prod = 0.8 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.8 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPSpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHPSpaceHeater"]["size_ton"] ≈ 88.0 atol=0.01 + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 @test results["ASHPSpaceHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 @@ -2509,7 +2509,7 @@ else # run HiGHS tests annual_ashp_consumption += 0.1 * sum(REopt.KWH_THERMAL_PER_TONHOUR / p.cooling_cop["ASHPSpaceHeater"][ts] for ts in p.time_steps) annual_energy_supplied = annual_ashp_consumption + 87600 - 2*876.0*REopt.KWH_THERMAL_PER_TONHOUR - @test results["ASHPSpaceHeater"]["size_ton"] ≈ 88.0 atol=0.01 #size increases when cooling load also served + @test results["ASHPSpaceHeater"]["size_ton"] ≈ 80.0 atol=0.01 #size increases when cooling load also served @test results["ASHPSpaceHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ASHPSpaceHeater"]["annual_thermal_production_tonhour"] ≈ 876.0 rtol=1e-4 @@ -2553,7 +2553,7 @@ else # run HiGHS tests annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHPWaterHeater"]["size_ton"] ≈ 41.22 atol=0.1 + @test results["ASHPWaterHeater"]["size_ton"] ≈ 37.477 atol=0.1 @test results["ASHPWaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 From 8ce8ebdc113ff0393f04d2270cccf2d9ccc94e25 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 29 Aug 2024 14:54:55 -0600 Subject: [PATCH 212/266] ren *_reference_temps *_reference_temps_degF --- data/ashp/ashp_defaults.json | 6 +++--- src/core/ashp.jl | 40 +++++++++++++++++------------------- 2 files changed, 22 insertions(+), 24 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index fc8176d58..bd6f7cb44 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -16,10 +16,10 @@ "sizing_factor": 1.1, "heating_cop_reference": [0.427, 2.737, 5.047], "heating_cf_reference": [0.2236, 0.8036, 1.3836], - "heating_reference_temps": [-20, 30, 80], + "heating_reference_temps_degF": [-20, 30, 80], "cooling_cop_reference": [3.39, 2.466, 1.542], "cooling_cf_reference": [1.041, 0.9234, 0.8058], - "cooling_reference_temps": [78, 99, 120] + "cooling_reference_temps_degF": [78, 99, 120] }, "DomesticHotWater": { @@ -38,6 +38,6 @@ "sizing_factor": 1.1, "heating_cop_reference": [0.427, 2.737, 5.047], "heating_cf_reference": [0.2236, 0.8036, 1.3836], - "heating_reference_temps": [-20, 30, 80] + "heating_reference_temps_degF": [-20, 30, 80] } } diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 0132c8654..7340b52f7 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -74,13 +74,13 @@ function ASHPSpaceHeater(; #The following inputs are used to create the attributes heating_cop and heating cf: heating_cop_reference::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) heating_cf_reference::Array{<:Real,1}, # ASHP's heating capacity factor curves - heating_reference_temps ::Array{<:Real,1}, # ASHP's reference temperatures for heating COP and CF + heating_reference_temps_degF ::Array{<:Real,1}, # ASHP's reference temperatures for heating COP and CF back_up_temp_threshold_degF::Real = 10, # Degree in F that system switches from ASHP to resistive heater #The following inputs are used to create the attributes cooling_cop and cooling cf: cooling_cop::Array{<:Real,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) cooling_cf::Array{<:Real,1}, # ASHP's cooling capacity factor curves - heating_reference_temps ::Array{<:Real,1}, # ASHP's reference temperatures for cooling COP and CF + cooling_reference_temps_degF ::Array{<:Real,1}, # ASHP's reference temperatures for cooling COP and CF #The following inputs are taken from the Site object: ambient_temp_degF::Array{<:Real,1} #time series of ambient temperature @@ -104,16 +104,14 @@ function ASHPSpaceHeater(; force_into_system::Union{Bool, Nothing} = nothing, heating_cop_reference::Array{<:Real,1} = Float64[], heating_cf_reference::Array{<:Real,1} = Float64[], - heating_reference_temps::Array{<:Real,1} = Float64[], + heating_reference_temps_degF::Array{<:Real,1} = Float64[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, cooling_cop_reference::Array{<:Real,1} = Float64[], cooling_cf_reference::Array{<:Real,1} = Float64[], - cooling_reference_temps::Array{<:Real,1} = Float64[], + cooling_reference_temps_degF::Array{<:Real,1} = Float64[], ambient_temp_degF::Array{Float64,1} = Float64[], heating_load::Array{<:Real,1} = Real[], - cooling_load::Array{<:Real,1} = Real[], - includes_heat_recovery::Bool = false, - heat_recovery_cop::Union{Real, Nothing} = nothing + cooling_load::Array{<:Real,1} = Real[] ) defaults = get_ashp_defaults("SpaceHeating") @@ -149,22 +147,22 @@ function ASHPSpaceHeater(; end end - if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps) - throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps must all be the same length.")) + if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps_degF) + throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 heating_cop_reference = defaults["heating_cop_reference"] heating_cf_reference = defaults["heating_cf_reference"] - heating_reference_temps = defaults["heating_reference_temps"] + heating_reference_temps_degF = defaults["heating_reference_temps_degF"] end end - if length(cooling_cop_reference) != length(cooling_cf_reference) || length(cooling_cf_reference) != length(cooling_reference_temps) - throw(@error("cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps must all be the same length.")) + if length(cooling_cop_reference) != length(cooling_cf_reference) || length(cooling_cf_reference) != length(cooling_reference_temps_degF) + throw(@error("cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps_degF must all be the same length.")) else if length(cooling_cop_reference) == 0 && can_serve_cooling cooling_cop_reference = defaults["cooling_cop_reference"] cooling_cf_reference = defaults["cooling_cf_reference"] - cooling_reference_temps = defaults["cooling_reference_temps"] + cooling_reference_temps_degF = defaults["cooling_reference_temps_degF"] end end @@ -185,7 +183,7 @@ function ASHPSpaceHeater(; heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, heating_cf_reference, - heating_reference_temps, + heating_reference_temps_degF, ambient_temp_degF, back_up_temp_threshold_degF ) @@ -195,7 +193,7 @@ function ASHPSpaceHeater(; if can_serve_cooling cooling_cop, cooling_cf = get_ashp_performance(cooling_cop_reference, cooling_cf_reference, - cooling_reference_temps, + cooling_reference_temps_degF, ambient_temp_degF, -460 ) @@ -267,7 +265,7 @@ function ASHPWaterHeater(; #The following inputs are used to create the attributes heating_cop and heating cf: heating_cop_reference::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) heating_cf_reference::Array{<:Real,1}, # ASHP's heating capacity factor curves - heating_reference_temps ::Array{<:Real,1}, # ASHP's reference temperatures for heating COP and CF + heating_reference_temps_degF ::Array{<:Real,1}, # ASHP's reference temperatures for heating COP and CF back_up_temp_threshold_degF::Real = 10 # temperature threshold at which backup resistive heater is used #The following inputs are taken from the Site object: @@ -290,7 +288,7 @@ function ASHPWaterHeater(; force_into_system::Union{Bool, Nothing} = nothing, heating_cop_reference::Array{<:Real,1} = Real[], heating_cf_reference::Array{<:Real,1} = Real[], - heating_reference_temps::Array{<:Real,1} = Real[], + heating_reference_temps_degF::Array{<:Real,1} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, ambient_temp_degF::Array{<:Real,1} = Real[], heating_load::Array{<:Real,1} = Real[] @@ -326,13 +324,13 @@ function ASHPWaterHeater(; end end - if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps) - throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps must all be the same length.")) + if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps_degF) + throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 heating_cop_reference = defaults["heating_cop_reference"] heating_cf_reference = defaults["heating_cf_reference"] - heating_reference_temps = defaults["heating_reference_temps"] + heating_reference_temps_degF = defaults["heating_reference_temps_degF"] end end @@ -352,7 +350,7 @@ function ASHPWaterHeater(; heating_cop, heating_cf = get_ashp_performance(heating_cop_reference, heating_cf_reference, - heating_reference_temps, + heating_reference_temps_degF, ambient_temp_degF, back_up_temp_threshold_degF ) From 8c527ecbdec52098082d54548a23d752da78f5bc Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 29 Aug 2024 14:55:07 -0600 Subject: [PATCH 213/266] update test values for sizing_factor=1.0 --- test/runtests.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index 307b93549..614c2ad9f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -66,7 +66,7 @@ else # run HiGHS tests @testset "ASHP min allowable size and COP, CF Profiles" begin #Heating profiles - heating_reference_temps = [10,20,30] + heating_reference_temps_degF = [10,20,30] heating_cop_reference = [1,3,4] heating_cf_performance = [1.2,1.3,1.5] back_up_temp_threshold_degF = 10 @@ -75,13 +75,13 @@ else # run HiGHS tests test_cfs = [1.0,1.25,1.4,1.5] heating_cop, heating_cf = REopt.get_ashp_performance(heating_cop_reference, heating_cf_performance, - heating_reference_temps, + heating_reference_temps_degF, test_temps, back_up_temp_threshold_degF) @test all(heating_cop .== test_cops) @test all(heating_cf .== test_cfs) #Cooling profiles - cooling_reference_temps = [30,20,10] + cooling_reference_temps_degF = [30,20,10] cooling_cop_reference = [1,3,4] cooling_cf_performance = [1.2,1.3,1.5] back_up_temp_threshold_degF = -200 @@ -90,7 +90,7 @@ else # run HiGHS tests test_cfs = [1.2,1.25,1.4,1.5] cooling_cop, cooling_cf = REopt.get_ashp_performance(cooling_cop_reference, cooling_cf_performance, - cooling_reference_temps, + cooling_reference_temps_degF, test_temps, back_up_temp_threshold_degF) @test all(cooling_cop .== test_cops) From 4666d61388b1582fbbce7a57dfd4ad28242d3038 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 29 Aug 2024 15:04:36 -0600 Subject: [PATCH 214/266] update default reference cop, cf curves --- data/ashp/ashp_defaults.json | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index bd6f7cb44..6bba313d3 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -14,12 +14,12 @@ "force_into_system": false, "back_up_temp_threshold_degF": 10.0, "sizing_factor": 1.1, - "heating_cop_reference": [0.427, 2.737, 5.047], - "heating_cf_reference": [0.2236, 0.8036, 1.3836], - "heating_reference_temps_degF": [-20, 30, 80], - "cooling_cop_reference": [3.39, 2.466, 1.542], - "cooling_cf_reference": [1.041, 0.9234, 0.8058], - "cooling_reference_temps_degF": [78, 99, 120] + "heating_cop_reference": [1.1,2.1,3.5], + "heating_cf_reference": [0.4, 0.65, 1.0], + "heating_reference_temps_degF": [-5, 17, 47], + "cooling_cop_reference": [3.2, 2.6], + "cooling_cf_reference": [1.01, 0.94], + "cooling_reference_temps_degF": [82, 95] }, "DomesticHotWater": { @@ -36,8 +36,8 @@ "force_into_system": false, "back_up_temp_threshold_degF": 10.0, "sizing_factor": 1.1, - "heating_cop_reference": [0.427, 2.737, 5.047], - "heating_cf_reference": [0.2236, 0.8036, 1.3836], - "heating_reference_temps_degF": [-20, 30, 80] + "heating_cop_reference": [1.1,2.1,3.5], + "heating_cf_reference": [0.4, 0.65, 1.0], + "heating_reference_temps_degF": [-5, 17, 47], } } From 3738960628104a20fff75b31ceea33326c2c0c14 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 30 Aug 2024 08:58:03 -0600 Subject: [PATCH 215/266] fix formatting in ASHP defaults JSON --- data/ashp/ashp_defaults.json | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 6bba313d3..526f6a5a8 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -36,8 +36,8 @@ "force_into_system": false, "back_up_temp_threshold_degF": 10.0, "sizing_factor": 1.1, - "heating_cop_reference": [1.1,2.1,3.5], + "heating_cop_reference": [1.1, 2.1, 3.5], "heating_cf_reference": [0.4, 0.65, 1.0], - "heating_reference_temps_degF": [-5, 17, 47], + "heating_reference_temps_degF": [-5, 17, 47] } } From 0e2759b87f4b42a313761d710f1483e60e7a936f Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 30 Aug 2024 11:01:39 -0600 Subject: [PATCH 216/266] rm superfluous end in test suite --- test/runtests.jl | 1 - 1 file changed, 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 614c2ad9f..ec512d051 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -103,7 +103,6 @@ else # run HiGHS tests @test space_heating_min_allowable_size ≈ 9.166666666666666 atol=1e-8 @test wh_min_allowable_size ≈ 5.0 atol=1e-8 end - end @testset "January Export Rates" begin model = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) From 46a2848e1f25ee265aa915558e106e0444099a43 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 3 Sep 2024 12:39:52 -0600 Subject: [PATCH 217/266] update default and specific types for ASHPSpaceHeater --- src/core/ashp.jl | 14 +++++++------- 1 file changed, 7 insertions(+), 7 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 7340b52f7..f078c8816 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -102,14 +102,14 @@ function ASHPSpaceHeater(; avoided_capex_by_ashp_present_value::Real = 0.0, can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::Array{<:Real,1} = Float64[], - heating_cf_reference::Array{<:Real,1} = Float64[], - heating_reference_temps_degF::Array{<:Real,1} = Float64[], + heating_cop_reference::Array{<:Real,1} = Real[], + heating_cf_reference::Array{<:Real,1} = Real[], + heating_reference_temps_degF::Array{<:Real,1} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, - cooling_cop_reference::Array{<:Real,1} = Float64[], - cooling_cf_reference::Array{<:Real,1} = Float64[], - cooling_reference_temps_degF::Array{<:Real,1} = Float64[], - ambient_temp_degF::Array{Float64,1} = Float64[], + cooling_cop_reference::Array{<:Real,1} = Real[], + cooling_cf_reference::Array{<:Real,1} = Real[], + cooling_reference_temps_degF::Array{<:Real,1} = Real[], + ambient_temp_degF::Array{<:Real,1} = Real[], heating_load::Array{<:Real,1} = Real[], cooling_load::Array{<:Real,1} = Real[] ) From 9f4657128424b18a5c6758a902816762866d313a Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 3 Sep 2024 18:11:11 -0600 Subject: [PATCH 218/266] more permissive type defs for ASHP techs for API inputs --- src/core/ashp.jl | 76 ++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index f078c8816..ac86131dc 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -102,16 +102,16 @@ function ASHPSpaceHeater(; avoided_capex_by_ashp_present_value::Real = 0.0, can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::Array{<:Real,1} = Real[], - heating_cf_reference::Array{<:Real,1} = Real[], - heating_reference_temps_degF::Array{<:Real,1} = Real[], + heating_cop_reference::AbstractVector{<:Real} = Real[], + heating_cf_reference::AbstractVector{<:Real} = Real[], + heating_reference_temps_degF::AbstractVector{<:Real} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, - cooling_cop_reference::Array{<:Real,1} = Real[], - cooling_cf_reference::Array{<:Real,1} = Real[], - cooling_reference_temps_degF::Array{<:Real,1} = Real[], - ambient_temp_degF::Array{<:Real,1} = Real[], - heating_load::Array{<:Real,1} = Real[], - cooling_load::Array{<:Real,1} = Real[] + cooling_cop_reference::AbstractVector{<:Real} = Real[], + cooling_cf_reference::AbstractVector{<:Real} = Real[], + cooling_reference_temps_degF::AbstractVector{<:Real} = Real[], + ambient_temp_degF::AbstractVector{<:Real} = Real[], + heating_load::AbstractVector{<:Real} = Real[], + cooling_load::AbstractVector{<:Real} = Real[] ) defaults = get_ashp_defaults("SpaceHeating") @@ -151,18 +151,18 @@ function ASHPSpaceHeater(; throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 - heating_cop_reference = defaults["heating_cop_reference"] - heating_cf_reference = defaults["heating_cf_reference"] - heating_reference_temps_degF = defaults["heating_reference_temps_degF"] + heating_cop_reference = Vector{Real}(defaults["heating_cop_reference"]) + heating_cf_reference = Vector{Real}(defaults["heating_cf_reference"]) + heating_reference_temps_degF = Vector{Real}(defaults["heating_reference_temps_degF"]) end end if length(cooling_cop_reference) != length(cooling_cf_reference) || length(cooling_cf_reference) != length(cooling_reference_temps_degF) throw(@error("cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps_degF must all be the same length.")) else if length(cooling_cop_reference) == 0 && can_serve_cooling - cooling_cop_reference = defaults["cooling_cop_reference"] - cooling_cf_reference = defaults["cooling_cf_reference"] - cooling_reference_temps_degF = defaults["cooling_reference_temps_degF"] + cooling_cop_reference = Vector{Real}(defaults["cooling_cop_reference"]) + cooling_cf_reference = Vector{Real}(defaults["cooling_cf_reference"]) + cooling_reference_temps_degF = Vector{Real}(defaults["cooling_reference_temps_degF"]) end end @@ -286,9 +286,9 @@ function ASHPWaterHeater(; macrs_bonus_fraction::Real = 0.0, avoided_capex_by_ashp_present_value::Real = 0.0, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::Array{<:Real,1} = Real[], - heating_cf_reference::Array{<:Real,1} = Real[], - heating_reference_temps_degF::Array{<:Real,1} = Real[], + heating_cop_reference::AbstractVector{<:Real} = Real[], + heating_cf_reference::AbstractVector{<:Real} = Real[], + heating_reference_temps_degF::AbstractVector{<:Real} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, ambient_temp_degF::Array{<:Real,1} = Real[], heating_load::Array{<:Real,1} = Real[] @@ -328,9 +328,9 @@ function ASHPWaterHeater(; throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 - heating_cop_reference = defaults["heating_cop_reference"] - heating_cf_reference = defaults["heating_cf_reference"] - heating_reference_temps_degF = defaults["heating_reference_temps_degF"] + heating_cop_reference = Vector{Real}(defaults["heating_cop_reference"]) + heating_cf_reference = Vector{Real}(defaults["heating_cf_reference"]) + heating_reference_temps_degF = Vector{Real}(defaults["heating_reference_temps_degF"]) end end @@ -380,9 +380,9 @@ function ASHPWaterHeater(; macrs_bonus_fraction, can_supply_steam_turbine, heating_cop, - Float64[], + Real[], heating_cf, - Float64[], + Real[], can_serve_dhw, can_serve_space_heating, can_serve_process_heat, @@ -415,18 +415,18 @@ function get_ashp_defaults(load_served::String="SpaceHeating") end """ -function get_ashp_performance(cop_reference, - cf_reference, - reference_temps, - ambient_temp_degF, - back_up_temp_threshold_degF = 10.0 - ) +function get_ashp_performance(cop_reference::Array{<:Real,1}, + cf_reference::Array{<:Real,1}, + reference_temps::Array{<:Real,1}, + ambient_temp_degF::Array{<:Real,1}, + back_up_temp_threshold_degF::Real = 10.0 + ) """ -function get_ashp_performance(cop_reference, - cf_reference, - reference_temps, - ambient_temp_degF, - back_up_temp_threshold_degF = 10.0 +function get_ashp_performance(cop_reference::AbstractVector{<:Real} = Real[], + cf_reference::AbstractVector{<:Real} = Real[], + reference_temps::AbstractVector{<:Real} = Real[], + ambient_temp_degF::AbstractVector{<:Real} = Real[], + back_up_temp_threshold_degF::Real = 10.0 ) num_timesteps = length(ambient_temp_degF) cop = zeros(num_timesteps) @@ -466,10 +466,10 @@ function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, # ti Obtains the default minimum allowable size for ASHP system. This is calculated as half of the peak site thermal load(s) addressed by the system, including the capacity factor """ -function get_ashp_default_min_allowable_size(heating_load::Array{<:Real,1}, - heating_cf::Array{<:Real,1}, - cooling_load::Array{<:Real,1} = Real[], - cooling_cf::Array{<:Real,1} = Real[], +function get_ashp_default_min_allowable_size(heating_load::AbstractVector{<:Real}, + heating_cf::AbstractVector{<:Real} = Real[], + cooling_load::AbstractVector{<:Real} = Real[], + cooling_cf::AbstractVector{<:Real} = Real[], peak_load_thermal_factor::Real = 0.5 ) From 893c896fb4700c90ac0a30fd19270bdfd63fa74c Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 3 Sep 2024 19:30:14 -0600 Subject: [PATCH 219/266] Revert "more permissive type defs for ASHP techs for API inputs" This reverts commit 9f4657128424b18a5c6758a902816762866d313a. --- src/core/ashp.jl | 76 ++++++++++++++++++++++++------------------------ 1 file changed, 38 insertions(+), 38 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index ac86131dc..f078c8816 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -102,16 +102,16 @@ function ASHPSpaceHeater(; avoided_capex_by_ashp_present_value::Real = 0.0, can_serve_cooling::Union{Bool, Nothing} = nothing, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::AbstractVector{<:Real} = Real[], - heating_cf_reference::AbstractVector{<:Real} = Real[], - heating_reference_temps_degF::AbstractVector{<:Real} = Real[], + heating_cop_reference::Array{<:Real,1} = Real[], + heating_cf_reference::Array{<:Real,1} = Real[], + heating_reference_temps_degF::Array{<:Real,1} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, - cooling_cop_reference::AbstractVector{<:Real} = Real[], - cooling_cf_reference::AbstractVector{<:Real} = Real[], - cooling_reference_temps_degF::AbstractVector{<:Real} = Real[], - ambient_temp_degF::AbstractVector{<:Real} = Real[], - heating_load::AbstractVector{<:Real} = Real[], - cooling_load::AbstractVector{<:Real} = Real[] + cooling_cop_reference::Array{<:Real,1} = Real[], + cooling_cf_reference::Array{<:Real,1} = Real[], + cooling_reference_temps_degF::Array{<:Real,1} = Real[], + ambient_temp_degF::Array{<:Real,1} = Real[], + heating_load::Array{<:Real,1} = Real[], + cooling_load::Array{<:Real,1} = Real[] ) defaults = get_ashp_defaults("SpaceHeating") @@ -151,18 +151,18 @@ function ASHPSpaceHeater(; throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 - heating_cop_reference = Vector{Real}(defaults["heating_cop_reference"]) - heating_cf_reference = Vector{Real}(defaults["heating_cf_reference"]) - heating_reference_temps_degF = Vector{Real}(defaults["heating_reference_temps_degF"]) + heating_cop_reference = defaults["heating_cop_reference"] + heating_cf_reference = defaults["heating_cf_reference"] + heating_reference_temps_degF = defaults["heating_reference_temps_degF"] end end if length(cooling_cop_reference) != length(cooling_cf_reference) || length(cooling_cf_reference) != length(cooling_reference_temps_degF) throw(@error("cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps_degF must all be the same length.")) else if length(cooling_cop_reference) == 0 && can_serve_cooling - cooling_cop_reference = Vector{Real}(defaults["cooling_cop_reference"]) - cooling_cf_reference = Vector{Real}(defaults["cooling_cf_reference"]) - cooling_reference_temps_degF = Vector{Real}(defaults["cooling_reference_temps_degF"]) + cooling_cop_reference = defaults["cooling_cop_reference"] + cooling_cf_reference = defaults["cooling_cf_reference"] + cooling_reference_temps_degF = defaults["cooling_reference_temps_degF"] end end @@ -286,9 +286,9 @@ function ASHPWaterHeater(; macrs_bonus_fraction::Real = 0.0, avoided_capex_by_ashp_present_value::Real = 0.0, force_into_system::Union{Bool, Nothing} = nothing, - heating_cop_reference::AbstractVector{<:Real} = Real[], - heating_cf_reference::AbstractVector{<:Real} = Real[], - heating_reference_temps_degF::AbstractVector{<:Real} = Real[], + heating_cop_reference::Array{<:Real,1} = Real[], + heating_cf_reference::Array{<:Real,1} = Real[], + heating_reference_temps_degF::Array{<:Real,1} = Real[], back_up_temp_threshold_degF::Union{Real, Nothing} = nothing, ambient_temp_degF::Array{<:Real,1} = Real[], heating_load::Array{<:Real,1} = Real[] @@ -328,9 +328,9 @@ function ASHPWaterHeater(; throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 - heating_cop_reference = Vector{Real}(defaults["heating_cop_reference"]) - heating_cf_reference = Vector{Real}(defaults["heating_cf_reference"]) - heating_reference_temps_degF = Vector{Real}(defaults["heating_reference_temps_degF"]) + heating_cop_reference = defaults["heating_cop_reference"] + heating_cf_reference = defaults["heating_cf_reference"] + heating_reference_temps_degF = defaults["heating_reference_temps_degF"] end end @@ -380,9 +380,9 @@ function ASHPWaterHeater(; macrs_bonus_fraction, can_supply_steam_turbine, heating_cop, - Real[], + Float64[], heating_cf, - Real[], + Float64[], can_serve_dhw, can_serve_space_heating, can_serve_process_heat, @@ -415,18 +415,18 @@ function get_ashp_defaults(load_served::String="SpaceHeating") end """ -function get_ashp_performance(cop_reference::Array{<:Real,1}, - cf_reference::Array{<:Real,1}, - reference_temps::Array{<:Real,1}, - ambient_temp_degF::Array{<:Real,1}, - back_up_temp_threshold_degF::Real = 10.0 - ) +function get_ashp_performance(cop_reference, + cf_reference, + reference_temps, + ambient_temp_degF, + back_up_temp_threshold_degF = 10.0 + ) """ -function get_ashp_performance(cop_reference::AbstractVector{<:Real} = Real[], - cf_reference::AbstractVector{<:Real} = Real[], - reference_temps::AbstractVector{<:Real} = Real[], - ambient_temp_degF::AbstractVector{<:Real} = Real[], - back_up_temp_threshold_degF::Real = 10.0 +function get_ashp_performance(cop_reference, + cf_reference, + reference_temps, + ambient_temp_degF, + back_up_temp_threshold_degF = 10.0 ) num_timesteps = length(ambient_temp_degF) cop = zeros(num_timesteps) @@ -466,10 +466,10 @@ function get_ashp_default_min_allowable_size(heating_load::Array{Float64}, # ti Obtains the default minimum allowable size for ASHP system. This is calculated as half of the peak site thermal load(s) addressed by the system, including the capacity factor """ -function get_ashp_default_min_allowable_size(heating_load::AbstractVector{<:Real}, - heating_cf::AbstractVector{<:Real} = Real[], - cooling_load::AbstractVector{<:Real} = Real[], - cooling_cf::AbstractVector{<:Real} = Real[], +function get_ashp_default_min_allowable_size(heating_load::Array{<:Real,1}, + heating_cf::Array{<:Real,1}, + cooling_load::Array{<:Real,1} = Real[], + cooling_cf::Array{<:Real,1} = Real[], peak_load_thermal_factor::Real = 0.5 ) From 53007991ecdc8ebc9f19015ea43f862b6992b4f7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 3 Sep 2024 19:32:26 -0600 Subject: [PATCH 220/266] add cop and cf references to vector type conversions --- src/core/utils.jl | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/src/core/utils.jl b/src/core/utils.jl index 1357531f6..2ba9f4d4b 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -159,6 +159,12 @@ function dictkeys_tosymbols(d::Dict) "emissions_factor_series_lb_NOx_per_kwh", "emissions_factor_series_lb_SO2_per_kwh", "emissions_factor_series_lb_PM25_per_kwh", + "heating_cop_reference", + "heating_cf_reference", + "heating_reference_temps_degF", + "cooling_cop_reference", + "cooling_cf_reference", + "cooling_reference_temps_degF", #for ERP "pv_production_factor_series", "wind_production_factor_series", "battery_starting_soc_series_fraction", From 1cf63cb2c10d8c14a04dc7c2f6a7a58674e49e0c Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 11:48:57 -0600 Subject: [PATCH 221/266] update ASHP default COP ad CF curves --- data/ashp/ashp_defaults.json | 18 +++++++++--------- test/runtests.jl | 2 +- 2 files changed, 10 insertions(+), 10 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 526f6a5a8..7667db76b 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -14,12 +14,12 @@ "force_into_system": false, "back_up_temp_threshold_degF": 10.0, "sizing_factor": 1.1, - "heating_cop_reference": [1.1,2.1,3.5], - "heating_cf_reference": [0.4, 0.65, 1.0], - "heating_reference_temps_degF": [-5, 17, 47], - "cooling_cop_reference": [3.2, 2.6], - "cooling_cf_reference": [1.01, 0.94], - "cooling_reference_temps_degF": [82, 95] + "heating_cop_reference": [1.5,2.3,3.3,4.5], + "heating_cf_reference": [0.38,0.64,1.0,1.4], + "heating_reference_temps_degF": [-5,17,47,80], + "cooling_cop_reference": [4.0, 3.5, 2.9, 2.2], + "cooling_cf_reference": [1.03, 0.98, 0.93, 0.87], + "cooling_reference_temps_degF": [70, 82, 95, 110] }, "DomesticHotWater": { @@ -36,8 +36,8 @@ "force_into_system": false, "back_up_temp_threshold_degF": 10.0, "sizing_factor": 1.1, - "heating_cop_reference": [1.1, 2.1, 3.5], - "heating_cf_reference": [0.4, 0.65, 1.0], - "heating_reference_temps_degF": [-5, 17, 47] + "heating_cop_reference": [1.5,2.3,3.3,4.5], + "heating_cf_reference": [0.38,0.64,1.0,1.4], + "heating_reference_temps_degF": [-5,17,47,80] } } diff --git a/test/runtests.jl b/test/runtests.jl index ec512d051..3010b5f1f 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -2552,7 +2552,7 @@ else # run HiGHS tests annual_thermal_prod = 0.4 * 8760 #80% efficient boiler --> 0.8 MMBTU of heat load per hour annual_ashp_consumption = sum(0.4 * REopt.KWH_PER_MMBTU / p.heating_cop["ASHPWaterHeater"][ts] for ts in p.time_steps) annual_energy_supplied = 87600 + annual_ashp_consumption - @test results["ASHPWaterHeater"]["size_ton"] ≈ 37.477 atol=0.1 + @test results["ASHPWaterHeater"]["size_ton"] ≈ 37.673 atol=0.1 @test results["ASHPWaterHeater"]["annual_thermal_production_mmbtu"] ≈ annual_thermal_prod rtol=1e-4 @test results["ASHPWaterHeater"]["annual_electric_consumption_kwh"] ≈ annual_ashp_consumption rtol=1e-4 @test results["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ annual_energy_supplied rtol=1e-4 From 8871d47e420713670716e2c8e9d0be4e83c822b3 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 11:55:42 -0600 Subject: [PATCH 222/266] ren min_allowable_peak_load_fraction min_allowable_peak_capacity_fraction --- src/core/ashp.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index f078c8816..065df083b 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -93,7 +93,7 @@ function ASHPSpaceHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, min_allowable_ton::Union{Real, Nothing} = nothing, - min_allowable_peak_load_fraction::Union{Real, Nothing} = nothing, + min_allowable_peak_capacity_fraction::Union{Real, Nothing} = nothing, sizing_factor::Union{Real, Nothing} = nothing, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, @@ -278,7 +278,7 @@ function ASHPWaterHeater(; min_ton::Real = 0.0, max_ton::Real = BIG_NUMBER, min_allowable_ton::Union{Real, Nothing} = nothing, - min_allowable_peak_load_fraction::Union{Real, Nothing} = nothing, + min_allowable_peak_capacity_fraction::Union{Real, Nothing} = nothing, sizing_factor::Union{Real, Nothing} = nothing, installed_cost_per_ton::Union{Real, Nothing} = nothing, om_cost_per_ton::Union{Real, Nothing} = nothing, From b5819ce04ff970a33b4abcbe58ecf246f9860b1a Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 12:41:52 -0600 Subject: [PATCH 223/266] ren min_allowable_peak_load_fraction min_allowable_peak_capacity_fraction globally --- src/core/ashp.jl | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 065df083b..caa89b3f9 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -62,7 +62,7 @@ function ASHPSpaceHeater(; min_ton::Real = 0.0, # Minimum thermal power size max_ton::Real = BIG_NUMBER, # Maximum thermal power size min_allowable_ton::Union{Real, Nothing} = nothing, # Minimum nonzero thermal power size if included - min_allowable_peak_load_fraction::Union{Real, Nothing} = nothing, # minimum allowable fraction of peak heating + cooling load + min_allowable_peak_capacity_fraction::Union{Real, Nothing} = nothing, # minimum allowable fraction of peak heating + cooling load sizing_factor::::Union{Real, Nothing} = nothing, # Size multiplier of system, relative that of the max load given by dispatch profile om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable @@ -202,14 +202,14 @@ function ASHPSpaceHeater(; cooling_cf = Float64[] end - if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_load_fraction) - throw(@error("at most one of min_allowable_ton and min_allowable_peak_load_fraction may be input.")) + if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_capacity_fraction) + throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input.")) elseif !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") else - if isnothing(min_allowable_peak_load_fraction) - min_allowable_peak_load_fraction = 0.5 + if isnothing(min_allowable_peak_capacity_fraction) + min_allowable_peak_capacity_fraction = 0.5 end min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) end @@ -357,16 +357,16 @@ function ASHPWaterHeater(; heating_cf[heating_cop .== 1] .= 1 - if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_load_fraction) - throw(@error("at most one of min_allowable_ton and min_allowable_peak_load_fraction may be input.")) + if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_capacity_fraction) + throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input.")) elseif !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") else - if isnothing(min_allowable_peak_load_fraction) - min_allowable_peak_load_fraction = 0.5 + if isnothing(min_allowable_peak_capacity_fraction) + min_allowable_peak_capacity_fraction = 0.5 end - min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Real[], Real[], min_allowable_peak_load_fraction) + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Real[], Real[], min_allowable_peak_capacity_fraction) end ASHP( From 8f3d1afad05b1beed4b0f927bad6ef6b91f7bc39 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 20:52:18 -0600 Subject: [PATCH 224/266] error string correction --- src/core/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index caa89b3f9..7dd0acd57 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -358,7 +358,7 @@ function ASHPWaterHeater(; heating_cf[heating_cop .== 1] .= 1 if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_capacity_fraction) - throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input.")) + throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input.")) elseif !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") From 45d43ba02fda4f831f9beba32db59a38044b23be Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 21:02:09 -0600 Subject: [PATCH 225/266] fix set in expression ASHPColdElectricConsumptionSeries --- src/results/ashp.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index b5373be82..86da96927 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -88,7 +88,7 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["thermal_to_storage_series_ton"] = zeros(length(p.time_steps)) r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) r["annual_thermal_production_tonhour"] = 0.0 - @expression(m, ASHPColdElectricConsumptionSeries, 0.0) + @expression(m, ASHPColdElectricConsumptionSeries[ts in p.time_steps], 0.0) r["cooling_cop"] = zeros(length(p.time_steps)) r["cooling_cf"] = zeros(length(p.time_steps)) end From e1de07cad858b7dfc24e2478d9329d41cea78977 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 21:02:33 -0600 Subject: [PATCH 226/266] remove non-electric segmented techs from microgrid outage constraints --- src/constraints/outage_constraints.jl | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/src/constraints/outage_constraints.jl b/src/constraints/outage_constraints.jl index 8a34f6c2f..49b9244e1 100644 --- a/src/constraints/outage_constraints.jl +++ b/src/constraints/outage_constraints.jl @@ -49,22 +49,22 @@ function add_outage_cost_constraints(m,p) end end - if !isempty(p.techs.segmented) + if !isempty(intersect(p.techs.segmented, p.techs.elec)) @warn "Adding binary variable(s) to model cost curves in stochastic outages" if solver_is_compatible_with_indicator_constraints(p.s.settings.solver_name) - @constraint(m, [t in p.techs.segmented], # cannot have this for statement in sum( ... for t in ...) ??? + @constraint(m, [t in intersect(p.techs.segmented, p.techs.elec)], # cannot have this for statement in sum( ... for t in ...) ??? m[:binMGTechUsed][t] => {m[:dvMGTechUpgradeCost][t] >= p.s.financial.microgrid_upgrade_cost_fraction * p.third_party_factor * sum(p.cap_cost_slope[t][s] * m[Symbol("dvSegmentSystemSize"*t)][s] + p.seg_yint[t][s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t])} ) else - @constraint(m, [t in p.techs.segmented], + @constraint(m, [t in intersect(p.techs.segmented, p.techs.elec)], m[:dvMGTechUpgradeCost][t] >= p.s.financial.microgrid_upgrade_cost_fraction * p.third_party_factor * sum(p.cap_cost_slope[t][s] * m[Symbol("dvSegmentSystemSize"*t)][s] + p.seg_yint[t][s] * m[Symbol("binSegment"*t)][s] for s in 1:p.n_segs_by_tech[t]) - (maximum(p.cap_cost_slope[t][s] for s in 1:p.n_segs_by_tech[t]) * p.max_sizes[t] + maximum(p.seg_yint[t][s] for s in 1:p.n_segs_by_tech[t]))*(1-m[:binMGTechUsed][t]) ) - @constraint(m, [t in p.techs.segmented], m[:dvMGTechUpgradeCost][t] >= 0.0) + @constraint(m, [t in intersect(p.techs.segmented, p.techs.elec)], m[:dvMGTechUpgradeCost][t] >= 0.0) end end From 50e3e4ea29a092a37c0cc41a1fc628f60d4b6fdf Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 4 Sep 2024 21:55:26 -0600 Subject: [PATCH 227/266] fix default calculation of ASHP allowable size, and throw error when min_allowable_kw > max_kw --- src/core/ashp.jl | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 7dd0acd57..dab5efc0a 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -203,7 +203,7 @@ function ASHPSpaceHeater(; end if !isnothing(min_allowable_ton) && !isnothing(min_allowable_peak_capacity_fraction) - throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input.")) + throw(@error("at most one of min_allowable_ton and min_allowable_peak_capacity_fraction may be input.")) elseif !isnothing(min_allowable_ton) min_allowable_kw = min_allowable_ton * KWH_THERMAL_PER_TONHOUR @warn("user-provided minimum allowable ton is used in the place of the default; this may provided very small sizes if set to zero.") @@ -211,7 +211,11 @@ function ASHPSpaceHeater(; if isnothing(min_allowable_peak_capacity_fraction) min_allowable_peak_capacity_fraction = 0.5 end - min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf) + min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, cooling_load, cooling_cf, min_allowable_peak_capacity_fraction) + end + + if min_allowable_kw > max_kw + throw(@error("The ASHPSpaceHeater minimum allowable size of $min_allowable_kw kW is larger than the maximum size of $max_kw kW.")) end installed_cost_per_kw *= sizing_factor @@ -369,6 +373,10 @@ function ASHPWaterHeater(; min_allowable_kw = get_ashp_default_min_allowable_size(heating_load, heating_cf, Real[], Real[], min_allowable_peak_capacity_fraction) end + if min_allowable_kw > max_kw + throw(@error("The ASHPWaterHeater minimum allowable size of $min_allowable_kw kW is larger than the maximum size of $max_kw kW.")) + end + ASHP( min_kw, max_kw, From bdff1fe3b65921635010d34ac14ccd9dfb1e8e45 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Fri, 6 Sep 2024 14:11:31 -0600 Subject: [PATCH 228/266] Update comments in code to be more accurate, remove done TODO --- src/results/proforma.jl | 9 ++++----- 1 file changed, 4 insertions(+), 5 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index e5deff037..c04515743 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -97,7 +97,6 @@ function proforma_results(p::REoptInputs, d::Dict) # In the two party case the developer does not include the fuel cost in their costs # It is assumed that the offtaker will pay for this at a rate that is not marked up # to cover developer profits - # TODO escalate fuel cost with p.s.financial.generator_fuel_cost_escalation_rate_fraction fixed_and_var_om = d["Generator"]["year_one_fixed_om_cost_before_tax"] + d["Generator"]["year_one_variable_om_cost_before_tax"] fixed_and_var_om_bau = 0.0 year_one_fuel_cost_bau = 0.0 @@ -131,7 +130,7 @@ function proforma_results(p::REoptInputs, d::Dict) update_metrics(m, p, p.s.chp, "CHP", d, third_party) end - # calculate (new) Boiler o+m costs (just fuel, no non-fuel operating costs currently) + # calculate ExistingBoiler o+m costs (just fuel, no non-fuel operating costs currently) # the optional installed_cost inputs assume net present cost so no option for MACRS or incentives if "ExistingBoiler" in keys(d) && d["ExistingBoiler"]["size_mmbtu_per_hour"] > 0 fuel_cost = d["ExistingBoiler"]["year_one_fuel_cost_before_tax"] @@ -150,7 +149,7 @@ function proforma_results(p::REoptInputs, d::Dict) m.om_series_bau += escalate_om(annual_om_bau) end - # calculate (new) Boiler o+m costs and depreciation (no incentives currently) + # calculate (new) Boiler o+m costs and depreciation (no incentives currently, other than MACRS) if "Boiler" in keys(d) && d["Boiler"]["size_mmbtu_per_hour"] > 0 fuel_cost = d["Boiler"]["year_one_fuel_cost_before_tax"] m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.boiler_fuel_cost_escalation_rate_fraction) @@ -166,7 +165,7 @@ function proforma_results(p::REoptInputs, d::Dict) end end - # calculate Steam Turbine o+m costs and depreciation (no incentives currently) + # calculate Steam Turbine o+m costs and depreciation (no incentives currently, other than MACRS) if "SteamTurbine" in keys(d) && get(d["SteamTurbine"], "size_kw", 0) > 0 fixed_om = p.s.steam_turbine.om_cost_per_kw * d["SteamTurbine"]["size_kw"] var_om = p.s.steam_turbine.om_cost_per_kwh * d["SteamTurbine"]["annual_electric_production_kwh"] @@ -180,7 +179,7 @@ function proforma_results(p::REoptInputs, d::Dict) end end - # calculate Absorption Chiller o+m costs and depreciation (no incentives currently) + # calculate Absorption Chiller o+m costs and depreciation (no incentives currently, other than MACRS) if "AbsorptionChiller" in keys(d) && d["AbsorptionChiller"]["size_ton"] > 0 # Some thermal techs (e.g. Boiler) only have struct fields for O&M "per_kw" (converted from e.g. per_mmbtu_per_hour or per_ton) # but Absorption Chiller also has the input-style "per_ton" O&M, so no need to convert like for Boiler From 504f3aba4441cf5aa74ac42fd76f36cac58b27fb Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Fri, 6 Sep 2024 15:51:27 -0600 Subject: [PATCH 229/266] Update more comments and docstrings --- src/results/existing_boiler.jl | 1 + src/results/proforma.jl | 2 +- 2 files changed, 2 insertions(+), 1 deletion(-) diff --git a/src/results/existing_boiler.jl b/src/results/existing_boiler.jl index 2b81a9e4d..3fdac87b6 100644 --- a/src/results/existing_boiler.jl +++ b/src/results/existing_boiler.jl @@ -1,6 +1,7 @@ # REopt®, Copyright (c) Alliance for Sustainable Energy, LLC. See also https://github.com/NREL/REopt.jl/blob/master/LICENSE. """ `ExistingBoiler` results keys: +- `size_mmbtu_per_hour` - `fuel_consumption_series_mmbtu_per_hour` - `annual_fuel_consumption_mmbtu` - `thermal_production_series_mmbtu_per_hour` diff --git a/src/results/proforma.jl b/src/results/proforma.jl index c04515743..0af40e575 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -352,7 +352,7 @@ function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_nam capital_cost = new_kw * tech.installed_cost_per_kw end - # owner is responsible for both new and existing PV (or Generator) maintenance in optimal case + # owner is responsible for only new technologies operating and maintenance cost in optimal case # CHP doesn't have existing CHP, and it has different O&M cost parameters if tech_name == "CHP" hours_operating = sum(results["CHP"]["electric_production_series_kw"] .> 0.0) / (8760 * p.s.settings.time_steps_per_hour) From facb0a0f5870b40558a61591f16991c2018ff565 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Fri, 6 Sep 2024 17:13:57 -0600 Subject: [PATCH 230/266] Fuel costs are all paid for by offtaker for 3rd party in proforma.jl --- src/results/proforma.jl | 40 +++++++++++++++------------------------- 1 file changed, 15 insertions(+), 25 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 0af40e575..d8b8d61b9 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -8,6 +8,8 @@ mutable struct Metrics federal_itc::Float64 om_series::Array{Float64, 1} om_series_bau::Array{Float64, 1} + fuel_cost_series::Array{Float64, 1} + fuel_cost_series_bau::Array{Float64, 1} total_pbi::Array{Float64, 1} total_pbi_bau::Array{Float64, 1} total_depreciation::Array{Float64, 1} @@ -52,7 +54,7 @@ function proforma_results(p::REoptInputs, d::Dict) third_party = p.s.financial.third_party_ownership # Create placeholder variables to store summed totals across all relevant techs - m = Metrics(0, zeros(years), zeros(years), zeros(years), zeros(years), zeros(years), 0) + m = Metrics(0, zeros(years), zeros(years), zeros(years), zeros(years), zeros(years), zeros(years), zeros(years), 0) # calculate PV o+m costs, incentives, and depreciation for pv in p.s.pvs @@ -119,10 +121,10 @@ function proforma_results(p::REoptInputs, d::Dict) annual_om_bau = -1 * fixed_and_var_om_bau end - m.om_series += escalate_fuel(annual_fuel, p.s.financial.generator_fuel_cost_escalation_rate_fraction) m.om_series += escalate_om(annual_om) - m.om_series_bau += escalate_fuel(annual_fuel_bau, p.s.financial.generator_fuel_cost_escalation_rate_fraction) + m.fuel_cost_series += escalate_fuel(annual_fuel, p.s.financial.generator_fuel_cost_escalation_rate_fraction) m.om_series_bau += escalate_om(annual_om_bau) + m.fuel_cost_series_bau += escalate_fuel(annual_fuel_bau, p.s.financial.generator_fuel_cost_escalation_rate_fraction) end # calculate CHP o+m costs, incentives, and depreciation @@ -134,7 +136,7 @@ function proforma_results(p::REoptInputs, d::Dict) # the optional installed_cost inputs assume net present cost so no option for MACRS or incentives if "ExistingBoiler" in keys(d) && d["ExistingBoiler"]["size_mmbtu_per_hour"] > 0 fuel_cost = d["ExistingBoiler"]["year_one_fuel_cost_before_tax"] - m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.existing_boiler_fuel_cost_escalation_rate_fraction) + m.fuel_cost_series += escalate_fuel(-1 * fuel_cost, p.s.financial.existing_boiler_fuel_cost_escalation_rate_fraction) var_om = 0.0 fixed_om = 0.0 annual_om = -1 * (var_om + fixed_om) @@ -142,7 +144,7 @@ function proforma_results(p::REoptInputs, d::Dict) # BAU ExistingBoiler fuel_cost_bau = d["ExistingBoiler"]["year_one_fuel_cost_before_tax_bau"] - m.om_series_bau += escalate_fuel(-1 * fuel_cost_bau, p.s.financial.existing_boiler_fuel_cost_escalation_rate_fraction) + m.fuel_cost_series_bau += escalate_fuel(-1 * fuel_cost_bau, p.s.financial.existing_boiler_fuel_cost_escalation_rate_fraction) var_om_bau = 0.0 fixed_om_bau = 0.0 annual_om_bau = -1 * (var_om_bau + fixed_om_bau) @@ -152,7 +154,7 @@ function proforma_results(p::REoptInputs, d::Dict) # calculate (new) Boiler o+m costs and depreciation (no incentives currently, other than MACRS) if "Boiler" in keys(d) && d["Boiler"]["size_mmbtu_per_hour"] > 0 fuel_cost = d["Boiler"]["year_one_fuel_cost_before_tax"] - m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.boiler_fuel_cost_escalation_rate_fraction) + m.fuel_cost_series += escalate_fuel(-1 * fuel_cost, p.s.financial.boiler_fuel_cost_escalation_rate_fraction) var_om = p.s.boiler.om_cost_per_kwh * d["Boiler"]["annual_thermal_production_mmbtu"] * KWH_PER_MMBTU fixed_om = p.s.boiler.om_cost_per_kw * d["Boiler"]["size_mmbtu_per_hour"] * KWH_PER_MMBTU annual_om = -1 * (var_om + fixed_om) @@ -209,7 +211,7 @@ function proforma_results(p::REoptInputs, d::Dict) total_operating_expenses = m.om_series tax_rate_fraction = p.s.financial.owner_tax_rate_fraction else - total_operating_expenses = electricity_bill_series + export_credit_series + m.om_series + total_operating_expenses = electricity_bill_series + export_credit_series + m.om_series + m.fuel_cost_series tax_rate_fraction = p.s.financial.offtaker_tax_rate_fraction end @@ -257,20 +259,8 @@ function proforma_results(p::REoptInputs, d::Dict) annual_income_from_host_series = repeat([-1 * r["annualized_payment_to_third_party"]], years) - if "Generator" in keys(d) && d["Generator"]["size_kw"] > 0 - generator_fuel_cost_series = escalate_om(-1 * d["Generator"]["year_one_fuel_cost_before_tax"]) - if p.s.generator.existing_kw > 0 - existing_genertor_fuel_cost_series = escalate_om(-1 * d["Generator"]["year_one_fuel_cost_before_tax_bau"]) - else - existing_genertor_fuel_cost_series = zeros(years) - end - else - existing_genertor_fuel_cost_series = zeros(years) - generator_fuel_cost_series = zeros(years) - end - net_energy_costs = -electricity_bill_series_bau - export_credit_series_bau + electricity_bill_series + - export_credit_series + annual_income_from_host_series - existing_genertor_fuel_cost_series + - generator_fuel_cost_series + net_energy_costs = -electricity_bill_series_bau - export_credit_series_bau - m.fuel_cost_series_bau + electricity_bill_series + + export_credit_series + annual_income_from_host_series + m.fuel_cost_series if p.s.financial.owner_tax_rate_fraction > 0 deductable_net_energy_costs = copy(net_energy_costs) @@ -279,16 +269,16 @@ function proforma_results(p::REoptInputs, d::Dict) end r["offtaker_annual_free_cashflows"] = append!([0.0], - electricity_bill_series + export_credit_series + generator_fuel_cost_series + annual_income_from_host_series + electricity_bill_series + export_credit_series + m.fuel_cost_series + annual_income_from_host_series ) r["offtaker_annual_free_cashflows_bau"] = append!([0.0], - electricity_bill_series_bau + export_credit_series_bau + existing_genertor_fuel_cost_series + electricity_bill_series_bau + export_credit_series_bau + m.fuel_cost_series_bau ) else # get cumulative cashflow for offtaker electricity_bill_series_bau = escalate_elec(d["ElectricTariff"]["year_one_bill_before_tax_bau"]) export_credit_series_bau = escalate_elec(-d["ElectricTariff"]["year_one_export_benefit_before_tax_bau"]) - total_operating_expenses_bau = electricity_bill_series_bau + export_credit_series_bau + m.om_series_bau + total_operating_expenses_bau = electricity_bill_series_bau + export_credit_series_bau + m.om_series_bau + m.fuel_cost_series_bau total_cash_incentives_bau = m.total_pbi_bau * (1 - p.s.financial.offtaker_tax_rate_fraction) if p.s.financial.offtaker_tax_rate_fraction > 0 @@ -372,7 +362,7 @@ function update_metrics(m::Metrics, p::REoptInputs, tech::AbstractTech, tech_nam if tech_name == "CHP" escalate_fuel(val, esc_rate) = [val * (1 + esc_rate)^yr for yr in 1:years] fuel_cost = results["CHP"]["year_one_fuel_cost_before_tax"] - m.om_series += escalate_fuel(-1 * fuel_cost, p.s.financial.chp_fuel_cost_escalation_rate_fraction) + m.fuel_cost_series += escalate_fuel(-1 * fuel_cost, p.s.financial.chp_fuel_cost_escalation_rate_fraction) end # incentive calculations, in the spreadsheet utility incentives are applied first From 0c60b7f3db66d4758b5ea37caaf7039d8c2bde57 Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Sat, 7 Sep 2024 09:19:07 -0600 Subject: [PATCH 231/266] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 546b09f94..2e25186d5 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -31,12 +31,14 @@ Classify the change according to the following categories: - Updated/specified User-Agent header of "REopt.jl" for PVWatts and Wind Toolkit API requests; default before was "HTTP.jl"; this allows specific tracking of REopt.jl usage which call PVWatts and Wind Toolkit through api.data.gov. - Improves DRY coding by replacing multiple instances of the same chunks of code for MACRS deprecation and CHP capital cost into functions that are now in financial.jl. - Simplifies the CHP sizing test to avoid a ~30 minute solve time, by avoiding the fuel burn y-intercept binaries which come with differences between full-load and part-load efficiency. +- For third party analysis proforma.jl metrics, O&M cost for existing Generator is now kept with offtaker, not the owner/developer ### Fixed - Proforma calcs including "simple" payback and IRR for thermal techs/scenarios. - The operating costs of fuel and O&M were missing for all thermal techs such as ExistingBoiler, CHP, and others; this adds those sections of code to properly calculate the operating costs. - Added a test to validate the simple payback calculation with CHP (and ExistingBoiler) and checks the REopt result value against a spreadsheet proforma calculation (see Bill's spreadsheet). - Added a couple of missing techs for the initial capital cost calculation in financial.jl. - An issue with setup_boiler_inputs in reopt_inputs.jl. +- Fuel costs in proforma.jl were not consistent with the optimization costs, so that was corrected so that they are only added to the offtaker cashflows and not the owner/developer cashflows for third party. ## v0.47.2 ### Fixed From 9a2e96a9987aa990b36ce9338e490818b6a67baa Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 9 Sep 2024 09:20:59 -0600 Subject: [PATCH 232/266] Add GHP-to-load outputs --- src/results/ghp.jl | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/results/ghp.jl b/src/results/ghp.jl index 3e315f403..6ee06f31d 100644 --- a/src/results/ghp.jl +++ b/src/results/ghp.jl @@ -11,6 +11,9 @@ GHP results: - `size_heat_pump_ton` Total heat pump capacity [ton] - `space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour` - `cooling_thermal_load_reduction_with_ghp_ton` +- `thermal_to_space_heating_load_series_mmbtu_per_hour` +- `thermal_to_dhw_load_series_mmbtu_per_hour` +- `thermal_to_load_series_ton` """ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @@ -40,11 +43,21 @@ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") sum(p.cooling_thermal_load_reduction_with_ghp_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options)) r["cooling_thermal_load_reduction_with_ghp_ton"] = round.(value.(CoolingThermalReductionWithGHP) ./ KWH_THERMAL_PER_TONHOUR, digits=3) r["ghx_residual_value_present_value"] = value(m[:ResidualGHXCapCost]) + r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = d["HeatingLoad"]["space_heating_thermal_load_series_mmbtu_per_hour"] + r["thermal_to_load_series_ton"] = d["CoolingLoad"]["load_series_ton"] + if p.s.ghp_option_list[ghp_option_chosen].can_serve_dhw + r["thermal_to_dhw_load_series_mmbtu_per_hour"] = d["HeatingLoad"]["dhw_thermal_load_series_mmbtu_per_hour"] + else + r["thermal_to_dhw_load_series_mmbtu_per_hour"] = zeros(length(p.time_steps)) + end else r["ghpghx_chosen_outputs"] = Dict() r["space_heating_thermal_load_reduction_with_ghp_mmbtu_per_hour"] = zeros(length(p.time_steps)) r["cooling_thermal_load_reduction_with_ghp_ton"] = zeros(length(p.time_steps)) r["ghx_residual_value_present_value"] = 0.0 + r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = zeros(length(p.time_steps)) + r["thermal_to_load_series_ton"] = zeros(length(p.time_steps)) + r["thermal_to_dhw_load_series_mmbtu_per_hour"] = zeros(length(p.time_steps)) end d["GHP"] = r nothing From 5ef48b371c4e0e54bb0290d35d56a29efee4b636 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 9 Sep 2024 09:21:19 -0600 Subject: [PATCH 233/266] Move heat_cooling_load.jl up in code loading order --- src/REopt.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/REopt.jl b/src/REopt.jl index 14929ba08..5ff756e2f 100644 --- a/src/REopt.jl +++ b/src/REopt.jl @@ -182,6 +182,7 @@ include("results/thermal_storage.jl") include("results/outages.jl") include("results/wind.jl") include("results/electric_load.jl") +include("results/heating_cooling_load.jl") include("results/existing_boiler.jl") include("results/boiler.jl") include("results/existing_chiller.jl") @@ -192,7 +193,6 @@ include("results/ghp.jl") include("results/steam_turbine.jl") include("results/electric_heater.jl") include("results/ashp.jl") -include("results/heating_cooling_load.jl") include("core/reopt.jl") include("core/reopt_multinode.jl") From 5e269b8d1171516f3297e8fb1d5f0f7b757db97b Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 9 Sep 2024 09:37:48 -0600 Subject: [PATCH 234/266] Reduce GHP thermal produced by the thermal efficiency reductions --- src/results/ghp.jl | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/results/ghp.jl b/src/results/ghp.jl index 6ee06f31d..a9a8a01a7 100644 --- a/src/results/ghp.jl +++ b/src/results/ghp.jl @@ -43,8 +43,8 @@ function add_ghp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") sum(p.cooling_thermal_load_reduction_with_ghp_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options)) r["cooling_thermal_load_reduction_with_ghp_ton"] = round.(value.(CoolingThermalReductionWithGHP) ./ KWH_THERMAL_PER_TONHOUR, digits=3) r["ghx_residual_value_present_value"] = value(m[:ResidualGHXCapCost]) - r["thermal_to_space_heating_load_series_mmbtu_per_hour"] = d["HeatingLoad"]["space_heating_thermal_load_series_mmbtu_per_hour"] - r["thermal_to_load_series_ton"] = d["CoolingLoad"]["load_series_ton"] + 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"] + r["thermal_to_load_series_ton"] = d["CoolingLoad"]["load_series_ton"] .- r["cooling_thermal_load_reduction_with_ghp_ton"] if p.s.ghp_option_list[ghp_option_chosen].can_serve_dhw r["thermal_to_dhw_load_series_mmbtu_per_hour"] = d["HeatingLoad"]["dhw_thermal_load_series_mmbtu_per_hour"] else From aceecf319cdf4ade6a870ca7bd338b3bf05d6c6e Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 9 Sep 2024 14:21:17 -0600 Subject: [PATCH 235/266] Add site.outdoor_air_temp_degF to Site struct --- src/core/scenario.jl | 24 ++++++++++++------------ src/core/site.jl | 5 ++++- 2 files changed, 16 insertions(+), 13 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index c67567a11..7aee9dd25 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -482,7 +482,8 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # Call PVWatts for hourly dry-bulb outdoor air temperature ambient_temp_degF = [] - if !haskey(d["GHP"]["ghpghx_inputs"][1], "ambient_temperature_f") || isempty(d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"]) + if (!haskey(d["GHP"]["ghpghx_inputs"][1], "ambient_temperature_f") || isempty(d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"])) && + isnothing(site.outdoor_air_temp_degF) # 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) @@ -494,14 +495,14 @@ function Scenario(d::Dict; flex_hvac_from_json=false) else pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - ambient_temp_degF = ambient_temp_celsius * 1.8 .+ 32.0 - else - ambient_temp_degF = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"] + site.outdoor_air_temp_degF = ambient_temp_celsius * 1.8 .+ 32.0 + elseif isnothing(site.outdoor_air_temp_degF) + site.outdoor_air_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_temp_degF + d["GHP"]["ghpghx_inputs"][i]["ambient_temperature_f"] = site.outdoor_air_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 get(d["GHP"], "can_serve_dhw", false) # This is assuming the default stays false @@ -672,7 +673,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celsius) + if isnothing(site.outdoor_air_temp_degF) if !isempty(pvs) for pv in pvs pv.production_factor_series, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, @@ -683,10 +684,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # if PV is not evaluated, call PVWatts to get ambient temperature series pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end + site.outdoor_air_temp_degF = (9/5 .* ambient_temp_celsius) .+ 32 end - ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - d["ASHPSpaceHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHPSpaceHeater"]["ambient_temp_degF"] = site.outdoor_air_temp_degF d["ASHPSpaceHeater"]["heating_load"] = space_heating_load.loads_kw d["ASHPSpaceHeater"]["cooling_load"] = cooling_load.loads_kw_thermal @@ -713,7 +714,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(ambient_temp_celsius) + if isnothing(site.outdoor_air_temp_degF) if !isempty(pvs) for pv in pvs pv.production_factor_series, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, @@ -724,11 +725,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # if PV is not evaluated, call PVWatts to get ambient temperature series pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end + site.outdoor_air_temp_degF = (9/5 .* ambient_temp_celsius) .+ 32 end - - ambient_temp_fahrenheit = (9/5 .* ambient_temp_celsius) .+ 32 - d["ASHPWaterHeater"]["ambient_temp_degF"] = ambient_temp_fahrenheit + d["ASHPWaterHeater"]["ambient_temp_degF"] = site.outdoor_air_temp_degF d["ASHPWaterHeater"]["heating_load"] = dhw_load.loads_kw ashp_wh = ASHPWaterHeater(;dictkeys_tosymbols(d["ASHPWaterHeater"])...) diff --git a/src/core/site.jl b/src/core/site.jl index b9185cadb..0ecb42752 100644 --- a/src/core/site.jl +++ b/src/core/site.jl @@ -19,6 +19,7 @@ Inputs related to the physical location: renewable_electricity_max_fraction::Union{Float64, Nothing} = nothing, include_exported_elec_emissions_in_total::Bool = true, include_exported_renewable_electricity_in_total::Bool = true, + outdoor_air_temperature_degF::Union{Nothing, Array{<:Real,1}} = nothing, ``` """ mutable struct Site @@ -38,6 +39,7 @@ mutable struct Site renewable_electricity_max_fraction include_exported_elec_emissions_in_total include_exported_renewable_electricity_in_total + outdoor_air_temperature_degF node # TODO validate that multinode Sites do not share node numbers? Or just raise warning function Site(; latitude::Real, @@ -54,6 +56,7 @@ mutable struct Site renewable_electricity_max_fraction::Union{Float64, Nothing} = nothing, include_exported_elec_emissions_in_total::Bool = true, include_exported_renewable_electricity_in_total::Bool = true, + outdoor_air_temperature_degF::Union{Nothing, Array{<:Real,1}} = nothing, node::Int = 1, ) invalid_args = String[] @@ -77,6 +80,6 @@ mutable struct Site CO2_emissions_reduction_max_fraction, bau_emissions_lb_CO2_per_year, bau_grid_emissions_lb_CO2_per_year, renewable_electricity_min_fraction, renewable_electricity_max_fraction, include_exported_elec_emissions_in_total, - include_exported_renewable_electricity_in_total, node) + include_exported_renewable_electricity_in_total, outdoor_air_temperature_degF, node) end end \ No newline at end of file From 2aeffe33a12d017584c501aa4356a8586eaa9ebf Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 9 Sep 2024 14:52:58 -0600 Subject: [PATCH 236/266] Fix change temp to temperature in reference to Site field name --- src/core/scenario.jl | 22 +++++++++++----------- 1 file changed, 11 insertions(+), 11 deletions(-) diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 7aee9dd25..5e5c83c42 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -483,7 +483,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # Call PVWatts for hourly dry-bulb outdoor air temperature ambient_temp_degF = [] if (!haskey(d["GHP"]["ghpghx_inputs"][1], "ambient_temperature_f") || isempty(d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"])) && - isnothing(site.outdoor_air_temp_degF) + isnothing(site.outdoor_air_temperature_degF) # 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) @@ -495,14 +495,14 @@ function Scenario(d::Dict; flex_hvac_from_json=false) else pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - site.outdoor_air_temp_degF = ambient_temp_celsius * 1.8 .+ 32.0 - elseif isnothing(site.outdoor_air_temp_degF) - site.outdoor_air_temp_degF = d["GHP"]["ghpghx_inputs"][1]["ambient_temperature_f"] + site.outdoor_air_temperature_degF = ambient_temp_celsius * 1.8 .+ 32.0 + elseif isnothing(site.outdoor_air_temperature_degF) + site.outdoor_air_temperature_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"] = site.outdoor_air_temp_degF + d["GHP"]["ghpghx_inputs"][i]["ambient_temperature_f"] = site.outdoor_air_temperature_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 get(d["GHP"], "can_serve_dhw", false) # This is assuming the default stays false @@ -673,7 +673,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(site.outdoor_air_temp_degF) + if isnothing(site.outdoor_air_temperature_degF) if !isempty(pvs) for pv in pvs pv.production_factor_series, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, @@ -684,10 +684,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # if PV is not evaluated, call PVWatts to get ambient temperature series pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - site.outdoor_air_temp_degF = (9/5 .* ambient_temp_celsius) .+ 32 + site.outdoor_air_temperature_degF = (9/5 .* ambient_temp_celsius) .+ 32 end - d["ASHPSpaceHeater"]["ambient_temp_degF"] = site.outdoor_air_temp_degF + d["ASHPSpaceHeater"]["ambient_temp_degF"] = site.outdoor_air_temperature_degF d["ASHPSpaceHeater"]["heating_load"] = space_heating_load.loads_kw d["ASHPSpaceHeater"]["cooling_load"] = cooling_load.loads_kw_thermal @@ -714,7 +714,7 @@ function Scenario(d::Dict; flex_hvac_from_json=false) end # If PV is evaluated, get ambient temperature series from PVWatts and assign PV production factor - if isnothing(site.outdoor_air_temp_degF) + if isnothing(site.outdoor_air_temperature_degF) if !isempty(pvs) for pv in pvs pv.production_factor_series, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; tilt=pv.tilt, azimuth=pv.azimuth, module_type=pv.module_type, @@ -725,10 +725,10 @@ function Scenario(d::Dict; flex_hvac_from_json=false) # if PV is not evaluated, call PVWatts to get ambient temperature series pv_prodfactor, ambient_temp_celsius = call_pvwatts_api(site.latitude, site.longitude; time_steps_per_hour=settings.time_steps_per_hour) end - site.outdoor_air_temp_degF = (9/5 .* ambient_temp_celsius) .+ 32 + site.outdoor_air_temperature_degF = (9/5 .* ambient_temp_celsius) .+ 32 end - d["ASHPWaterHeater"]["ambient_temp_degF"] = site.outdoor_air_temp_degF + d["ASHPWaterHeater"]["ambient_temp_degF"] = site.outdoor_air_temperature_degF d["ASHPWaterHeater"]["heating_load"] = dhw_load.loads_kw ashp_wh = ASHPWaterHeater(;dictkeys_tosymbols(d["ASHPWaterHeater"])...) From f774972823b73ab7835fe52293d4b36d0008cb70 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 10 Sep 2024 21:32:28 -0600 Subject: [PATCH 237/266] Make Generator O&M and fuel cashflows consistent between third party and direct --- src/results/proforma.jl | 15 ++++----------- 1 file changed, 4 insertions(+), 11 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index d8b8d61b9..6ab4f05e2 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -107,19 +107,12 @@ function proforma_results(p::REoptInputs, d::Dict) d["Generator"]["year_one_variable_om_cost_before_tax_bau"] year_one_fuel_cost_bau = d["Generator"]["year_one_fuel_cost_before_tax_bau"] end - if !third_party - annual_fuel = -1 * d["Generator"]["year_one_fuel_cost_before_tax"] - annual_om = -1 * fixed_and_var_om - annual_fuel_bau = -1 * year_one_fuel_cost_bau - annual_om_bau = -1 * fixed_and_var_om_bau - else - annual_fuel = 0.0 - annual_om = -1 * fixed_and_var_om + annual_fuel = -1 * d["Generator"]["year_one_fuel_cost_before_tax"] + annual_om = -1 * fixed_and_var_om - annual_fuel_bau = 0.0 - annual_om_bau = -1 * fixed_and_var_om_bau - end + annual_fuel_bau = -1 * year_one_fuel_cost_bau + annual_om_bau = -1 * fixed_and_var_om_bau m.om_series += escalate_om(annual_om) m.fuel_cost_series += escalate_fuel(annual_fuel, p.s.financial.generator_fuel_cost_escalation_rate_fraction) From 2c40d69a33a4c409f6f7ce7a680b9091c71a7f88 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Tue, 10 Sep 2024 21:33:17 -0600 Subject: [PATCH 238/266] Remove unused proforma code --- src/results/proforma.jl | 9 --------- 1 file changed, 9 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 6ab4f05e2..53d41252e 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -252,15 +252,6 @@ function proforma_results(p::REoptInputs, d::Dict) annual_income_from_host_series = repeat([-1 * r["annualized_payment_to_third_party"]], years) - net_energy_costs = -electricity_bill_series_bau - export_credit_series_bau - m.fuel_cost_series_bau + electricity_bill_series + - export_credit_series + annual_income_from_host_series + m.fuel_cost_series - - if p.s.financial.owner_tax_rate_fraction > 0 - deductable_net_energy_costs = copy(net_energy_costs) - else - deductable_net_energy_costs = zeros(years) - end - r["offtaker_annual_free_cashflows"] = append!([0.0], electricity_bill_series + export_credit_series + m.fuel_cost_series + annual_income_from_host_series ) From e97373d8b79128f8ad7c4baaefbcd36b860fd15b Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Thu, 12 Sep 2024 22:36:08 -0600 Subject: [PATCH 239/266] Update expected Solar dataset test from updated NSRDB --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 5176ecab0..af6392c5a 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,7 +54,7 @@ else # run HiGHS tests latitude, longitude = 3.8603988398663125, 11.528880303663136 radius = 0 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "intl" + @test dataset ≈ "nsrdb" # 4. Fairbanks, AK site = "Fairbanks" From f8829a1339d78601960798dd95aaf8c72ff12fb5 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Thu, 12 Sep 2024 22:41:51 -0600 Subject: [PATCH 240/266] Add CapEx and OpEx for ASHP for proforma results metrics --- src/results/financial.jl | 8 ++++++++ src/results/proforma.jl | 26 ++++++++++++++++++++++++++ 2 files changed, 34 insertions(+) diff --git a/src/results/financial.jl b/src/results/financial.jl index eee14740b..9f40459fc 100644 --- a/src/results/financial.jl +++ b/src/results/financial.jl @@ -191,6 +191,14 @@ function initial_capex(m::JuMP.AbstractModel, p::REoptInputs; _n="") end end + if "ASHPSpaceHeater" in p.techs.all + initial_capex += p.s.ashp.installed_cost_per_kw * value.(m[Symbol("dvPurchaseSize"*_n)])["ASHPSpaceHeater"] + end + + if "ASHPWaterHeater" in p.techs.all + initial_capex += p.s.ashp_wh.installed_cost_per_kw * value.(m[Symbol("dvPurchaseSize"*_n)])["ASHPWaterHeater"] + end + return initial_capex end diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 53d41252e..df4065d1f 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -195,6 +195,32 @@ function proforma_results(p::REoptInputs, d::Dict) update_ghp_metrics(m, p, p.s.ghp_option_list[d["GHP"]["ghp_option_chosen"]], "GHP", d, third_party) end + # calculate ASHPSpaceHeater o+m costs and depreciation (no incentives currently, other than MACRS) + if "ASHPSpaceHeater" in keys(d) && d["ASHPSpaceHeater"]["size_ton"] > 0 + fixed_om = p.s.ashp.om_cost_per_kw * KWH_THERMAL_PER_TONHOUR * d["ASHPSpaceHeater"]["size_ton"] + annual_om = -1 * (fixed_om) + m.om_series += escalate_om(annual_om) + + # Depreciation + if p.s.ashp.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, p.s.ashp) + m.total_depreciation += depreciation_schedule + end + end + + # calculate ASHPWaterHeater o+m costs and depreciation (no incentives currently, other than MACRS) + if "ASHPWaterHeater" in keys(d) && d["ASHPWaterHeater"]["size_ton"] > 0 + fixed_om = p.s.ashp.om_cost_per_kw * KWH_THERMAL_PER_TONHOUR * d["ASHPWaterHeater"]["size_ton"] + annual_om = -1 * (fixed_om) + m.om_series += escalate_om(annual_om) + + # Depreciation + if p.s.ashp.macrs_option_years in [5 ,7] + depreciation_schedule = get_depreciation_schedule(p, p.s.ashp_wh) + m.total_depreciation += depreciation_schedule + end + end + # Optimal Case calculations electricity_bill_series = escalate_elec(d["ElectricTariff"]["year_one_bill_before_tax"]) export_credit_series = escalate_elec(-d["ElectricTariff"]["year_one_export_benefit_before_tax"]) From 8f948c7288831d163755ccdf2eca2e075b115fd5 Mon Sep 17 00:00:00 2001 From: Zolan Date: Mon, 16 Sep 2024 20:47:53 -0600 Subject: [PATCH 241/266] bring back net cooling load impact on electric load --- src/constraints/load_balance.jl | 2 ++ 1 file changed, 2 insertions(+) diff --git a/src/constraints/load_balance.jl b/src/constraints/load_balance.jl index 859afdce8..bdb93fa24 100644 --- a/src/constraints/load_balance.jl +++ b/src/constraints/load_balance.jl @@ -15,6 +15,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) + p.s.electric_load.loads_kw[ts] + - p.s.cooling_load.loads_kw_thermal[ts] / p.cooling_cop["ExistingChiller"][ts] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) else @@ -30,6 +31,7 @@ function add_elec_load_balance_constraints(m, p; _n="") + sum(m[Symbol("dvCoolingProduction"*_n)][t, ts] / p.cooling_cop[t][ts] for t in setdiff(p.techs.cooling,p.techs.ghp)) + sum(m[Symbol("dvHeatingProduction"*_n)][t, q, ts] / p.heating_cop[t][ts] for q in p.heating_loads, t in p.techs.electric_heater) + p.s.electric_load.loads_kw[ts] + - p.s.cooling_load.loads_kw_thermal[ts] / p.cooling_cop["ExistingChiller"][ts] + sum(p.ghp_electric_consumption_kw[g,ts] * m[Symbol("binGHP"*_n)][g] for g in p.ghp_options) ) end From d65f1996ae2368a8667ad6dc8af22a2b712f264a Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 17 Sep 2024 14:15:58 -0600 Subject: [PATCH 242/266] update cop to cooling_cop in MPCInputs --- src/mpc/inputs.jl | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/src/mpc/inputs.jl b/src/mpc/inputs.jl index 9bf13a105..3aeece174 100644 --- a/src/mpc/inputs.jl +++ b/src/mpc/inputs.jl @@ -20,7 +20,7 @@ struct MPCInputs <: AbstractInputs ratchets::UnitRange techs_by_exportbin::DenseAxisArray{Array{String,1}} # indexed on [:NEM, :WHL] export_bins_by_tech::Dict{String, Array{Symbol, 1}} - cop::Dict{String, Float64} # (techs.cooling) + cooling_cop::Dict{String, Array{Float64,1}} # (techs.cooling, time_steps) thermal_cop::Dict{String, Float64} # (techs.absorption_chiller) ghp_options::UnitRange{Int64} # Range of the number of GHP options fuel_cost_per_kwh::Dict{String, AbstractArray} # Fuel cost array for all time_steps @@ -66,7 +66,7 @@ function MPCInputs(s::MPCScenario) # TODO implement export bins by tech (rather than assuming that all techs share the export_bins) #Placeholder COP because the REopt model expects it - cop = Dict("ExistingChiller" => s.cooling_load.cop) + cooling_cop = Dict("ExistingChiller" => ones(length(s.electric_load.loads_kw)) .* s.cooling_load.cop) thermal_cop = Dict{String, Float64}() ghp_options = 1:0 heating_loads = Vector{String}() @@ -92,7 +92,7 @@ function MPCInputs(s::MPCScenario) 1:length(s.electric_tariff.tou_demand_ratchet_time_steps), # ratchets techs_by_exportbin, export_bins_by_tech, - cop, + cooling_cop, thermal_cop, ghp_options, # s.site.min_resil_time_steps, From 0a21a31b5241a2d4ba73f13d1b6011147821171a Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 17 Sep 2024 14:27:48 -0600 Subject: [PATCH 243/266] incorporate waste heat in calculation of ASHP results --- src/results/ashp.jl | 24 +++++++++++++++++------- 1 file changed, 17 insertions(+), 7 deletions(-) diff --git a/src/results/ashp.jl b/src/results/ashp.jl index 86da96927..9a34682c0 100644 --- a/src/results/ashp.jl +++ b/src/results/ashp.jl @@ -47,15 +47,20 @@ function add_ashp_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") @expression(m, ASHPToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) end r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPToHotTESKW) / KWH_PER_MMBTU, digits=3) - + @expression(m, ASHPToWaste[ts in p.time_steps], + sum(m[:dvProductionToWaste]["ASHPSpaceHeater", q, ts] for q in p.heating_loads) + ) + @expression(m, ASHPToWasteByQualityKW[q in p.heating_loads, ts in p.time_steps], + m[:dvProductionToWaste]["ASHPSpaceHeater",q,ts] + ) @expression(m, ASHPToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHPSpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] + sum(m[:dvHeatingProduction]["ASHPSpaceHeater", q, ts] for q in p.heating_loads) - ASHPToHotTESKW[ts] - ASHPToWaste[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPToLoad) ./ KWH_PER_MMBTU, digits=3) if "SpaceHeating" in p.heating_loads && p.s.ashp.can_serve_space_heating @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHPSpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] + m[:dvHeatingProduction]["ASHPSpaceHeater","SpaceHeating",ts] - ASHPToHotTESByQualityKW["SpaceHeating",ts] - ASHPToWasteByQualityKW["SpaceHeating",ts] ) else @expression(m, ASHPToSpaceHeatingKW[ts in p.time_steps], 0.0) @@ -148,15 +153,20 @@ function add_ashp_wh_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n= @expression(m, ASHPWHToHotTESByQualityKW[q in p.heating_loads, ts in p.time_steps], 0.0) end r["thermal_to_storage_series_mmbtu_per_hour"] = round.(value.(ASHPWHToHotTESKW) / KWH_PER_MMBTU, digits=3) - - @expression(m, ASHPWHToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ASHPWaterHeater", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] + @expression(m, ASHPWHToWaste[ts in p.time_steps], + sum(m[:dvProductionToWaste]["ASHPWaterHeater", q, ts] for q in p.heating_loads) + ) + @expression(m, ASHPWHToWasteByQualityKW[q in p.heating_loads, ts in p.time_steps], + m[:dvProductionToWaste]["ASHPWaterHeater",q,ts] + ) + @expression(m, ASHPWHToLoad[ts in p.time_steps], + sum(m[:dvHeatingProduction]["ASHPWaterHeater", q, ts] for q in p.heating_loads) - ASHPWHToHotTESKW[ts] - ASHPWHToWaste[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ASHPWHToLoad) ./ KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.ashp_wh.can_serve_dhw @expression(m, ASHPWHToDHWKW[ts in p.time_steps], - m[:dvHeatingProduction]["ASHPWaterHeater","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] + m[:dvHeatingProduction]["ASHPWaterHeater","DomesticHotWater",ts] - ASHPWHToHotTESByQualityKW["DomesticHotWater",ts] - ASHPWHToWasteByQualityKW["DomesticHotWater",ts] ) else @expression(m, ASHPWHToDHWKW[ts in p.time_steps], 0.0) From 93d70c7d3352f3f7335594d92b877a30faae086c Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 17 Sep 2024 14:28:09 -0600 Subject: [PATCH 244/266] incorporate waste heat in electric heater results --- src/results/electric_heater.jl | 15 +++++++++++---- 1 file changed, 11 insertions(+), 4 deletions(-) diff --git a/src/results/electric_heater.jl b/src/results/electric_heater.jl index be16ad2e1..024804423 100644 --- a/src/results/electric_heater.jl +++ b/src/results/electric_heater.jl @@ -54,14 +54,21 @@ function add_electric_heater_results(m::JuMP.AbstractModel, p::REoptInputs, d::D end r["thermal_to_steamturbine_series_mmbtu_per_hour"] = round.(value.(ElectricHeaterToSteamTurbine) / KWH_PER_MMBTU, digits=3) + @expression(m, ElectricHeaterToWaste[ts in p.time_steps], + sum(m[:dvProductionToWaste]["ElectricHeater", q, ts] for q in p.heating_loads) + ) + @expression(m, ElectricHeaterToWasteByQualityKW[q in p.heating_loads, ts in p.time_steps], + m[:dvProductionToWaste]["ElectricHeater",q,ts] + ) + @expression(m, ElectricHeaterToLoad[ts in p.time_steps], - sum(m[:dvHeatingProduction]["ElectricHeater", q, ts] for q in p.heating_loads) - ElectricHeaterToHotTESKW[ts] - ElectricHeaterToSteamTurbine[ts] + sum(m[:dvHeatingProduction]["ElectricHeater", q, ts] for q in p.heating_loads) - ElectricHeaterToHotTESKW[ts] - ElectricHeaterToSteamTurbine[ts] - ElectricHeaterToWaste[ts] ) r["thermal_to_load_series_mmbtu_per_hour"] = round.(value.(ElectricHeaterToLoad) / KWH_PER_MMBTU, digits=3) if "DomesticHotWater" in p.heating_loads && p.s.electric_heater.can_serve_dhw @expression(m, ElectricHeaterToDHWKW[ts in p.time_steps], - m[:dvHeatingProduction]["ElectricHeater","DomesticHotWater",ts] - ElectricHeaterToHotTESByQualityKW["DomesticHotWater",ts] - ElectricHeaterToSteamTurbineByQuality["DomesticHotWater",ts] + m[:dvHeatingProduction]["ElectricHeater","DomesticHotWater",ts] - ElectricHeaterToHotTESByQualityKW["DomesticHotWater",ts] - ElectricHeaterToSteamTurbineByQuality["DomesticHotWater",ts] - ElectricHeaterToWasteByQualityKW["DomesticHotWater",ts] ) else @expression(m, ElectricHeaterToDHWKW[ts in p.time_steps], 0.0) @@ -70,7 +77,7 @@ function add_electric_heater_results(m::JuMP.AbstractModel, p::REoptInputs, d::D if "SpaceHeating" in p.heating_loads && p.s.electric_heater.can_serve_space_heating @expression(m, ElectricHeaterToSpaceHeatingKW[ts in p.time_steps], - m[:dvHeatingProduction]["ElectricHeater","SpaceHeating",ts] - ElectricHeaterToHotTESByQualityKW["SpaceHeating",ts] - ElectricHeaterToSteamTurbineByQuality["SpaceHeating",ts] + m[:dvHeatingProduction]["ElectricHeater","SpaceHeating",ts] - ElectricHeaterToHotTESByQualityKW["SpaceHeating",ts] - ElectricHeaterToSteamTurbineByQuality["SpaceHeating",ts] - ElectricHeaterToWasteByQualityKW["SpaceHeating",ts] ) else @expression(m, ElectricHeaterToSpaceHeatingKW[ts in p.time_steps], 0.0) @@ -79,7 +86,7 @@ function add_electric_heater_results(m::JuMP.AbstractModel, p::REoptInputs, d::D if "ProcessHeat" in p.heating_loads && p.s.electric_heater.can_serve_process_heat @expression(m, ElectricHeaterToProcessHeatKW[ts in p.time_steps], - m[:dvHeatingProduction]["ElectricHeater","ProcessHeat",ts] - ElectricHeaterToHotTESByQualityKW["ProcessHeat",ts] - ElectricHeaterToSteamTurbineByQuality["ProcessHeat",ts] + m[:dvHeatingProduction]["ElectricHeater","ProcessHeat",ts] - ElectricHeaterToHotTESByQualityKW["ProcessHeat",ts] - ElectricHeaterToSteamTurbineByQuality["ProcessHeat",ts] - ElectricHeaterToWasteByQualityKW["ProcessHeat",ts] ) else @expression(m, ElectricHeaterToProcessHeatKW[ts in p.time_steps], 0.0) From e65383cdcbb25415955c3bbba522360ef45a6ee5 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 18 Sep 2024 07:43:48 -0600 Subject: [PATCH 245/266] Enforce no waste heat for any technology that isn't both electricity- and heat-producing --- src/constraints/thermal_tech_constraints.jl | 10 +++++++++- 1 file changed, 9 insertions(+), 1 deletion(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index b20dd5619..0ede9b78f 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -88,7 +88,15 @@ function add_heating_tech_constraints(m, p; _n="") end end end - # Enfore + + # Enforce no waste heat for any technology that isn't both electricity- and heat-producing + for t in setdiff(p.techs.heating, p.techs.elec) + for q in p.heating_loads + for ts in p.time_steps + fix(m[Symbol("dvProductionToWaste"*_n)][t,q,ts], 0.0, force=true) + end + end + end end function add_heating_cooling_constraints(m, p; _n="") From 77773d66e4ef69f90ce0e8d576886fe73eb04be9 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 18 Sep 2024 09:23:59 -0600 Subject: [PATCH 246/266] update tech set for no waste heat --- src/constraints/thermal_tech_constraints.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 0ede9b78f..ef3fafc36 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -90,7 +90,7 @@ function add_heating_tech_constraints(m, p; _n="") end # Enforce no waste heat for any technology that isn't both electricity- and heat-producing - for t in setdiff(p.techs.heating, p.techs.elec) + for t in setdiff(p.techs.heating, union(p.techs.elec, p.techs.ghp)) for q in p.heating_loads for ts in p.time_steps fix(m[Symbol("dvProductionToWaste"*_n)][t,q,ts], 0.0, force=true) From 8f167336c117179e4bf5b119b3aa0c08d7563021 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Wed, 18 Sep 2024 21:59:27 -0600 Subject: [PATCH 247/266] Remove BAU/existing Generator O&M from Developer/Owner in 3rd Party proforma cashflow Also add BAU O&M to offtaker BAU and Optimal cases --- src/results/proforma.jl | 5 +++-- 1 file changed, 3 insertions(+), 2 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 53d41252e..edcd9857d 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -105,6 +105,7 @@ function proforma_results(p::REoptInputs, d::Dict) if p.s.generator.existing_kw > 0 fixed_and_var_om_bau = d["Generator"]["year_one_fixed_om_cost_before_tax_bau"] + d["Generator"]["year_one_variable_om_cost_before_tax_bau"] + fixed_and_var_om -= fixed_and_var_om_bau year_one_fuel_cost_bau = d["Generator"]["year_one_fuel_cost_before_tax_bau"] end @@ -253,10 +254,10 @@ function proforma_results(p::REoptInputs, d::Dict) annual_income_from_host_series = repeat([-1 * r["annualized_payment_to_third_party"]], years) r["offtaker_annual_free_cashflows"] = append!([0.0], - electricity_bill_series + export_credit_series + m.fuel_cost_series + annual_income_from_host_series + electricity_bill_series + export_credit_series + m.fuel_cost_series + annual_income_from_host_series + m.om_series_bau ) r["offtaker_annual_free_cashflows_bau"] = append!([0.0], - electricity_bill_series_bau + export_credit_series_bau + m.fuel_cost_series_bau + electricity_bill_series_bau + export_credit_series_bau + m.fuel_cost_series_bau + m.om_series_bau ) else # get cumulative cashflow for offtaker From b3fd4bfd10b5a957f44beda75a07c858587c42b7 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Thu, 19 Sep 2024 15:27:11 -0600 Subject: [PATCH 248/266] Add net_capital_cost_without_macrs to enable easier "more simple" payback period calculation --- src/results/proforma.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index edcd9857d..a3b7385cd 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -45,7 +45,8 @@ function proforma_results(p::REoptInputs, d::Dict) "offtaker_annual_free_cashflows_bau" => Float64[], "offtaker_discounted_annual_free_cashflows" => Float64[], "offtaker_discounted_annual_free_cashflows_bau" => Float64[], - "developer_annual_free_cashflows" => Float64[] + "developer_annual_free_cashflows" => Float64[], + "net_capital_costs_without_macrs" => 0.0 ) years = p.s.financial.analysis_years escalate_elec(val) = [-1 * val * (1 + p.s.financial.elec_cost_escalation_rate_fraction)^yr for yr in 1:years] @@ -221,6 +222,7 @@ function proforma_results(p::REoptInputs, d::Dict) total_cash_incentives = m.total_pbi * (1 - tax_rate_fraction) free_cashflow_without_year_zero = m.total_depreciation * tax_rate_fraction + total_cash_incentives + operating_expenses_after_tax free_cashflow_without_year_zero[1] += m.federal_itc + r["net_capital_costs_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc free_cashflow = append!([(-1 * d["Financial"]["initial_capital_costs"]) + m.total_ibi_and_cbi], free_cashflow_without_year_zero) # At this point the logic branches based on third-party ownership or not - see comments From 3c449f52e9d451e5a176a4ccaed0bfbf5142082e Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Thu, 12 Sep 2024 22:36:08 -0600 Subject: [PATCH 249/266] Update expected Solar dataset test from updated NSRDB --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 2e5077abb..a4e3aac3c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,7 +54,7 @@ else # run HiGHS tests latitude, longitude = 3.8603988398663125, 11.528880303663136 radius = 0 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "intl" + @test dataset ≈ "nsrdb" # 4. Fairbanks, AK site = "Fairbanks" From f0dbd1bcbaa4114025a68f045a1160ee651cf85a Mon Sep 17 00:00:00 2001 From: Bill Becker <42586683+Bill-Becker@users.noreply.github.com> Date: Fri, 20 Sep 2024 11:59:37 -0600 Subject: [PATCH 250/266] Update CHANGELOG.md --- CHANGELOG.md | 2 ++ 1 file changed, 2 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2e25186d5..937d81892 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -24,6 +24,8 @@ Classify the change according to the following categories: ### Removed ## Develop 08-09-2024 +### Added +- Ouput which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" ### Changed - Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage. - Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. From ae617e5f27ca682331be35906919cfc6a07e004f Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 20 Sep 2024 17:19:39 -0600 Subject: [PATCH 251/266] update help text --- src/results/proforma.jl | 10 +++++----- 1 file changed, 5 insertions(+), 5 deletions(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index a3b7385cd..88a43c224 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -24,7 +24,7 @@ Recreates the ProForma spreadsheet calculations to get the simple payback period party case), and payment to third party (3rd party case). return Dict( - "simple_payback_years" => 0.0, + "simple_payback_years" => 0.0, # The year in which cumulative net free cashflows become positive. For a third party analysis, the SPP is for the developer. "internal_rate_of_return" => 0.0, "net_present_cost" => 0.0, "annualized_payment_to_third_party" => 0.0, @@ -32,7 +32,8 @@ return Dict( "offtaker_annual_free_cashflows_bau" => Float64[], "offtaker_discounted_annual_free_cashflows" => Float64[], "offtaker_discounted_annual_free_cashflows_bau" => Float64[], - "developer_annual_free_cashflows" => Float64[] + "developer_annual_free_cashflows" => Float64[], + "net_capital_costs_without_macrs" => 0.0 # Initial capital costs after ibi, cbi, and ITC incentives ) """ function proforma_results(p::REoptInputs, d::Dict) @@ -97,9 +98,8 @@ function proforma_results(p::REoptInputs, d::Dict) # calculate Generator o+m costs, incentives, and depreciation if "Generator" in keys(d) && d["Generator"]["size_kw"] > 0 - # In the two party case the developer does not include the fuel cost in their costs - # It is assumed that the offtaker will pay for this at a rate that is not marked up - # to cover developer profits + # In the two party case the developer does not include the fuel cost or O&M costs for existing assets in their costs + # It is assumed that the offtaker will pay for this at a rate that is not marked up to cover developer profits fixed_and_var_om = d["Generator"]["year_one_fixed_om_cost_before_tax"] + d["Generator"]["year_one_variable_om_cost_before_tax"] fixed_and_var_om_bau = 0.0 year_one_fuel_cost_bau = 0.0 From f8b7254d5934c8637ce2a253ce9f8173beee6900 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Fri, 20 Sep 2024 21:15:24 -0600 Subject: [PATCH 252/266] Only remove BAU/Existing Generator O&M from optimal cash flow for third party --- src/results/proforma.jl | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 88a43c224..67567dbda 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -106,7 +106,9 @@ function proforma_results(p::REoptInputs, d::Dict) if p.s.generator.existing_kw > 0 fixed_and_var_om_bau = d["Generator"]["year_one_fixed_om_cost_before_tax_bau"] + d["Generator"]["year_one_variable_om_cost_before_tax_bau"] - fixed_and_var_om -= fixed_and_var_om_bau + if third_party + fixed_and_var_om -= fixed_and_var_om_bau + end year_one_fuel_cost_bau = d["Generator"]["year_one_fuel_cost_before_tax_bau"] end From 9eea0a74e09ac8022b9d27daf9ad87b6e31c7206 Mon Sep 17 00:00:00 2001 From: adfarth Date: Sun, 22 Sep 2024 13:10:29 -0600 Subject: [PATCH 253/266] Update CHANGELOG.md --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 937d81892..19f761ba4 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,7 @@ Classify the change according to the following categories: ## Develop 08-09-2024 ### Added -- Ouput which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" +- Financial output **net_capital_costs_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" ### Changed - Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage. - Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. From 107c118c5211aa5a10d910f0bdae721b0db561ba Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Sun, 22 Sep 2024 15:43:36 -0600 Subject: [PATCH 254/266] Add unaddressable heating load and emissions to outputs --- src/core/heating_cooling_loads.jl | 67 +++++++++++++++++++++++++---- src/results/heating_cooling_load.jl | 6 +++ test/runtests.jl | 13 ++++++ 3 files changed, 77 insertions(+), 9 deletions(-) diff --git a/src/core/heating_cooling_loads.jl b/src/core/heating_cooling_loads.jl index bd769a015..3294286b1 100644 --- a/src/core/heating_cooling_loads.jl +++ b/src/core/heating_cooling_loads.jl @@ -25,6 +25,7 @@ There are many ways in which a DomesticHotWaterLoad can be defined: struct DomesticHotWaterLoad loads_kw::Array{Real, 1} annual_mmbtu::Real + unaddressable_annual_fuel_mmbtu::Real function DomesticHotWaterLoad(; doe_reference_name::String = "", @@ -62,6 +63,7 @@ struct DomesticHotWaterLoad end loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * existing_boiler_efficiency) .* addressable_load_fraction + unaddressable_annual_fuel_mmbtu = sum(fuel_loads_mmbtu_per_hour .* (1 .- addressable_load_fraction)) / time_steps_per_hour if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0 @warn "DomesticHotWaterLoad `fuel_loads_mmbtu_per_hour` was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored." @@ -72,12 +74,14 @@ struct DomesticHotWaterLoad if length(blended_doe_reference_names) > 0 @warn "DomesticHotWaterLoad 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(BuiltInDomesticHotWaterLoad, latitude, longitude, 2017, blended_doe_reference_names, blended_doe_reference_percents, city, annual_mmbtu, monthly_mmbtu, addressable_load_fraction, existing_boiler_efficiency) + unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency) else throw(@error("Cannot construct DomesticHotWaterLoad. You must provide either [fuel_loads_mmbtu_per_hour], [doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city].")) @@ -90,7 +94,8 @@ struct DomesticHotWaterLoad new( loads_kw, - (sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU + (sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU, + unaddressable_annual_fuel_mmbtu ) end end @@ -133,6 +138,7 @@ In this case the values provided for `doe_reference_name`, or `blended_doe_refe struct SpaceHeatingLoad loads_kw::Array{Real, 1} annual_mmbtu::Real + unaddressable_annual_fuel_mmbtu::Real function SpaceHeatingLoad(; doe_reference_name::String = "", @@ -170,6 +176,7 @@ struct SpaceHeatingLoad end loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * existing_boiler_efficiency) .* addressable_load_fraction + unaddressable_annual_fuel_mmbtu = sum(fuel_loads_mmbtu_per_hour .* (1 .- addressable_load_fraction)) / time_steps_per_hour if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0 @warn "SpaceHeatingLoad fuel_loads_mmbtu_per_hour was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored." @@ -180,12 +187,14 @@ struct SpaceHeatingLoad 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(BuiltInSpaceHeatingLoad, latitude, longitude, 2017, blended_doe_reference_names, blended_doe_reference_percents, city, annual_mmbtu, monthly_mmbtu, addressable_load_fraction, existing_boiler_efficiency) + unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency) else throw(@error("Cannot construct BuiltInSpaceHeatingLoad. You must provide either [fuel_loads_mmbtu_per_hour], [doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city].")) @@ -198,7 +207,8 @@ struct SpaceHeatingLoad new( loads_kw, - (sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU + (sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU, + unaddressable_annual_fuel_mmbtu ) end end @@ -1426,17 +1436,28 @@ end """ `ProcessHeatLoad` is an optional REopt input with the following keys and default values: ```julia - annual_mmbtu::Union{Real, Nothing} = nothing - fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[] + industry_reference_name::String = "", + sector::String = "", + blended_industry_reference_names::Array{String, 1} = String[], + blended_industry_reference_percents::Array{<:Real, 1} = Real[], + annual_mmbtu::Union{Real, Nothing} = nothing, + monthly_mmbtu::Array{<:Real,1} = Real[], + addressable_load_fraction::Any = 1.0, + fuel_loads_mmbtu_per_hour::Array{<:Real,1} = Real[], + time_steps_per_hour::Int = 1, # corresponding to `fuel_loads_mmbtu_per_hour` + latitude::Real = 0.0, + longitude::Real = 0.0, + existing_boiler_efficiency::Real = NaN ``` There are many ways in which a ProcessHeatLoad can be defined: -1. One can provide the `fuel_loads_mmbtu_per_hour` value in the `ProcessHeatLoad` key within the `Scenario`. -2. One can provide the `annual_mmbtu` value in the `ProcessHeatLoad` key within the `Scenario`; this assumes a flat load. +1. When using either `industry_reference_name` or `blended_industry_reference_names` +2. One can provide the `industry_reference_name` or `blended_industry_reference_names` directly in the `ProcessHeatLoad` key within the `Scenario`. These values can be combined with the `annual_mmbtu` or `monthly_mmbtu` inputs to scale the industry reference profile(s). +3. One can provide the `fuel_loads_mmbtu_per_hour` value in the `ProcessHeatLoad` key within the `Scenario`. !!! note "Process heat loads" - These loads are presented in terms of process heat required without regard to the efficiency of the input heating, - unlike the hot-water and space heating loads which are provided in terms of fuel input. + Process heat "load" inputs are in terms of fuel energy input required (boiler fuel), not the actual thermal demand. + The fuel energy is multiplied by the existing_boiler_efficiency to get the actual energy demand. """ function BuiltInProcessHeatLoad( @@ -1485,9 +1506,11 @@ function BuiltInProcessHeatLoad( built_in_load("process_heat", city, buildingtype, year, annual_mmbtu, monthly_mmbtu, existing_boiler_efficiency) end + struct ProcessHeatLoad loads_kw::Array{Real, 1} annual_mmbtu::Real + unaddressable_annual_fuel_mmbtu::Real function ProcessHeatLoad(; industry_reference_name::String = "", @@ -1532,6 +1555,7 @@ struct ProcessHeatLoad end loads_kw = fuel_loads_mmbtu_per_hour .* (KWH_PER_MMBTU * existing_boiler_efficiency) .* addressable_load_fraction + unaddressable_annual_fuel_mmbtu = sum(fuel_loads_mmbtu_per_hour .* (1 .- addressable_load_fraction)) / time_steps_per_hour if !isempty(doe_reference_name) || length(blended_doe_reference_names) > 0 @warn "ProcessHeatLoad fuel_loads_mmbtu_per_hour was provided, so doe_reference_name and/or blended_doe_reference_names will be ignored." @@ -1542,12 +1566,15 @@ struct ProcessHeatLoad if length(blended_doe_reference_names) > 0 @warn "ProcessHeatLoad 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(BuiltInProcessHeatLoad, latitude, longitude, 2017, blended_doe_reference_names, blended_doe_reference_percents, city, annual_mmbtu, monthly_mmbtu, addressable_load_fraction, existing_boiler_efficiency) + + unaddressable_annual_fuel_mmbtu = get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency) else throw(@error("Cannot construct BuiltInProcessHeatLoad. You must provide either [fuel_loads_mmbtu_per_hour], [doe_reference_name, city], or [blended_doe_reference_names, blended_doe_reference_percents, city].")) @@ -1560,7 +1587,29 @@ struct ProcessHeatLoad new( loads_kw, - (sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU + (sum(loads_kw)/time_steps_per_hour)/KWH_PER_MMBTU, + unaddressable_annual_fuel_mmbtu + ) end +end + +""" + get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency) + +Get unaddressable fuel load, for reporting + :addressable_load_fraction is the fraction of the input fuel load that is addressable to supply by energy technologies, like CHP + :annual_mmbtu and :monthly_mmbtu is assumed to be fuel, not thermal, in this function + :loads_kw is assumed to be thermal in this function, with units of kw_thermal, so needs to be converted to fuel mmbtu +""" +function get_unaddressable_fuel(addressable_load_fraction, annual_mmbtu, monthly_mmbtu, loads_kw, existing_boiler_efficiency) + # Get unaddressable fuel load, for reporting + if !isempty(monthly_mmbtu) + unaddressable_annual_fuel_mmbtu = sum(monthly_mmbtu .* (1 .- addressable_load_fraction)) + elseif !isnothing(annual_mmbtu) + unaddressable_annual_fuel_mmbtu = annual_mmbtu * (1 - addressable_load_fraction) + else # using the default CRB annual_mmbtu, so rely on loads_kw (thermal) assuming single addressable_load_fraction + unaddressable_annual_fuel_mmbtu = sum(loads_kw) / (KWH_PER_MMBTU * existing_boiler_efficiency) + end + return unaddressable_annual_fuel_mmbtu end \ No newline at end of file diff --git a/src/results/heating_cooling_load.jl b/src/results/heating_cooling_load.jl index d543b7ca4..c08583833 100644 --- a/src/results/heating_cooling_load.jl +++ b/src/results/heating_cooling_load.jl @@ -91,6 +91,12 @@ function add_heating_load_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict r["annual_calculated_process_heat_boiler_fuel_load_mmbtu"] = r["annual_calculated_process_heat_thermal_load_mmbtu"] / existing_boiler_efficiency r["annual_calculated_total_heating_boiler_fuel_load_mmbtu"] = r["annual_calculated_total_heating_thermal_load_mmbtu"] / existing_boiler_efficiency + r["annual_total_unaddressable_heating_load_mmbtu"] = (p.s.dhw_load.unaddressable_annual_fuel_mmbtu + + p.s.space_heating_load.unaddressable_annual_fuel_mmbtu + + p.s.process_heat_load.unaddressable_annual_fuel_mmbtu) + + r["annual_emissions_from_unaddressable_heating_load_mmbtu"] = r["annual_total_unaddressable_heating_load_mmbtu"] * p.s.existing_boiler.emissions_factor_lb_CO2_per_mmbtu * TONNE_PER_LB + d["HeatingLoad"] = r nothing end diff --git a/test/runtests.jl b/test/runtests.jl index 4aee1d809..c3cb3ad59 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -629,6 +629,19 @@ else # run HiGHS tests results = run_reopt(m, inputs) @test round(results["ExistingBoiler"]["annual_fuel_consumption_mmbtu"], digits=0) ≈ 8760 * (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) atol = 1.0 + # Test for unaddressable heating load fuel and emissions outputs + unaddressable = results["HeatingLoad"]["annual_total_unaddressable_heating_load_mmbtu"] + addressable = results["HeatingLoad"]["annual_calculated_total_heating_boiler_fuel_load_mmbtu"] + total = unaddressable + addressable + # Find the weighted average addressable_load_fraction from the fractions and loads above + weighted_avg_addressable_fraction = (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) / (0.5 + 0.5 + 0.3) + @test round(abs(addressable / total - weighted_avg_addressable_fraction), digits=3) == 0 + + unaddressable_emissions = results["HeatingLoad"]["annual_emissions_from_unaddressable_heating_load_mmbtu"] + addressable_site_fuel_emissions = results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] + total_site_emissions = unaddressable_emissions + addressable_site_fuel_emissions + @test round(abs(addressable_site_fuel_emissions / total_site_emissions - weighted_avg_addressable_fraction), digits=3) == 0 + # Monthly fuel load input with addressable_load_fraction is processed to expected thermal load data = JSON.parsefile("./scenarios/thermal_load.json") data["DomesticHotWaterLoad"]["monthly_mmbtu"] = repeat([100], 12) From d25ba18536121eb6576be2ddbbf21093e5407fe4 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Thu, 12 Sep 2024 22:36:08 -0600 Subject: [PATCH 255/266] Update expected Solar dataset test from updated NSRDB --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index c3cb3ad59..56072c18b 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -54,7 +54,7 @@ else # run HiGHS tests latitude, longitude = 3.8603988398663125, 11.528880303663136 radius = 0 dataset, distance, datasource = REopt.call_solar_dataset_api(latitude, longitude, radius) - @test dataset ≈ "intl" + @test dataset ≈ "nsrdb" # 4. Fairbanks, AK site = "Fairbanks" From ded48a93e4586e101d686e209d757c502f369082 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Sun, 22 Sep 2024 23:00:48 -0600 Subject: [PATCH 256/266] Avoid heating results with no heating input Remove CHP from initiating heating results, which CHP can be evaluated for electric-only --- src/results/results.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/results/results.jl b/src/results/results.jl index 61202e03a..797eb08e8 100644 --- a/src/results/results.jl +++ b/src/results/results.jl @@ -61,7 +61,7 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="") @debug "Outage results processing took $(round(time_elapsed, digits=3)) seconds." end - if !isempty(union(p.techs.chp, p.techs.heating)) + if !isempty(p.techs.heating) add_heating_load_results(m, p, d) end From 8ef48e4680eea5d789aadfb8f3330d00c748a584 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 23 Sep 2024 14:43:48 -0600 Subject: [PATCH 257/266] Change net_capital_costs_without_macrs to initial_capital_costs_after_incentives_without_macrs to be consistent --- CHANGELOG.md | 2 +- src/results/proforma.jl | 6 +++--- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index a63ce7d7a..7c8a8377d 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,7 +27,7 @@ Classify the change according to the following categories: ### Added - Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly - In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct -- Financial output **net_capital_costs_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" +- Financial output **initial_capital_costs_after_incentives_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" ### Changed - Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage. - Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 6bbd2f2c5..a765573e3 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -33,7 +33,7 @@ return Dict( "offtaker_discounted_annual_free_cashflows" => Float64[], "offtaker_discounted_annual_free_cashflows_bau" => Float64[], "developer_annual_free_cashflows" => Float64[], - "net_capital_costs_without_macrs" => 0.0 # Initial capital costs after ibi, cbi, and ITC incentives + "initial_capital_costs_after_incentives_without_macrs" => 0.0 # Initial capital costs after ibi, cbi, and ITC incentives ) """ function proforma_results(p::REoptInputs, d::Dict) @@ -47,7 +47,7 @@ function proforma_results(p::REoptInputs, d::Dict) "offtaker_discounted_annual_free_cashflows" => Float64[], "offtaker_discounted_annual_free_cashflows_bau" => Float64[], "developer_annual_free_cashflows" => Float64[], - "net_capital_costs_without_macrs" => 0.0 + "initial_capital_costs_after_incentives_without_macrs" => 0.0 ) years = p.s.financial.analysis_years escalate_elec(val) = [-1 * val * (1 + p.s.financial.elec_cost_escalation_rate_fraction)^yr for yr in 1:years] @@ -250,7 +250,7 @@ function proforma_results(p::REoptInputs, d::Dict) total_cash_incentives = m.total_pbi * (1 - tax_rate_fraction) free_cashflow_without_year_zero = m.total_depreciation * tax_rate_fraction + total_cash_incentives + operating_expenses_after_tax free_cashflow_without_year_zero[1] += m.federal_itc - r["net_capital_costs_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc + r["initial_capital_costs_after_incentives_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc free_cashflow = append!([(-1 * d["Financial"]["initial_capital_costs"]) + m.total_ibi_and_cbi], free_cashflow_without_year_zero) # At this point the logic branches based on third-party ownership or not - see comments From 9d3c90655aff354fc999cf5cc34e149a55f29c42 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 23 Sep 2024 14:43:48 -0600 Subject: [PATCH 258/266] Change net_capital_costs_without_macrs to initial_capital_costs_after_incentives_without_macrs to be consistent --- CHANGELOG.md | 4 +++- src/results/proforma.jl | 6 +++--- 2 files changed, 6 insertions(+), 4 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 19f761ba4..99a3175aa 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,7 +25,9 @@ Classify the change according to the following categories: ## Develop 08-09-2024 ### Added -- Financial output **net_capital_costs_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" +- Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly +- In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct +- Financial output **initial_capital_costs_after_incentives_without_macrs** which has "net year one" CapEx after incentives except for MACRS, which helps with users defining their own "simple payback period" ### Changed - Improve the full test suite reporting with a verbose summary table, and update the structure to reflect long-term open-source solver usage. - Removed MacOS from the runner list and just run with Windows OS, since MacOS commonly freezes and gets cancelled. We have not seen Windows OS pass while other OS's fail. diff --git a/src/results/proforma.jl b/src/results/proforma.jl index 67567dbda..65ba456f5 100644 --- a/src/results/proforma.jl +++ b/src/results/proforma.jl @@ -33,7 +33,7 @@ return Dict( "offtaker_discounted_annual_free_cashflows" => Float64[], "offtaker_discounted_annual_free_cashflows_bau" => Float64[], "developer_annual_free_cashflows" => Float64[], - "net_capital_costs_without_macrs" => 0.0 # Initial capital costs after ibi, cbi, and ITC incentives + "initial_capital_costs_after_incentives_without_macrs" => 0.0 # Initial capital costs after ibi, cbi, and ITC incentives ) """ function proforma_results(p::REoptInputs, d::Dict) @@ -47,7 +47,7 @@ function proforma_results(p::REoptInputs, d::Dict) "offtaker_discounted_annual_free_cashflows" => Float64[], "offtaker_discounted_annual_free_cashflows_bau" => Float64[], "developer_annual_free_cashflows" => Float64[], - "net_capital_costs_without_macrs" => 0.0 + "initial_capital_costs_after_incentives_without_macrs" => 0.0 ) years = p.s.financial.analysis_years escalate_elec(val) = [-1 * val * (1 + p.s.financial.elec_cost_escalation_rate_fraction)^yr for yr in 1:years] @@ -224,7 +224,7 @@ function proforma_results(p::REoptInputs, d::Dict) total_cash_incentives = m.total_pbi * (1 - tax_rate_fraction) free_cashflow_without_year_zero = m.total_depreciation * tax_rate_fraction + total_cash_incentives + operating_expenses_after_tax free_cashflow_without_year_zero[1] += m.federal_itc - r["net_capital_costs_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc + r["initial_capital_costs_after_incentives_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc free_cashflow = append!([(-1 * d["Financial"]["initial_capital_costs"]) + m.total_ibi_and_cbi], free_cashflow_without_year_zero) # At this point the logic branches based on third-party ownership or not - see comments From 78b04293bf219dbabb3dfb67ba83e1ea5edca205 Mon Sep 17 00:00:00 2001 From: Bill Becker Date: Mon, 23 Sep 2024 14:57:14 -0600 Subject: [PATCH 259/266] Fix units for unaddressable heating emissions --- src/results/heating_cooling_load.jl | 2 +- test/runtests.jl | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/results/heating_cooling_load.jl b/src/results/heating_cooling_load.jl index c08583833..1a1d9fe19 100644 --- a/src/results/heating_cooling_load.jl +++ b/src/results/heating_cooling_load.jl @@ -95,7 +95,7 @@ function add_heating_load_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict p.s.space_heating_load.unaddressable_annual_fuel_mmbtu + p.s.process_heat_load.unaddressable_annual_fuel_mmbtu) - r["annual_emissions_from_unaddressable_heating_load_mmbtu"] = r["annual_total_unaddressable_heating_load_mmbtu"] * p.s.existing_boiler.emissions_factor_lb_CO2_per_mmbtu * TONNE_PER_LB + r["annual_emissions_from_unaddressable_heating_load_tonnes_CO2"] = r["annual_total_unaddressable_heating_load_mmbtu"] * p.s.existing_boiler.emissions_factor_lb_CO2_per_mmbtu * TONNE_PER_LB d["HeatingLoad"] = r nothing diff --git a/test/runtests.jl b/test/runtests.jl index 83abc9f6b..3a63a2a38 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -677,7 +677,7 @@ else # run HiGHS tests weighted_avg_addressable_fraction = (0.5 * 0.6 + 0.5 * 0.8 + 0.3 * 0.7) / (0.5 + 0.5 + 0.3) @test round(abs(addressable / total - weighted_avg_addressable_fraction), digits=3) == 0 - unaddressable_emissions = results["HeatingLoad"]["annual_emissions_from_unaddressable_heating_load_mmbtu"] + unaddressable_emissions = results["HeatingLoad"]["annual_emissions_from_unaddressable_heating_load_tonnes_CO2"] addressable_site_fuel_emissions = results["Site"]["annual_emissions_from_fuelburn_tonnes_CO2"] total_site_emissions = unaddressable_emissions + addressable_site_fuel_emissions @test round(abs(addressable_site_fuel_emissions / total_site_emissions - weighted_avg_addressable_fraction), digits=3) == 0 From f7534f109dd28efca3c6b29aca91a944c99df9c7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 24 Sep 2024 23:19:44 -0600 Subject: [PATCH 260/266] include force_into_system as input to get_ashp_defaults --- data/ashp/ashp_defaults.json | 6 ++--- src/core/ashp.jl | 49 +++++++++++++----------------------- 2 files changed, 20 insertions(+), 35 deletions(-) diff --git a/data/ashp/ashp_defaults.json b/data/ashp/ashp_defaults.json index 7667db76b..9d568579c 100644 --- a/data/ashp/ashp_defaults.json +++ b/data/ashp/ashp_defaults.json @@ -11,9 +11,8 @@ "can_serve_dhw": false, "can_serve_space_heating": true, "can_serve_cooling": true, - "force_into_system": false, "back_up_temp_threshold_degF": 10.0, - "sizing_factor": 1.1, + "sizing_factor": 1.0, "heating_cop_reference": [1.5,2.3,3.3,4.5], "heating_cf_reference": [0.38,0.64,1.0,1.4], "heating_reference_temps_degF": [-5,17,47,80], @@ -33,9 +32,8 @@ "can_serve_dhw": true, "can_serve_space_heating": false, "can_serve_cooling": false, - "force_into_system": false, "back_up_temp_threshold_degF": 10.0, - "sizing_factor": 1.1, + "sizing_factor": 1.0, "heating_cop_reference": [1.5,2.3,3.3,4.5], "heating_cf_reference": [0.38,0.64,1.0,1.4], "heating_reference_temps_degF": [-5,17,47,80] diff --git a/src/core/ashp.jl b/src/core/ashp.jl index dab5efc0a..17b474077 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -68,7 +68,7 @@ function ASHPSpaceHeater(; macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load - force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true + force_into_system::Bool = false # force into system to serve all space heating loads if true avoided_capex_by_ashp_present_value::Real = 0.0 # avoided capital expenditure due to presence of ASHP system vs. defaults heating and cooling techs #The following inputs are used to create the attributes heating_cop and heating cf: @@ -101,7 +101,7 @@ function ASHPSpaceHeater(; macrs_bonus_fraction::Real = 0.0, avoided_capex_by_ashp_present_value::Real = 0.0, can_serve_cooling::Union{Bool, Nothing} = nothing, - force_into_system::Union{Bool, Nothing} = nothing, + force_into_system::Bool = false, heating_cop_reference::Array{<:Real,1} = Real[], heating_cf_reference::Array{<:Real,1} = Real[], heating_reference_temps_degF::Array{<:Real,1} = Real[], @@ -114,18 +114,14 @@ function ASHPSpaceHeater(; cooling_load::Array{<:Real,1} = Real[] ) - defaults = get_ashp_defaults("SpaceHeating") + defaults = get_ashp_defaults("SpaceHeating",force_into_system) # populate defaults as needed if isnothing(installed_cost_per_ton) installed_cost_per_ton = defaults["installed_cost_per_ton"] end if isnothing(om_cost_per_ton) - if force_into_system == true - om_cost_per_ton = 0 - else - om_cost_per_ton = defaults["om_cost_per_ton"] - end + om_cost_per_ton = defaults["om_cost_per_ton"] end if isnothing(can_serve_cooling) can_serve_cooling = defaults["can_serve_cooling"] @@ -140,11 +136,7 @@ function ASHPSpaceHeater(; max_ton = defaults["max_ton"] end if isnothing(sizing_factor) - if force_into_system == true - sizing_factor = defaults["sizing_factor"] - else - sizing_factor = 1 - end + sizing_factor = defaults["sizing_factor"] end if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps_degF) @@ -264,7 +256,7 @@ function ASHPWaterHeater(; macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS can_supply_steam_turbine::Union{Bool, nothing} = nothing # If the boiler can supply steam to the steam turbine for electric production avoided_capex_by_ashp_present_value::Real = 0.0 # avoided capital expenditure due to presence of ASHP system vs. defaults heating and cooling techs - force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all hot water loads if true + force_into_system::Bool = false # force into system to serve all hot water loads if true #The following inputs are used to create the attributes heating_cop and heating cf: heating_cop_reference::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) @@ -289,7 +281,7 @@ function ASHPWaterHeater(; macrs_option_years::Int = 0, macrs_bonus_fraction::Real = 0.0, avoided_capex_by_ashp_present_value::Real = 0.0, - force_into_system::Union{Bool, Nothing} = nothing, + force_into_system::Bool = false, heating_cop_reference::Array{<:Real,1} = Real[], heating_cf_reference::Array{<:Real,1} = Real[], heating_reference_temps_degF::Array{<:Real,1} = Real[], @@ -298,21 +290,14 @@ function ASHPWaterHeater(; heating_load::Array{<:Real,1} = Real[] ) - defaults = get_ashp_defaults("DomesticHotWater") + defaults = get_ashp_defaults("DomesticHotWater",force_into_system) # populate defaults as needed if isnothing(installed_cost_per_ton) installed_cost_per_ton = defaults["installed_cost_per_ton"] end if isnothing(om_cost_per_ton) - if force_into_system == true - om_cost_per_ton = 0 - else - om_cost_per_ton = defaults["om_cost_per_ton"] - end - end - if isnothing(force_into_system) - force_into_system = defaults["force_into_system"] + om_cost_per_ton = defaults["om_cost_per_ton"] end if isnothing(back_up_temp_threshold_degF) back_up_temp_threshold_degF = defaults["back_up_temp_threshold_degF"] @@ -321,11 +306,7 @@ function ASHPWaterHeater(; max_ton = defaults["max_ton"] end if isnothing(sizing_factor) - if force_into_system == true - sizing_factor = defaults["sizing_factor"] - else - sizing_factor = 1 - end + sizing_factor = defaults["sizing_factor"] end if length(heating_cop_reference) != length(heating_cf_reference) || length(heating_cf_reference) != length(heating_reference_temps_degF) @@ -410,16 +391,22 @@ Obtains defaults for the ASHP from a JSON data file. inputs load_served::String -- identifier of heating load served by AHSP system +force_into_system::Bool -- exclusively serves compatible thermal loads if true (i.e., replaces existing technologies) returns ashp_defaults::Dict -- Dictionary containing defaults for ASHP """ -function get_ashp_defaults(load_served::String="SpaceHeating") +function get_ashp_defaults(load_served::String="SpaceHeating", force_into_system::Bool=false) if !(load_served in ["SpaceHeating", "DomesticHotWater"]) throw(@error("Invalid inputs: argument `load_served` to function get_ashp_defaults() must be a String in the set ['SpaceHeating', 'DomesticHotWater'].")) end all_ashp_defaults = JSON.parsefile(joinpath(dirname(@__FILE__), "..", "..", "data", "ashp", "ashp_defaults.json")) - return all_ashp_defaults[load_served] + defaults = all_ashp_defaults[load_served] + if force_into_system + defaults["sizing_factor"] = 1.1 + defaults["om_cost_per_ton"] = 0.0 + end + return defaults end """ From d63f27ab85d9eeed2e563c9ee2f4018dfd442178 Mon Sep 17 00:00:00 2001 From: Zolan Date: Tue, 24 Sep 2024 23:20:02 -0600 Subject: [PATCH 261/266] update ASHP docstrings --- src/core/ashp.jl | 30 +++++++++++++++--------------- 1 file changed, 15 insertions(+), 15 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 17b474077..b382eb591 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -9,21 +9,21 @@ to meet the heating load. ASHPSpaceHeater has the following attributes: ```julia - min_kw::Real = 0.0, # Minimum thermal power size - max_kw::Real = BIG_NUMBER, # Maximum thermal power size - min_allowable_kw::Real = 0.0 # Minimum nonzero thermal power size if included - sizing_factor::Real = 1.1 # Size multiplier of system, relative that of the max load given by dispatch profile - installed_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based cost - om_cost_per_ton::Union{Real, nothing} = nothing, # Thermal power-based fixed O&M cost - macrs_option_years::Int = 0, # MACRS schedule for financial analysis. Set to zero to disable - macrs_bonus_fraction::Real = 0.0, # Fraction of upfront project costs to depreciate under MACRS - heating_cop::Array{<:Real,1}, # COP of the heating (i.e., thermal produced / electricity consumed) - cooling_cop::Array{<:Real,1}, # COP of the cooling (i.e., thermal produced / electricity consumed) - heating_cf::Array{<:Real,1}, # ASHP's heating capacity factor curves - cooling_cf::Array{<:Real,1}, # ASHP's cooling capacity factor curves - can_serve_cooling::Union{Bool, Nothing} = nothing # If ASHP can supply heat to the cooling load - force_into_system::Union{Bool, Nothing} = nothing # force into system to serve all space heating loads if true - back_up_temp_threshold_degF::Real = 10 # Degree in F that system switches from ASHP to resistive heater + min_kw::Real # Minimum thermal power size + max_kw::Real # Maximum thermal power size + min_allowable_kw::Real # Minimum nonzero thermal power size if included + sizing_factor::Real # Size multiplier of system, relative that of the max load given by dispatch profile + installed_cost_per_ton::Real # Thermal power-based cost + om_cost_per_ton::Real # Thermal power-based fixed O&M cost + macrs_option_years::Int # MACRS schedule for financial analysis. Set to zero to disable + macrs_bonus_fraction::Real # Fraction of upfront project costs to depreciate under MACRS + heating_cop::Array{<:Real,1} # COP of the heating (i.e., thermal produced / electricity consumed) + cooling_cop::Array{<:Real,1} # COP of the cooling (i.e., thermal produced / electricity consumed) + heating_cf::Array{<:Real,1} # ASHP's heating capacity factor curves + cooling_cf::Array{<:Real,1} # ASHP's cooling capacity factor curves + can_serve_cooling::Bool # If ASHP can supply heat to the cooling load + force_into_system::Bool # force into system to serve all space heating loads if true + back_up_temp_threshold_degF::Real # Degree in F that system switches from ASHP to resistive heater ``` """ struct ASHP <: AbstractThermalTech From af19daf5e4b62688617ec5beea8d84dc1e1d43bb Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 25 Sep 2024 09:15:47 -0600 Subject: [PATCH 262/266] add fields to ASHP that have defaults for API access --- src/core/ashp.jl | 20 ++++++++++++++++---- 1 file changed, 16 insertions(+), 4 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index b382eb591..d1cbe5632 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -13,8 +13,8 @@ ASHPSpaceHeater has the following attributes: max_kw::Real # Maximum thermal power size min_allowable_kw::Real # Minimum nonzero thermal power size if included sizing_factor::Real # Size multiplier of system, relative that of the max load given by dispatch profile - installed_cost_per_ton::Real # Thermal power-based cost - om_cost_per_ton::Real # Thermal power-based fixed O&M cost + installed_cost_per_kw::Real # Thermal power-based cost + om_cost_per_kw::Real # Thermal power-based fixed O&M cost macrs_option_years::Int # MACRS schedule for financial analysis. Set to zero to disable macrs_bonus_fraction::Real # Fraction of upfront project costs to depreciate under MACRS heating_cop::Array{<:Real,1} # COP of the heating (i.e., thermal produced / electricity consumed) @@ -24,6 +24,9 @@ ASHPSpaceHeater has the following attributes: can_serve_cooling::Bool # If ASHP can supply heat to the cooling load force_into_system::Bool # force into system to serve all space heating loads if true back_up_temp_threshold_degF::Real # Degree in F that system switches from ASHP to resistive heater + avoided_capex_by_ashp_present_value::Real # avoided capital expenditure due to presence of ASHP system vs. defaults heating and cooling techs + max_ton::Real # maximum allowable thermal power (tons) + ``` """ struct ASHP <: AbstractThermalTech @@ -47,6 +50,9 @@ struct ASHP <: AbstractThermalTech force_into_system::Bool back_up_temp_threshold_degF::Real avoided_capex_by_ashp_present_value::Real + max_ton::Real + installed_cost_per_ton::Real + om_cost_per_ton::Real end @@ -233,7 +239,10 @@ function ASHPSpaceHeater(; can_serve_cooling, force_into_system, back_up_temp_threshold_degF, - avoided_capex_by_ashp_present_value + avoided_capex_by_ashp_present_value, + max_ton, + installed_cost_per_ton, + om_cost_per_ton ) end @@ -378,7 +387,10 @@ function ASHPWaterHeater(; can_serve_cooling, force_into_system, back_up_temp_threshold_degF, - avoided_capex_by_ashp_present_value + avoided_capex_by_ashp_present_value, + max_ton, + installed_cost_per_ton, + om_cost_per_ton ) end From 0cf2c1ecb50ed08d5cda42c9a737e732462d9395 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 25 Sep 2024 11:09:03 -0600 Subject: [PATCH 263/266] add reference temps to ASHP object for defaults updates --- src/core/ashp.jl | 22 ++++++++++++++++++++-- 1 file changed, 20 insertions(+), 2 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index d1cbe5632..4472f1de5 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -53,6 +53,12 @@ struct ASHP <: AbstractThermalTech max_ton::Real installed_cost_per_ton::Real om_cost_per_ton::Real + heating_cop_reference::Array{<:Real,1} + heating_cf_reference::Array{<:Real,1} + heating_reference_temps_degF::Array{<:Real,1} + cooling_cop_reference::Array{<:Real,1} + cooling_cf_reference::Array{<:Real,1} + cooling_reference_temps_degF::Array{<:Real,1} end @@ -242,7 +248,13 @@ function ASHPSpaceHeater(; avoided_capex_by_ashp_present_value, max_ton, installed_cost_per_ton, - om_cost_per_ton + om_cost_per_ton, + heating_cop_reference, + heating_cf_reference, + heating_reference_temps_degF, + cooling_cop_reference, + cooling_cf_reference, + cooling_reference_temps_degF ) end @@ -390,7 +402,13 @@ function ASHPWaterHeater(; avoided_capex_by_ashp_present_value, max_ton, installed_cost_per_ton, - om_cost_per_ton + om_cost_per_ton, + heating_cop_reference, + heating_cf_reference, + heating_reference_temps_degF, + Real[], + Real[], + Real[] ) end From a4c77596c4ce358c108a7bb6af84dcab78ecfc93 Mon Sep 17 00:00:00 2001 From: Zolan Date: Wed, 25 Sep 2024 11:31:48 -0600 Subject: [PATCH 264/266] convert ashp reference vectors to Float64 when querying defaults --- src/core/ashp.jl | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/src/core/ashp.jl b/src/core/ashp.jl index 4472f1de5..f2f2bfcc0 100644 --- a/src/core/ashp.jl +++ b/src/core/ashp.jl @@ -155,18 +155,18 @@ function ASHPSpaceHeater(; throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 - heating_cop_reference = defaults["heating_cop_reference"] - heating_cf_reference = defaults["heating_cf_reference"] - heating_reference_temps_degF = defaults["heating_reference_temps_degF"] + heating_cop_reference = convert(Vector{Float64}, defaults["heating_cop_reference"]) + heating_cf_reference = convert(Vector{Float64}, defaults["heating_cf_reference"]) + heating_reference_temps_degF = convert(Vector{Float64}, defaults["heating_reference_temps_degF"]) end end if length(cooling_cop_reference) != length(cooling_cf_reference) || length(cooling_cf_reference) != length(cooling_reference_temps_degF) throw(@error("cooling_cop_reference, cooling_cf_reference, and cooling_reference_temps_degF must all be the same length.")) else if length(cooling_cop_reference) == 0 && can_serve_cooling - cooling_cop_reference = defaults["cooling_cop_reference"] - cooling_cf_reference = defaults["cooling_cf_reference"] - cooling_reference_temps_degF = defaults["cooling_reference_temps_degF"] + cooling_cop_reference = convert(Vector{Float64}, defaults["cooling_cop_reference"]) + cooling_cf_reference = convert(Vector{Float64}, defaults["cooling_cf_reference"]) + cooling_reference_temps_degF = convert(Vector{Float64}, defaults["cooling_reference_temps_degF"]) end end @@ -334,9 +334,9 @@ function ASHPWaterHeater(; throw(@error("heating_cop_reference, heating_cf_reference, and heating_reference_temps_degF must all be the same length.")) else if length(heating_cop_reference) == 0 - heating_cop_reference = defaults["heating_cop_reference"] - heating_cf_reference = defaults["heating_cf_reference"] - heating_reference_temps_degF = defaults["heating_reference_temps_degF"] + heating_cop_reference = convert(Vector{Float64}, defaults["heating_cop_reference"]) + heating_cf_reference = convert(Vector{Float64}, defaults["heating_cf_reference"]) + heating_reference_temps_degF = convert(Vector{Float64}, defaults["heating_reference_temps_degF"]) end end From 2f882bf8ebb2fe68a1716f977cdb2f68c03c92c2 Mon Sep 17 00:00:00 2001 From: Alex Zolan Date: Thu, 26 Sep 2024 09:59:55 -0600 Subject: [PATCH 265/266] Update CHANGELOG.md header to v0.48.0 --- CHANGELOG.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 7c8a8377d..6f1fc300c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,7 +23,7 @@ Classify the change according to the following categories: ### Deprecated ### Removed -## Develop 2024-08-19 +## v0.48.0 ### Added - Added new file `src/core/ASHP.jl` with new technology **ASHP**, which uses electricity as input and provides heating and/or cooling as output; load balancing and technology-specific constraints have been updated and added accordingly - In `src/core/existing_chiller.jl`, Added new atttribute **retire_in_optimal** to the **ExistingChiller** struct From c30ff01d059921728796f7d96060116fe3c437c8 Mon Sep 17 00:00:00 2001 From: Alex Zolan Date: Thu, 26 Sep 2024 10:28:57 -0600 Subject: [PATCH 266/266] Update Project.toml --- Project.toml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/Project.toml b/Project.toml index 7e492ee02..ce6381328 100644 --- a/Project.toml +++ b/Project.toml @@ -1,7 +1,7 @@ name = "REopt" uuid = "d36ad4e8-d74a-4f7a-ace1-eaea049febf6" authors = ["Nick Laws", "Hallie Dunham ", "Bill Becker ", "Bhavesh Rathod ", "Alex Zolan ", "Amanda Farthing "] -version = "0.47.2" +version = "0.48.0" [deps] ArchGDAL = "c9ce4bd3-c3d5-55b8-8973-c0e20141b8c3"