From ef56a633d574046b2838bb699185457be702bd2d Mon Sep 17 00:00:00 2001
From: =?UTF-8?q?Pedro=20L=2E=20Magalh=C3=A3es?= <pmlpm@posteo.de>
Date: Sat, 27 Jul 2024 00:18:38 +0200
Subject: [PATCH] Both ways of using price functions are implemented and
 tested.

---
 src/topupopt/problems/esipp/blocks/prices.py | 11 ++-
 src/topupopt/problems/esipp/model.py         |  3 +-
 src/topupopt/problems/esipp/problem.py       | 25 +++---
 src/topupopt/problems/esipp/utils.py         | 88 +++++++++++++-------
 tests/test_esipp_prices.py                   | 45 ++++++----
 5 files changed, 115 insertions(+), 57 deletions(-)

diff --git a/src/topupopt/problems/esipp/blocks/prices.py b/src/topupopt/problems/esipp/blocks/prices.py
index 870a71e..2af7aa3 100644
--- a/src/topupopt/problems/esipp/blocks/prices.py
+++ b/src/topupopt/problems/esipp/blocks/prices.py
@@ -8,14 +8,19 @@ import math
 
 def add_price_functions(
     model: pyo.AbstractModel,
+    use_blocks: bool = False,
     **kwargs
 ):  
     
     # *************************************************************************
     # *************************************************************************
     
-    # price_other(model, **kwargs)
-    price_other_block(model, **kwargs)
+    if use_blocks:
+        # blocks
+        price_other_block(model, **kwargs)
+    else:
+        # no blocks
+        price_other(model, **kwargs)
         
     # *************************************************************************
     # *************************************************************************
@@ -132,7 +137,7 @@ def price_other_block(
         # *********************************************************************
         
         # non-convex price functions
-        
+        # TODO: fix the variables if the function is convex
         # delta variables
         b.var_active_segment_s = pyo.Var(b.set_S, within=pyo.Binary)
         
diff --git a/src/topupopt/problems/esipp/model.py b/src/topupopt/problems/esipp/model.py
index ade66bb..a378cc8 100644
--- a/src/topupopt/problems/esipp/model.py
+++ b/src/topupopt/problems/esipp/model.py
@@ -14,6 +14,7 @@ def create_model(
     enable_default_values: bool = True,
     enable_validation: bool = True,
     enable_initialisation: bool = True,
+    use_prices_block: bool = False
 ):
     # TODO: test initialisation
 
@@ -2156,7 +2157,7 @@ def create_model(
     )
     
     # prices
-    add_price_functions(model)
+    add_price_functions(model, use_blocks=use_prices_block)
 
     # *************************************************************************
     # *************************************************************************
diff --git a/src/topupopt/problems/esipp/problem.py b/src/topupopt/problems/esipp/problem.py
index c05b664..ced3fb7 100644
--- a/src/topupopt/problems/esipp/problem.py
+++ b/src/topupopt/problems/esipp/problem.py
@@ -89,7 +89,8 @@ class InfrastructurePlanningProblem(EnergySystem):
         converters: dict = None,
         prepare_model: bool = True,
         validate_inputs: bool = True,
-        node_price_model = NODE_PRICE_DELTA
+        node_price_model = NODE_PRICE_DELTA,
+        use_prices_block: bool = True #False
     ):  # TODO: switch to False when everything is more mature
         # *********************************************************************
 
@@ -122,6 +123,15 @@ class InfrastructurePlanningProblem(EnergySystem):
         #     self.number_time_intervals = {
         #         q: len(time_frame.time_intervals[q]) for q in self.time_frame.assessments
         #     }
+        
+        # *********************************************************************
+        # *********************************************************************
+        
+        # options
+        self.use_prices_block = use_prices_block
+        
+        # *********************************************************************
+        # *********************************************************************
 
         # initialise
 
