From 53cbc4780326307413c3c90828965ad6a7dbfa2f Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 10:18:03 -0600 Subject: [PATCH 01/10] refactor heating flow constraints, move from storage to thermal_tech_constraints set --- src/constraints/storage_constraints.jl | 54 --------------------- src/constraints/thermal_tech_constraints.jl | 35 +++++++++++++ 2 files changed, 35 insertions(+), 54 deletions(-) diff --git a/src/constraints/storage_constraints.jl b/src/constraints/storage_constraints.jl index 782076dd4..96089f94a 100644 --- a/src/constraints/storage_constraints.jl +++ b/src/constraints/storage_constraints.jl @@ -103,60 +103,6 @@ end function add_hot_thermal_storage_dispatch_constraints(m, p, b; _n="") - # # Constraint (4f)-1: (Hot) Thermal production sent to storage or grid must be less than technology's rated production - # # Constraint (4f)-1a: BoilerTechs - for t in p.techs.boiler - 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 - - 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) - @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 - 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] - ) - end - - # # Constraint (4g): CHP Thermal production sent to storage or grid must be less than technology's rated production - if !isempty(p.techs.chp) - if !isempty(p.techs.steam_turbine) && p.s.chp.can_supply_steam_turbine - @constraint(m, CHPTechProductionFlowCon[b in p.s.storage.types.hot, t in p.techs.chp, q in p.heating_loads, ts in p.time_steps], - m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] + m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] <= - m[Symbol("dvHeatingProduction"*_n)][t,q,ts] - ) - else - @constraint(m, CHPTechProductionFlowCon[b in p.s.storage.types.hot, t in p.techs.chp, q in p.heating_loads, ts in p.time_steps], - m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= - m[Symbol("dvHeatingProduction"*_n)][t,q,ts] - ) - end - end - # Constraint (4j)-1: Reconcile state-of-charge for (hot) thermal storage @constraint(m, [b in p.s.storage.types.hot, ts in p.time_steps], m[Symbol("dvStoredEnergy"*_n)][b,ts] == m[Symbol("dvStoredEnergy"*_n)][b,ts-1] + (1/p.s.settings.time_steps_per_hour) * ( diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index a03d2b080..29195bb42 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -24,6 +24,37 @@ function add_boiler_tech_constraints(m, p; _n="") end function add_heating_tech_constraints(m, p; _n="") + # Constraint (7_heating_flow): Flows to Steam turbine, waste, and turbine must be less than or equal to total production + if !isempty(p.techs.steam_turbine) + if !isempty(p.s.storage.types.hot) + @constraint(m, [t in p.techs.can_supply_steam_turbine, q in p.heating_loads, ts in p.time_steps], + sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for b in p.s.storage.types.hot) + m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= + m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + else + @constraint(m, [t in p.techs.can_supply_steam_turbine, q in p.heating_loads, ts in p.time_steps], + m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= + m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + if !isempty(setdiff(union(p.techs.heating, p.techs.chp),p.techs.can_supply_steam_turbine)) + @constraint(m, [setdiff(union(p.techs.heating,p.techs.chp),p.techs.can_supply_steam_turbine), q in p.heating_loads, ts in p.time_steps], + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + end + end + else + if !isempty(p.s.storage.types.hot) + @constraint(m, [t in union(p.techs.heating, p.techs.chp), q in p.heating_loads, ts in p.time_steps], + sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for b in p.s.storage.types.hot) + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= + m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + else + @constraint(m, [t in union(p.techs.heating, p.techs.chp), q in p.heating_loads, ts in p.time_steps], + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + end + end + # 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], @@ -35,19 +66,23 @@ function add_heating_tech_constraints(m, p; _n="") if !(t in p.techs.can_serve_space_heating) 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 if !(t in p.techs.can_serve_dhw) 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 if !(t in p.techs.can_serve_process_heat) for ts in p.time_steps fix(m[Symbol("dvHeatingProduction"*_n)][t,"ProcessHeat",ts], 0.0, force=true) + fix(m[Symbol("dvProductionToWaste"*_n)][t,"ProcessHeat",ts], 0.0, force=true) end end end + # Enfore end function no_existing_boiler_production(m, p; _n="") From 46264a315bb0916f75cbb25f9c890d7498fb0bdc Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 10:26:14 -0600 Subject: [PATCH 02/10] throw error if steam turbine is present but no techs can supply it --- src/core/reopt.jl | 6 +++++- 1 file changed, 5 insertions(+), 1 deletion(-) diff --git a/src/core/reopt.jl b/src/core/reopt.jl index e3a821a46..e9344b703 100644 --- a/src/core/reopt.jl +++ b/src/core/reopt.jl @@ -636,7 +636,11 @@ function add_variables!(m::JuMP.AbstractModel, p::REoptInputs) end if !isempty(p.techs.steam_turbine) - @variable(m, dvThermalToSteamTurbine[p.techs.can_supply_steam_turbine, p.heating_loads, p.time_steps] >= 0) + if !isempty(p.techs.can_supply_steam_turbine) + @variable(m, dvThermalToSteamTurbine[p.techs.can_supply_steam_turbine, p.heating_loads, p.time_steps] >= 0) + else + throw(@error("Steam turbine is present, but set p.techs.can_supply_steam_turbine is empty.")) + end end if !isempty(p.s.electric_utility.outage_durations) # add dvUnserved Load if there is at least one outage From e2e65ab3c3d7bd26337433f8d5245594d9835638 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 11:52:11 -0600 Subject: [PATCH 03/10] new testset "CHP to Waste Heat" --- test/runtests.jl | 8 ++ test/scenarios/chp_waste.json | 176 ++++++++++++++++++++++++++++++++++ 2 files changed, 184 insertions(+) create mode 100644 test/scenarios/chp_waste.json diff --git a/test/runtests.jl b/test/runtests.jl index e9a0bca7b..f75b1659e 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -801,6 +801,14 @@ else # run HiGHS tests @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 + @test sum(results["ExistingBoiler"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 0.0 atol 1e-3 + end end @testset "FlexibleHVAC" begin diff --git a/test/scenarios/chp_waste.json b/test/scenarios/chp_waste.json new file mode 100644 index 000000000..6869f0259 --- /dev/null +++ b/test/scenarios/chp_waste.json @@ -0,0 +1,176 @@ +{ + "user_uuid": "1d2ef71e-fd93-4c4a-b5c3-1485a87f772e", + "webtool_uuid": "73518b4b-2836-4a44-9852-a4889c33b76a", + "Settings": { + "solver_name": "HiGHS", + "off_grid_flag": false, + "include_climate_in_objective": false, + "include_health_in_objective": false + }, + "Meta": { + "address": "800 Hoffman Rd Watertown WI 53094 USA" + }, + "Site": { + "latitude": 43.1752702, + "longitude": -88.7344012, + "include_exported_renewable_electricity_in_total": true, + "include_exported_elec_emissions_in_total": true, + "land_acres": 16.33, + "roof_squarefeet": 0 + }, + "ElectricLoad": { + "monthly_totals_kwh": [ + 217080.0, + 209585.0, + 225612.0, + 225612.0, + 248200.0, + 242739.0, + 240361.0, + 279924.0, + 254856.0, + 217403.0, + 177556.0, + 222634.0 + ], + "doe_reference_name": "FlatLoad" + }, + "ElectricTariff": { + "monthly_energy_rates": [ + 0.098, + 0.11, + 0.119, + 0.107, + 0.112, + 0.116, + 0.124, + 0.116, + 0.118, + 0.116, + 0.107, + 0.101 + ], + "monthly_demand_rates": [ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0 + ] + }, + "Generator": { + "max_kw": 0, + "existing_kw": 0 + }, + "CHP": { + "can_net_meter": true, + "prime_mover": "recip_engine", + "size_class": 2, + "fuel_type": "natural_gas", + "electric_efficiency_full_load": 0.34, + "electric_efficiency_half_load": 0.34, + "macrs_option_years": 5, + "macrs_bonus_fraction": 0.6, + "can_wholesale": false, + "fuel_cost_per_mmbtu": [ + 6.26, + 8.36, + 8.35, + 4.61, + 4.66, + 5.22, + 5.83, + 5.37, + 4.69, + 4.65, + 5.07, + 6.04 + ] + }, + "DomesticHotWaterLoad": { + "monthly_mmbtu": [ + 135.097, + 117.915, + 96.579, + 57.283, + 15.684, + 14.794, + 19.994, + 28.147, + 37.784, + 46.926, + 147.828, + 162.636 + ], + "addressable_load_fraction": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "doe_reference_name": "FlatLoad" + }, + "SpaceHeatingLoad": { + "monthly_mmbtu": [ + 821.003, + 716.585, + 586.921, + 348.117, + 95.316, + 89.906, + 121.506, + 171.053, + 229.616, + 285.174, + 898.372, + 988.364 + ], + "addressable_load_fraction": [ + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0 + ], + "doe_reference_name": "FlatLoad" + }, + "ExistingBoiler": { + "fuel_type": "natural_gas", + "fuel_cost_per_mmbtu": [ + 6.26, + 8.36, + 8.35, + 4.61, + 4.66, + 5.22, + 5.83, + 5.37, + 4.69, + 4.65, + 5.07, + 6.04 + ] + } +} \ No newline at end of file From 707cd704af47f901dd7217bf826c91654027745d Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 12:18:57 -0600 Subject: [PATCH 04/10] fix tests in new set --- test/runtests.jl | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/test/runtests.jl b/test/runtests.jl index f75b1659e..7380c9911 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -806,8 +806,7 @@ else # run HiGHS tests 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 - @test sum(results["ExistingBoiler"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 0.0 atol 1e-3 + @test sum(results["CHP"]["thermal_curtailed_series_mmbtu_per_hour"]) ≈ 4174.455 atol=1e-3 end end From 266c278d8e7e923375d5f8b347dab7bf6df4541c Mon Sep 17 00:00:00 2001 From: Alex Zolan Date: Thu, 23 May 2024 13:47:56 -0600 Subject: [PATCH 05/10] Update CHANGELOG.md --- CHANGELOG.md | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 10a7bb54a..aba4d8f1e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,12 +23,14 @@ Classify the change according to the following categories: ### Deprecated ### Removed -## Develop +## Develop 2024-05-23 ### 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. +- Refactored heating flow constraints to be in ./src/constraints/thermal_tech_constraints.jl instead of its previous separate locations in the storage and turbine constraints. ### Fixed - Updated the PV result **lifecycle_om_cost_after_tax** to account for the third-party factor for third-party ownership analyses. - Convert `max_electric_load_kw` to _Float64_ before passing to function `get_chp_defaults_prime_mover_size_class` +- Fixed a bug in which excess heat from one heating technology resulted in waste heat from another technology. ## v0.46.1 ### Changed From 8e837dda3b7af27e63d52cdf0450e1765df94366 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 15:54:36 -0600 Subject: [PATCH 06/10] update constraint sets when turbine is present --- src/constraints/thermal_tech_constraints.jl | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/constraints/thermal_tech_constraints.jl b/src/constraints/thermal_tech_constraints.jl index 29195bb42..56699b5e9 100644 --- a/src/constraints/thermal_tech_constraints.jl +++ b/src/constraints/thermal_tech_constraints.jl @@ -31,15 +31,21 @@ function add_heating_tech_constraints(m, p; _n="") sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for b in p.s.storage.types.hot) + m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= m[Symbol("dvHeatingProduction"*_n)][t,q,ts] ) + if !isempty(setdiff(union(p.techs.heating, p.techs.chp),p.techs.can_supply_steam_turbine)) + @constraint(m, [t in setdiff(union(p.techs.heating,p.techs.chp),p.techs.can_supply_steam_turbine), q in p.heating_loads, ts in p.time_steps], + sum(m[Symbol("dvHeatToStorage"*_n)][b,t,q,ts] for b in p.s.storage.types.hot) + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= + m[Symbol("dvHeatingProduction"*_n)][t,q,ts] + ) + end else @constraint(m, [t in p.techs.can_supply_steam_turbine, q in p.heating_loads, ts in p.time_steps], m[Symbol("dvThermalToSteamTurbine"*_n)][t,q,ts] + m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= m[Symbol("dvHeatingProduction"*_n)][t,q,ts] ) if !isempty(setdiff(union(p.techs.heating, p.techs.chp),p.techs.can_supply_steam_turbine)) - @constraint(m, [setdiff(union(p.techs.heating,p.techs.chp),p.techs.can_supply_steam_turbine), q in p.heating_loads, ts in p.time_steps], + @constraint(m, [t in setdiff(union(p.techs.heating,p.techs.chp),p.techs.can_supply_steam_turbine), q in p.heating_loads, ts in p.time_steps], m[Symbol("dvProductionToWaste"*_n)][t,q,ts] <= m[Symbol("dvHeatingProduction"*_n)][t,q,ts] - ) + ) end end else From 77482fe17add3952dbdd0451f75bcc07922b2bc6 Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 16:21:24 -0600 Subject: [PATCH 07/10] turn on presolve for CHP test --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 7380c9911..48c9b41ce 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -637,7 +637,7 @@ else # run HiGHS tests 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)) + 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) ≈ 330.0 atol=20.0 From 6e2ad5be92adfeb4463409f68c814a36c0f8978f Mon Sep 17 00:00:00 2001 From: Zolan Date: Thu, 23 May 2024 16:22:13 -0600 Subject: [PATCH 08/10] rm zero-size techs from test case (avoid PVWatts call) --- test/scenarios/chp_sizing.json | 9 +-------- 1 file changed, 1 insertion(+), 8 deletions(-) diff --git a/test/scenarios/chp_sizing.json b/test/scenarios/chp_sizing.json index 6062dff95..eefb65416 100644 --- a/test/scenarios/chp_sizing.json +++ b/test/scenarios/chp_sizing.json @@ -58,12 +58,5 @@ "duration_hours": 0 } ] - }, - "PV": { - "max_kw": 0.0 - }, - "ElectricStorage": { - "max_kw": 0.0, - "max_kwh": 0.0 - } + } } \ No newline at end of file From 668384f692cbf650c500b420129327860c46a399 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 10:26:45 -0600 Subject: [PATCH 09/10] initiate PV in CHP cost curve test --- test/runtests.jl | 1 + 1 file changed, 1 insertion(+) diff --git a/test/runtests.jl b/test/runtests.jl index 48c9b41ce..288626a75 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -673,6 +673,7 @@ else # run HiGHS tests [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 From d79aecdc9d34f95b5dcd5e9d4ab6c190242632f7 Mon Sep 17 00:00:00 2001 From: Zolan Date: Fri, 24 May 2024 10:26:57 -0600 Subject: [PATCH 10/10] make CHP size test more flexible --- test/runtests.jl | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/test/runtests.jl b/test/runtests.jl index 288626a75..10f7e5f90 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -640,7 +640,7 @@ 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) ≈ 330.0 atol=20.0 + @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