From 0d7472f9814c9b8390c48f1cad4b858fe5fb429e Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?=
 <pedro.magalhaes@uni-bremen.de>
Date: Wed, 13 Mar 2024 00:38:34 +0100
Subject: [PATCH] Added tests for TimeFrame class. Cleaned up.

---
 src/topupopt/data/finance/utils.py     |  37 ++
 src/topupopt/problems/esipp/problem.py |  91 ++--
 src/topupopt/problems/esipp/time.py    | 313 ++++++++++++
 src/topupopt/problems/esipp/utils.py   |   7 +-
 tests/test_esipp_problem.py            | 131 ++---
 tests/test_esipp_time.py               | 641 ++++++++++++++++++++++++-
 tests/test_esipp_utils.py              |  13 +-
 7 files changed, 1066 insertions(+), 167 deletions(-)
 create mode 100644 src/topupopt/data/finance/utils.py
 create mode 100644 src/topupopt/problems/esipp/time.py

diff --git a/src/topupopt/data/finance/utils.py b/src/topupopt/data/finance/utils.py
new file mode 100644
index 0000000..5ec01a1
--- /dev/null
+++ b/src/topupopt/data/finance/utils.py
@@ -0,0 +1,37 @@
+# *****************************************************************************
+# *****************************************************************************
+
+from ...problems.esipp.network import Arcs
+
+# *****************************************************************************
+# *****************************************************************************
+
+class ArcInvestments(Arcs):
+    """A class for defining arcs linked to investments."""
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def __init__(self, investments: tuple, **kwargs):
+        # keep investment data
+        self.investments = investments
+        # initialise object
+        Arcs.__init__(
+            self,
+            minimum_cost=tuple([inv.net_present_value() for inv in self.investments]),
+            # validate=False,
+            **kwargs
+        )
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def update_minimum_cost(self):
+        "Updates the minimum costs using the Investment objects."
+        self.minimum_cost = tuple([inv.net_present_value() for inv in self.investments])
+
+    # *************************************************************************
+    # *************************************************************************
+
+# *****************************************************************************
+# *****************************************************************************
\ No newline at end of file
diff --git a/src/topupopt/problems/esipp/problem.py b/src/topupopt/problems/esipp/problem.py
index a12b6d5..dfc9f4d 100644
--- a/src/topupopt/problems/esipp/problem.py
+++ b/src/topupopt/problems/esipp/problem.py
@@ -15,6 +15,7 @@ from ...data.finance.invest import discount_factor
 from .network import Network, Arcs
 from .system import EnergySystem
 from .resource import ResourcePrice
+from .time import EconomicTimeFrame
 
 # *****************************************************************************
 # *****************************************************************************
@@ -67,10 +68,10 @@ class InfrastructurePlanningProblem(EnergySystem):
 
     def __init__(
         self,
-        name: str,
-        discount_rates: dict,  # key: assessment; value: list
-        reporting_periods: dict,  # key: assessment; value: periods
-        time_intervals: dict,  # key: assessment; value: intervals
+        # discount_rates: dict,  # key: assessment; value: list
+        time_frame: EconomicTimeFrame,
+        # reporting_periods: dict,  # key: assessment; value: periods
+        # time_intervals: dict,  # key: assessment; value: intervals
         time_weights: dict = None,  # key: assessment, period, interval; value: weight
         normalised_time_interval_duration: dict = None,
         assessment_weights: dict = None,
@@ -81,52 +82,54 @@ class InfrastructurePlanningProblem(EnergySystem):
     ):  # TODO: switch to False when everything is more mature
         # *********************************************************************
 
-        if validate_inputs:
-            # validate the inputs
-
-            (
-                self.assessment_keys,
-                self.number_assessments,
-                self.number_reporting_periods,
-                self.number_time_intervals,
-            ) = self._validate_inputs(
-                discount_rates=discount_rates,
-                reporting_periods=reporting_periods,
-                time_intervals=time_intervals,
-                time_weights=time_weights,
-                normalised_time_interval_duration=(normalised_time_interval_duration),
-                assessment_weights=assessment_weights,
-            )
+        # if validate_inputs:
+        #     # validate the inputs
 
-        else:  # skip validation
-            self.assessment_keys = tuple(discount_rates.keys())
+        #     (
+        #         self.assessment_keys,
+        #         self.number_assessments,
+        #         self.number_reporting_periods,
+        #         self.number_time_intervals,
+        #     ) = self._validate_inputs(
+        #         discount_rates=discount_rates,
+        #         reporting_periods=time_frame.reporting_periods,
+        #         time_intervals=time_frame.time_intervals,
+        #         time_weights=time_weights,
+        #         normalised_time_interval_duration=(normalised_time_interval_duration),
+        #         assessment_weights=assessment_weights,
+        #     )
 
-            self.number_assessments = len(self.assessment_keys)
+        # else:  # skip validation
+        #     self.time_frame.assessments = tuple(discount_rates.keys())
 
-            self.number_reporting_periods = {
-                q: len(reporting_periods[q]) for q in self.assessment_keys
-            }
+        #     self.number_assessments = len(self.time_frame.assessments)
 
-            self.number_time_intervals = {
-                q: len(time_intervals[q]) for q in self.assessment_keys
-            }
+        #     self.number_reporting_periods = {
+        #         q: len(time_frame.reporting_periods[q]) for q in self.time_frame.assessments
+        #     }
+
+        #     self.number_time_intervals = {
+        #         q: len(time_frame.time_intervals[q]) for q in self.time_frame.assessments
+        #     }
 
         # initialise
 
-        self.discount_rates = dict(discount_rates)
+        # self.discount_rates = dict(discount_rates)
+        
+        self.time_frame = time_frame
 
-        self.reporting_periods = dict(reporting_periods)
+        # self.reporting_periods = dict(reporting_periods)
 
-        self.time_intervals = dict(time_intervals)
+        # self.time_intervals = dict(time_intervals)
 
         self.average_time_interval = {
-            q: mean(self.time_intervals[q]) for q in self.assessment_keys
+            q: mean(self.time_frame.time_intervals[q]) for q in self.time_frame.assessments
         }
 
         self.normalised_time_interval_duration = {
             (q, k): duration / self.average_time_interval[q]
-            for q in self.assessment_keys
-            for k, duration in enumerate(self.time_intervals[q])
+            for q in self.time_frame.assessments
+            for k, duration in enumerate(self.time_frame.time_intervals[q])
         }
 
         # relation between reporting periods and time intervals
