-
Notifications
You must be signed in to change notification settings - Fork 33
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
base: develop
Are you sure you want to change the base?
Conversation
…nd other relevant capex metrics
…ecause it is combined across both
…e BESS replacement, consistent with other
@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]) |
There was a problem hiding this comment.
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. |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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 | |||
|
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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"] |
There was a problem hiding this comment.
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 |
There was a problem hiding this comment.
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) |
There was a problem hiding this comment.
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.
There was a problem hiding this 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.
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 withFinancial.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:
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 withFinancial.simple_payback_years
metric but does not require an array for year-over-year escalation/inflation.capital_costs_after_non_discounted_incentives
for the numerator of the simple X/Y payback period calculationFinancial.simple_payback_years
, as it should.In the process of achieving above, a few related issues were caught and fixed in this PR:
Financial.lifecycle_capital_costs
did not includeOffgridOtherCapexAfterDepr
,AvoidedCapexByGHP
,AvoidedCapexByASHP
, andResidualGHXCapCost
Financial.initial_capital_costs
did not includep.s.financial.offgrid_other_capital_costs
CHP
standby charge costs were not included in the proforma metric cash flows andFinancial.simple_payback_years
calculationUnrelated, this PR fixes a couple of bugs related to CoolingLoad:
src/core/scenario.jl
: Added type conversion for array inputs fromVector{Any}
toVector{Float64}
in theCoolingLoad
dictionary.src/core/simulated_load.jl
: Removed unnecessary validation check fordoe_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 fromVector{Any}
toVector{Float64}
in theCoolingLoad
dictionary.Financial Calculations:
src/results/financial.jl
: Added calculations foryear_one_chp_standby_cost_after_tax
,year_one_chp_standby_cost_before_tax
, and adjusted lifecycle capital costs to include new factors likeOffgridOtherCapexAfterDepr
andAvoidedCapexByASHP
. [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
: Addedyear_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:
src/core/simulated_load.jl
: Removed unnecessary validation check fordoe_reference_name
when it is not required.These changes collectively enhance the robustness, clarity, and functionality of the codebase.