Skip to content

Commit 96feb9b

Browse files
committed
Merge branch 'master' of https://github.com/BioSTEAMDevelopmentGroup/biosteam into qsdsan
2 parents dd50f53 + 6f94939 commit 96feb9b

File tree

6 files changed

+307
-56
lines changed

6 files changed

+307
-56
lines changed

biosteam/_tea.py

+89-42
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@
3737
'Sales [MM$]',
3838
'Tax [MM$]',
3939
'Incentives [MM$]',
40+
'Taxed earnings [MM$]',
41+
'Forwarded losses [MM$]',
4042
'Net earnings [MM$]',
4143
'Cash flow [MM$]',
4244
'Discount factor',
@@ -72,7 +74,7 @@ def NPV_at_IRR(IRR, cashflow_array, duration_array):
7274
return (cashflow_array/(1.+IRR)**duration_array).sum()
7375

7476
@njit(cache=True)
75-
def initial_loan_principal(loan, interest):
77+
def loan_principal_with_interest(loan, interest):
7678
principal = 0
7779
k = 1. + interest
7880
for i in loan:
@@ -81,18 +83,21 @@ def initial_loan_principal(loan, interest):
8183
return principal
8284

8385
@njit(cache=True)
84-
def final_loan_principal(payment, principal, interest, years):
85-
for iter in range(years):
86-
principal += principal * interest - payment
87-
return principal
86+
def solve_payment(loan_principal, interest, years):
87+
f = 1 + interest
88+
fn = f ** years
89+
return loan_principal * interest * fn / (fn - 1)
8890

89-
def solve_payment(payment, loan, interest, years):
90-
principal = initial_loan_principal(loan, interest)
91-
payment = flx.aitken_secant(final_loan_principal,
92-
payment, payment+10., 1., 10.,
93-
args=(principal, interest, years),
94-
maxiter=200, checkiter=False)
95-
return payment
91+
@njit(cache=True)
92+
def taxable_earnings_with_fowarded_losses(taxable_cashflow): # Forwards losses to later years to reduce future taxes
93+
taxed_earnings = taxable_cashflow.copy()
94+
for i in range(taxed_earnings.size - 1):
95+
x = taxed_earnings[i]
96+
if x < 0:
97+
taxed_earnings[i] = 0
98+
taxed_earnings[i + 1] += x
99+
if taxed_earnings[-1] < 0: taxed_earnings[-1] = 0
100+
return taxed_earnings
96101

97102
@njit(cache=True)
98103
def add_replacement_cost_to_cashflow_array(equipment_installed_cost,
@@ -170,6 +175,7 @@ def taxable_and_nontaxable_cashflows(
170175
finance_fraction,
171176
start, years,
172177
lang_factor,
178+
accumulate_interest_during_construction,
173179
):
174180
# Cash flow data and parameters
175181
# C_FC: Fixed capital
@@ -186,18 +192,23 @@ def taxable_and_nontaxable_cashflows(
186192
startup_FOCfrac,
187193
startup_salesfrac,
188194
construction_schedule,
189-
start
195+
start,
190196
)
191197
for i in unit_capital_costs:
192198
add_all_replacement_costs_to_cashflow_array(i, C_FC, years, start, lang_factor)
193199
if finance_interest:
194200
interest = finance_interest
195201
years = finance_years
196-
Loan[:start] = loan = finance_fraction*(C_FC[:start]+C_WC[:start])
197-
LP[start:start + years] = solve_payment(loan.sum()/years * (1. + interest),
198-
loan, interest, years)
202+
Loan[:start] = loan = finance_fraction*(C_FC[:start])
203+
if accumulate_interest_during_construction:
204+
loan_principal = loan_principal_with_interest(loan, interest)
205+
else:
206+
loan_principal = loan.sum()
207+
LP[start:start + years] = solve_payment(loan_principal, interest, years)
199208
taxable_cashflow = S - C - D - LP
200209
nontaxable_cashflow = D + Loan - C_FC - C_WC
210+
if not accumulate_interest_during_construction:
211+
nontaxable_cashflow[:start] -= loan * interest
201212
else:
202213
taxable_cashflow = S - C - D
203214
nontaxable_cashflow = D - C_FC - C_WC
@@ -214,9 +225,13 @@ def NPV_with_sales(
214225
):
215226
"""Return NPV with an additional annualized sales."""
216227
taxable_cashflow = taxable_cashflow + sales * sales_coefficients
217-
tax = np.zeros_like(taxable_cashflow)
228+
tax = np.zeros_like(taxable_cashflow, dtype=float)
218229
incentives = tax.copy()
219-
fill_tax_and_incentives(incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation)
230+
fill_tax_and_incentives(
231+
incentives,
232+
taxable_earnings_with_fowarded_losses(taxable_cashflow),
233+
nontaxable_cashflow, tax, depreciation
234+
)
220235
cashflow = nontaxable_cashflow + taxable_cashflow + incentives - tax
221236
return (cashflow/discount_factors).sum()
222237

@@ -312,7 +327,7 @@ class TEA:
312327
'_startup_schedule', '_operating_days',
313328
'_duration', '_depreciation_key', '_depreciation',
314329
'_years', '_duration', '_start', 'IRR', '_IRR', '_sales',
315-
'_duration_array_cache')
330+
'_duration_array_cache', 'accumulate_interest_during_construction')
316331