@@ -136,7 +139,7 @@ class InfrastructurePlanningProblem(EnergySystem):
 
             # self.time_weights = {
             #     (q,p,k): 1
-            #     for q in self.assessment_keys
+            #     for q in self.time_frame.assessments
             #     for p in self.reporting_periods[q]
             #     for k in self.time_intervals[q]
             #     }
@@ -160,10 +163,6 @@ class InfrastructurePlanningProblem(EnergySystem):
 
         # *********************************************************************
 
-        # set the name
-
-        self.name = name
-
         # identify the type of problem
 
         # TODO: develop method to automatically identify the type of problem
@@ -1651,12 +1650,12 @@ class InfrastructurePlanningProblem(EnergySystem):
     # *************************************************************************
     # *************************************************************************
 
-    def prepare(self):
+    def prepare(self, name = None):
         """Sets up the problem model with which instances can be built."""
 
         # create pyomo model (AbstractModel)
 
-        self.model = create_model(self.name)
+        self.model = create_model(name)
 
     # *************************************************************************
     # *************************************************************************
@@ -1782,7 +1781,7 @@ class InfrastructurePlanningProblem(EnergySystem):
         set_P_q = {q: tuple(p for p in self.reporting_periods[q]) for q in set_Q}
 
         # set of time intervals
-
+        
         set_K_q = {
             q: tuple(k for k in range(self.number_time_intervals[q])) for q in set_Q
         }
@@ -2532,8 +2531,8 @@ class InfrastructurePlanningProblem(EnergySystem):
         # discount factors for each period
 
         param_c_df_qp = {
-            (q, p): discount_factor(self.discount_rates[q][0 : p + 1])
-            # (q,p): self.investments[q].discount_factors[p+1]
+            (q, p): self.time_frame.discount_factor(q, p)
+            # (q, p): discount_factor(self.discount_rates[q][0 : p + 1])
             for (q, p) in set_QP
         }
 
@@ -3844,7 +3843,7 @@ def simplify_peak_total_problem(
             / problem.time_intervals[q_peak][k_total]
         ),
     }
-
+    
     # discount factors (use the reference assessment)
     problem.discount_rates[q_peak] = tuple(problem.discount_rates[q_ref])
     problem.discount_rates[q_total] = tuple(problem.discount_rates[q_ref])