@@ -1625,28 +1635,22 @@ class InfrastructurePlanningProblem(EnergySystem):
             raise ValueError(
                 "The method to determine the SOS1 weights was not recognised."
             )
-
         if method == self.SOS1_ARC_WEIGHTS_CAP:
             sos1_weights = tuple(capacity for capacity in arcs.capacity)
-
         elif method == self.SOS1_ARC_WEIGHTS_COST:
             sos1_weights = tuple(cost for cost in arcs.minimum_cost)
-
         elif method == self.SOS1_ARC_WEIGHTS_SPEC_CAP:
             sos1_weights = tuple(
                 cap / cost for cap, cost in zip(arcs.capacity, arcs.minimum_cost)
             )
-
         elif method == self.SOS1_ARC_WEIGHTS_SPEC_COST:
             sos1_weights = tuple(
                 cost / cap for cap, cost in zip(arcs.capacity, arcs.minimum_cost)
             )
-
         else:  # SOS1_ARC_WEIGHTS_NONE
             return None
 
         # make sure they are unique
-
         if verify_weights:
             for weight in sos1_weights:
                 if sos1_weights.count(weight) >= 2:  # TODO: reach this point
@@ -1655,7 +1659,6 @@ class InfrastructurePlanningProblem(EnergySystem):
                         + "special ordered sets of type 1 (SOS1),"
                         + " since some weights are not unique."
                     )
-
         return sos1_weights
 
     # *************************************************************************
@@ -1663,10 +1666,8 @@ class InfrastructurePlanningProblem(EnergySystem):
 
     def prepare(self, name = None):
         """Sets up the problem model with which instances can be built."""
-
         # create pyomo model (AbstractModel)
-
-        self.model = create_model(name)
+        self.model = create_model(name, use_prices_block=self.use_prices_block)
 
     # *************************************************************************
     # *************************************************************************
@@ -3296,6 +3297,8 @@ class InfrastructurePlanningProblem(EnergySystem):
         # *********************************************************************
 
         # produce a dictionary with the data for the problem
