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