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 NPV/solve_IRR/solve_price methods consistent with cashflow table #180 #183

Merged
merged 5 commits into from
Jan 10, 2024
Merged
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
2 changes: 1 addition & 1 deletion Bioindustrial-Park
Submodule Bioindustrial-Park updated 1 files
+0 −5 README.rst
43 changes: 28 additions & 15 deletions biosteam/_tea.py
Original file line number Diff line number Diff line change
Expand Up @@ -204,10 +204,11 @@ def taxable_and_nontaxable_cashflows(
loan_principal = loan_principal_with_interest(loan, interest)
else:
loan_principal = loan.sum()
LP[:start] = loan * interest
LP[start:start + years] = solve_payment(loan_principal, interest, years)
taxable_cashflow = S - C - D - LP
nontaxable_cashflow = D + Loan - C_FC - C_WC
if not accumulate_interest_during_construction:
nontaxable_cashflow[:start] -= loan * interest
else:
taxable_cashflow = S - C - D
nontaxable_cashflow = D - C_FC - C_WC
Expand All @@ -224,9 +225,13 @@ def NPV_with_sales(
):
"""Return NPV with an additional annualized sales."""
taxable_cashflow = taxable_cashflow + sales * sales_coefficients
tax = np.zeros_like(taxable_cashflow)
tax = np.zeros_like(taxable_cashflow, dtype=float)
incentives = tax.copy()
fill_tax_and_incentives(incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation)
fill_tax_and_incentives(
incentives,
taxable_earnings_with_fowarded_losses(taxable_cashflow),
nontaxable_cashflow, tax, depreciation
)
cashflow = nontaxable_cashflow + taxable_cashflow + incentives - tax
return (cashflow/discount_factors).sum()

Expand Down Expand Up @@ -790,8 +795,10 @@ def get_cashflow_table(self):
taxable_cashflow = S - C - D
nontaxable_cashflow = D - C_FC - C_WC
TE[:] = taxable_earnings_with_fowarded_losses(taxable_cashflow)
FL[:] = (taxable_cashflow - TE).cumsum()
self._fill_tax_and_incentives(I, taxable_cashflow, nontaxable_cashflow, T, D)
FL[1:] = (taxable_cashflow - TE).cumsum()[:-1]
self._fill_tax_and_incentives(
I, TE, nontaxable_cashflow, T, D
)
NE[:] = taxable_cashflow + I - T
CF[:] = NE + nontaxable_cashflow
DF[:] = 1/(1.+self.IRR)**self._get_duration_array()
Expand All @@ -808,7 +815,11 @@ def NPV(self) -> float:
taxable_cashflow, nontaxable_cashflow, depreciation = self._taxable_nontaxable_depreciation_cashflows()
tax = np.zeros_like(taxable_cashflow)
incentives = tax.copy()
self._fill_tax_and_incentives(incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation)
self._fill_tax_and_incentives(
incentives,
taxable_earnings_with_fowarded_losses(taxable_cashflow),
nontaxable_cashflow, tax, depreciation
)
cashflow = nontaxable_cashflow + taxable_cashflow + incentives - tax
return NPV_at_IRR(self.IRR, cashflow, self._get_duration_array())

Expand Down Expand Up @@ -859,14 +870,18 @@ def _taxable_nontaxable_depreciation_cashflows(self):
)

def _fill_tax_and_incentives(self, incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation):
tax[:] = self.income_tax * taxable_earnings_with_fowarded_losses(taxable_cashflow)
tax[:] = self.income_tax * taxable_cashflow

def _net_earnings_and_nontaxable_cashflow_arrays(self):
taxable_cashflow, nontaxable_cashflow, depreciation = self._taxable_nontaxable_depreciation_cashflows()
size = taxable_cashflow.size
tax = np.zeros(size)
incentives = tax.copy()
self._fill_tax_and_incentives(incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation)
self._fill_tax_and_incentives(
incentives,
taxable_earnings_with_fowarded_losses(taxable_cashflow),
nontaxable_cashflow, tax, depreciation
)
net_earnings = taxable_cashflow + incentives - tax
return net_earnings, nontaxable_cashflow