diff --git a/src/topupopt/problems/esipp/time.py b/src/topupopt/problems/esipp/time.py
new file mode 100644
index 0000000..1f5ba75
--- /dev/null
+++ b/src/topupopt/problems/esipp/time.py
@@ -0,0 +1,313 @@
+# *****************************************************************************
+# *****************************************************************************
+
+from numbers import Real
+from ...data.finance.invest import discount_factor as _discount_factor
+
+# *****************************************************************************
+# *****************************************************************************
+
+class TimeFrame:
+    "A class to model time-dependent problems and systems."
+
+    # why?
+    # 1) to ensure a consistent time frame for all time-dependent objects
+    # 2) to validate conformity with this time frame
+
+    # input modes:
+    # 1) uniform time discretisation
+    # - number of reporting periods
+    # - duration of the reporting period(s)
+    # - number of intervals
+    # - duration of the interval(s)
+    # 2) customised
+    # - dicts
+
+    def __init__(
+        self,
+        # number_reporting_periods: int,
+        # number_time_intervals:int,
+        reporting_periods: dict,
+        reporting_period_durations: dict,
+        time_intervals: dict,
+        time_interval_durations: dict,
+    ):
+        self.assessments = set(time_intervals.keys())
+
+        self.time_intervals = time_intervals
+        self.time_interval_durations = time_interval_durations
+
+        self.reporting_periods = reporting_periods
+        self.reporting_period_durations = reporting_period_durations
+
+        # check for nonpositive durations
+        for q, durations in reporting_period_durations.items():
+            for duration in durations:
+                if duration <= 0:
+                    raise ValueError("Durations need to be positive.")
+        for q, durations in time_interval_durations.items():
+            for duration in durations:
+                if duration <= 0:
+                    raise ValueError("Durations need to be positive.")
+
+        # declare internal states
+        self.number_assessments(redo=True)
+        self.number_reporting_periods(next(iter(self.assessments)), redo=True)
+        self.number_time_intervals(next(iter(self.assessments)), redo=True)
+        self.qk(redo=True)
+        self.qp(redo=True)
+        self.qpk(redo=True)
+
+    # *************************************************************************
+    # *************************************************************************
+    
+    def number_assessments(self, redo=False) -> int:
+        "Returns the number of assessments under consideration."
+        if redo:
+            self._number_q = len(self.assessments)
+        return self._number_q
+    
+    def number_time_intervals(self, assessment, redo=False) -> int:
+        "Returns the number of intervals for a given assessment."
+        if redo:
+            self._number_k_q = {
+                q: len(self.time_intervals[q])
+                for q in self.assessments
+                }
+        return self._number_k_q[assessment]
+    
+    def number_reporting_periods(self, assessment, redo=False) -> int:
+        "Returns the number of reporting periods for a given assessment."
+        if redo:
+            self._number_p_q = {
+                q: len(self.reporting_periods[q])
+                for q in self.assessments
+                }
+        return self._number_p_q[assessment]
+
+    # *************************************************************************
+    # *************************************************************************
+    
+    def valid_q(self, q_keyed_dict: dict) -> bool:
+        "Returns True if all q keys are valid, and False otherwise."
+        for q in q_keyed_dict.keys():
+            if q not in self.assessments:
+                return False
+        # all q keys are valid
+        return True
+        
+    def complete_q(self, q_keyed_dict: dict) -> bool:
+        "Returns True if all valid q keys are provided."
+        for q in self.assessments:
+            if q not in q_keyed_dict:
+                # the dict keys do not include this q tuple
+                return False
+        # if all q tuples are valid and complete, return True
+        return True
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def valid_qpk(self, qpk_keyed_dict: dict) -> bool:
+        "Returns True if all (q,p,k) tuple keys are valid, and False otherwise."
+        for qpk in qpk_keyed_dict.keys():
+            if (
+                len(qpk) != 3
+                or qpk[0] not in self.assessments
+                or qpk[1] not in self.reporting_periods[qpk[0]]
+                or qpk[2] not in self.time_intervals[qpk[0]]
+            ):
+                return False
+        # if all (q,p) tuples are valid, return True
+        return True
+
+    def complete_qpk(self, qpk_keyed_dict: dict) -> bool:
+        "Returns True if all valid (q,p,k) tuple keys are provided."
+        for q, _p in self.reporting_periods.items():
+            for p in _p:
+                for k in self.time_intervals[q]:
+                    if (q, p, k) not in qpk_keyed_dict:
+                        # the dict keys do not include this (q,p,k) tuple
+                        return False
+        # if all (q,p,k) tuples are valid and complete, return True
+        return True
+
+    def consecutive_qpk(self, qpk_keyed_dict: dict) -> bool:
+        "Returns True if all (q,p,k) tuple keys are valid and consecutive."
+        # TODO: here
+        raise NotImplementedError
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def valid_qp(self, qp_keyed_dict: dict) -> bool:
+        "Returns True if all (q,p) tuple keys are valid, and False otherwise."
+        for qp in qp_keyed_dict.keys():
+            if (
+                len(qp) != 2
+                or qp[0] not in self.assessments
+                or qp[1] not in self.reporting_periods[qp[0]]
+            ):
+                return False
+        # if all (q,p) tuples are valid, return True
+        return True
+
+    def complete_qp(self, qp_keyed_dict: dict) -> bool:
+        "Returns True if all valid (q,p) tuple keys are provided."
+        for q, _p in self.reporting_periods.items():
+            for p in _p:
+                if (q, p) not in qp_keyed_dict:
+                    # the dict keys do not include this (q,p) tuple
+                    return False
+        # if all (q,p) tuples are valid and complete, return True
+        return True
+
+    def consecutive_qp(self, qp_keyed_dict: dict) -> bool:
+        "Returns True if all p entries for each q are consecutive."
+        return self.consecutive_qk(qk_keyed_dict=qp_keyed_dict)
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def valid_qk(self, qk_keyed_dict: dict) -> bool:
+        "Returns True if all (q,k) tuple keys are valid, and False otherwise."
+        for qk in qk_keyed_dict.keys():
+            if (
+                len(qk) != 2
+                or qk[0] not in self.assessments
+                or qk[1] not in self.time_intervals[qk[0]]
+            ):
+                return False
+        # if all (q,k) tuples are valid, return True
+        return True
+
+    def complete_qk(self, qk_keyed_dict: dict):
+        "Returns True if all valid (q,k) tuple keys are provided."
+        for q, _k in self.time_intervals.items():
+            for k in _k:
+                if (q, k) not in qk_keyed_dict:
+                    # the dict keys do not include this (q,k) tuple
+                    return False
+        # if all (q,k) tuples are valid and complete, return True
+        return True
+
+    def consecutive_qk(self, qk_keyed_dict: dict) -> bool:
+        "Returns True if all k entries for each q are consecutive."
+        # create q-keyed dict with the respective k-values
+        new_dict = {
+            q: set(tuple(qk[1] for qk in qk_keyed_dict.keys() if qk[0] == q))
+            for q in set(tuple(qk[0] for qk in qk_keyed_dict.keys()))
+            }
+        # for each key, verify that the ks are consecutive
+        for q, k_set in new_dict.items():
+            # if not consecutive, return false
+            min_k = min(k_set)
+            for i in range(len(k_set)-1):
+                if min_k+i+1 not in k_set:
+                    return False
+        # return True if all ks are consecutive
+        return True
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def qk(self, redo: bool = False):
+        "Return all valid (q,k) tuples."
+        if redo:
+            self._qk = tuple(
+                (q, k) for q, _k in self.time_intervals.items() for k in _k
+            )
+        return self._qk
+
+    def qp(self, redo: bool = False):
+        "Return all valid (q,p) tuples."
+        if redo:
+            self._qp = tuple(
+                (q, p) for q, _p in self.reporting_periods.items() for p in _p
+            )
+        return self._qp
+
+    def qpk(self, redo: bool = False):
+        "Return all valid (q,p,k) tuples."
+        if redo:
+            self._qpk = (
+                (q, p, k)
+                for q, _p in self.reporting_periods.items()
+                for p in _p
+                for k in self.time_intervals[q]
+            )
+        return self._qpk
+
+    # *************************************************************************
+    # *************************************************************************
+
+# *****************************************************************************
+# *****************************************************************************
+
+class EconomicTimeFrame(TimeFrame):
+    """A class to define investments within a given time frame."""
+    
+    def __init__(
+            self, 
+            discount_rate=None,         # real: same value for all q and p
+            discount_rates_p=None,      # list: same values for all q (1 per p)
+            discount_rates_qp=None,     # dict: 1 value per p and q
+            **kwargs
+            ):
+        
+        TimeFrame.__init__(
+            self, 
+            **kwargs
+            )
+        
+        if isinstance(discount_rate, Real):
+            # real: same value for all q and p
+            self._discount_rates = {
+                q: tuple([discount_rate for p in self.reporting_periods[q]])
+                for q in self.assessments
+                }
+        elif (type(discount_rates_p) == tuple or 
+              type(discount_rates_p) == list):
+            # list: same values for all q (1 per p)
+            self._discount_rates = {
+                q: tuple(discount_rates_p[p] 
+                         for p in self.reporting_periods[q])
+                for q in self.assessments
+                }
+        elif (type(discount_rates_qp) == dict and 
+              self.complete_qp(discount_rates_qp)):
+            # dict: 1 value per p and q
+            self._discount_rates = {
+                q: tuple(discount_rates_qp[(q,p)] 
+                         for p in self.reporting_periods[q])
+                for q in self.assessments
+                }
+        else:
+            raise ValueError('Unrecognised inputs.')
+        
+        # TODO: validate the discount rate object
+        
+    # *************************************************************************
+    # *************************************************************************
+        
+    def discount_rate(self, assessment, reporting_period):
+        "Returns the discount rate for a given assessment and period."
+        return self._discount_rates[assessment][
+            self.reporting_periods[assessment].index(reporting_period)
+            ]
+    
+    # *************************************************************************
+    # *************************************************************************
+    
+    def discount_factor(self, assessment, reporting_period):
+        "Returns the discount factor for a given assessment and period."
+        
+        discount_rates = tuple(
+            self.discount_rate(assessment, p)
+            for p in self.reporting_periods[assessment]
+            if p <= reporting_period
+            )
+        return _discount_factor(discount_rates)
+
+# *****************************************************************************
+# *****************************************************************************
diff --git a/src/topupopt/problems/esipp/utils.py b/src/topupopt/problems/esipp/utils.py
index 2908cd1..0c9fec7 100644
--- a/src/topupopt/problems/esipp/utils.py
+++ b/src/topupopt/problems/esipp/utils.py
@@ -8,21 +8,16 @@
 # local, external
 
 import networkx as nx