+        
+        # TODO: built the dict as a function of the price submodel
 
         data_dict = {
             None: {
diff --git a/src/topupopt/problems/esipp/utils.py b/src/topupopt/problems/esipp/utils.py
index 4047696..08f6f61 100644
--- a/src/topupopt/problems/esipp/utils.py
+++ b/src/topupopt/problems/esipp/utils.py
@@ -69,6 +69,7 @@ def statistics(ipp: InfrastructurePlanningProblem,
                other_node_keys: tuple = None):
     "Returns flow statistics using the optimisation results."
     
+    prices_in_block = ipp.use_prices_block
     
     if type(import_node_keys) == type(None):
         # pick all import nodes
@@ -94,36 +95,67 @@ def statistics(ipp: InfrastructurePlanningProblem,
             for node_key in net.nodes()
             if Network.KEY_NODE_BASE_FLOW in net.nodes[node_key] 
             )
-    
-    # imports
-    imports_qpk = {
-        qpk: pyo.value( 
-            sum(
-                ipp.instance.var_trans_flows_glqpks[(g,l_imp,*qpk, s)]
-                for g, l_imp in import_node_keys
-                # for g in ipp.networks
-                # for l_imp in ipp.networks[g].import_nodes
-                for s in ipp.instance.set_S[(g,l_imp,*qpk)]
+    if prices_in_block:
+        # imports
+        imports_qpk = {
+            qpk: pyo.value( 
+                sum(
+                    ipp.instance.block_prices[(g,l_imp,*qpk)].var_trans_flows_s[s]
+                    for g, l_imp in import_node_keys
+                    # for g in ipp.networks
+                    # for l_imp in ipp.networks[g].import_nodes
+                    for s in ipp.instance.block_prices[(g,l_imp,*qpk)].set_S
+                    )
+                *ipp.instance.param_c_time_qpk[qpk]
                 )
-            *ipp.instance.param_c_time_qpk[qpk]
-            )
-        for qpk in ipp.time_frame.qpk()
-        }
-    
-    # exports
-    exports_qpk = {
-        qpk: pyo.value(
-            sum(
-                ipp.instance.var_trans_flows_glqpks[(g,l_exp,*qpk, s)]
-                for g, l_exp in export_node_keys
-                # for g in ipp.networks
-                # for l_exp in ipp.networks[g].export_nodes
-                for s in ipp.instance.set_S[(g,l_exp,*qpk)]
+            for qpk in ipp.time_frame.qpk()
+            }
+        
+        # exports
+        exports_qpk = {
+            qpk: pyo.value(
+                sum(
+                    ipp.instance.block_prices[(g,l_exp,*qpk)].var_trans_flows_s[s]
+                    for g, l_exp in export_node_keys
+                    # for g in ipp.networks
+                    # for l_exp in ipp.networks[g].export_nodes
+                    for s in ipp.instance.block_prices[(g,l_exp,*qpk)].set_S
+                    )
+                *ipp.instance.param_c_time_qpk[qpk]
                 )
-            *ipp.instance.param_c_time_qpk[qpk]
-            )
-        for qpk in ipp.time_frame.qpk()
-        }
+            for qpk in ipp.time_frame.qpk()
+            }
+    else:
+        # not in a block
+        # imports
+        imports_qpk = {
+            qpk: pyo.value( 
+                sum(
+                    ipp.instance.var_trans_flows_glqpks[(g,l_imp,*qpk, s)]
+                    for g, l_imp in import_node_keys
+                    # for g in ipp.networks
+                    # for l_imp in ipp.networks[g].import_nodes
+                    for s in ipp.instance.set_S[(g,l_imp,*qpk)]
+                    )
+                *ipp.instance.param_c_time_qpk[qpk]
+                )
+            for qpk in ipp.time_frame.qpk()
+            }
+        
+        # exports
+        exports_qpk = {
+            qpk: pyo.value(
+                sum(
+                    ipp.instance.var_trans_flows_glqpks[(g,l_exp,*qpk, s)]
+                    for g, l_exp in export_node_keys
+                    # for g in ipp.networks
+                    # for l_exp in ipp.networks[g].export_nodes
+                    for s in ipp.instance.set_S[(g,l_exp,*qpk)]
+                    )
+                *ipp.instance.param_c_time_qpk[qpk]
+                )
+            for qpk in ipp.time_frame.qpk()
+            }
     # balance
     balance_qpk = {
         qpk: imports_qpk[qpk]-exports_qpk[qpk]
diff --git a/tests/test_esipp_prices.py b/tests/test_esipp_prices.py
index 9f3bf25..a7c6d59 100644
--- a/tests/test_esipp_prices.py
+++ b/tests/test_esipp_prices.py
@@ -2,12 +2,14 @@
 
 # standard
 import math
+import pytest
 
 # local
 # import numpy as np
 # import networkx as nx
 import pyomo.environ as pyo
 
+
 # import src.topupopt.problems.esipp.utils as utils
 from src.topupopt.data.misc.utils import generate_pseudo_unique_key
 from src.topupopt.problems.esipp.problem import InfrastructurePlanningProblem
@@ -54,6 +56,7 @@ class TestESIPPProblem:
         # discount_rates: dict = None,
         assessment_weights: dict = None,
         simplify_problem: bool = False,
+        use_prices_block: bool = False
     ):
         if type(solver) == type(None):
             solver = self.solver
@@ -90,6 +93,7 @@ class TestESIPPProblem:
             time_weights=time_weights,
             normalised_time_interval_duration=normalised_time_interval_duration,
             assessment_weights=assessment_weights,
+            use_prices_block=use_prices_block
         )
 
         # add networks and systems
@@ -243,7 +247,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_increasing_imp_prices(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_increasing_imp_prices(self, use_prices_block):
         
         # assessment
         q = 0
@@ -298,7 +303,8 @@ class TestESIPPProblem:
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
-            simplify_problem=False
+            simplify_problem=False,
+            use_prices_block=use_prices_block
         )
 
         assert not ipp.has_peak_total_assessments()
@@ -345,7 +351,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_decreasing_imp_prices(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_decreasing_imp_prices(self, use_prices_block):
         
         # assessment
         q = 0
@@ -400,7 +407,8 @@ class TestESIPPProblem:
             static_losses_mode=True,  # just to reach a line,
             mandatory_arcs=[],
             max_number_parallel_arcs={},
-            simplify_problem=False
+            simplify_problem=False,
+            use_prices_block=use_prices_block
         )
 
         assert not ipp.has_peak_total_assessments()
@@ -447,7 +455,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_decreasing_imp_prices_infinite_capacity(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_decreasing_imp_prices_infinite_capacity(self, use_prices_block):
         
         # assessment
         q = 0
@@ -514,7 +523,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_decreasing_exp_prices(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_decreasing_exp_prices(self, use_prices_block):
         # assessment
         q = 0
         # time
@@ -620,7 +630,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_increasing_exp_prices(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_increasing_exp_prices(self, use_prices_block):
         # assessment
         q = 0
         # time
@@ -681,7 +692,7 @@ class TestESIPPProblem:
             max_number_parallel_arcs={},
             simplify_problem=False,
         )
-        ipp.instance.pprint()
+        
         assert not ipp.has_peak_total_assessments()
         assert ipp.results["Problem"][0]["Number of constraints"] == 14 # 10 before nonconvex block
         assert ipp.results["Problem"][0]["Number of variables"] == 13 # 11 before nonconvex block
@@ -726,7 +737,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_increasing_exp_prices_infinite_capacity(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_increasing_exp_prices_infinite_capacity(self, use_prices_block):
         # assessment
         q = 0
         # time
@@ -789,6 +801,7 @@ class TestESIPPProblem:
                 mandatory_arcs=[],
                 max_number_parallel_arcs={},
                 simplify_problem=False,
+                use_prices_block=use_prices_block
             )
         except Exception:
             error_raised = True
@@ -797,7 +810,8 @@ class TestESIPPProblem:
     # *************************************************************************
     # *************************************************************************
 
-    def test_problem_increasing_imp_decreasing_exp_prices(self):
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_problem_increasing_imp_decreasing_exp_prices(self, use_prices_block):
         # scenario
         q = 0
         # time
@@ -885,6 +899,7 @@ class TestESIPPProblem:
             max_number_parallel_arcs={},
             simplify_problem=False,
             # discount_rates={0: (0.0,)},
+            use_prices_block=use_prices_block
         )
 
         assert not ipp.has_peak_total_assessments()
@@ -960,8 +975,9 @@ class TestESIPPProblem:
             
     # *************************************************************************
     # *************************************************************************
-        
-    def test_direct_imp_exp_network_higher_exp_prices(self):
+    
+    @pytest.mark.parametrize("use_prices_block", [True, False])
+    def test_direct_imp_exp_network_higher_exp_prices(self, use_prices_block):
         
         # time frame
         q = 0
@@ -1031,7 +1047,8 @@ class TestESIPPProblem:
             time_frame=tf,
             static_losses_mode=InfrastructurePlanningProblem.STATIC_LOSS_MODE_DEP,
             mandatory_arcs=[],
-            max_number_parallel_arcs={}
+            max_number_parallel_arcs={},
+            use_prices_block=use_prices_block
         )
     
         # export prices are higher: it makes sense to install the arc since the
@@ -1043,7 +1060,7 @@ class TestESIPPProblem:
             .edges[(imp_node_key, exp_node_key, 0)][Network.KEY_ARC_TECH]
             .options_selected
         )
-
+        
         # overview
         (imports_qpk, 
          exports_qpk, 
-- 
GitLab