diff --git a/CHANGELOG.md b/CHANGELOG.md index cb81b62d7..db9bcef3c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -25,6 +25,10 @@ Classify the change according to the following categories: ### Deprecated ### Removed +## Develop fix-nonhourly-fuel-cost +### Fixed +- Modified the fuel cost calculation to correctly account for the time step duration when using non-hourly data. + ## Develop degradation-cleanup ### Added - Battery residual value if choosing replacement strategy for degradation diff --git a/src/core/utils.jl b/src/core/utils.jl index 8f92c9f6f..2a0a2175b 100644 --- a/src/core/utils.jl +++ b/src/core/utils.jl @@ -290,7 +290,7 @@ end Convert a per hour value (eg. dollars/kWh) to time series that matches the settings.time_steps_per_hour """ function per_hour_value_to_time_series(x::T, time_steps_per_hour::Int, name::String) where T <: Real - repeat([x / time_steps_per_hour], 8760 * time_steps_per_hour) + repeat([x], 8760 * time_steps_per_hour) end diff --git a/test/runtests.jl b/test/runtests.jl index a998c75b2..03e4edb0c 100644 --- a/test/runtests.jl +++ b/test/runtests.jl @@ -1829,45 +1829,81 @@ else # run HiGHS tests end @testset "OffGrid" begin - ## Scenario 1: Solar, Storage, Fixed Generator - post_name = "off_grid.json" + ## Scenario 1: Solar, Storage, Fixed Generator - Baseline Test + post_name = "off_grid.json" post = JSON.parsefile("./scenarios/$post_name") + + # Set up model for time_steps_per_hour = 1 (1-hour intervals) + post["Settings"]["time_steps_per_hour"] = 1 m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) scen = Scenario(post) - r = run_reopt(m, scen) - + r_1hr = run_reopt(m, scen) + # 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 scen.generator.min_turn_down_fraction ≈ 0.05 @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 baseline outputs + @test r_1hr["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ 0 + @test r_1hr["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ 2617.092 atol=0.01 + @test sum(r_1hr["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) >= sum(r_1hr["ElectricLoad"]["offgrid_annual_oper_res_required_series_kwh"]) + @test r_1hr["ElectricLoad"]["offgrid_load_met_fraction"] >= scen.electric_load.min_load_met_annual_fraction + @test r_1hr["PV"]["size_kw"] ≈ 5050.0 + + # Test financial components sum to LCC + f = r_1hr["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 + + ## Scenario 2: Solar, Storage, Fixed Generator - 30-minute intervals + post["Settings"]["time_steps_per_hour"] = 2 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + scen_30min = Scenario(post) + r_30min = run_reopt(m, scen_30min) + + # Validate consistency with 1-hour intervals + @test r_30min["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ r_1hr["ElectricUtility"]["annual_energy_supplied_kwh"] atol=0.01 + @test r_30min["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ r_1hr["Financial"]["lifecycle_offgrid_other_capital_costs"] atol=0.01 + @test sum(r_30min["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) / 2 ≈ sum(r_1hr["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) rtol=0.002 + @test r_30min["ElectricLoad"]["offgrid_load_met_fraction"] ≈ r_1hr["ElectricLoad"]["offgrid_load_met_fraction"] atol=0.01 + @test r_30min["PV"]["size_kw"] ≈ r_1hr["PV"]["size_kw"] atol=0.01 + @test r_30min["Generator"]["annual_fuel_consumption_gal"] ≈ r_1hr["Generator"]["annual_fuel_consumption_gal"] rtol=0.002 + @test r_30min["Generator"]["year_one_fuel_cost_before_tax"] ≈ r_1hr["Generator"]["year_one_fuel_cost_before_tax"] rtol=0.002 + + ## Scenario 3: Solar, Storage, Fixed Generator - 15-minute intervals + post["Settings"]["time_steps_per_hour"] = 4 + m = Model(optimizer_with_attributes(HiGHS.Optimizer, "output_flag" => false, "log_to_console" => false)) + scen_15min = Scenario(post) + r_15min = run_reopt(m, scen_15min) + + # Validate consistency with 1-hour intervals + @test r_15min["ElectricUtility"]["annual_energy_supplied_kwh"] ≈ r_1hr["ElectricUtility"]["annual_energy_supplied_kwh"] atol=0.01 + @test r_15min["Financial"]["lifecycle_offgrid_other_capital_costs"] ≈ r_1hr["Financial"]["lifecycle_offgrid_other_capital_costs"] atol=0.01 + @test sum(r_15min["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) / 4 ≈ sum(r_1hr["ElectricLoad"]["offgrid_annual_oper_res_provided_series_kwh"]) rtol=0.002 + @test r_15min["ElectricLoad"]["offgrid_load_met_fraction"] ≈ r_1hr["ElectricLoad"]["offgrid_load_met_fraction"] atol=0.01 + @test r_15min["PV"]["size_kw"] ≈ r_1hr["PV"]["size_kw"] atol=0.01 + @test r_15min["Generator"]["annual_fuel_consumption_gal"] ≈ r_1hr["Generator"]["annual_fuel_consumption_gal"] rtol=0.002 + @test r_15min["Generator"]["year_one_fuel_cost_before_tax"] ≈ r_1hr["Generator"]["year_one_fuel_cost_before_tax"] rtol=0.002 + + ## Scenario 4: Fixed Generator only + @info "Running Scenario 3: 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 @@ -1878,8 +1914,9 @@ else # run HiGHS tests @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 + + ## Scenario 5: Fixed Generator that can meet load, but cannot meet load operating reserve requirement + @info "Running Scenario 4: Fixed Generator with 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 @@ -1887,21 +1924,21 @@ else # run HiGHS tests 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 + + ## Scenario 6: 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 @@ -1911,11 +1948,10 @@ else # run HiGHS tests 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 diff --git a/test/scenarios/off_grid.json b/test/scenarios/off_grid.json index b5b29bd52..c76844dd6 100644 --- a/test/scenarios/off_grid.json +++ b/test/scenarios/off_grid.json @@ -29,7 +29,8 @@ "electric_efficiency_full_load": 0.3232898, "electric_efficiency_half_load": 0.3232898, "om_cost_per_kw": 20.0, - "fuel_cost_per_gallon": 3.0 + "fuel_cost_per_gallon": 3.0, + "min_turn_down_fraction": 0.05 }, "ElectricLoad": { "doe_reference_name": "RetailStore",