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

After tax savings for simple payback #477

Open
wants to merge 29 commits into
base: develop
Choose a base branch
from
Open

Conversation

Bill-Becker
Copy link
Collaborator

@Bill-Becker Bill-Becker commented Feb 13, 2025

While the current REopt payback period output metric (Financial.simple_payback_years) is considered the most accurate, and it is consistent with NREL's SAM tool's payback calculation, there was a request for a simple X/Y payback where the user can understand and access X (the numerator capital cost) and Y (denominator total yearly cost savings) in the results and confirm the payback calculation. The savings should be after-tax, and the capital costs should include non-discounted incentives, especially for MACRS, to be consistent with Financial.simple_payback_years. This update to REopt.jl will mainly be used to update the results table spreadsheet in the REopt_API, to add after-tax cost/savings and the alternative X/Y payback in the "playground" section.

To achieve what is described above, this PR:

  • Added after-tax cost metrics to culminate in a year_one_total_cost_savings_after_tax metric which can be used for a simple X/Y payback period calculation which closely (but not exactly) aligns with Financial.simple_payback_years metric but does not require an array for year-over-year escalation/inflation.
  • Added capital_costs_after_non_discounted_incentives for the numerator of the simple X/Y payback period calculation
  • Added tests to confirm scenarios where this alternative X/Y payback period aligns exactly with the Financial.simple_payback_years, as it should.

In the process of achieving above, a few related issues were caught and fixed in this PR:

  • The Financial.lifecycle_capital_costs did not include OffgridOtherCapexAfterDepr, AvoidedCapexByGHP, AvoidedCapexByASHP, and ResidualGHXCapCost
  • The Financial.initial_capital_costs did not include p.s.financial.offgrid_other_capital_costs
  • CHP standby charge costs were not included in the proforma metric cash flows and Financial.simple_payback_years calculation

Unrelated, this PR fixes a couple of bugs related to CoolingLoad:

  • src/core/scenario.jl: Added type conversion for array inputs from Vector{Any} to Vector{Float64} in the CoolingLoad dictionary.
  • src/core/simulated_load.jl: Removed unnecessary validation check for doe_reference_name when it is not required.

(For reference, this is what GitHub Copilot autogenerated for the PR)

This pull request includes several changes across multiple files to improve type consistency, add new financial calculations, and enhance documentation. The most important changes are grouped by theme below.

Type Consistency Improvements:

  • src/core/scenario.jl: Added type conversion for array inputs from Vector{Any} to Vector{Float64} in the CoolingLoad dictionary.

Financial Calculations:

  • src/results/financial.jl: Added calculations for year_one_chp_standby_cost_after_tax, year_one_chp_standby_cost_before_tax, and adjusted lifecycle capital costs to include new factors like OffgridOtherCapexAfterDepr and AvoidedCapexByASHP. [1] [2] [3] [4]
  • src/results/boiler.jl, src/results/chp.jl, src/results/electric_tariff.jl, src/results/existing_boiler.jl, src/results/generator.jl: Added year_one_fuel_cost_after_tax calculations. [1] [2] [3] [4] [5]

Documentation Enhancements:

  • src/results/boiler.jl, src/results/chp.jl, src/results/existing_boiler.jl, src/results/financial.jl, src/results/generator.jl, src/results/ghp.jl: Updated documentation to include new financial metrics and improved descriptions for existing metrics. [1] [2] [3] [4] [5] [6]

Error Handling and Validation:

These changes collectively enhance the robustness, clarity, and functionality of the codebase.

Bill-Becker and others added 26 commits November 21, 2024 08:56
@Bill-Becker Bill-Becker marked this pull request as ready for review March 3, 2025 17:40
@Bill-Becker Bill-Becker requested a review from adfarth March 3, 2025 17:45
@Bill-Becker
Copy link
Collaborator Author

@adfarth Here's the after-tax savings PR - I'm not sure if this gets us much close to a "host/off-taker" payback period, if that's what you were looking for. I'm not clear on the meaning of the payback period for the host/off-taker, as they don't typically pay anything/much up front, right? I'm going to wait on making CHANGELOG updates until after the review, and I'll mostly leverage what's in the PR description along with any edits in the review process.

@@ -183,8 +200,10 @@ function initial_capex(m::JuMP.AbstractModel, p::REoptInputs; _n="")

if option[2].heat_pump_configuration == "WSHP"
initial_capex += option[2].installed_cost_per_kw[2]*option[2].heatpump_capacity_ton*value(m[Symbol("binGHP"*_n)][option[1]])
initial_capex -= value(m[:AvoidedCapexByGHP])
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

This could be included just once outside of the if statements because it defaults to 0, right?