-
 import pyomo.environ as pyo
-
 from matplotlib import pyplot as plt
 
 # local, internal
-
 from .problem import InfrastructurePlanningProblem
-
 from .network import Network
-
+        
 # *****************************************************************************
 # *****************************************************************************
 
-
 def review_final_network(network: Network):
     # check that the network topology is a tree
     if network.has_tree_topology():
diff --git a/tests/test_esipp_problem.py b/tests/test_esipp_problem.py
index 69bab4a..79f7754 100644
--- a/tests/test_esipp_problem.py
+++ b/tests/test_esipp_problem.py
@@ -21,7 +21,6 @@ from src.topupopt.problems.esipp.time import TimeFrame
 # *****************************************************************************
 # *****************************************************************************
 
-
 class TestESIPPProblem:
     def build_solve_ipp(
         self,
@@ -40,65 +39,24 @@ class TestESIPPProblem:
         plot_results: bool = False,
         print_solver_output: bool = False,
         time_frame: TimeFrame = None,
-        irregular_time_intervals: bool = False,
         networks: dict = None,
         converters: dict = None,
-        number_intraperiod_time_intervals: int = 4,
         static_losses_mode=None,
         mandatory_arcs: list = None,
         max_number_parallel_arcs: dict = None,
         arc_groups_dict: dict = None,
         init_aux_sets: bool = False,
         discount_rates: dict = None,
-        # reporting_periods: dict = None,
-        time_intervals: dict = None,
         assessment_weights: dict = None,
         simplify_problem: bool = False,
     ):
-        reporting_period_duration = 365 * 24 * 3600
-
-        if type(discount_rates) != dict:
-            discount_rates = {0: tuple([0.035, 0.035])}
-
+        
         if type(assessment_weights) != dict:
             assessment_weights = {}  # default
 
-        # if type(reporting_periods) != dict:
-        #     reporting_periods = {0: (0, 1)}
-
         if type(converters) != dict:
             converters = {}
-
-        # time intervals
-
-        if type(time_intervals) != dict:
-            if irregular_time_intervals:
-                time_step_max_relative_variation = 0.25
-
-                intraperiod_time_interval_duration = [
-                    (reporting_period_duration / number_intraperiod_time_intervals)
-                    * (
-                        1
-                        + (k / (number_intraperiod_time_intervals - 1) - 0.5)
-                        * time_step_max_relative_variation
-                    )
-                    for k in range(number_intraperiod_time_intervals)
-                ]
-
-            else:
-                intraperiod_time_interval_duration = [
-                    reporting_period_duration / number_intraperiod_time_intervals
-                    for k in range(number_intraperiod_time_intervals)
-                ]
-
-            # average time interval duration
-
-            average_time_interval_duration = round(
-                mean(intraperiod_time_interval_duration)
-            )
-
-            time_intervals = {0: tuple(dt for dt in intraperiod_time_interval_duration)}
-
+            
         # time weights
 
         # relative weight of time period
@@ -118,10 +76,10 @@ class TestESIPPProblem:
         # create problem object
 
         ipp = InfrastructurePlanningProblem(
-            name="problem",
             discount_rates=discount_rates,
-            reporting_periods=time_frame.reporting_periods,
-            time_intervals=time_intervals,
+            time_frame=time_frame,
+            # reporting_periods=time_frame.reporting_periods,
+            # time_intervals=time_frame.time_interval_durations,
             time_weights=time_weights,
             normalised_time_interval_duration=normalised_time_interval_duration,
             assessment_weights=assessment_weights,
@@ -287,10 +245,10 @@ class TestESIPPProblem:
         number_intervals = 3
 
         tf = TimeFrame(
-            reporting_periods={q: [0, 1]},
-            reporting_period_durations={q: [365 * 24 * 3600, 365 * 24 * 3600]},
-            time_intervals={q: [0, 1, 2]},
-            time_interval_durations={q: [1, 1, 1]},
+            reporting_periods={q: (0, 1)},
+            reporting_period_durations={q: (365 * 24 * 3600, 365 * 24 * 3600)},
+            time_intervals={q: (0, 1, 2)},
+            time_interval_durations={q: (1, 1, 1)},
         )
 
         # 2 nodes: one import, one regular
@@ -357,10 +315,10 @@ class TestESIPPProblem:
             # irregular_time_intervals=irregular_time_intervals,
             time_frame=tf,
             networks={"mynet": mynet},
-            number_intraperiod_time_intervals=number_intervals,
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
+            discount_rates={0: tuple([0.035, 0.035])},
             # init_aux_sets=init_aux_sets,
             simplify_problem=False,
         )
@@ -426,10 +384,10 @@ class TestESIPPProblem:
         number_intervals = 3
 
         tf = TimeFrame(
-            reporting_periods={q: [0, 1]},
-            reporting_period_durations={q: [365 * 24 * 3600, 365 * 24 * 3600]},
-            time_intervals={q: [0, 1, 2]},
-            time_interval_durations={q: [1, 1, 1]},
+            reporting_periods={q: (0, 1)},
+            reporting_period_durations={q: (365 * 24 * 3600, 365 * 24 * 3600)},
+            time_intervals={q: (0, 1, 2)},
+            time_interval_durations={q: (1, 1, 1)},
         )
 
         # 2 nodes: one import, one regular
@@ -495,13 +453,12 @@ class TestESIPPProblem:
             perform_analysis=False,
             plot_results=False,  # True,
             print_solver_output=False,
-            # irregular_time_intervals=irregular_time_intervals,
             time_frame=tf,
             networks={"mynet": mynet},
-            number_intraperiod_time_intervals=number_intervals,
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
+            discount_rates={0: tuple([0.035, 0.035])},
             # init_aux_sets=init_aux_sets,
             simplify_problem=True,
         )
@@ -596,14 +553,14 @@ class TestESIPPProblem:
         q = 0
         # time
         number_intervals = 1