317332
#: Available depreciation schedules. Defaults include modified
318333
#: accelerated cost recovery system from U.S. IRS publication 946 (MACRS),
@@ -389,7 +404,8 @@ def __init__(self, system: System, IRR: float, duration: tuple[int, int],
389404
construction_schedule: Sequence[float],
390405
startup_months: float, startup_FOCfrac: float, startup_VOCfrac: float,
391406
startup_salesfrac: float, WC_over_FCI: float, finance_interest: float,
392-
finance_years: int, finance_fraction: float):
407+
finance_years: int, finance_fraction: float,
408+
accumulate_interest_during_construction: bool=False):
393409
#: System being evaluated.
394410
self.system: System = system
395411

@@ -434,6 +450,9 @@ def __init__(self, system: System, IRR: float, duration: tuple[int, int],
434450
#: Guess cost for solve_price method
435451
self._sales: float = 0
436452

453+
#: Whether to immediately pay interest before operation or to accumulate interest during construction
454+
self.accumulate_interest_during_construction = accumulate_interest_during_construction
455+
437456
#: For convenience, set a TEA attribute for the system
438457
system._TEA = self
439458

@@ -708,6 +727,8 @@ def get_cashflow_table(self):
708727
# S: Sales
709728
# T: Tax
710729
# I: Incentives
730+
# TE: Taxed earnings
731+
# FL: Forwarded losses
711732
# NE: Net earnings
712733
# CF: Cash flow
713734
# DF: Discount factor
@@ -721,7 +742,7 @@ def get_cashflow_table(self):
721742
VOC = self.VOC
722743
sales = self.sales
723744
length = start + years
724-
C_D, C_FC, C_WC, D, L, LI, LP, LPl, C, S, T, I, NE, CF, DF, NPV, CNPV = data = np.zeros((17, length))
745+
C_D, C_FC, C_WC, D, L, LI, LP, LPl, C, S, T, I, TE, FL, NE, CF, DF, NPV, CNPV = data = np.zeros((19, length))
725746
self._fill_depreciation_array(D, start, years, TDC)
726747
w0 = self._startup_time
727748
w1 = 1. - w0
@@ -744,24 +765,44 @@ def get_cashflow_table(self):
744765
interest = self.finance_interest
745766
years = self.finance_years
746767
end = start + years
747-
L[:start] = loan = self.finance_fraction*(C_FC[:start]+C_WC[:start])
748-
f_interest = (1. + interest)
749-
LP[start:end] = solve_payment(loan.sum()/years * f_interest,
750-
loan, interest, years)
768+
L[:start] = loan = self.finance_fraction*(C_FC[:start])
769+
accumulate_interest_during_construction = self.accumulate_interest_during_construction
770+
if accumulate_interest_during_construction:
771+
initial_loan_principal = loan_principal_with_interest(loan, interest)
772+
else:
773+
initial_loan_principal = loan.sum()
774+
LP[start:end] = solve_payment(initial_loan_principal, interest, years)
751775
loan_principal = 0
752-
for i in range(end):
753-
LI[i] = li = (loan_principal + L[i]) * interest
754-
LPl[i] = loan_principal = loan_principal - LP[i] + li + L[i]
776+
if accumulate_interest_during_construction:
777+
for i in range(end):
778+
LI[i] = li = (loan_principal + L[i]) * interest
779+
LPl[i] = loan_principal = loan_principal - LP[i] + li + L[i]
780+
else:
781+
for i in range(end):
782+
if i < start:
783+
li = 0
784+
else:
785+
li = (loan_principal + L[i]) * interest
786+
LI[i] = li
787+
LPl[i] = loan_principal = loan_principal - LP[i] + li + L[i]
788+
LI[:start] = L[:start] * interest # Interest still needs to be payed
789+
755790
taxable_cashflow = S - C - D - LP
756791
nontaxable_cashflow = D + L - C_FC - C_WC
792+
if not accumulate_interest_during_construction:
793+
nontaxable_cashflow[:start] -= LI[:start] # Subtract the interest during construction from NPV
757794
else:
758795
taxable_cashflow = S - C - D
759796
nontaxable_cashflow = D - C_FC - C_WC
760-
self._fill_tax_and_incentives(I, taxable_cashflow, nontaxable_cashflow, T, D)
797+
TE[:] = taxable_earnings_with_fowarded_losses(taxable_cashflow)
798+
FL[1:] = (taxable_cashflow - TE).cumsum()[:-1]
799+
self._fill_tax_and_incentives(
800+
I, TE, nontaxable_cashflow, T, D
801+
)
761802
NE[:] = taxable_cashflow + I - T
762803
CF[:] = NE + nontaxable_cashflow
763804
DF[:] = 1/(1.+self.IRR)**self._get_duration_array()
764-
NPV[:] = CF*DF
805+
NPV[:] = CF * DF
765806
CNPV[:] = NPV.cumsum()
766807
DF *= 1e6
767808
data /= 1e6
@@ -774,7 +815,11 @@ def NPV(self) -> float:
774815
taxable_cashflow, nontaxable_cashflow, depreciation = self._taxable_nontaxable_depreciation_cashflows()
775816
tax = np.zeros_like(taxable_cashflow)
776817
incentives = tax.copy()
777-
self._fill_tax_and_incentives(incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation)
818+
self._fill_tax_and_incentives(
819+
incentives,
820+
taxable_earnings_with_fowarded_losses(taxable_cashflow),
821+
nontaxable_cashflow, tax, depreciation
822+
)
778823
cashflow = nontaxable_cashflow + taxable_cashflow + incentives - tax
779824
return NPV_at_IRR(self.IRR, cashflow, self._get_duration_array())
780825

@@ -818,21 +863,25 @@ def _taxable_nontaxable_depreciation_cashflows(self):
818863
self.finance_years,
819864
self.finance_fraction,
820865
start, years,
821-
self.lang_factor
866+
self.lang_factor,
867+
self.accumulate_interest_during_construction,
822868
),
823869
D
824870
)
825871