@@ -16,10 +16,12 @@
- `lifecycle_om_costs_before_tax` Present value of all O&M costs, before tax.
- `year_one_om_costs_before_tax` Year one O&M costs, before tax.
- `year_one_om_costs_after_tax` Year one O&M costs, after tax.
- `lifecycle_capital_costs_plus_om_after_tax` Capital cost for all technologies plus present value of operations and maintenance over anlaysis period. This value does not include offgrid_other_capital_costs.
- `lifecycle_capital_costs` Net capital costs for all technologies, in present value, including replacement costs and incentives. This value does not include offgrid_other_capital_costs.
- `initial_capital_costs` Up-front capital costs for all technologies, in present value, excluding replacement costs and incentives. This value does not include offgrid_other_capital_costs.
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bill-Becker I removed "This value does not include offgrid_other_capital_costs." to align with your changes

@@ -33,7 +33,8 @@ return Dict(
"offtaker_discounted_annual_free_cashflows" => Float64[],
"offtaker_discounted_annual_free_cashflows_bau" => Float64[],
"developer_annual_free_cashflows" => Float64[],
"initial_capital_costs_after_incentives_without_macrs" => 0.0 # Initial capital costs after ibi, cbi, and ITC incentives
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just confirming this was deemed to be not useful for users, or incorrect?

@@ -250,7 +255,9 @@ 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["initial_capital_costs_after_incentives_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc
battery_replacement_net_present_cost = -1*battery_replacement_cost * (1 - tax_rate_fraction) / (1 + p.s.financial.offtaker_discount_rate_fraction) ^ battery_replacement_year # battery_replacement_cost is negative, from above
Copy link
Collaborator

@adfarth adfarth Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@Bill-Becker should the discount rate used here depend on whether the analysis is third party, similar to the tax rate? This would also align with the "effective_cost" function

@@ -250,7 +255,9 @@ 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["initial_capital_costs_after_incentives_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc
battery_replacement_net_present_cost = -1*battery_replacement_cost * (1 - tax_rate_fraction) / (1 + p.s.financial.offtaker_discount_rate_fraction) ^ battery_replacement_year # battery_replacement_cost is negative, from above
r["capital_costs_after_incentives_without_macrs"] = d["Financial"]["initial_capital_costs"] - m.total_ibi_and_cbi - m.federal_itc + battery_replacement_net_present_cost
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Generator can also have a replacement cost, which I don't think is accounted for here

@@ -111,6 +111,18 @@ function reopt_results(m::JuMP.AbstractModel, p::REoptInputs; _n="")
if "ASHPWaterHeater" in p.techs.ashp_wh
add_ashp_wh_results(m, p, d; _n)
end

Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should all of these results fields be added to the results/financial.jl markdown text?

end
end

d["Financial"]["year_one_total_cost_before_tax"] = d["ElectricTariff"]["year_one_bill_before_tax"] - d["ElectricTariff"]["year_one_export_benefit_before_tax"] + d["Financial"]["year_one_chp_standby_cost_before_tax"] + d["Financial"]["year_one_fuel_cost_before_tax"] + d["Financial"]["year_one_om_costs_before_tax"]
Copy link
Collaborator

@adfarth adfarth Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Especially since this is being added to the financial results outputs, it could be helpful to change the name of year_one_total_cost_before_tax and year_one_total_cost_after_tax to indicate they do not include CAPEX

@@ -247,6 +268,9 @@ function combine_results(p::REoptInputs, bau::Dict, opt::Dict, bau_scenario::BAU
end
end

opt["Financial"]["year_one_total_cost_savings_before_tax"] = bau["Financial"]["year_one_total_cost_before_tax"] - opt["Financial"]["year_one_total_cost_before_tax"]
Copy link
Collaborator

@adfarth adfarth Mar 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

should these be added to the results/financial.jl help text too?

Check alignment between REopt simple_payback_years and a simple X/Y payback metric with
after-tax savings and a capital cost metric with non-discounted incentives to get simple X/Y payback
The REopt simple_payback_years output metric is after-tax, with no discounting, but it uses escalated and
inflated cashflows and it includes out-year, non-discounted battery replacement cost which is only included
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

noting here too that Generator can also be replaced

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)
payback = results["Financial"]["capital_costs_after_non_discounted_incentives"] / results["Financial"]["year_one_total_cost_savings_after_tax"]
@test round(results["Financial"]["simple_payback_years"], digits=2) round(payback, digits=2)
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

These metrics also differ in that simple_payback_years includes O&M costs, but the new "payback" calculation does not, right? This is a very light suggestion, but could consider adding a test with PV in which O&M costs are zeroed out to check that these are the same then? Maybe not necessary.

Copy link
Collaborator

@adfarth adfarth left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Just a few minor comments! Also noting to add CHANGELOG entry, and it would be great to list in there the new outputs available.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

2 participants