-        # periods
-        number_periods = 1
+        # # periods
+        # number_periods = 1
 
         tf = TimeFrame(
-            reporting_periods={q: [0]},
-            reporting_period_durations={q: [365 * 24 * 3600]},
-            time_intervals={q: [0]},
-            time_interval_durations={q: [1]},
+            reporting_periods={q: (0,)},
+            reporting_period_durations={q: (365 * 24 * 3600,)},
+            time_intervals={q: (0,)},
+            time_interval_durations={q: (1,)},
         )
 
         # 2 nodes: one import, one regular
@@ -657,16 +614,14 @@ class TestESIPPProblem:
             perform_analysis=False,
             plot_results=False,  # True,
             print_solver_output=False,
-            # irregular_time_intervals=irregular_time_intervals,
             time_frame=tf,
             networks={"mynet": mynet},
-            number_intraperiod_time_intervals=number_intervals,
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
             # init_aux_sets=init_aux_sets,
             simplify_problem=False,
-            reporting_periods={0: (0,)},
+            # reporting_periods={0: (0,)},
             discount_rates={0: (0.0,)},
         )
 
@@ -723,10 +678,10 @@ class TestESIPPProblem:
         number_periods = 1
 
         tf = TimeFrame(
-            reporting_periods={q: [0]},
-            reporting_period_durations={q: [365 * 24 * 3600]},
-            time_intervals={q: [0]},
-            time_interval_durations={q: [1]},
+            reporting_periods={q: (0,)},
+            reporting_period_durations={q: (365 * 24 * 3600,)},
+            time_intervals={q: (0,)},
+            time_interval_durations={q: (1,)},
         )
 
         # 2 nodes: one export, one regular
@@ -778,16 +733,14 @@ class TestESIPPProblem:
             perform_analysis=False,
             plot_results=False,  # True,
             print_solver_output=False,
-            # irregular_time_intervals=irregular_time_intervals,
             time_frame=tf,
             networks={"mynet": mynet},
-            number_intraperiod_time_intervals=number_intervals,
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
             # init_aux_sets=init_aux_sets,
             simplify_problem=False,
-            reporting_periods={0: (0,)},
+            # reporting_periods={0: (0,)},
             discount_rates={0: (0.0,)},
         )
 
@@ -844,10 +797,10 @@ class TestESIPPProblem:
         number_periods = 1
 
         tf = TimeFrame(
-            reporting_periods={q: [0]},
-            reporting_period_durations={q: [365 * 24 * 3600]},
-            time_intervals={q: [0]},
-            time_interval_durations={q: [1]},
+            reporting_periods={q: (0,)},
+            reporting_period_durations={q: (365 * 24 * 3600,)},
+            time_intervals={q: (0,1)},
+            time_interval_durations={q: (1,1)},
         )
 
         # 3 nodes: one import, one export, one regular
@@ -914,28 +867,16 @@ class TestESIPPProblem:
 
         # no sos, regular time intervals
         ipp = self.build_solve_ipp(
-            # solver=solver,
             solver_options={},
-            # use_sos_arcs=use_sos_arcs,
-            # arc_sos_weight_key=sos_weight_key,
-            # arc_use_real_variables_if_possible=use_real_variables_if_possible,
-            # use_sos_sense=use_sos_sense,
-            # sense_sos_weight_key=sense_sos_weight_key,
-            # sense_use_real_variables_if_possible=sense_use_real_variables_if_possible,
-            # sense_use_arc_interfaces=use_arc_interfaces,
             perform_analysis=False,
             plot_results=False,  # True,
             print_solver_output=False,
-            # irregular_time_intervals=irregular_time_intervals,
             time_frame=tf,
             networks={"mynet": mynet},
-            number_intraperiod_time_intervals=number_intervals,
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
-            # init_aux_sets=init_aux_sets,
             simplify_problem=False,
-            reporting_periods={0: (0,)},
             discount_rates={0: (0.0,)},
         )
 
@@ -1021,10 +962,10 @@ class TestESIPPProblem:
         number_periods = 1
 
         tf = TimeFrame(
-            reporting_periods={q: [0]},
-            reporting_period_durations={q: [365 * 24 * 3600]},
-            time_intervals={q: [0]},
-            time_interval_durations={q: [1]},
+            reporting_periods={q: (0,)},
+            reporting_period_durations={q: (365 * 24 * 3600,)},
+            time_intervals={q: (0,1,2)},
+            time_interval_durations={q: (1,1,1)},
         )
 
         # 2 nodes: one import, one regular
@@ -1134,11 +1075,9 @@ class TestESIPPProblem:
             perform_analysis=False,
             plot_results=False,  # True,
             print_solver_output=False,
-            # irregular_time_intervals=irregular_time_intervals,
             time_frame=tf,
             networks={"mynet": mynet},
             converters={"mycvt": cvt},
-            number_intraperiod_time_intervals=number_intervals,
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
diff --git a/tests/test_esipp_time.py b/tests/test_esipp_time.py
index 6658df1..6bdf4a9 100644
--- a/tests/test_esipp_time.py
+++ b/tests/test_esipp_time.py
@@ -2,6 +2,8 @@
 
 # local, internal
 from src.topupopt.problems.esipp.time import TimeFrame
+from src.topupopt.problems.esipp.time import EconomicTimeFrame
+from math import isclose
 
 # *****************************************************************************
 # *****************************************************************************
@@ -14,6 +16,7 @@ class TestTimeFrame:
     # test problem from table 2.1
 
     def test_nonpositive_durations(self):
+
         # negative reporting period durations
 
         reporting_periods = {0: [0, 1, 2]}
@@ -76,6 +79,23 @@ class TestTimeFrame:
             time_intervals=time_intervals,
             time_interval_durations=time_interval_durations,
         )
+        
+        assert tf.number_assessments() == 1
+        # number of reporting periods
+        assert tf.number_reporting_periods(0) == 3
+        # number of time intervals
+        assert tf.number_time_intervals(0) == 2
+        
+        # q: valid
+        assert tf.valid_q(reporting_periods)
+        assert tf.valid_q(time_intervals)
+        # q: invalid
+        assert not tf.valid_q({1: 0})
+        # q: complete
+        assert tf.complete_q(reporting_periods)
+        assert tf.complete_q(time_intervals)
+        # q: incomplete
+        assert not tf.complete_q({1: 0})
 
         # qk: valid
         assert tf.valid_qk(
@@ -104,6 +124,21 @@ class TestTimeFrame:
                 (0, 0): 1,
             }
         )