Expand Down Expand Up @@ -984,12 +999,11 @@ def solve_sales(self):

"""
discount_factors = (1 + self.IRR)**self._get_duration_array()
sales_coefficients = np.ones_like(discount_factors)
sales_coefficients = np.ones_like(discount_factors, dtype=float)
start = self._start
sales_coefficients[:start] = 0
w0 = self._startup_time
sales_coefficients[self._start] = w0*self.startup_VOCfrac + (1-w0)
sales = self._sales
sales_coefficients[start] = w0*self.startup_salesfrac + (1.-w0)
taxable_cashflow, nontaxable_cashflow, depreciation = self._taxable_nontaxable_depreciation_cashflows()
if np.isnan(taxable_cashflow).any():
warn('nan encountered in cashflow array; resimulating system', category=RuntimeWarning)
Expand All @@ -1003,17 +1017,16 @@ def solve_sales(self):
sales_coefficients,
discount_factors,
self._fill_tax_and_incentives)
x0 = sales
x0 = self._sales if np.isfinite(self._sales) else 0
f = NPV_with_sales
if not np.isfinite(x0): x0 = 0.
y0 = f(x0, *args)
x1 = x0 - y0 / self._years # First estimate
try:
sales = flx.aitken_secant(f, x0, x1, xtol=10, ytol=1000.,
sales = flx.aitken_secant(f, x0, x1, xtol=10, ytol=100.,
maxiter=1000, args=args, checkiter=True)
except:
bracket = flx.find_bracket(f, x0, x1, args=args)
sales = flx.IQ_interpolation(f, *bracket, args=args, xtol=10, ytol=1000, maxiter=1000, checkiter=False)
sales = flx.IQ_interpolation(f, *bracket, args=args, xtol=10, ytol=100, maxiter=1000, checkiter=False)
self._sales = sales
return sales

Expand Down
215 changes: 211 additions & 4 deletions tests/test_tea.py
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
# -*- coding: utf-8 -*-
# BioSTEAM: The Biorefinery Simulation and Techno-Economic Analysis Modules
# Copyright (C) 2021-2022, Yalin Li <zoe[email protected]>
# Copyright (C) 2021-2024, Yalin Li <mailto[email protected]>
#
# This module is under the UIUC open-source license. See
# github.com/BioSTEAMDevelopmentGroup/biosteam/blob/master/LICENSE.txt
# for license details.
"""
"""

import pytest, numpy as np
import pytest, numpy as np, biosteam as bst
from numpy.testing import assert_allclose
from biosteam import TEA, System
from biosteam import TEA, System, Stream, Unit, Chemical, settings
from biorefineries.tea import create_cellulosic_ethanol_tea

def test_depreciation_schedule():
correct_arrs = {
Expand Down Expand Up @@ -51,6 +52,212 @@ def test_depreciation_schedule():
with pytest.raises(ValueError):
tea.depreciation = 'bad'

def test_cashflow_consistency():
settings.set_thermo([
Chemical('Dummy', default=True, phase='s', MW=1, search_db=False)
])

cost = Stream(Dummy=9793.983511363867 - 1280.916880939845, price=1) # Includes co-product electricity credits
ethanol = Stream(flow=[21978.374283953395], price=0.7198608114634679)

class MockCellulosicEthanolBiorefinery(Unit):
_N_ins = _N_outs = 1

def _run(self): pass

def _cost(self):
self.baseline_purchase_costs['Biorefinery'] = 85338080.48935215

class OSBL(Unit):
N_outs = _N_ins = 0

def _run(self): pass

def _cost(self):
self.baseline_purchase_costs['Biorefinery'] = 122287135.13152598

unit = MockCellulosicEthanolBiorefinery(ins=cost, outs=ethanol)
osbl = OSBL()
sys = System.from_units(units=[unit, osbl])
sys.simulate()
tea = create_cellulosic_ethanol_tea(sys, OSBL_units=[osbl])
table = tea.get_cashflow_table()
assert_allclose(tea.NPV, 32131936.781448975)
assert_allclose(tea.NPV, table['Cumulative NPV [MM$]'].iloc[-1]*1e6)
tea.IRR = tea.solve_IRR()
assert_allclose(tea.NPV, 0, atol=100)
assert_allclose(tea.IRR, 0.11246761316144724)
tea.IRR = 0.10
ethanol.price = tea.solve_price(ethanol)
assert_allclose(ethanol.price, 0.6952016482242149)
assert_allclose(tea.NPV, 0, atol=100)

def test_tea():
cost = bst.decorators.cost
# Total installed equipment cost to be $1 MM
bst.CE = CE = bst.design_tools.CEPCI_by_year[2013]
@cost('Fake scaler', 'Lumped cost', CE=CE, cost=1e6, S=1, n=1, BM=1)
class LumpedCost(bst.Unit):
'''Does nothing but adding given costs.'''
_units = {'Fake scaler': ''}

def _design(self):
self.design_results['Fake scaler'] = 1

class TEA(bst.TEA):
def __init__(
self,
system,
FOC_over_installed=0.5, # annual O&M
DPI_over_installed=(1+1),
TDC_over_DPI=(1+0.2)*(1+0.4), # 20% non-installed & 40% indirect
FCI_over_TDC=1,
**kwargs,
):
self.FOC_over_installed = FOC_over_installed
self.DPI_over_installed = DPI_over_installed
self.TDC_over_DPI = TDC_over_DPI
self.FCI_over_TDC = FCI_over_TDC
bst.TEA.__init__(self, system, **kwargs)

def _FOC(self, installed_equipment_cost): # fixed operating cost
return installed_equipment_cost*self.FOC_over_installed

def _DPI(self, installed_equipment_cost): # direct permanent investment
return installed_equipment_cost*self.DPI_over_installed

def _TDC(self, DPI): # total depreciable cost
return DPI*self.TDC_over_DPI

def _FCI(self, TDC): # fixed capital investment
return TDC*self.FCI_over_TDC

bst.settings.set_thermo([bst.Chemical('Water')])
reactant = bst.Stream('reactant', Water=1, units='kg/hr')
# Total annual sales to be $2.5 MM
product = bst.Stream('product', Water=1, price=2.5e6/365/24, units='kg/hr')

U101 = LumpedCost('U101', ins=reactant, outs=product)
sys = bst.System('sys', path=(U101,))
sys.simulate()

tea = TEA(
system=sys,
IRR=0.1,
duration=(2013, 2013+15), # 15 years
income_tax=0.21+0.1,
construction_schedule=(1,),
depreciation='MACRS7',
operating_days=365,
startup_months=0,
startup_FOCfrac=1,
startup_VOCfrac=1,
startup_salesfrac=1,
lang_factor=None,
WC_over_FCI=0.05,
finance_interest=0.08,
finance_years=10,
finance_fraction =0.6,
accumulate_interest_during_construction=False,
)

# Below test NPV and discounted cashflow calculation
table = tea.get_cashflow_table()
data = np.array([
# Depreciable capital [MM$]
# Fixed capital investment [MM$]
# Working capital [MM$]
# Depreciation [MM$]
# Loan [MM$]
[ 3.36 , 3.36 , 0.168 , 0. , 2.016 ,
# Loan interest payment [MM$]
# Loan payment [MM$]
# Loan principal [MM$]
# Annual operating cost (excluding depreciation) [MM$]
# Sales [MM$]
0.16128 , 0. , 2.016 , 0. , 0. ,
# Tax [MM$]
# Incentives [MM$]
# Taxed earnings [MM$]
# Forwarded losses [MM$]
# Net earnings [MM$]
0. , 0. , 0. , 0. , 0. ,
# Cash flow [MM$]
# Discount factor
# Net present value (NPV) [MM$]
# Cumulative NPV [MM$]
-1.67328 , 1. , -1.67328 , -1.67328 ],
[ 0. , 0. , 0. , 0.480144 , 0. ,
0.16128 , 0.30044345, 1.87683655, 1.68 , 2.5 ,
0.01221789, 0. , 0.03941255, 0. , 0.02719466,
0.50733866, 0.90909091, 0.46121696, -1.21206304],
[ 0. , 0. , 0. , 0.822864 , 0. ,
0.15014692, 0.30044345, 1.72654003, 1.68 , 2.5 ,
0. , 0. , 0. , 0. , -0.30330745,
0.51955655, 0.82644628, 0.42938558, -0.78267746],
[ 0. , 0. , 0. , 0.587664 , 0. ,
0.1381232 , 0.30044345, 1.56421978, 1.68 , 2.5 ,
0. , 0. , 0. , -0.30330745, -0.06810745,
0.51955655, 0.7513148 , 0.39035053, -0.39232693],
[ 0. , 0. , 0. , 0.419664 , 0. ,
0.12513758, 0.30044345, 1.38891391, 1.68 , 2.5 ,
0. , 0. , 0. , -0.3714149 , 0.09989255,
0.51955655, 0.68301346, 0.35486412, -0.03746282],
[ 0. , 0. , 0. , 0.300048 , 0. ,
0.11111311, 0.30044345, 1.19958358, 1.68 , 2.5 ,
0. , 0. , 0. , -0.27152235, 0.21950855,
0.51955655, 0.62092132, 0.32260374, 0.28514093],
[ 0. , 0. , 0. , 0.299712 , 0. ,
0.09596669, 0.30044345, 0.99510681, 1.68 , 2.5 ,
0.05202753, 0. , 0.16783075, -0.0520138 , 0.16781702,
0.46752902, 0.56447393, 0.26390794, 0.54904887],
[ 0. , 0. , 0. , 0.300048 , 0. ,
0.07960854, 0.30044345, 0.77427191, 1.68 , 2.5 ,
0.06804765, 0. , 0.21950855, 0. , 0.1514609 ,
0.4515089 , 0.51315812, 0.23169546, 0.78074432],
[ 0. , 0. , 0. , 0.149856 , 0. ,
0.06194175, 0.30044345, 0.53577021, 1.68 , 2.5 ,
0.11460717, 0. , 0.36970055, 0. , 0.25509338,
0.40494938, 0.46650738, 0.18891187, 0.9696562 ],
[ 0. , 0. , 0. , 0. , 0. ,
0.04286162, 0.30044345, 0.27818838, 1.68 , 2.5 ,
0.16106253, 0. , 0.51955655, 0. , 0.35849402,
0.35849402, 0.42409762, 0.15203646, 1.12169266],
[ 0. , 0. , 0. , 0. , 0. ,
0.02225507, 0.30044345, 0. , 1.68 , 2.5 ,
0.16106253, 0. , 0.51955655, 0. , 0.35849402,
0.35849402, 0.38554329, 0.13821496, 1.25990762],
[ 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 1.68 , 2.5 ,
0.2542 , 0. , 0.82 , 0. , 0.5658 ,
0.5658 , 0.3504939 , 0.19830945, 1.45821707],
[ 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 1.68 , 2.5 ,
0.2542 , 0. , 0.82 , 0. , 0.5658 ,
0.5658 , 0.31863082, 0.18028132, 1.63849839],
[ 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 1.68 , 2.5 ,
0.2542 , 0. , 0.82 , 0. , 0.5658 ,
0.5658 , 0.28966438, 0.16389211, 1.80239049],
[ 0. , 0. , 0. , 0. , 0. ,
0. , 0. , 0. , 1.68 , 2.5 ,
0.2542 , 0. , 0.82 , 0. , 0.5658 ,
0.5658 , 0.26333125, 0.14899282, 1.95138332],
[ 0. , 0. , -0.168 , 0. , 0. ,
0. , 0. , 0. , 1.68 , 2.5 ,
0.2542 , 0. , 0.82 , 0. , 0.5658 ,
0.7338 , 0.23939205, 0.17566589, 2.1270492 ]])

assert_allclose(table.values, data, atol=1e-4)
assert_allclose(tea.NPV, table.iloc[-1,-1]*1e6, atol=1e-4)
total_interest_payment1 = table['Loan payment [MM$]'].sum()
total_interest_payment2 = (
table['Loan interest payment [MM$]'].iloc[1:].sum() + # payment during construction (year 0) is equity/cash
table['Loan principal [MM$]'].iloc[0])
assert_allclose(total_interest_payment1, total_interest_payment2, atol=1e-4)


if __name__ == '__main__':
test_depreciation_schedule()
test_depreciation_schedule()
test_cashflow_consistency()
test_tea()
Loading