From 61479667cac03465afd36a5ab6be843f7c653ecd Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 21 Sep 2023 15:38:17 -0700 Subject: [PATCH 1/4] add timeseries OR outputs and change kWh units to kW --- src/results/electric_load.jl | 4 ++-- src/results/electric_storage.jl | 3 +++ src/results/generator.jl | 9 ++++++++- src/results/pv.jl | 4 ++++ src/results/wind.jl | 6 ++++++ 5 files changed, 23 insertions(+), 3 deletions(-) diff --git a/src/results/electric_load.jl b/src/results/electric_load.jl index 3024f6943..af000adb7 100644 --- a/src/results/electric_load.jl +++ b/src/results/electric_load.jl @@ -33,8 +33,8 @@ function add_electric_load_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dic sum(p.s.electric_load.critical_loads_kw)) r["offgrid_load_met_fraction"] = round(value(LoadMetPct), digits=6) - r["offgrid_annual_oper_res_required_series_kwh"] = round.(value.(m[:OpResRequired][ts] for ts in p.time_steps_without_grid), digits=3) - r["offgrid_annual_oper_res_provided_series_kwh"] = round.(value.(m[:OpResProvided][ts] for ts in p.time_steps_without_grid), digits=3) + r["offgrid_annual_oper_res_required_series_kw"] = round.(value.(m[:OpResRequired][ts] for ts in p.time_steps_without_grid), digits=3) + r["offgrid_annual_oper_res_provided_series_kw"] = round.(value.(m[:OpResProvided][ts] for ts in p.time_steps_without_grid), digits=3) end d["ElectricLoad"] = r diff --git a/src/results/electric_storage.jl b/src/results/electric_storage.jl index 4c2b1483c..041ea47b5 100644 --- a/src/results/electric_storage.jl +++ b/src/results/electric_storage.jl @@ -43,6 +43,9 @@ function add_electric_storage_results(m::JuMP.AbstractModel, p::REoptInputs, d:: )) end end + if p.s.settings.off_grid_flag + r["operating_reserve_provided_series_kw"] = value.(m[Symbol("dvOpResFromBatt"*_n)][b,ts] for ts in p.time_steps) + end else r["soc_series_fraction"] = [] r["storage_to_load_series_kw"] = [] diff --git a/src/results/generator.jl b/src/results/generator.jl index ec9c2efb7..24690fa08 100644 --- a/src/results/generator.jl +++ b/src/results/generator.jl @@ -13,6 +13,7 @@ - `electric_to_grid_series_kw` Vector of power sent to grid in an average year - `electric_to_load_series_kw` Vector of power sent to load in an average year - `annual_energy_produced_kwh` Average annual energy produced over analysis period +- `operating_reserve_provided_series_kw` For offgrid analyses: operating reserve provided by generator in each time step !!! note "'Series' and 'Annual' energy outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. @@ -68,6 +69,12 @@ function add_generator_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _ for t in p.techs.gen, ts in p.time_steps) ) r["annual_energy_produced_kwh"] = round(value(AverageGenProd), digits=0) + + if p.s.settings.off_grid_flag + GeneratorOR = @expression(m, [ts in p.time_steps], + sum(m[:dvOpResFromTechs][t,ts] for t in p.techs.gen)) + r["operating_reserve_provided_series_kw"] = round.(value.(GeneratorOR), digits=3) + end d["Generator"] = r nothing @@ -117,7 +124,7 @@ function add_generator_results(m::JuMP.AbstractModel, p::MPCInputs, d::Dict; _n= for t in p.techs.gen, ts in p.time_steps) ) r["energy_produced_kwh"] = round(value(Year1GenProd), digits=0) - + d["Generator"] = r nothing end diff --git a/src/results/pv.jl b/src/results/pv.jl index baaa4cfa5..e753150cf 100644 --- a/src/results/pv.jl +++ b/src/results/pv.jl @@ -12,6 +12,7 @@ - `electric_curtailed_series_kw` Vector of power curtailed over the first year - `annual_energy_exported_kwh` Average annual energy exported to the grid - `production_factor_series` PV production factor in each time step, either provided by user or obtained from PVWatts +- `operating_reserve_provided_series_kw` For offgrid analyses: operating reserve provided by PV in each time step !!! warn The key(s) used to access PV outputs in the results dictionary is determined by the `PV.name` value to allow for modeling multiple PV options. (The default `PV.name` is "PV".) @@ -66,6 +67,9 @@ function add_pv_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") PVPerUnitSizeOMCosts = p.om_cost_per_kw[t] * p.pwf_om * m[Symbol("dvSize"*_n)][t] r["lifecycle_om_cost_after_tax"] = round(value(PVPerUnitSizeOMCosts) * (1 - p.s.financial.owner_tax_rate_fraction), digits=0) r["lcoe_per_kwh"] = calculate_lcoe(p, r, get_pv_by_name(t, p.s.pvs)) + if p.s.settings.off_grid_flag + r["operating_reserve_provided_series_kw"] = value.(m[Symbol("dvOpResFromTechs"*_n)][t,ts] for ts in p.time_steps) + end d[t] = r end nothing diff --git a/src/results/wind.jl b/src/results/wind.jl index 022065d74..798e388d5 100644 --- a/src/results/wind.jl +++ b/src/results/wind.jl @@ -12,6 +12,7 @@ - `lcoe_per_kwh` Levelized Cost of Energy produced by the PV system - `electric_curtailed_series_kw` Vector of power curtailed over an average year - `production_factor_series` Wind production factor in each time step, either provided by user or obtained from SAM +- `operating_reserve_provided_series_kw` For offgrid analyses: operating reserve provided by wind in each time step !!! note "'Series' and 'Annual' energy outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. @@ -66,6 +67,11 @@ function add_wind_results(m::JuMP.AbstractModel, p::REoptInputs, d::Dict; _n="") r["annual_energy_produced_kwh"] = round(value(AvgWindProd), digits=0) r["lcoe_per_kwh"] = calculate_lcoe(p, r, p.s.wind) + + if p.s.settings.off_grid_flag + r["operating_reserve_provided_series_kw"] = value.(m[Symbol("dvOpResFromTechs"*_n)][t,ts] for ts in p.time_steps) + end + d[t] = r nothing end From 65e3afd1e5f3ea9df5c5c6a58c34d3dc462a30b5 Mon Sep 17 00:00:00 2001 From: adfarth Date: Thu, 21 Sep 2023 15:40:38 -0700 Subject: [PATCH 2/4] Update CHANGELOG.md --- CHANGELOG.md | 6 ++++++ 1 file changed, 6 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 647a28b1b..750e861ec 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,12 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## v0.33.1 +### Added +- Add timeseries operating reserve outputs for each tech for off-grid only +### Changed +- Change units for total timeseries operating reserves required and provided to kW (from kWh) + ## v0.33.0 ### Added - Functionality to evaluate scenarios with Wind can in the ERP (`backup_reliability`) From 5dc5d69aacfc41ec9d7dd2023780e1b3d7b5c221 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 22 Sep 2023 09:36:27 -0700 Subject: [PATCH 3/4] kwh to kw --- src/results/electric_load.jl | 4 ++-- test/test_with_xpress.jl | 6 +++--- 2 files changed, 5 insertions(+), 5 deletions(-) diff --git a/src/results/electric_load.jl b/src/results/electric_load.jl index af000adb7..e0b8e90bb 100644 --- a/src/results/electric_load.jl +++ b/src/results/electric_load.jl @@ -6,8 +6,8 @@ - `annual_calculated_kwh` sum of the `load_series_kw` - `offgrid_load_met_series_kw` vector of electric load met by generation techs, for off-grid scenarios only - `offgrid_load_met_fraction` percentage of total electric load met on an annual basis, for off-grid scenarios only -- `offgrid_annual_oper_res_required_series_kwh` , total operating reserves required (for load and techs) on an annual basis, for off-grid scenarios only -- `offgrid_annual_oper_res_provided_series_kwh` , total operating reserves provided on an annual basis, for off-grid scenarios only +- `offgrid_annual_oper_res_required_series_kw` , total operating reserves required (for load and techs) on an annual basis, for off-grid scenarios only +- `offgrid_annual_oper_res_provided_series_kw` , total operating reserves provided on an annual basis, for off-grid scenarios only !!! note "'Series' and 'Annual' energy outputs are average annual" REopt performs load balances using average annual production values for technologies that include degradation. diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 8ec785194..08d237c44 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -228,7 +228,7 @@ end =# # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC - pf, tamb = REopt.call_pvwatts_api(30.2672, -97.7431); + pf, tamb, dist, resourcename = 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 @@ -1075,7 +1075,7 @@ end # 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 sum(r["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kw"]) >= sum(r["ElectricLoad"]["offgrid_annual_oper_res_required_series_kw"]) # 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"] @@ -1140,7 +1140,7 @@ end 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 + @test sum(results["ElectricLoad"]["offgrid_annual_oper_res_required_series_kw"]) ≈ loadOR + windOR atol=1.0 end From 435918af0bccb7366c61d401025bbf4989f0f348 Mon Sep 17 00:00:00 2001 From: adfarth Date: Fri, 22 Sep 2023 12:00:55 -0700 Subject: [PATCH 4/4] update with pv station distance returned from pvwatts --- src/core/production_factor.jl | 4 ++-- src/core/reopt_inputs.jl | 8 ++++---- src/core/scenario.jl | 4 ++-- src/core/utils.jl | 4 +++- test/test_with_xpress.jl | 2 +- 5 files changed, 12 insertions(+), 10 deletions(-) diff --git a/src/core/production_factor.jl b/src/core/production_factor.jl index 4fd4ce81a..22396e0e7 100644 --- a/src/core/production_factor.jl +++ b/src/core/production_factor.jl @@ -7,12 +7,12 @@ 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_celcius, pv_station_distance_km = 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) - return watts + return watts, pv_station_distance_km end diff --git a/src/core/reopt_inputs.jl b/src/core/reopt_inputs.jl index 55e50ce4a..b7243d2ac 100644 --- a/src/core/reopt_inputs.jl +++ b/src/core/reopt_inputs.jl @@ -151,7 +151,7 @@ function REoptInputs(s::AbstractScenario) time_steps = 1:length(s.electric_load.loads_kw) hours_per_time_step = 1 / s.settings.time_steps_per_hour - techs, pv_to_location, maxsize_pv_locations, pvlocations, + techs, pv_to_location, maxsize_pv_locations, pvlocations, 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, @@ -349,7 +349,7 @@ function setup_tech_inputs(s::AbstractScenario) setup_operating_reserve_fraction(s, techs_operating_reserve_req_fraction) end - return techs, pv_to_location, maxsize_pv_locations, pvlocations, + return techs, pv_to_location, maxsize_pv_locations, pvlocations, 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, @@ -435,7 +435,7 @@ end function setup_pv_inputs(s::AbstractScenario, max_sizes, min_sizes, existing_sizes, cap_cost_slope, om_cost_per_kw, production_factor, - pvlocations, pv_to_location, maxsize_pv_locations, + pvlocations, pv_to_location, maxsize_pv_locations, segmented_techs, n_segs_by_tech, seg_min_size, seg_max_size, seg_yint, techs_by_exportbin, techs) @@ -444,7 +444,7 @@ function setup_pv_inputs(s::AbstractScenario, max_sizes, min_sizes, roof_max_kw, land_max_kw = 1.0e5, 1.0e5 for pv in s.pvs - production_factor[pv.name, :] = get_production_factor(pv, s.site.latitude, s.site.longitude; + production_factor[pv.name, :], pv_station_distance_km = get_production_factor(pv, s.site.latitude, s.site.longitude; time_steps_per_hour=s.settings.time_steps_per_hour) for location in pvlocations if pv.location == String(location) # Must convert symbol to string diff --git a/src/core/scenario.jl b/src/core/scenario.jl index 7b6db2f5d..9213be1ba 100644 --- a/src/core/scenario.jl +++ b/src/core/scenario.jl @@ -432,12 +432,12 @@ 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_celcius, pv_station_distance_km = 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_celcius, pv_station_distance_km = 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 else diff --git a/src/core/utils.jl b/src/core/utils.jl index dfae17daf..e86c06f92 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -363,6 +363,7 @@ end This calls the PVWatts API and returns both: - PV production factor - Ambient outdoor air dry bulb temperature profile [Celcius] + - pv_station_distance_km """ function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimuth=180, module_type=0, array_type=1, losses=14, dc_ac_ratio=1.2, gcr=0.4, inv_eff=96, timeframe="hourly", radius=0, time_steps_per_hour=1) @@ -394,6 +395,7 @@ function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimut # Get both possible data of interest watts = collect(get(response["outputs"], "ac", []) / 1000) # scale to 1 kW system (* 1 kW / 1000 W) tamb_celcius = collect(get(response["outputs"], "tamb", [])) # Celcius + pv_station_distance_km = response["station_info"]["distance"] / 1000 # Distance between the input location and the climate station. (meters) # Validate outputs if length(watts) != 8760 throw(@error("PVWatts did not return a valid prodfactor. Got $watts")) @@ -407,7 +409,7 @@ function call_pvwatts_api(latitude::Real, longitude::Real; tilt=latitude, azimut watts = repeat(watts, inner=time_steps_per_hour) tamb_celcius = repeat(tamb_celcius, inner=time_steps_per_hour) end - return watts, tamb_celcius + return watts, tamb_celcius, pv_station_distance_km catch e throw(@error("Error occurred when calling PVWatts: $e")) end diff --git a/test/test_with_xpress.jl b/test/test_with_xpress.jl index 08d237c44..059f0e14f 100644 --- a/test/test_with_xpress.jl +++ b/test/test_with_xpress.jl @@ -228,7 +228,7 @@ end =# # Austin, TX -> existing_chiller and existing_boiler added with FlexibleHVAC - pf, tamb, dist, resourcename = REopt.call_pvwatts_api(30.2672, -97.7431); + pf, tamb, dist = 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