# imports # local, internal from src.topupopt.data.finance.invest import Investment, discount_factor, npv from src.topupopt.data.finance.invest import salvage_value_linear_depreciation from src.topupopt.data.finance.invest import present_salvage_value_annuity from src.topupopt.data.finance.invest import salvage_value_annuity from src.topupopt.data.finance.utils import ArcInvestments # local, external import math # ****************************************************************************** # ****************************************************************************** class TestArcInvestments: def test_object_creation(self): discount_rate = 0.035 analysis_period = 20 discount_rates = tuple(discount_rate for i in range(analysis_period)) cash_flows_npv = [3.673079208612225, 1.5610827143687658, 17, 117, 1842] abs_cash_flows_npv = [1e-1, 1e-1, 1e-1, 0.5, 1] investments = [] # limited longevity operation, does not go beyond planning horizon cash_flow = 1 cash_flow_start = 1 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=cash_flow, start_period=cash_flow_start, longevity=4 ) investments.append(myinv) # limited longevity operation, goes beyond planning horizon cash_flow = 1 cash_flow_start = 18 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=cash_flow, start_period=cash_flow_start, longevity=4 ) investments.append(myinv) # D&V omkostninger kundeanlæg: Sengeløse Skole cash_flow = 1.2 cash_flow_start = 1 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=cash_flow, start_period=cash_flow_start, longevity=None ) investments.append(myinv) # D&V omkostninger kundeanlæg: Større forbrugere cash_flow = 6 / 10 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=10 * cash_flow, start_period=1, longevity=None ) myinv.add_operational_cash_flows( cash_flow=3 * cash_flow, start_period=2, longevity=None ) myinv.add_operational_cash_flows( cash_flow=1 * cash_flow, start_period=3, longevity=None ) investments.append(myinv) # D&V omkostninger kundeanlæg: Mindre forbrugere cash_flow = 0.320 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=197 * cash_flow, start_period=1, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(233 - 197) * cash_flow, start_period=2, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(269 - 233) * cash_flow, start_period=3, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(305 - 269) * cash_flow, start_period=4, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(341 - 305) * cash_flow, start_period=5, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(377 - 341) * cash_flow, start_period=6, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(413 - 377) * cash_flow, start_period=7, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(449 - 413) * cash_flow, start_period=8, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(488 - 449) * cash_flow, start_period=9, longevity=None ) investments.append(myinv) # check for inv, true_npv, _abs in zip(investments, cash_flows_npv, abs_cash_flows_npv): assert math.isclose(inv.net_present_value(), true_npv, abs_tol=_abs) # create object number_options = 5 static_loss = { (h, q, k): 1 + q * 0.25 + h * 0.15 + k * 0.05 for h in range(number_options) for q in range(2) for k in range(3) } arc_invs = ArcInvestments( investments=investments, name="any", efficiency=None, efficiency_reverse=None, capacity=tuple(1 + o for o in range(number_options)), # minimum_cost=tuple(1+o for o in range(number_options)), specific_capacity_cost=1, capacity_is_instantaneous=False, static_loss=static_loss, validate=True, ) # check the costs for _npv, true_npv, _abs in zip( arc_invs.minimum_cost, cash_flows_npv, abs_cash_flows_npv ): assert math.isclose(_npv, true_npv, abs_tol=_abs) # change something in the investments for inv in arc_invs.investments: inv.add_investment( investment=1, investment_period=0, investment_longevity=10 ) # update the minimum costs arc_invs.update_minimum_cost() # make sure the comparison fails for _npv, true_npv, _abs in zip( arc_invs.minimum_cost, cash_flows_npv, abs_cash_flows_npv ): error_raised = False try: assert math.isclose(_npv, true_npv, abs_tol=_abs) except AssertionError: error_raised = True assert error_raised # new true values new_true_npv = [ 4.673079208612225, 2.561082714368766, 18.054883962342764, 117.50524073646935, 1843.1804103901486, ] # print([inv.net_present_value() for inv in arc_invs]) for _npv, true_npv, _abs in zip( arc_invs.minimum_cost, new_true_npv, abs_cash_flows_npv ): assert math.isclose(_npv, true_npv, abs_tol=_abs) # ***************************************************************************** # ***************************************************************************** class TestDataFinance: # TODO: make sure that all methods work with variable discount rates # assert that the discount factors match def test_operational_cash_flows(self): discount_rate = 0.035 analysis_period = 20 discount_rates = tuple(discount_rate for i in range(analysis_period)) # limited longevity operation, does not go beyond planning horizon cash_flow = 1 cash_flow_start = 1 cash_flows_npv = 3.673079208612225 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=cash_flow, start_period=cash_flow_start, longevity=4 ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, cash_flows_npv, abs_tol=1e-1) # limited longevity operation, goes beyond planning horizon cash_flow = 1 cash_flow_start = 18 cash_flows_npv = 1.5610827143687658 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=cash_flow, start_period=cash_flow_start, longevity=4 ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, cash_flows_npv, abs_tol=1e-1) # D&V omkostninger kundeanlæg: Sengeløse Skole cash_flow = 1.2 cash_flow_start = 1 cash_flows_npv = 17 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=cash_flow, start_period=cash_flow_start, longevity=None ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, cash_flows_npv, abs_tol=1e-1) # D&V omkostninger kundeanlæg: Større forbrugere cash_flows_npv = 117 cash_flow = 6 / 10 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=10 * cash_flow, start_period=1, longevity=None ) myinv.add_operational_cash_flows( cash_flow=3 * cash_flow, start_period=2, longevity=None ) myinv.add_operational_cash_flows( cash_flow=1 * cash_flow, start_period=3, longevity=None ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, cash_flows_npv, abs_tol=0.5) # D&V omkostninger kundeanlæg: Mindre forbrugere cash_flows_npv = 1842 cash_flow = 0.320 myinv = Investment(discount_rates=discount_rates) myinv.add_operational_cash_flows( cash_flow=197 * cash_flow, start_period=1, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(233 - 197) * cash_flow, start_period=2, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(269 - 233) * cash_flow, start_period=3, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(305 - 269) * cash_flow, start_period=4, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(341 - 305) * cash_flow, start_period=5, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(377 - 341) * cash_flow, start_period=6, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(413 - 377) * cash_flow, start_period=7, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(449 - 413) * cash_flow, start_period=8, longevity=None ) myinv.add_operational_cash_flows( cash_flow=(488 - 449) * cash_flow, start_period=9, longevity=None ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, cash_flows_npv, abs_tol=1) # ************************************************************************* # ************************************************************************* # assert that the discount factors match def test_discount_factors(self): years = [ 2021, 2022, 2023, 2024, 2025, 2026, 2027, 2028, 2029, 2030, 2031, 2036, 2037, 2038, 2039, 2040, 2041, ] factors = [ 1.000, 0.966, 0.934, 0.902, 0.871, 0.842, 0.814, 0.786, 0.759, 0.734, 0.709, 0.597, 0.577, 0.557, 0.538, 0.520, 0.503, ] test_factors = [ discount_factor([0.035 for i in range(year - years[0])]) for year in years ] for i, factor in enumerate(factors): assert math.isclose(factor, test_factors[i], abs_tol=0.001) # ********************************************************************* # variable discount factors discount_rates = [0.035, 0.05, 0.075, 0.06] discount_factors = [1.00000, 0.96618, 0.92017, 0.85598, 0.80753] test_discount_factors = [ discount_factor(discount_rates[0:t]) for t in range(len(discount_rates) + 1) ] assert len(discount_factors) == len(test_discount_factors) for df_true, df in zip(discount_factors, test_discount_factors): assert math.isclose(df_true, df, abs_tol=0.001) # ************************************************************************* # ************************************************************************* def test_salvage_value_sengelose_linear_depreciation(self): # ********************************************************************* # ********************************************************************* # example # 1: Investering kundeanlæg, Sengeløse Skole commissioning_delay_after_investment = 0 investment_period = 1 investment = 180 # 1E3 DKK investment_longevity = ( 25 # if simultaneous_commissioning_investment else 25 # years ) analysis_period_span = 20 # years discount_rate = 0.035 # discount rates' tuple: size does not change with first_period_is_present_time discount_rates = tuple(discount_rate for i in range(analysis_period_span)) # ********************************************************************* residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, commissioning_delay_after_investment=commissioning_delay_after_investment, ) assert math.isclose(residual_value, 36, abs_tol=1) net_cash_flows = list(0 for i in range(analysis_period_span + 1)) net_cash_flows[investment_period] = investment net_cash_flows[analysis_period_span] = -residual_value npv_inv_horizon = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_inv_horizon, 155.82067163872074, abs_tol=1e-3) myinv = Investment(discount_rates=discount_rates) myinv.add_investment( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, commissioning_delay_after_investment=commissioning_delay_after_investment, salvage_value_method="asd", ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, 155.82067163872074, abs_tol=1e-3) # ********************************************************************* # ********************************************************************* # example # 2: Forsyningsledning investment_period = 1 investment = 13000 # 1E3 DKK investment_longevity = 60 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple(discount_rate for i in range(analysis_period_span)) # ********************************************************************* # using mean annual asset devaluation method residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, commissioning_delay_after_investment=commissioning_delay_after_investment, ) assert math.isclose(residual_value, 8667, abs_tol=1) net_cash_flows = list(0 for i in range(analysis_period_span + 1)) net_cash_flows[investment_period] = investment net_cash_flows[analysis_period_span] = -residual_value npv_inv_horizon = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_inv_horizon, 8204.815475022142, abs_tol=1e-3) myinv = Investment(discount_rates=discount_rates) myinv.add_investment( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, commissioning_delay_after_investment=commissioning_delay_after_investment, salvage_value_method="asd", ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, 8204.815475022142, abs_tol=1e-3) # ********************************************************************* # ********************************************************************* # example 3: Boosterpumpeanlæg investment_period = 7 investment = 1500 # 1E3 DKK investment_longevity = 25 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple(discount_rate for i in range(analysis_period_span)) # ********************************************************************* residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, commissioning_delay_after_investment=commissioning_delay_after_investment, ) assert math.isclose(residual_value, 660, abs_tol=1e-3) net_cash_flows = list(0 for i in range(analysis_period_span + 1)) net_cash_flows[investment_period] = investment net_cash_flows[analysis_period_span] = -residual_value npv_inv_horizon = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_inv_horizon, 847, abs_tol=0.3) myinv = Investment(discount_rates=discount_rates) myinv.add_investment( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, commissioning_delay_after_investment=commissioning_delay_after_investment, salvage_value_method="asd", ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, 847, abs_tol=0.3) # ********************************************************************* # ********************************************************************* # example 4: Investering, Sengeløse Skole, (varmepumper) investment_period = 1 investment = 1925 # 1E3 DKK investment_longevity = 20 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple(discount_rate for i in range(analysis_period_span)) # ********************************************************************* residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, commissioning_delay_after_investment=commissioning_delay_after_investment, ) assert math.isclose(residual_value, 0, abs_tol=1e-3) net_cash_flows = list(0 for i in range(analysis_period_span + 1)) net_cash_flows[investment_period] = investment net_cash_flows[analysis_period_span] = -residual_value npv_inv_horizon = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_inv_horizon, 1860, abs_tol=0.3) myinv = Investment(discount_rates=discount_rates) myinv.add_investment( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, commissioning_delay_after_investment=commissioning_delay_after_investment, salvage_value_method="asd", ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, 1860, abs_tol=0.3) # ********************************************************************* # ********************************************************************* # ************************************************************************* # ************************************************************************* def test_scrap_value_annuity(self): # Source: # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021 # Energistyrelsen, page 19 investment_period = 0 investment = 1e6 # 1E3 DKK investment_longevity = 30 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple( discount_rate for i in range(investment_longevity + investment_period) ) # ********************************************************************* # ********************************************************************* # calculate the net present value with the salvage value deducted # annuity method annuity = ( investment * discount_rate / (1 - (1 + discount_rate) ** (-investment_longevity)) ) net_cash_flows = list( annuity for i in range(investment_longevity + investment_period + 1) ) for year_index in range(investment_period + 1): net_cash_flows[year_index] = 0 npv_inv_horizon = npv( discount_rates=discount_rates[0:analysis_period_span], net_cash_flows=net_cash_flows[0 : analysis_period_span + 1], ) assert math.isclose(npv_inv_horizon, 772747.2928688908, abs_tol=1e-3) # ********************************************************************* # net present value for the whole investment npv_asset_long = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3) # calculate discounted salvage value directly npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3) # salvage value, as seen from the last period und_salvage_value = salvage_value_annuity( investment=investment, discount_rate=discount_rate, investment_longevity=investment_longevity, investment_period=investment_period, analysis_period_span=analysis_period_span, ) assert math.isclose(und_salvage_value, 452184.9058419504, abs_tol=1e-3) # ********************************************************************* # use only the part of discount_rates that overlaps with the planni. period myinv = Investment(discount_rates=discount_rates[0:analysis_period_span]) myinv.add_investment( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, ) mynpv = myinv.net_present_value() assert math.isclose(mynpv, npv_inv_horizon, abs_tol=1e-3) # ********************************************************************* # ********************************************************************* # trigger ValueError error_raised = False investment_period = analysis_period_span + 1 try: npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) except ValueError: error_raised = True assert error_raised # ********************************************************************* investment = 1 investment_period = 0 investment_longevity = 4 analysis_period_span = 3 discount_rate = 0.035 # ********************************************************************* # analysis period equals longevity: no salvage value npv_salvage, annuity = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span + 1, return_annuity=True, ) assert npv_salvage == 0.0 assert annuity > 0 # ********************************************************************* # increased longevity npv_salvage, annuity2 = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity + 1, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, return_annuity=True, ) assert math.isclose(npv_salvage, 0.37948959437673335, abs_tol=1e-3) assert annuity > annuity2 # ********************************************************************* # ********************************************************************* # ************************************************************************* # ************************************************************************* def test_scrap_value_annuity_longer_longevity(self): # Source: # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021 # Energistyrelsen, page 19 investment_period = 0 investment = 1e6 # 1E3 DKK investment_longevity = 35 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple( discount_rate for i in range(investment_longevity + investment_period) ) # ********************************************************************* # ********************************************************************* # calculate the net present value with the salvage value deducted # annuity method annuity = ( investment * discount_rate / (1 - (1 + discount_rate) ** (-investment_longevity)) ) net_cash_flows = list( annuity for i in range(investment_longevity + investment_period + 1) ) for year_index in range(investment_period + 1): net_cash_flows[year_index] = 0 npv_inv_horizon = npv( discount_rates=discount_rates[0:analysis_period_span], net_cash_flows=net_cash_flows[0 : analysis_period_span + 1], ) assert math.isclose(npv_inv_horizon, 710596.68, abs_tol=1e-2) # ********************************************************************* # net present value for the whole investment npv_asset_long = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3) # calculate discounted salvage value directly npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3) # salvage value, as seen from the last period und_salvage_value = salvage_value_annuity( investment=investment, discount_rate=discount_rate, investment_longevity=investment_longevity, investment_period=investment_period, analysis_period_span=analysis_period_span, ) assert math.isclose(und_salvage_value, 575851.51, abs_tol=1e-3) # ************************************************************************* # ************************************************************************* def test_scrap_value_annuity_starting_later(self): # Source: # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021 # Energistyrelsen, page 19 investment_period = 10 investment = 1e6 # 1E3 DKK investment_longevity = 30 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple( discount_rate for i in range(investment_longevity + investment_period) ) # ********************************************************************* # ********************************************************************* # calculate the net present value with the salvage value deducted # annuity method annuity = ( investment * discount_rate / (1 - (1 + discount_rate) ** (-investment_longevity)) ) net_cash_flows = list( annuity for i in range(investment_longevity + investment_period + 1) ) for year_index in range(investment_period + 1): net_cash_flows[year_index] = 0 npv_inv_horizon = npv( discount_rates=discount_rates[0:analysis_period_span], net_cash_flows=net_cash_flows[0 : analysis_period_span + 1], ) assert math.isclose(npv_inv_horizon, 320562.3870269, abs_tol=1e-2) # ********************************************************************* # net present value for the whole investment npv_asset_long = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_asset_long, 708918.8137098, abs_tol=1e-3) # calculate discounted salvage value directly npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3) assert math.isclose(npv_salvage, 388356.4266828, abs_tol=1e-3) # salvage value, as seen from the last period und_salvage_value = salvage_value_annuity( investment=investment, discount_rate=discount_rate, investment_longevity=investment_longevity, investment_period=investment_period, analysis_period_span=analysis_period_span, ) assert math.isclose(und_salvage_value, 772747.2928689, abs_tol=1e-3) # ************************************************************************* # ************************************************************************* def test_scrap_value_annuity_longer_planning_period(self): # Source: # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021 # Energistyrelsen, page 19 investment_period = 0 investment = 1e6 # 1E3 DKK investment_longevity = 30 # years analysis_period_span = 23 # years discount_rate = 0.035 discount_rates = tuple( discount_rate for i in range(investment_longevity + investment_period) ) # ********************************************************************* # calculate the net present value with the salvage value deducted # annuity method annuity = ( investment * discount_rate / (1 - (1 + discount_rate) ** (-investment_longevity)) ) net_cash_flows = list( annuity for i in range(investment_longevity + investment_period + 1) ) for year_index in range(investment_period + 1): net_cash_flows[year_index] = 0 npv_inv_horizon = npv( discount_rates=discount_rates[0:analysis_period_span], net_cash_flows=net_cash_flows[0 : analysis_period_span + 1], ) assert math.isclose(npv_inv_horizon, 849302.517460684, abs_tol=1e-3) # ********************************************************************* # net present value for the whole investment npv_asset_long = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3) # calculate discounted salvage value directly npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3) # salvage value, as seen from the last period und_salvage_value = salvage_value_annuity( investment=investment, discount_rate=discount_rate, investment_longevity=investment_longevity, investment_period=investment_period, analysis_period_span=analysis_period_span, ) assert math.isclose(und_salvage_value, 332455.89838989300, abs_tol=1e-3) # ************************************************************************* # ************************************************************************* def test_scrap_value_annuity_matching_periods(self): # Source: # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021 # Energistyrelsen, page 19 investment_period = 0 investment = 1e6 # 1E3 DKK investment_longevity = 20 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple( discount_rate for i in range(investment_longevity + investment_period) ) # ********************************************************************* # calculate the net present value with the salvage value deducted # annuity method annuity = ( investment * discount_rate / (1 - (1 + discount_rate) ** (-investment_longevity)) ) net_cash_flows = list( annuity for i in range(investment_longevity + investment_period + 1) ) for year_index in range(investment_period + 1): net_cash_flows[year_index] = 0 npv_inv_horizon = npv( discount_rates=discount_rates[0:analysis_period_span], net_cash_flows=net_cash_flows[0 : analysis_period_span + 1], ) assert math.isclose(npv_inv_horizon, 1e6, abs_tol=1e-3) # ********************************************************************* # net present value for the whole investment npv_asset_long = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3) # calculate discounted salvage value directly npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3) # salvage value, as seen from the last period und_salvage_value = salvage_value_annuity( investment=investment, discount_rate=discount_rate, investment_longevity=investment_longevity, investment_period=investment_period, analysis_period_span=analysis_period_span, ) assert math.isclose(und_salvage_value, 0, abs_tol=1e-3) # ************************************************************************* # ************************************************************************* def test_scrap_value_annuity_ending_before_horizon(self): # Source: # Vejledning i samfundsøkonomiske analyser på energiområdet, juli 2021 # Energistyrelsen, page 19 investment_period = 0 investment = 1e6 # 1E3 DKK investment_longevity = 15 # years analysis_period_span = 20 # years discount_rate = 0.035 discount_rates = tuple( discount_rate for i in range(investment_longevity + investment_period) ) # ********************************************************************* # calculate the net present value with the salvage value deducted # annuity method annuity = ( investment * discount_rate / (1 - (1 + discount_rate) ** (-investment_longevity)) ) net_cash_flows = list( annuity for i in range(investment_longevity + investment_period + 1) ) for year_index in range(investment_period + 1): net_cash_flows[year_index] = 0 npv_inv_horizon = npv( discount_rates=discount_rates[0:analysis_period_span], net_cash_flows=net_cash_flows[0 : analysis_period_span + 1], ) assert math.isclose(npv_inv_horizon, 1e6, abs_tol=1e-3) # ********************************************************************* # net present value for the whole investment npv_asset_long = npv( discount_rates=discount_rates, net_cash_flows=net_cash_flows ) assert math.isclose(npv_asset_long, 1e6, abs_tol=1e-3) # calculate discounted salvage value directly npv_salvage = present_salvage_value_annuity( investment=investment, investment_longevity=investment_longevity, investment_period=investment_period, discount_rate=discount_rate, analysis_period_span=analysis_period_span, ) assert math.isclose(npv_salvage, npv_asset_long - npv_inv_horizon, abs_tol=1e-3) # salvage value, as seen from the last period und_salvage_value = salvage_value_annuity( investment=investment, discount_rate=discount_rate, investment_longevity=investment_longevity, investment_period=investment_period, analysis_period_span=analysis_period_span, ) assert math.isclose(und_salvage_value, 0, abs_tol=1e-3) # ************************************************************************* # ************************************************************************* def test_scrap_value_commissioning_delay_linear_depreciation(self): # ************************************************************************** investment = 10 investment_period = 0 investment_longevity = 4 analysis_period_span = 3 # ********************************************************************* # the investment still produces benefits after the evaluation phase residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, ) assert residual_value == 2.5 # the investment is delayed investment_period = 1 residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, ) assert residual_value == 5.0 # the investment is delayed even more investment_period = 2 residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, ) assert residual_value == 7.5 # the evaluation phase is longer investment_period = 0 analysis_period_span = 4 residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, ) assert residual_value == 0.0 # trigger ValueError: the investment takes place after the eval. phase investment_period = analysis_period_span + 1 error_raised = False try: residual_value = salvage_value_linear_depreciation( investment=investment, investment_period=investment_period, investment_longevity=investment_longevity, analysis_period_span=analysis_period_span, ) except ValueError: error_raised = True assert error_raised # ************************************************************************* # ************************************************************************* def test_npv(self): # ********************************************************************* # data R_t = [ 0, 21579, 4002, 3302, 2952, 2952, 2952, 2952, 2952, 3198, 0, 0, 0, 16154, 2952, 6452, -21930, ] n_periods = len(R_t) - 1 R_t2 = [ncf_t * 1.5 for ncf_t in R_t] i_t = tuple([0.035 for k in range(n_periods)]) i_t2 = tuple([ii_t * 1.5 for ii_t in i_t]) # ********************************************************************* # compute the NPV via the object my_inv = Investment(i_t, R_t) assert math.isclose( my_inv.net_present_value(), 45287.96018387402, abs_tol=0.001 ) # compute the NPV via the object using i_t2 and R_t my_inv.discount_rates = i_t2 my_npv = my_inv.net_present_value() assert math.isclose(my_npv, 42923.405014, abs_tol=0.001) # compute the NPV via the object using i_t2 and R_t2 my_inv.net_cash_flows = R_t2 my_npv = my_inv.net_present_value() assert math.isclose(my_npv, 64385.107522, abs_tol=0.001) # compute the NPV via the _npv method using i_t and R_t2 my_inv.discount_rates = i_t my_inv.net_cash_flows = R_t2 my_npv = my_inv.net_present_value() assert math.isclose(my_npv, 67931.940276, abs_tol=0.001) # compute the NPV via the npv method using i_t and R_t my_npv, my_df = npv( discount_rates=i_t, net_cash_flows=R_t, return_discount_factors=True ) assert math.isclose(my_npv, 45287.960184, abs_tol=0.001) # create new object without specifying the net cash flows my_inv = Investment(discount_rates=i_t) for ncf_t in my_inv.net_cash_flows: assert ncf_t >= 0 my_inv.net_cash_flows = R_t my_npv = my_inv.net_present_value() assert math.isclose(my_npv, 45287.960184, abs_tol=0.001) # create new object by specifying the discount rate and the analysis period my_inv = Investment( None, net_cash_flows=R_t, discount_rate=i_t[0], analysis_period_span=len(i_t), ) my_npv = my_inv.net_present_value() assert math.isclose(my_npv, 45287.960184, abs_tol=0.001) # ********************************************************************* # force errors # ********************************************************************* # TypeError('The discount rates must be provided as a tuple.') error_raised = False try: my_inv = Investment(list(i_t), R_t) except TypeError: error_raised = True assert error_raised # ********************************************************************* # ValueError('The duration of the period under analysis must be positive.') error_raised = False try: my_inv = Investment(tuple()) except ValueError: error_raised = True assert error_raised # ********************************************************************* # TypeError('The discount rate must be provided as a float.') error_raised = False try: my_inv = Investment(None, None, 5, 10) except TypeError: error_raised = True assert error_raised # ********************************************************************* # ValueError('The discount rate must be in the open interval between 0 and 1.) error_raised = False try: my_inv = Investment(None, None, 1.35, 10) except ValueError: error_raised = True assert error_raised # ********************************************************************* # TypeError('The duration of the period under consideration must be provided as an integer.') error_raised = False try: my_inv = Investment(None, None, 0.35, 10.0) except TypeError: error_raised = True assert error_raised # ********************************************************************* # ValueError('The duration of the period under analysis must be positive.) error_raised = False try: my_inv = Investment(None, None, 0.35, 0) except ValueError: error_raised = True assert error_raised # ********************************************************************* # TypeError('The net cash flows must be provided as a list.') error_raised = False try: my_inv = Investment(i_t, tuple(R_t)) except TypeError: error_raised = True assert error_raised # ********************************************************************* # trigger the error for differently-sized inputs using npv() and not _npv() number_errors = 0 try: my_npv = npv(i_t[0:-1], R_t) except ValueError: number_errors += 1 assert number_errors == 1 # ********************************************************************* # trigger the error for differently-sized inputs using the __init__ method number_errors = 0 try: my_inv = Investment(i_t[0:-1], R_t) except ValueError: number_errors += 1 assert number_errors == 1 # ********************************************************************* # ****************************************************************************** # ******************************************************************************