#!/usr/bin/env python3
# -*- coding: utf-8 -*-
"""This module contains utility functions for the model.
Here, we determine factors to covert future value to present value for costs
and benefits. To account for the variable factor, fixed factor, and capital
factor, we need to convert all future costs to their net present value.
This means adjusting for the time value of money so that all costs are
expressed in terms of today's dollars.
We also assume that variable cost and fixed cost for non-modelled years are
assumed to be equal to the cost of the last modelled year preceding them.
This allows for consistent comparison across different time periods and
technologies.
**Variable Factor**
.. image:: ../_static/varcost.png
:width: 278
:align: center
:alt: Calculation of variable costs
Given the following:
* Variable cost of modeled year: :math:`B`
* Discount rate: :math:`r`
* :math:`m`-th modeled year: :math:`m = y - y_\\text{min}`
* Depreciation periods: :math:`n`
The total present value can be calculated as follows:
.. math::
:nowrap:
\\begin{align}
\\text{total present value} &= \\frac{B}{(1+r)^m} + \\frac{B}{(1+r)^{m+1}}
+ \\cdots + \\frac{B}{(1+r)^{(m+k-1)}} \\\\
&= B(1+r)^{(1-m)}\\frac{1-(1+r)^k}{r}
\\end{align}
And we can calculate the variable factor as follows:
.. math::
:nowrap:
\\begin{align}
\\text{factor}_{y}^{var} &= (1+r)^{1-m_y}\\frac{1-(1+r)^{k_y}}{r} \\\\
m_{y} &= y - y_\\text{min} \\\\
k_{y} &= y_\\text{periods}
\\end{align}
**Fixed Factor**
We can equate the fixed factor with the variable factor as follows:
.. math:: \\text{factor}_{y}^\\text{fix} = factor_{y}^\\text{var}
**Investment Factor**
.. image:: ../_static/invcost.png
:width: 400
:align: center
:alt: Calculation of investment costs
Given the following:
* Weighted Average Cost of Capital
(WACC, or otherwise known as the interest rate): :math:`i`
* Discount rate: :math:`r`
* :math:`m`-th modeled year: :math:`m = y - y_\\text{min}`
* Length of :math:`m`-th planning periods: :math:`k`
The total present value can be calculated as follows:
.. math::
:nowrap:
\\begin{align}
\\text{total present value} &= \\frac{P}{(1+r)^m} \\\\
&= \\frac{\\frac{A}{(1+i)} + \\frac{A}{(1+i)^2} + \\cdots
+ \\frac{A}{(1+i)^n}}{(1+r)^m} \\\\
&= A\\frac{1-(1+i)^{-n}}{i}\\times\\frac{1}{(1+r)^m}
\\end{align}
From the above, we can solve for the annualized cost of depreciation periods,
:math:`A`, as:
.. math::
A = P\\frac{i}{1-(1+i)^{-n}}
The capital recovery factor is then calculated as:
.. math::
\\text{capital recovery factor} = \\frac{i}{1-(1+i)^{-n}}
Let's focus on the time periods that fall within the modelled time horizon
(indicated in black colour).
We can calculate the length of time periods, :math:`k`, as follows:
.. math::
k = y_{max} - y
Using :math:`k`, we can calculate the net present value as follows:
.. math::
\\text{net present value} =
\\begin{cases}
\\frac{\\frac{A}{(1+r)} + \\frac{A}{(1+r)^2} + \\cdots
+ \\frac{A}{(1+r)^{min(n, k)}}}{(1+r)^m} \\text{if} \\quad n \\le k \\\\
\\text{total present value}\\quad \\text{if }n > k \\\\
\\frac{A\\frac{1-(1+r)^{-k}}{r}}{(1+r)^m} = P\\frac{i}{1-(1+i)^{-n}}\\times
\\frac{1-(1+r)^{-k}}{r(1+r)^m} & \\text{otherwise}
\\end{cases}
And we can calculate the investment factor as follows:
.. math::
factor_{y}^{inv} = \\frac{i}{1-(1+i)^{-n}}\\times
\\frac{1-(1+r)^{-min(n,k)}}{r(1+r)^m}
"""
from typing import Union, Tuple, List
from itertools import product
from scipy import interpolate
import pandas as pd
import numpy as np
[docs]def check_positive(*values : Union[int, float]) -> None:
"""Ensure all values are greater than 0.
Parameters
----------
values : Union[int, float]
Values to be checked.
Raises
------
ValueError
If any value is less than or equal to 0.
"""
for value in values:
if value <= 0:
raise ValueError("All arguments must be greater than 0.")
[docs]def calc_interest_rate(
public_debt_ratio : float,
private_debt_ratio : float,
cost_of_public_debt : float,
cost_of_private_equity : float,
cost_of_private_debt : float,
) -> float:
"""Weighted-average cost of capital for a single project.
Combines public-debt, private-debt and equity tranches into a
single project-level interest rate that the investment cost factor
uses to discount construction outlays. Equity ratio is the
residual ``1 - public_debt_ratio - private_debt_ratio``.
Parameters
----------
public_debt_ratio : float
Share of project capital financed by public debt, in [0, 1].
private_debt_ratio : float
Share financed by private debt, in [0, 1].
cost_of_public_debt : float
Annual interest rate on the public-debt tranche.
cost_of_private_equity : float
Required annual return on the equity tranche.
cost_of_private_debt : float
Annual interest rate on the private-debt tranche.
Returns
-------
float
Weighted-average interest rate for the project.
Raises
------
ValueError
If any of the three ratios is negative or their sum exceeds 1.
"""
equity_ratio = 1 - public_debt_ratio - private_debt_ratio
if (public_debt_ratio < 0) or (private_debt_ratio < 0) or (equity_ratio < 0):
raise ValueError(
"Debt and equity ratios must be non-negative and sum to 1 "
"or less."
)
return (
public_debt_ratio * cost_of_public_debt
+ private_debt_ratio * cost_of_private_debt
+ equity_ratio * cost_of_private_equity
)
[docs]def calc_inv_cost_factor(
dep_period : int,
interest_rate : float,
year_built : int,
discount_rate : int,
year_min : int,
year_max : int
) -> float:
"""Compute the investment cost factor. When the depreciation period is
greater than the planning horizon, the investment cost factor is calculated
by only considering the period within the planning horizon.
Parameters
----------
dep_period : int
Depreciation period, in years, i.e., lifetime of the infrastructure.
interest_rate : float
Interest rate.
year_built : int
Year of investment.
discount_rate : float
Discount rate.
year_min : int
Minimum year, i.e., the first year of the planning horizon.
year_max : int
Maximum year, i.e., the last year of the planning horizon.
Returns
-------
float
Investment cost factor.
Raises
------
ValueError
If year_max <= year_min, year_max < year_built, or
year_built < year_min.
Examples
--------
Given a depreciation period of 20 years, interest rate of 0.05, year of
investment in 2025, discount rate of 0.05, planning horizon from 2020 to
2050, compute the investment cost factor:
>>> calc_inv_cost_factor(20, 0.05, 2025, 0.05, 2020, 2050)
0.783526
If the depreciation perios is 100 years, compute the investment cost factor
for the same scenario:
>>> calc_inv_cost_factor(100, 0.05, 2025, 0.05, 2020, 2050)
0.567482
"""
check_positive(dep_period, interest_rate, year_built, year_min, year_max)
if (year_max < year_min) or (year_max < year_built) \
or (year_built < year_min):
raise ValueError("Invalid year values.")
years_since_min = year_built - year_min
years_to_max = year_max - year_built + 1
return (interest_rate / (1 - (1 + interest_rate) ** (-dep_period))
* (1 - (1 + discount_rate) ** (-min(dep_period, years_to_max)))
/ (discount_rate * (1 + discount_rate) ** years_since_min))
[docs]def calc_cost_factor(
discount_rate : float,
modeled_year : int,
year_min : int,
next_modeled_year : int
) -> float:
"""Compute the variable and fixed cost factor while considering the
multi-stage planning horizon.
Parameters
----------
discount_rate : float
The discount rate to apply.
modeled_year : int
The year in which the cost occurs.
year_min : int
The first year of the planning horizon. All costs are discounted to
this year.
next_modeled_year : int
The subsequent modeled year. The cost incurred bewteen modeled_year
and modeled_year and next_modeled_year is calculated.
Returns
-------
float
The computed cost factor.
Raises
------
ValueError
if next_modeled_year < modeled_year.
Examples
--------
Given annual cost incurred in 2025, next_modeled_year = 2030, and starting
year = 2020, compute present value in 2020 of the cost incurred in
2025-2029:
>>> calc_cost_factor(0.05, 2025, 2020, 2030)
3.561871
"""
check_positive(discount_rate, modeled_year, year_min, next_modeled_year)
if next_modeled_year <= modeled_year:
raise ValueError(
"Next modeled year must be greater than or"
+ "equal to the current modeled year."
)
years_since_min = modeled_year - year_min
years_to_next = next_modeled_year - modeled_year
return (1 - (1 + discount_rate) ** (-years_to_next)) \
/ (discount_rate * (1 + discount_rate) ** (years_since_min - 1))
[docs]def interpolate_z_by_q_or_s(
name : str,
qs : Union[np.ndarray, float],
zqv : pd.DataFrame
) -> float:
"""Interpolate forebay water level (Z) by reservoir storage (S) or tailrace
water level (Z) by the reservoir outflow (Q).
Parameters
----------
name : str
Code of the hydropower station.
qs : Union[np.ndarray, float]
Reservoir storage or outflow values.
zqv : pandas.DataFrame
DataFrame of ZQ or ZV values.
Returns
-------
Union[np.ndarray, float]
Interpolated values.
"""
# Match by string tech name; fallback to int comparison for backwards
# compatibility with legacy datasets that still use integer station IDs.
zqv_station = zqv[zqv.tech == name]
if zqv_station.empty:
try:
zqv_station = zqv[zqv.tech == int(name)]
except (ValueError, TypeError):
pass
if 'discharge' in zqv_station.columns:
x = zqv_station.discharge
z = zqv_station.tailrace_level
else:
x = zqv_station.volume
z = zqv_station.forebay_level
f_zqv = interpolate.interp1d(x, z, fill_value='extrapolate')
return f_zqv(qs)
[docs]def cartesian_product(
*args : List[Union[int, str]]
) -> List[Tuple[Union[int, str]]]:
"""Generate cartesian product of input iterables.
Parameters
----------
args : List[Union[int, str]]
Iterables to be combined.
Returns
-------
List[Tuple[Union[int, str]]]
List of tuples representing the Cartesian product.
Examples
--------
Combine two lists [1, 2] and [7, 8]:
>>> cartesian_product([1, 2], [7, 8])
[(1, 7), (1, 8), (2, 7), (2, 8)]
"""
return list(product(*args))