826872
def _fill_tax_and_incentives(self, incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation):
827-
index = taxable_cashflow > 0.
828-
tax[index] = self.income_tax * taxable_cashflow[index]
873+
tax[:] = self.income_tax * taxable_cashflow
829874

830875
def _net_earnings_and_nontaxable_cashflow_arrays(self):
831876
taxable_cashflow, nontaxable_cashflow, depreciation = self._taxable_nontaxable_depreciation_cashflows()
832877
size = taxable_cashflow.size
833878
tax = np.zeros(size)
834879
incentives = tax.copy()
835-
self._fill_tax_and_incentives(incentives, taxable_cashflow, nontaxable_cashflow, tax, depreciation)
880+
self._fill_tax_and_incentives(
881+
incentives,
882+
taxable_earnings_with_fowarded_losses(taxable_cashflow),
883+
nontaxable_cashflow, tax, depreciation
884+
)
836885
net_earnings = taxable_cashflow + incentives - tax
837886
return net_earnings, nontaxable_cashflow
838887

@@ -950,12 +999,11 @@ def solve_sales(self):
950999
9511000
"""
9521001
discount_factors = (1 + self.IRR)**self._get_duration_array()
953-
sales_coefficients = np.ones_like(discount_factors)
1002+
sales_coefficients = np.ones_like(discount_factors, dtype=float)
9541003
start = self._start
9551004
sales_coefficients[:start] = 0
9561005
w0 = self._startup_time
957-
sales_coefficients[self._start] = w0*self.startup_VOCfrac + (1-w0)
958-
sales = self._sales
1006+
sales_coefficients[start] = w0*self.startup_salesfrac + (1.-w0)
9591007
taxable_cashflow, nontaxable_cashflow, depreciation = self._taxable_nontaxable_depreciation_cashflows()
9601008
if np.isnan(taxable_cashflow).any():
9611009
warn('nan encountered in cashflow array; resimulating system', category=RuntimeWarning)
@@ -969,17 +1017,16 @@ def solve_sales(self):
9691017
sales_coefficients,
9701018
discount_factors,
9711019
self._fill_tax_and_incentives)
972-
x0 = sales
1020+
x0 = self._sales if np.isfinite(self._sales) else 0
9731021
f = NPV_with_sales
974-
if not np.isfinite(x0): x0 = 0.
9751022
y0 = f(x0, *args)
9761023
x1 = x0 - y0 / self._years # First estimate
9771024
try:
978-
sales = flx.aitken_secant(f, x0, x1, xtol=10, ytol=1000.,
1025+
sales = flx.aitken_secant(f, x0, x1, xtol=10, ytol=100.,
9791026
maxiter=1000, args=args, checkiter=True)
9801027
except:
9811028
bracket = flx.find_bracket(f, x0, x1, args=args)
982-
sales = flx.IQ_interpolation(f, *bracket, args=args, xtol=10, ytol=1000, maxiter=1000, checkiter=False)
1029+
sales = flx.IQ_interpolation(f, *bracket, args=args, xtol=10, ytol=100, maxiter=1000, checkiter=False)
9831030
self._sales = sales
9841031
return sales
9851032

docs/_static/css/custom.css

+1-4
Original file line numberDiff line numberDiff line change
@@ -1,14 +1,11 @@
11
html[data-theme="dark"] img {
22
filter: none;
33
}
4+
45
html[data-theme="dark"] .bd-content img:not(.only-dark) {
56
background: unset;
67
}
78

8-
.bd-header-announcement:after {
9-
background-color: rgb(225, 173, 106);
10-
}
11-
129
html[data-theme="light"] {
1310
--pst-color-title: rgb(69, 157, 185);
1411
}

docs/conf.py

+4-4
Original file line numberDiff line numberDiff line change
@@ -263,10 +263,10 @@ def typehints_formatter(annotation, config):
263263
'image_dark': 'logo_dark.png'
264264
},
265265
"show_toc_level": 2,
266-
# "announcement": (
267-
# "<p>Join us on Feb 17, 9:15-10:15am CST, for a BioSTEAM workshop! "
268-
# "<a href='mailto: [email protected]'>Email us for details</a></p>"
269-
# ),
266+
"announcement": (
267+
"<p>Join us on Feb 20, 10:15-11:15am CST, for a BioSTEAM workshop! "
268+
"<a href='mailto: [email protected]'>Email us for details</a></p>"
269+
),
270270
"external_links": [
271271
{"name": "Bioindustrial-Park", "url": "https://github.com/BioSTEAMDevelopmentGroup/Bioindustrial-Park"},
272272
{"name": "How2STEAM", "url": "https://mybinder.org/v2/gh/BioSTEAMDevelopmentGroup/How2STEAM/HEAD"},

0 commit comments

Comments
 (0)