+        # qk: consecutive
+        assert tf.consecutive_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+            }
+        )
+        # qk: not consecutive
+        assert not tf.consecutive_qk(
+            {
+                (0, 0): 1,
+                (0, 2): 1,
+            }
+        )
+        
 
         # qp: valid
         assert tf.valid_qp(
@@ -117,6 +152,20 @@ class TestTimeFrame:
         assert tf.complete_qp({(0, 0): 1, (0, 1): 1, (0, 2): 1})
         # qp: valid but not complete
         assert not tf.complete_qp({(0, 0): 1, (0, 1): 1})
+        # qp: consecutive
+        assert tf.consecutive_qp(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+            }
+        )
+        # qp: not consecutive
+        assert not tf.consecutive_qp(
+            {
+                (0, 0): 1,
+                (0, 3): 1,
+            }
+        )
 
         # qpk: valid
         assert tf.valid_qpk(
@@ -185,6 +234,26 @@ class TestTimeFrame:
             time_intervals=time_intervals,
             time_interval_durations=time_interval_durations,
         )
+        
+        # number of assessments
+        assert tf.number_assessments() == 2
+        # number of reporting periods
+        assert tf.number_reporting_periods(0) == 1
+        assert tf.number_reporting_periods(1) == 2
+        # number of time intervals
+        assert tf.number_time_intervals(0) == 2
+        assert tf.number_time_intervals(1) == 2
+        
+        # q: valid
+        assert tf.valid_q(reporting_periods)
+        assert tf.valid_q(time_intervals)
+        # q: invalid
+        assert not tf.valid_q({2: 0})
+        # q: complete
+        assert tf.complete_q(reporting_periods)
+        assert tf.complete_q(time_intervals)
+        # q: incomplete
+        assert not tf.complete_q({1: 0})
 
         # qk: valid
         assert tf.valid_qk(
@@ -296,8 +365,6 @@ class TestTimeFrame:
     # *************************************************************************
     # *************************************************************************
 
-    # TODO: table 2.3
-
     def test_table_23(self):
         # Problem with three reporting periods relying on two assessments
         # with two and three intervals: one assessment for the first two periods with two
@@ -317,6 +384,12 @@ class TestTimeFrame:
             time_intervals=time_intervals,
             time_interval_durations=time_interval_durations,
         )
+        
+        # q: valid
+        assert tf.valid_q(reporting_periods)
+        assert not tf.valid_q({2: 1})
+        assert tf.complete_q(reporting_periods)
+        assert not tf.complete_q({1: [365 * 24 * 3600]})
 
         # qk: valid
         assert tf.valid_qk(
@@ -440,30 +513,576 @@ class TestTimeFrame:
     # *************************************************************************
     # *************************************************************************
 
-    # TODO: table 2.4
-
     def test_table_24(self):
-        pass
+        
+        # Problem with three reporting periods relying on two assessments
+        # with two intervals: each assessment is used for all three reporting periods.
+
+        reporting_periods = {0: [0, 1, 2], 1: [0, 1, 2]}
+        reporting_period_durations = {
+            0: [365 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600],
+            1: [365 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600],
+        }
+        time_intervals = {0: [0, 1], 1: [0, 1]}
+        time_interval_durations = {0: [1, 1], 1: [1, 1]}
+
+        tf = TimeFrame(
+            reporting_periods=reporting_periods,
+            reporting_period_durations=reporting_period_durations,
+            time_intervals=time_intervals,
+            time_interval_durations=time_interval_durations,
+        )
+        
+        # number of assessments
+        assert tf.number_assessments() == 2
+        # number of reporting periods
+        assert tf.number_reporting_periods(0) == 3
+        assert tf.number_reporting_periods(1) == 3
+        # number of time intervals
+        assert tf.number_time_intervals(0) == 2
+        assert tf.number_time_intervals(1) == 2
+        
+        # q: valid
+        assert tf.valid_q(reporting_periods)
+        assert tf.valid_q(time_intervals)
+        assert not tf.valid_q({2: 1})
+        assert tf.complete_q(reporting_periods)
+        assert tf.complete_q(time_intervals)
+        assert not tf.complete_q({1: [365 * 24 * 3600]})
+
+        # qk: valid
+        assert tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1
+            }
+        )
+        # qk: not valid
+        assert not tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qk: valid and complete
+        assert tf.complete_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1
+            }
+        )
+        # qk: valid but not complete
+        assert not tf.complete_qk(
+            {
+                (0, 0): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+            }
+        )
+
+        # qp: valid
+        assert tf.valid_qp(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qp: not valid
+        assert not tf.valid_qp(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 3): 1,
+            }
+        )
+        # qp: valid and complete
+        assert tf.complete_qp(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (0, 2): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qp: valid but not complete
+        assert not tf.complete_qp({(0, 0): 1, (1, 1): 1})
+
+        # qpk: valid
+        assert tf.valid_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 1, 1): 1,
+                (1, 2, 1): 1,
+            }
+        )
+        # qpk: not valid
+        assert not tf.valid_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 0, 1): 1,
+                (1, 1, 0): 1,
+                (1, 1, 1): 1,
+                (1, 3, 0): 1,
+                (1, 3, 1): 1,
+            }
+        )
+        # qpk: valid and complete
+        assert tf.complete_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 0, 1): 1,
+                (0, 1, 0): 1,
+                (0, 1, 1): 1,
+                (0, 2, 0): 1,
+                (0, 2, 1): 1,
+                (1, 0, 0): 1,
+                (1, 0, 1): 1,
+                (1, 1, 0): 1,
+                (1, 1, 1): 1,
+                (1, 2, 0): 1,
+                (1, 2, 1): 1,
+            }
+        )
+        # qpk: valid but not complete
+        assert not tf.complete_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 0, 1): 1,
+                (0, 1, 0): 1,
+                (0, 1, 1): 1,
+                (1, 2, 0): 1,
+                (1, 2, 1): 1,
+            }
+        )
+
+        qk_dict = {qk: None for qk in tf.qk()}
+        qp_dict = {qp: None for qp in tf.qp()}
+        qpk_dict = {qpk: None for qpk in tf.qpk()}
+
+        assert tf.complete_qk(qk_dict)
+        assert tf.complete_qp(qp_dict)
+        assert tf.complete_qpk(qpk_dict)
 
     # *************************************************************************
     # *************************************************************************
 
-    # TODO: table 2.5
-
     def test_table_25(self):
