Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Fix fuel cost calculation for non-hourly load data #453

Open
wants to merge 3 commits into
base: develop
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
4 changes: 4 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down
2 changes: 1 addition & 1 deletion src/core/utils.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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


Expand Down
88 changes: 62 additions & 26 deletions test/runtests.jl
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -1878,30 +1914,31 @@ 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
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
## 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
Expand All @@ -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
Expand Down
3 changes: 2 additions & 1 deletion test/scenarios/off_grid.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading