From 98ac2039b18af2038d6649c82ee16df88e0b1ffe Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?=
 <pedro.magalhaes@uni-bremen.de>
Date: Sun, 10 Mar 2024 01:21:40 +0100
Subject: [PATCH] Added TimeFrame class.

---
 src/topupopt/problems/esipp/converter.py | 231 +++--------
 src/topupopt/problems/esipp/problem.py   |  59 +--
 tests/test_esipp_converter.py            |   5 +-
 tests/test_esipp_problem.py              | 278 +++++++++++++-
 tests/test_esipp_time.py                 | 469 +++++++++++++++++++++++
 5 files changed, 799 insertions(+), 243 deletions(-)
 create mode 100644 tests/test_esipp_time.py

diff --git a/src/topupopt/problems/esipp/converter.py b/src/topupopt/problems/esipp/converter.py
index 9a1ab26..fbb972d 100644
--- a/src/topupopt/problems/esipp/converter.py
+++ b/src/topupopt/problems/esipp/converter.py
@@ -1,32 +1,15 @@
-# -*- coding: utf-8 -*-
-"""
-Created on Wed Nov 17 13:04:53 2021
-
-@author: pmede
-"""
-
 # standard libraries
 
 # local libraries, external
-
 import numpy as np
 
 # local libraries, internal
-
 from .dynsys import DynamicSystem
-
 from .signal import Signal, FixedSignal
 
 # *****************************************************************************
 # *****************************************************************************
 
-# objectives:
-# 1) organise information for optimisation
-# 2) upload optimisation results and compute the various objectives
-# 3) retrieve information
-# 4)
-
-
 # TODO: create constant terms using fixed signals
 
 
@@ -35,8 +18,6 @@ class Converter:
 
     def __init__(
         self,
-        # converter name/key
-        key,
         # system information
         sys: DynamicSystem,
         # initial conditions
@@ -49,6 +30,14 @@ class Converter:
         outputs: list or Signal = None,
         # information about states
         states: list or Signal = None,
+        # final A matrix dict: a_nnk
+        a_nnk: dict = None,
+        # final B matrix dict: b_nmk
+        b_nmk: dict = None,
+        # final C matrix dict: c_rnk
+        c_rnk: dict = None,
+        # final D matrix dict: d_rmk
+        d_rmk: dict = None,
         # input amplitude costs
         input_specific_amplitude_costs: dict = None,
         # output amplitude costs
@@ -63,12 +52,24 @@ class Converter:
     ):
         # *********************************************************************
 
-        self.key = key
-
         self.sys = sys
 
         # *********************************************************************
 
+        if type(a_nnk) == dict:
+            self.a_nnk = a_nnk  # dict(a_nnk)
+
+        if type(b_nmk) == dict:
+            self.b_nmk = b_nmk  # dict(b_nmk)
+
+        if type(c_rnk) == dict:
+            self.c_rnk = c_rnk  # dict(c_rnk)
+
+        if type(d_rmk) == dict:
+            self.d_rmk = d_rmk  # dict(d_rmk)
+
+        # *********************************************************************
+
         # inputs
 
         if type(inputs) == list:
@@ -245,59 +246,59 @@ class Converter:
 
         number_intervals = self.inputs[0].number_samples
 
-        # a_innk
+        # a_nnk
 
-        a_innk = {
-            (self.key, n1, n2, k): self.sys.A_line_k[
-                k if self.sys.A_line_is_time_varying else 0
-            ][n1, n2]
+        a_nnk = {
+            (n1, n2, k): self.sys.A_line_k[k if self.sys.A_line_is_time_varying else 0][
+                n1, n2
+            ]
             for n1 in range(self.sys.number_states)  # the state being defined
             for n2 in range(self.sys.number_states)  # the influencing state
             for k in range(number_intervals)  # the time interval
         }
 
-        # b_inmk
+        # b_nmk
 
-        b_inmk = {
-            (self.key, n1, m, k): self.sys.B_line_k[
-                k if self.sys.B_line_is_time_varying else 0
-            ][n1, m]
+        b_nmk = {
+            (n1, m, k): self.sys.B_line_k[k if self.sys.B_line_is_time_varying else 0][
+                n1, m
+            ]
             for n1 in range(self.sys.number_states)  # the state being defined
             for m in range(self.sys.number_inputs)  # the influencing input
             if m not in self.fixed_inputs  # free inputs only
             for k in range(number_intervals)  # the time interval
         }
 
-        # c_irnk
+        # c_rnk
 
-        c_irnk = {
-            (self.key, r, n, k): self.sys.C_line_k[
-                k if self.sys.C_line_is_time_varying else 0
-            ][r, n]
+        c_rnk = {
+            (r, n, k): self.sys.C_line_k[k if self.sys.C_line_is_time_varying else 0][
+                r, n
+            ]
             for r in range(self.sys.number_outputs)  # the output being defined
             for n in range(self.sys.number_states)  # the influencing state
             for k in range(number_intervals)  # the time interval
         }
 
-        # d_irmk
+        # d_rmk
 
-        d_irmk = {
-            (self.key, r, m, k): self.sys.D_line_k[
-                k if self.sys.D_line_is_time_varying else 0
-            ][r, m]
+        d_rmk = {
+            (r, m, k): self.sys.D_line_k[k if self.sys.D_line_is_time_varying else 0][
+                r, m
+            ]
             for r in range(self.sys.number_outputs)  # the output being defined
             for m in range(self.sys.number_inputs)  # the influencing input
             if m not in self.fixed_inputs  # free inputs only
             for k in range(number_intervals)  # the time interval
         }
 
-        # note: e_x_ink does not depend on the initial conditions since the
-        # a_innk coefficients contain the information to handle them elsewhere
+        # note: e_x_nk does not depend on the initial conditions since the
+        # a_nnk coefficients contain the information to handle them elsewhere
 
-        # e_x_ink: depends on fixed signals
+        # e_x_nk: depends on fixed signals
 
-        e_x_ink = {
-            (self.key, n, k): sum(
+        e_x_nk = {
+            (n, k): sum(
                 self.sys.B_line_k[k if self.sys.B_line_is_time_varying else 0][n, m]
                 * self.inputs[m].samples[k]
                 for m in self.fixed_inputs  # b_inmk*u_imk for fixed inputs
@@ -306,13 +307,13 @@ class Converter:
             for k in range(number_intervals)  # the time interval
         }
 
-        # e_y_irk: depends on fixed signals
+        # e_y_rk: depends on fixed signals
 
-        e_y_irk = {
-            (self.key, r, k): sum(
+        e_y_rk = {
+            (r, k): sum(
                 self.sys.D_line_k[k if self.sys.D_line_is_time_varying else 0][r, m]
                 * self.inputs[m].samples[k]
-                for m in self.fixed_inputs  # d_irmk*u_imk for fixed inputs
+                for m in self.fixed_inputs  # d_rmk*u_mk for fixed inputs
             )
             for r in range(self.sys.number_outputs)  # the output being defined
             for k in range(number_intervals)  # the time interval
@@ -320,136 +321,8 @@ class Converter:
 
         # return statement
 
-        return a_innk, b_inmk, c_irnk, d_irmk, e_x_ink, e_y_irk
-
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def has_dimensionable_inputs(self):
-
-#     if len(self.dimensionable_inputs) == 0:
-
-#         # the system has no dimensionable inputs
-
-#         return False
-
-#     else: # the system has dimensionable inputs
-
-#         return True
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def has_binary_inputs(self):
-
-#     if len(self.binary_inputs) == 0:
-
-#         # the system has no binary inputs
-
-#         return False
-
-#     else: # the system has binary inputs
-
-#         return True
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def has_amplitude_penalised_inputs(self):
-
-#     if len(self.amplitude_penalised_inputs) == 0:
-
-#         # the system has no amplitude-penalised inputs
-
-#         return False
-
-#     else: # the system has amplitude-penalised inputs
-
-#         return True
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def has_externality_inducing_inputs(self):
-
-#     if len(self.externality_inducing_inputs) == 0:
-
-#         # the system has no externality-inducing inputs
-
-#         return False
-
-#     else: # the system has externality-inducing inputs
-
-#         return True
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def has_externality_inducing_outputs(self):
-
-#     if len(self.externality_inducing_outputs) == 0:
-
-#         # the system has no externality-inducing outputs
-
-#         return False
-
-#     else: # the system has externality-inducing outputs
-
-#         return True
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def identify_dimensionable_inputs(self):
-
-#     self.dimensionable_inputs = [
-#         i
-#         for i, u in enumerate(self.inputs)
-#         if u.is_dimensionable]
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def identify_binary_inputs(self):
-
-#     self.binary_inputs = [
-#         i
-#         for i, u in enumerate(self.inputs)
-#         if u.is_binary]
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def identify_externality_inducing_inputs(self):
-
-#     self.externality_inducing_inputs = [
-#         i
-#         for i, c in enumerate(self.input_externalities)
-#         if c != 0]
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def identify_externality_inducing_outputs(self):
-
-#     self.externality_inducing_outputs = [
-#         i
-#         for i, c in enumerate(self.output_externalities)
-#         if c != 0]
-
-# # *************************************************************************
-# # *************************************************************************
-
-# def identify_amplitude_penalised_inputs(self):
-
-#     self.amplitude_penalised_inputs = [
-#         i
-#         for i, c in enumerate(self.input_amplitude_costs)
-#         if c != 0]
+        return a_nnk, b_nmk, c_rnk, d_rmk, e_x_nk, e_y_rk
 
-# # *************************************************************************
-# # *************************************************************************
 
 # *****************************************************************************
 # *****************************************************************************
diff --git a/src/topupopt/problems/esipp/problem.py b/src/topupopt/problems/esipp/problem.py
index 5f013ff..a12b6d5 100644
--- a/src/topupopt/problems/esipp/problem.py
+++ b/src/topupopt/problems/esipp/problem.py
@@ -2768,80 +2768,57 @@ class InfrastructurePlanningProblem(EnergySystem):
         #     param_f_amp_y_irqk = {}
 
         # output equations: C matrix coefficients
-
         param_c_eq_y_irnqk = {
-            (converter_key, r, n, q, k): 0  # converter.dssm[k].C[r,n]
-            for converter_key, converter in self.converters.items()
-            for r in range(converter.number_outputs)
-            for n in range(converter.number_states)
-            for (q, k) in set_QK
+            (cvt_key, *rnqk): c_rnqk
+            for cvt_key, cvt in self.converters.items()
+            for rnqk, c_rnqk in cvt.c_rnqk.items()
         }
 
         # output equations: D matrix coefficients
-
         param_d_eq_y_irmqk = {
-            (converter_key, r, m, q, k): 0  # converter.dssm[k].C[r,n]
-            for converter_key, converter in self.converters.items()
-            for r in range(converter.number_outputs)
-            for m in range(converter.number_inputs)
-            for (q, k) in set_QK
+            (cvt_key, *rmqk): d_rmqk
+            for cvt_key, cvt in self.converters.items()
+            for rmqk, d_rmqk in cvt.d_rmqk.items()
         }
 
         # output equations: constant term
-
         param_e_eq_y_irqk = {
-            (converter_key, r, q, k): 0  # converter.dssm[k].C[r,n]
-            for converter_key, converter in self.converters.items()
-            for r in range(converter.number_outputs)
-            for (q, k) in set_QK
+            (cvt_key, *rqk): e_rqk
+            for cvt_key, cvt in self.converters.items()
+            for rqk, e_rqk in cvt.e_rqk.items()
         }
 
         # *********************************************************************
 
         # states
-
         # state equations: A matrix coefficients
-
         param_a_eq_x_innqk = {
-            # (converter_key, r, n, k):
-            #     0 #converter.dssm[k].C[r,n]
-            # for converter_key, converter in self.converters.items()
-            # for r in range(converter.number_outputs)
-            # for n in range(converter.number_states)
-            # for k in set_K
+            (cvt_key, *nnqk): a_nnqk
+            for cvt_key, cvt in self.converters.items()
+            for nnqk, a_nnqk in cvt.a_nnqk.items()
         }
 
         # state equations: B matrix coefficients
-
         param_b_eq_x_inmqk = {
-            # (converter_key, r, m, k):
-            #     0 #converter.dssm[k].C[r,n]
-            # for converter_key, converter in self.converters.items()
-            # for r in range(converter.number_outputs)
-            # for m in range(converter.number_inputs)
-            # for k in set_K
+            (cvt_key, *nmqk): b_nmqk
+            for cvt_key, cvt in self.converters.items()
+            for nmqk, b_nmqk in cvt.b_nmqk.items()
         }
 
         # state equations: constant term
-
         param_e_eq_x_inqk = {
-            # (converter_key, r, k):
-            #     0 #converter.dssm[k].C[r,n]
-            # for converter_key, converter in self.converters.items()
-            # for r in range(converter.number_outputs)
-            # for k in set_K
+            (cvt_key, *nqk): e_nqk
+            for cvt_key, cvt in self.converters.items()
+            for nqk, e_nqk in cvt.e_nqk.items()
         }
 
         # initial states
-
         param_x_inq0 = {}
 
         # upper bounds for states (default: none)
-
         param_x_ub_inqk = {}
 
         # lower bounds for states (default: none)
-
         param_x_lb_inqk = {}
 
         # maximum positive amplitude for inputs
diff --git a/tests/test_esipp_converter.py b/tests/test_esipp_converter.py
index d049d0e..15e64c2 100644
--- a/tests/test_esipp_converter.py
+++ b/tests/test_esipp_converter.py
@@ -272,7 +272,6 @@ def method_full_converter(time_step_durations: list):
 
     # create a converter
     cvn1 = cvn.Converter(
-        "cvn1",
         sys=ds,
         initial_states=x0,
         turn_key_cost=3,
@@ -282,7 +281,9 @@ def method_full_converter(time_step_durations: list):
     )
 
     # get the dictionaries
-    (a_innk, b_inmk, c_irnk, d_irmk, e_x_ink, e_y_irk) = cvn1.matrix_dictionaries()
+    (a_innk, b_inmk, c_irnk, d_irmk, e_x_ink, e_y_irk) = cvn1.matrix_dictionaries(
+        "cvn1"
+    )
 
     # TODO: check the dicts
 
diff --git a/tests/test_esipp_problem.py b/tests/test_esipp_problem.py
index 8fe0e21..69bab4a 100644
--- a/tests/test_esipp_problem.py
+++ b/tests/test_esipp_problem.py
@@ -16,6 +16,7 @@ from src.topupopt.problems.esipp.network import Arcs, Network
 from src.topupopt.problems.esipp.resource import ResourcePrice
 from src.topupopt.problems.esipp.problem import simplify_peak_total_problem
 from src.topupopt.problems.esipp.problem import is_peak_total_problem
+from src.topupopt.problems.esipp.time import TimeFrame
 
 # *****************************************************************************
 # *****************************************************************************
@@ -38,8 +39,10 @@ class TestESIPPProblem:
         perform_analysis: bool = False,
         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,
@@ -47,7 +50,7 @@ class TestESIPPProblem:
         arc_groups_dict: dict = None,
         init_aux_sets: bool = False,
         discount_rates: dict = None,
-        reporting_periods: dict = None,
+        # reporting_periods: dict = None,
         time_intervals: dict = None,
         assessment_weights: dict = None,
         simplify_problem: bool = False,
@@ -60,8 +63,11 @@ class TestESIPPProblem:
         if type(assessment_weights) != dict:
             assessment_weights = {}  # default
 
-        if type(reporting_periods) != dict:
-            reporting_periods = {0: (0, 1)}
+        # if type(reporting_periods) != dict:
+        #     reporting_periods = {0: (0, 1)}
+
+        if type(converters) != dict:
+            converters = {}
 
         # time intervals
 
@@ -114,7 +120,7 @@ class TestESIPPProblem:
         ipp = InfrastructurePlanningProblem(
             name="problem",
             discount_rates=discount_rates,
-            reporting_periods=reporting_periods,
+            reporting_periods=time_frame.reporting_periods,
             time_intervals=time_intervals,
             time_weights=time_weights,
             normalised_time_interval_duration=normalised_time_interval_duration,
@@ -126,6 +132,11 @@ class TestESIPPProblem:
         for netkey, net in networks.items():
             ipp.add_network(network_key=netkey, network=net)
 
+        # add converters
+
+        for cvtkey, cvt in converters.items():
+            ipp.add_converter(converter_key=cvtkey, converter=cvt)
+
         # define arcs as mandatory
 
         if type(mandatory_arcs) == list:
@@ -274,8 +285,13 @@ class TestESIPPProblem:
         q = 0
         # time
         number_intervals = 3
-        # periods
-        number_periods = 2
+
+        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]},
+        )
 
         # 2 nodes: one import, one regular
         mynet = Network()
@@ -285,9 +301,11 @@ class TestESIPPProblem:
         mynet.add_import_node(
             node_key=node_IMP,
             prices={
-                (q, p, k): ResourcePrice(prices=1.0, volumes=None)
-                for p in range(number_periods)
-                for k in range(number_intervals)
+                # (q, p, k): ResourcePrice(prices=1.0, volumes=None)
+                # for p in range(number_periods)
+                # for k in range(number_intervals)
+                qpk: ResourcePrice(prices=1.0, volumes=None)
+                for qpk in tf.qpk()
             },
         )
 
@@ -305,8 +323,7 @@ class TestESIPPProblem:
 
         arc_tech_IA = Arcs(
             name="any",
-            # efficiency=[0.5, 0.5, 0.5],
-            efficiency={(q, 0): 0.5, (q, 1): 0.5, (q, 2): 0.5},
+            efficiency={qk: 0.5 for qk in tf.qk()},
             efficiency_reverse=None,
             static_loss=None,
             capacity=[3],
@@ -338,6 +355,7 @@ class TestESIPPProblem:
             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,
@@ -406,8 +424,13 @@ class TestESIPPProblem:
         q = 0
         # time
         number_intervals = 3
-        # periods
-        number_periods = 2
+
+        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]},
+        )
 
         # 2 nodes: one import, one regular
         mynet = Network()
@@ -418,9 +441,11 @@ class TestESIPPProblem:
         mynet.add_import_node(
             node_key=node_IMP,
             prices={
-                (q, p, k): ResourcePrice(prices=1.0, volumes=None)
-                for p in range(number_periods)
-                for k in range(number_intervals)
+                # (q, p, k): ResourcePrice(prices=1.0, volumes=None)
+                # for p in range(number_periods)
+                # for k in range(number_intervals)
+                qpk: ResourcePrice(prices=1.0, volumes=None)
+                for qpk in tf.qpk()
             },
         )
 
@@ -439,8 +464,7 @@ class TestESIPPProblem:
 
         arc_tech_IA = Arcs(
             name="any",
-            # efficiency=[0.5, 0.5, 0.5],
-            efficiency={(q, 0): 0.5, (q, 1): 0.5, (q, 2): 0.5},
+            efficiency={qk: 0.5 for qk in tf.qk()},
             efficiency_reverse=None,
             static_loss=None,
             capacity=[3],
@@ -472,6 +496,7 @@ class TestESIPPProblem:
             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,
@@ -574,6 +599,13 @@ class TestESIPPProblem:
         # 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]},
+        )
+
         # 2 nodes: one import, one regular
         mynet = Network()
 
@@ -582,9 +614,11 @@ class TestESIPPProblem:
         mynet.add_import_node(
             node_key=node_IMP,
             prices={
-                (q, p, k): ResourcePrice(prices=[1.0, 2.0], volumes=[0.5, None])
-                for p in range(number_periods)
-                for k in range(number_intervals)
+                # (q, p, k): ResourcePrice(prices=[1.0, 2.0], volumes=[0.5, None])
+                # for p in range(number_periods)
+                # for k in range(number_intervals)
+                qpk: ResourcePrice(prices=[1.0, 2.0], volumes=[0.5, None])
+                for qpk in tf.qpk()
             },
         )
 
@@ -624,6 +658,7 @@ class TestESIPPProblem:
             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,
@@ -687,6 +722,13 @@ class TestESIPPProblem:
         # 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]},
+        )
+
         # 2 nodes: one export, one regular
         mynet = Network()
 
@@ -737,6 +779,7 @@ class TestESIPPProblem:
             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,
@@ -800,6 +843,13 @@ class TestESIPPProblem:
         # 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]},
+        )
+
         # 3 nodes: one import, one export, one regular
         mynet = Network()
 
@@ -877,6 +927,7 @@ class TestESIPPProblem:
             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,
@@ -961,6 +1012,191 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
+    def test_problem_converter_sink(self):
+        # scenario
+        q = 0
+        # time
+        number_intervals = 3
+        # 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]},
+        )
+
+        # 2 nodes: one import, one regular
+        mynet = Network()
+
+        # import node
+        node_IMP = generate_pseudo_unique_key(mynet.nodes())
+        mynet.add_import_node(
+            node_key=node_IMP,
+            prices={
+                (q, p, k): ResourcePrice(prices=1.0, volumes=None)
+                for p in range(number_periods)
+                for k in range(number_intervals)
+            },
+        )
+
+        # other nodes
+
+        node_A = generate_pseudo_unique_key(mynet.nodes())
+
+        mynet.add_source_sink_node(
+            node_key=node_A,
+            # base_flow=[0.5, 0.0, 1.0],
+            base_flow={(q, 0): 0.50, (q, 1): 0.00, (q, 2): 1.00},
+        )
+
+        # arc IA
+
+        arc_tech_IA = Arcs(
+            name="any",
+            # efficiency=[0.5, 0.5, 0.5],
+            efficiency={(q, 0): 0.5, (q, 1): 0.5, (q, 2): 0.5},
+            efficiency_reverse=None,
+            static_loss=None,
+            capacity=[3],
+            minimum_cost=[2],
+            specific_capacity_cost=1,
+            capacity_is_instantaneous=False,
+            validate=False,
+        )
+
+        mynet.add_directed_arc(node_key_a=node_IMP, node_key_b=node_A, arcs=arc_tech_IA)
+
+        # identify node types
+
+        mynet.identify_node_types()
+
+        # converters
+
+        # number of samples
+        time_step_durations = [1, 1, 1]
+        number_time_steps = len(time_step_durations)
+
+        # get the coefficients
+        import numpy as np
+
+        # a_innk
+        a_innk = {
+            ("cvt1", 0, 0, 0): 0.95,
+            ("cvt1", 0, 0, 1): 0.95,
+            ("cvt1", 0, 0, 2): 0.95,
+        }
+
+        # b_inmk
+        b_inmk = {("cvt1", 0, 0, 0): 3, ("cvt1", 0, 0, 1): 3, ("cvt1", 0, 0, 2): 3}
+
+        # c_irnk
+        c_irnk = {}
+        # d_irmk
+        d_irmk = {}
+        # e_x_ink: depends on fixed signals
+        e_x_ink = {}
+        # e_y_irk: depends on fixed signals
+        e_y_irk = {}
+
+        # get the signals
+        inputs, states, outputs = get_two_node_model_signals(number_time_steps)
+
+        # create a dynamic system
+        ds = dynsys.DynamicSystem(
+            time_interval_durations=time_step_durations, A=a, B=b, C=c, D=d
+        )
+
+        # create a converter
+        cvn1 = cvn.Converter(
+            "cvn1",
+            sys=ds,
+            initial_states=x0,
+            turn_key_cost=3,
+            inputs=inputs,
+            states=states,
+            outputs=outputs,
+        )
+
+        # 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},
+            converters={"mycvt": cvt},
+            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,
+        )
+
+        assert is_peak_total_problem(ipp)
+        assert ipp.results["Problem"][0]["Number of constraints"] == 24
+        assert ipp.results["Problem"][0]["Number of variables"] == 22
+        assert ipp.results["Problem"][0]["Number of nonzeros"] == 49
+
+        # *********************************************************************
+        # *********************************************************************
+
+        # validation
+
+        # the arc should be installed since it is required for feasibility
+        assert (
+            True
+            in ipp.networks["mynet"]
+            .edges[(node_IMP, node_A, 0)][Network.KEY_ARC_TECH]
+            .options_selected
+        )
+
+        # the flows should be 1.0, 0.0 and 2.0
+        assert math.isclose(
+            pyo.value(ipp.instance.var_v_glljqk[("mynet", node_IMP, node_A, 0, q, 0)]),
+            1.0,
+            abs_tol=1e-6,
+        )
+        assert math.isclose(
+            pyo.value(ipp.instance.var_v_glljqk[("mynet", node_IMP, node_A, 0, q, 1)]),
+            0.0,
+            abs_tol=1e-6,
+        )
+        assert math.isclose(
+            pyo.value(ipp.instance.var_v_glljqk[("mynet", node_IMP, node_A, 0, q, 2)]),
+            2.0,
+            abs_tol=1e-6,
+        )
+
+        # arc amplitude should be two
+        assert math.isclose(
+            pyo.value(ipp.instance.var_v_amp_gllj[("mynet", node_IMP, node_A, 0)]),
+            2.0,
+            abs_tol=0.01,
+        )
+
+        # capex should be four
+        assert math.isclose(pyo.value(ipp.instance.var_capex), 4.0, abs_tol=1e-3)
+
+        # sdncf should be -5.7
+        assert math.isclose(pyo.value(ipp.instance.var_sdncf_q[q]), -5.7, abs_tol=1e-3)
+
+        # the objective function should be -9.7
+        assert math.isclose(pyo.value(ipp.instance.obj_f), -9.7, abs_tol=1e-3)
+
 
 # *****************************************************************************
 # *****************************************************************************
diff --git a/tests/test_esipp_time.py b/tests/test_esipp_time.py
new file mode 100644
index 0000000..6658df1
--- /dev/null
+++ b/tests/test_esipp_time.py
@@ -0,0 +1,469 @@
+# imports
+
+# local, internal
+from src.topupopt.problems.esipp.time import TimeFrame
+
+# *****************************************************************************
+# *****************************************************************************
+
+
+class TestTimeFrame:
+    # *************************************************************************
+    # *************************************************************************
+
+    # test problem from table 2.1
+
+    def test_nonpositive_durations(self):
+        # negative reporting period durations
+
+        reporting_periods = {0: [0, 1, 2]}
+        reporting_period_durations = {0: [1, -1, 1]}
+        time_intervals = {0: [0, 1]}
+        time_interval_durations = {0: [1, 1]}
+
+        error_raised = True
+        try:
+            TimeFrame(
+                reporting_periods=reporting_periods,
+                reporting_period_durations=reporting_period_durations,
+                time_intervals=time_intervals,
+                time_interval_durations=time_interval_durations,
+            )
+        except ValueError:
+            error_raised = True
+        assert error_raised
+
+        # zero time interval durations
+
+        reporting_periods = {0: [0, 1, 2]}
+        reporting_period_durations = {0: [1, 1, 1]}
+        time_intervals = {0: [0, 1]}
+        time_interval_durations = {0: [1, 0]}
+
+        error_raised = True
+        try:
+            TimeFrame(
+                reporting_periods=reporting_periods,
+                reporting_period_durations=reporting_period_durations,
+                time_intervals=time_intervals,
+                time_interval_durations=time_interval_durations,
+            )
+        except ValueError:
+            error_raised = True
+        assert error_raised
+
+    # *************************************************************************
+    # *************************************************************************
+
+    # test problem from table 2.1
+
+    def test_table_21(self):
+        # summary:
+        # 1 assessment
+        # 3 reporting periods
+        # 2 time intervals
+
+        reporting_periods = {0: [0, 1, 2]}
+        reporting_period_durations = {
+            0: [365 * 24 * 3600, 365 * 24 * 3600, 365 * 24 * 3600]
+        }
+        time_intervals = {0: [0, 1]}
+        time_interval_durations = {0: [1, 1]}
+
+        tf = TimeFrame(
+            reporting_periods=reporting_periods,
+            reporting_period_durations=reporting_period_durations,
+            time_intervals=time_intervals,
+            time_interval_durations=time_interval_durations,
+        )
+
+        # qk: valid
+        assert tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+            }
+        )
+        # qk: not valid
+        assert not tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 2): 1,
+            }
+        )
+        # qk: valid and complete
+        assert tf.complete_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+            }
+        )
+        # qk: valid but not complete
+        assert not tf.complete_qk(
+            {
+                (0, 0): 1,
+            }
+        )
+
+        # qp: valid
+        assert tf.valid_qp(
+            {
+                (0, 0): 1,
+            }
+        )
+        # qp: not valid
+        assert not tf.valid_qp({(0, 0): 1, (0, 1): 1, (0, 3): 1})
+        # qp: valid and complete
+        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})
+
+        # qpk: valid
+        assert tf.valid_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 0, 1): 1,
+            }
+        )
+        # qpk: not valid
+        assert not tf.valid_qpk(
+            {
+                (0, 0, 0): 1,
+                (0, 0, 1): 1,
+                (0, 1, 0): 1,
+                (0, 1, 1): 1,
+                (0, 3, 0): 1,
+                (0, 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,
+            }
+        )
+        # 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}
+        )
+
+        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)
+
+    # *************************************************************************
+    # *************************************************************************
+
+    # test problem from table 2.2
+
+    def test_table_22(self):
+        # summary:
+        # 1 assessment
+        # 3 reporting periods
+        # 2 time intervals
+
+        reporting_periods = {0: [0], 1: [1, 2]}
+        reporting_period_durations = {
+            0: [365 * 24 * 3600],
+            1: [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,
+        )
+
+        # 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,
+                # (0,1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+            }
+        )
+
+        # qp: valid
+        assert tf.valid_qp(
+            {
+                (0, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qp: not valid
+        assert not tf.valid_qp(
+            {
+                (0, 0): 1,
+                (1, 1): 1,
+                (1, 3): 1,
+            }
+        )
+        # qp: valid and complete
+        assert tf.complete_qp(
+            {
+                (0, 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,
+                (1, 1, 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,
+                (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, (1, 1, 0): 1, (1, 1, 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.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
+        # intervals; and another for last period with three intervals.
+
+        reporting_periods = {0: [0, 1], 1: [2]}
+        reporting_period_durations = {
+            1: [365 * 24 * 3600],
+            0: [365 * 24 * 3600, 365 * 24 * 3600],
+        }
+        time_intervals = {0: [0, 1], 1: [0, 1, 2]}
+        time_interval_durations = {0: [1, 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,
+        )
+
+        # qk: valid
+        assert tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qk: not valid
+        assert not tf.valid_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+                (1, 3): 1,
+            }
+        )
+        # qk: valid and complete
+        assert tf.complete_qk(
+            {
+                (0, 0): 1,
+                (0, 1): 1,
+                (1, 0): 1,
+                (1, 1): 1,
+                (1, 2): 1,
+            }
+        )
+        # qk: valid but not complete
+        assert not tf.complete_qk(
+            {
+                (0, 0): 1,
+                # (0,1): 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,
+                (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,
+                (1, 2, 0): 1,
+                (1, 2, 1): 1,
+                (1, 2, 2): 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.4
+
+    def test_table_24(self):
+        pass
+
+    # *************************************************************************
+    # *************************************************************************
+
+    # TODO: table 2.5
+
+    def test_table_25(self):
+        pass
+
+    # *************************************************************************
+    # *************************************************************************
+
+    # TODO: table 2.6
+
+    def test_table_26(self):
+        pass
+
+    # *************************************************************************
+    # *************************************************************************
+
+
+# *****************************************************************************
+# *****************************************************************************
-- 
GitLab