-        pass
+        
+        # Problem with three reporting periods relying on six assessments with
+        # two intervals: each reporting period is covered by two assessments.
+
+        reporting_periods = {
+            0: [0], 
+            1: [0], 
+            2: [1], 
+            3: [1], 
+            4: [2], 
+            5: [2]
+            }
+        reporting_period_durations = {
+            0: [365 * 24 * 3600],
+            1: [365 * 24 * 3600],
+            2: [365 * 24 * 3600],
+            3: [365 * 24 * 3600],
+            4: [365 * 24 * 3600],
+            5: [365 * 24 * 3600],
+            }
+        time_intervals = {
+            0: [0, 1], 
+            1: [0, 1], 
+            2: [0, 1], 
+            3: [0, 1], 
+            4: [0, 1], 
+            5: [0, 1]
+            }
+        time_interval_durations = {
+            0: [1, 1], 
+            1: [1, 1], 
+            2: [1, 1], 
+            3: [1, 1], 
+            4: [1, 1], 
+            5: [1, 1]
+            }
+
+        tf = TimeFrame(
+            reporting_periods=reporting_periods,
+            reporting_period_durations=reporting_period_durations,
+            time_intervals=time_intervals,
+            time_interval_durations=time_interval_durations,
+        )
+        
+        # number of assessments
+        assert tf.number_assessments() == 6
+        # number of reporting periods
+        assert tf.number_reporting_periods(0) == 1
+        assert tf.number_reporting_periods(1) == 1
+        assert tf.number_reporting_periods(2) == 1
+        assert tf.number_reporting_periods(3) == 1
+        assert tf.number_reporting_periods(4) == 1
+        assert tf.number_reporting_periods(5) == 1
+        # number of time intervals
+        assert tf.number_time_intervals(0) == 2
+        assert tf.number_time_intervals(1) == 2
+        assert tf.number_time_intervals(2) == 2
+        assert tf.number_time_intervals(3) == 2
+        assert tf.number_time_intervals(4) == 2
+        assert tf.number_time_intervals(5) == 2
+        
+        # q: valid
+        assert tf.valid_q(reporting_periods)
+        assert tf.valid_q(time_intervals)
+        assert not tf.valid_q({7: 1})
+        assert tf.complete_q(reporting_periods)
+        assert tf.complete_q(time_intervals)
+        assert not tf.complete_q({1: [365 * 24 * 3600]})
+
+        # qk: valid
+        assert tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1
+            }
+        )
+        # qk: not valid
+        assert not tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qk: valid and complete
+        assert tf.complete_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (2, 0): 1,
+                (2, 1): 1,
+                (3, 0): 1,
+                (3, 1): 1,
+                (4, 0): 1,
+                (4, 1): 1,
+                (5, 0): 1,
+                (5, 1): 1
+            }
+        )
+        # qk: valid but not complete
+        assert not tf.complete_qk(
+            {
+                #(0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (2, 0): 1,
+                (2, 1): 1,
+                (3, 0): 1,
+                (3, 1): 1,
+                (4, 0): 1,
+                (4, 1): 1,
+                (5, 0): 1,
+                (5, 1): 1
+            }
+        )
+
+        # qp: valid
+        assert tf.valid_qp(
+            {
+                (0, 0): 1,
+                (1, 0): 1,
+                (2, 1): 1,
+                (3, 1): 1,
+                (4, 2): 1,
+                (5, 2): 1,
+            }
+        )
+        # qp: not valid
+        assert not tf.valid_qp(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 3): 1,
+            }
+        )
+        # qp: valid and complete
+        assert tf.complete_qp(
+            {
+                (0, 0): 1,
+                (1, 0): 1,
+                (2, 1): 1,
+                (3, 1): 1,
+                (4, 2): 1,
+                (5, 2): 1,
+            }
+        )
+        # qp: valid but not complete
+        assert not tf.complete_qp({(0, 0): 1, (1, 1): 1})
+
+        # qpk: valid
+        assert tf.valid_qpk(
+            {
+                (0, 0, 0): 1,
+                (1, 0, 0): 1,
+                (2, 1, 0): 1,
+                (3, 1, 0): 1,
+                (4, 2, 0): 1,
+                (5, 2, 0): 1,
+                (0, 0, 1): 1,
+                (1, 0, 1): 1,
+                (2, 1, 1): 1,
+                (3, 1, 1): 1,
+                (4, 2, 1): 1,
+                (5, 2, 1): 1,
+            }
+        )
+        # qpk: not valid
+        assert not tf.valid_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 0, 1): 1,
+                (1, 1, 0): 1,
+                (1, 1, 1): 1,
+                (1, 3, 0): 1,
+                (1, 3, 1): 1,
+            }
+        )
+        # qpk: valid and complete
+        assert tf.complete_qpk(
+            {
+                (0, 0, 0): 1,
+                (1, 0, 0): 1,
+                (2, 1, 0): 1,
+                (3, 1, 0): 1,
+                (4, 2, 0): 1,
+                (5, 2, 0): 1,
+                (0, 0, 1): 1,
+                (1, 0, 1): 1,
+                (2, 1, 1): 1,
+                (3, 1, 1): 1,
+                (4, 2, 1): 1,
+                (5, 2, 1): 1,
+            }
+        )
+        # qpk: valid but not complete
+        assert not tf.complete_qpk(
+            {
+                (0, 0, 0): 1,
+                (1, 0, 0): 1,
+                (2, 1, 0): 1,
+                (3, 1, 0): 1,
+                (4, 2, 0): 1,
+                (5, 2, 0): 1,
+                (0, 0, 1): 1,
+                (1, 0, 1): 1,
+                (2, 1, 1): 1,
+                (3, 1, 1): 1,
+                # (4, 2, 1): 1,
+                # (5, 2, 1): 1,
+            }
+        )
+
+        qk_dict = {qk: None for qk in tf.qk()}
+        qp_dict = {qp: None for qp in tf.qp()}
+        qpk_dict = {qpk: None for qpk in tf.qpk()}
+
+        assert tf.complete_qk(qk_dict)
+        assert tf.complete_qp(qp_dict)
+        assert tf.complete_qpk(qpk_dict)
 
     # *************************************************************************
     # *************************************************************************
 
-    # TODO: table 2.6
+    def test_economic_timeframe_mode1(self):
+        
+        # using a single discount rate for all q and p
+        
+        reporting_periods = {0: [0, 1], 1: [2]}
+        reporting_period_durations = {
+            0: [365 * 24 * 3600, 365 * 24 * 3600],
+            1: [365 * 24 * 3600],
+        }
+        time_intervals = {0: [0, 1], 1: [0, 1, 2]}
+        time_interval_durations = {0: [1, 1], 1: [1, 1, 1]}
+
+        discount_rate = 3.5/100
+        
+        etf = EconomicTimeFrame(
+            discount_rate=discount_rate, 
+            reporting_periods=reporting_periods, 
+            reporting_period_durations=reporting_period_durations, 
+            time_intervals=time_intervals, 
+            time_interval_durations=time_interval_durations
+            )
+        
+        # check the discount rates
+        for q, p_list in reporting_periods.items():
+            for p in p_list:
+                assert etf.discount_rate(q, p) == 3.5/100
+
+    # *************************************************************************
+    # *************************************************************************
+
+    def test_economic_timeframe_mode2(self):
+        
+        # using a list of discount rates for each q
+        
+        reporting_periods = {0: [0, 1], 1: [2]}
+        reporting_period_durations = {
+            
+            0: [365 * 24 * 3600, 365 * 24 * 3600],
+            1: [365 * 24 * 3600],
+        }
+        time_intervals = {0: [0, 1], 1: [0, 1, 2]}
+        time_interval_durations = {0: [1, 1], 1: [1, 1, 1]}
 
-    def test_table_26(self):
-        pass
+        discount_rates = [3.5/100, 3.6/100, 3.7/100]
+        
+        etf = EconomicTimeFrame(
+            discount_rates_p=discount_rates, 
+            reporting_periods=reporting_periods, 
+            reporting_period_durations=reporting_period_durations, 
+            time_intervals=time_intervals, 
+            time_interval_durations=time_interval_durations
+            )
+        
+        # check the discount rates    
+        assert etf.discount_rate(0, 0) == 3.5/100
+        assert etf.discount_rate(0, 1) == 3.6/100
+        assert etf.discount_rate(1, 2) == 3.7/100
 
     # *************************************************************************
     # *************************************************************************
+    
+    def test_economic_timeframe_mode3(self):
+        
+        # using a list of discount rates for each q
+        
+        reporting_periods = {0: [0, 1, 2], 1: [0, 1, 2]}
+        reporting_period_durations = {
+            0: [365 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600],
+            1: [365 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600],
+        }
+        time_intervals = {0: [0, 1], 1: [0, 1]}
+        time_interval_durations = {0: [1, 1], 1: [1, 1]}
+    
+        discount_rates = {
+            (0,0): 3.5/100,
+            (0,1): 3.6/100,
+            (0,2): 3.7/100,
+            (1,0): 1.5/100,
+            (1,1): 1.6/100,
+            (1,2): 1.7/100,
+            }
+        
+        etf = EconomicTimeFrame(
+            discount_rates_qp=discount_rates, 
+            reporting_periods=reporting_periods, 
+            reporting_period_durations=reporting_period_durations, 
+            time_intervals=time_intervals, 
+            time_interval_durations=time_interval_durations
+            )
+        
+        # check the discount rates
+        assert etf.discount_rate(0, 0) == 3.5/100
+        assert etf.discount_rate(0, 1) == 3.6/100
+        assert etf.discount_rate(0, 2) == 3.7/100
+        assert etf.discount_rate(1, 0) == 1.5/100
+        assert etf.discount_rate(1, 1) == 1.6/100
+        assert etf.discount_rate(1, 2) == 1.7/100
+
+    # *************************************************************************
+    # *************************************************************************
+    
+    def test_discount_factor_constant(self):
+        
+        discount_rate = 3.5/100
+        
+        etf = EconomicTimeFrame(
+            discount_rate=discount_rate,
+            reporting_periods={
+                0: [0,1,2,3,4,5,6,7,8,9]
+                }, 
+            reporting_period_durations={
+                0: [1,1,1,1,1,1,1,1,1,1]
+                }, 
+            time_intervals={
+                0: [0]
+                },
+            time_interval_durations={
+                0: [1]
+                },
+            )
+
+        true_factors = [
+            0.966, # 0
+            0.934, # 1
+            0.902, # 2
+            0.871, # 3
+            0.842, # 4
+            0.814, # 5
+            0.786, # 6
+            0.759, # 7
+            0.734, # 8
+            0.709, # 9
+        ]
+        
+        assessment = 0
+        factors = [
+            etf.discount_factor(assessment, p)
+            for p in etf.reporting_periods[assessment]
+            ]
+
+        for df, true_df in zip(factors, true_factors):
+            assert isclose(df, true_df, abs_tol=0.001)
+            
+
+    # *************************************************************************
+    # *************************************************************************
+    
+    def test_discount_factor_variable(self):
+
+        # variable discount factors
+        
+        etf = EconomicTimeFrame(
+            discount_rates_p=[0.035, 0.05, 0.075, 0.06],
+            reporting_periods={
+                0: [0,1,2,3]
+                }, 
+            reporting_period_durations={
+                0: [1,1,1,1]
+                }, 
+            time_intervals={
+                0: [0]
+                },
+            time_interval_durations={
+                0: [1]
+                },
+            )
+        true_factors = [0.96618, 0.92017, 0.85598, 0.80753]
+        
+        assessment = 0
+        factors = [
+            etf.discount_factor(assessment, p)
+            for p in etf.reporting_periods[assessment]
+            ]
 
+        for df, true_df in zip(factors, true_factors):
+            assert isclose(df, true_df, abs_tol=0.001)
 
 # *****************************************************************************
 # *****************************************************************************
diff --git a/tests/test_esipp_utils.py b/tests/test_esipp_utils.py
index 2c27b98..49c7c0e 100644
--- a/tests/test_esipp_utils.py
+++ b/tests/test_esipp_utils.py
@@ -8,8 +8,9 @@
 
 import src.topupopt.problems.esipp.utils as utils
 
-# ******************************************************************************
-# ******************************************************************************
+
+# *****************************************************************************
+# *****************************************************************************
 
 
 class TestProblemUtils:
@@ -50,9 +51,5 @@ class TestProblemUtils:
             error_raised = True
         assert error_raised
 
-    # **************************************************************************
-    # **************************************************************************
-
-
-# ******************************************************************************
-# ******************************************************************************
+# *****************************************************************************
+# *****************************************************************************
-- 
GitLab