diff --git a/src/topupopt/problems/esipp/model.py b/src/topupopt/problems/esipp/model.py index e686001a0a68374f52726398e2ef5d87ec7bd023..9a23a7554d629174e522783a4d0864cfd15c27f3 100644 --- a/src/topupopt/problems/esipp/model.py +++ b/src/topupopt/problems/esipp/model.py @@ -4,8 +4,8 @@ import pyomo.environ as pyo from math import isfinite, inf -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** def create_model(name: str, enable_default_values: bool = True, @@ -14,15 +14,15 @@ def create_model(name: str, # TODO: make default values, validation, and initialisation optional - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # create model object model = pyo.AbstractModel(name) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # naming convention: # variables start with "var_" @@ -41,19 +41,19 @@ def create_model(name: str, # TODO: add piecewise constraints for the import and export flows/prices - #************************************************************************** - #************************************************************************** - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* # sets - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # input sets - #************************************************************************** + # ************************************************************************* # set of assessments @@ -93,8 +93,8 @@ def create_model(name: str, model.set_L_max_in_g = pyo.Set(model.set_G, within=model.set_L) # should inherently exclude import nodes - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # sparse sets @@ -210,8 +210,8 @@ def create_model(name: str, ) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # input sets @@ -298,8 +298,8 @@ def create_model(name: str, model.set_J_stt_ds = pyo.Set(model.set_GLL, within=model.set_J_stt) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # systems @@ -311,7 +311,7 @@ def create_model(name: str, model.set_I_new = pyo.Set(within=model.set_I) - #************************************************************************** + # ************************************************************************* # inputs @@ -344,7 +344,7 @@ def create_model(name: str, model.set_M_ext = pyo.Set(model.set_I, within=model.set_M) - #************************************************************************** + # ************************************************************************* # outputs @@ -376,7 +376,7 @@ def create_model(name: str, model.set_R_ext = pyo.Set(model.set_I) - #************************************************************************** + # ************************************************************************* # states @@ -429,12 +429,12 @@ def create_model(name: str, model.set_N_ref_d = pyo.Set(model.set_I, within=model.set_N) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # sparse index sets - #************************************************************************** + # ************************************************************************* # set of price segments @@ -480,7 +480,7 @@ def create_model(name: str, initialize=(init_set_GLQPKS_imp if enable_initialisation else None) ) - #************************************************************************** + # ************************************************************************* # all arcs @@ -538,7 +538,7 @@ def create_model(name: str, ) ) - #************************************************************************** + # ************************************************************************* # sets of GLLJ tuples for directed arcs @@ -609,7 +609,7 @@ def create_model(name: str, model.set_GLLJ_dir_new - model.set_GLLJ_dir_new_mdt ) - #************************************************************************** + # ************************************************************************* # sets of GLLJ tuples for undirected arcs @@ -698,7 +698,7 @@ def create_model(name: str, initialize=(init_set_GLLJ_int if enable_initialisation else None) ) - #************************************************************************** + # ************************************************************************* # set of complementary GLLJ tuples for undirected arcs @@ -824,7 +824,7 @@ def create_model(name: str, model.set_GLLJ_static & model.set_GLLJ_und_new ) - #************************************************************************** + # ************************************************************************* # set of GLLJ tuples for undirected arcs with redundancies @@ -860,7 +860,7 @@ def create_model(name: str, model.set_GLLJ_und_pre_red - model.set_GLLJ_und_pre_inf_red ) - #************************************************************************** + # ************************************************************************* # sets of GLLJ tuples for directed and undirected arcs (no redundancies) @@ -896,7 +896,7 @@ def create_model(name: str, ) ) - #************************************************************************** + # ************************************************************************* # sets of GLLJ tuples for directed and undirected arcs (with redundancies) @@ -935,7 +935,7 @@ def create_model(name: str, ) ) - #************************************************************************** + # ************************************************************************* # set of arc options @@ -977,7 +977,7 @@ def create_model(name: str, ) ) - #************************************************************************** + # ************************************************************************* # arc selection using SOS1 @@ -1026,7 +1026,7 @@ def create_model(name: str, ) ) - #************************************************************************** + # ************************************************************************* # flow sense determination using SOS1 @@ -1067,7 +1067,7 @@ def create_model(name: str, dimen=6, initialize=init_set_GLLJQK_und_sns_sos1_red_gllj) - #************************************************************************** + # ************************************************************************* # inputs @@ -1122,7 +1122,7 @@ def create_model(name: str, initialize=init_set_IM_ext, within=model.set_IM) - #************************************************************************** + # ************************************************************************* # states @@ -1225,7 +1225,7 @@ def create_model(name: str, initialize=init_set_IN_ref_d, within=model.set_IN) - #************************************************************************** + # ************************************************************************* # outputs @@ -1292,7 +1292,7 @@ def create_model(name: str, model.set_IR_ext = pyo.Set(dimen=2, initialize=init_set_IR_ext) - #************************************************************************** + # ************************************************************************* # combined inputs/states/outputs @@ -1333,8 +1333,8 @@ def create_model(name: str, model.set_IRN = pyo.Set(dimen=3, initialize=init_set_IRN) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # set of arc groups @@ -1469,19 +1469,19 @@ def create_model(name: str, model.var_v_amp_t = pyo.Var(model.set_T) - #************************************************************************** - #************************************************************************** - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* # parameters - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # objective function - #************************************************************************** + # ************************************************************************* # general parameters @@ -1603,7 +1603,7 @@ def create_model(name: str, within=pyo.NonNegativeReals, default=0) - #************************************************************************** + # ************************************************************************* # arcs @@ -1652,7 +1652,7 @@ def create_model(name: str, model.set_GLLJQK_und_sns_sos1_red, within=pyo.NonNegativeReals) - #************************************************************************** + # ************************************************************************* # relative variation of arc-dependent losses @@ -1668,7 +1668,7 @@ def create_model(name: str, model.set_QK, within=pyo.NonNegativeReals) - #************************************************************************** + # ************************************************************************* # network @@ -1711,7 +1711,7 @@ def create_model(name: str, default=0, # default: no effect within=pyo.Reals) - #************************************************************************** + # ************************************************************************* # inputs @@ -1734,7 +1734,7 @@ def create_model(name: str, within=pyo.PositiveReals, default=1) - #************************************************************************** + # ************************************************************************* # states @@ -1801,7 +1801,7 @@ def create_model(name: str, default=0, # default: no effect within=pyo.Reals) - #************************************************************************** + # ************************************************************************* # outputs @@ -1862,15 +1862,15 @@ def create_model(name: str, default=0, # default: no effect within=pyo.Reals) - #************************************************************************** - #************************************************************************** - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* # variables - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # objective function @@ -1938,7 +1938,7 @@ def create_model(name: str, within=pyo.NonNegativeReals, bounds=bounds_var_if_glqpks) - #************************************************************************** + # ************************************************************************* # arcs @@ -1978,7 +1978,7 @@ def create_model(name: str, model.set_QK, within=pyo.NonNegativeReals) - #************************************************************************** + # ************************************************************************* # decision to install a given arc @@ -2013,7 +2013,7 @@ def create_model(name: str, model.var_xi_arc_inv_gllj = pyo.Var(model.set_GLLJ_int, within=pyo.UnitInterval) - #************************************************************************** + # ************************************************************************* # converters @@ -2054,7 +2054,7 @@ def create_model(name: str, model.var_u_amp_im = pyo.Var(model.set_IM_dim, within=pyo.NonNegativeReals) - #************************************************************************** + # ************************************************************************* # outputs @@ -2093,7 +2093,7 @@ def create_model(name: str, model.var_y_amp_neg_ir = pyo.Var(model.set_IR_dim_neg, within=pyo.Reals) - #************************************************************************** + # ************************************************************************* # states @@ -2135,15 +2135,15 @@ def create_model(name: str, model.set_QK, within=pyo.NonNegativeReals) - #************************************************************************** - #************************************************************************** - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* # objective function - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # maximise npv @@ -2155,19 +2155,19 @@ def create_model(name: str, ) model.obj_f = pyo.Objective(rule=obj_f_rule, sense=pyo.maximize) - #************************************************************************** - #************************************************************************** - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* + # ************************************************************************* # Constraints - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # objective function - #************************************************************************** + # ************************************************************************* # opex @@ -2267,7 +2267,7 @@ def create_model(name: str, model.set_QPK, rule=rule_constr_imp_flows) - #************************************************************************** + # ************************************************************************* # sum of discounted externalities @@ -2326,7 +2326,7 @@ def create_model(name: str, ) model.constr_sdext_q = pyo.Constraint(model.set_Q, rule=rule_sdext_q) - #************************************************************************** + # ************************************************************************* # capex @@ -2411,13 +2411,13 @@ def create_model(name: str, model.constr_capex_system = pyo.Constraint(model.set_I_new, rule=rule_capex_converter) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # network - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # flow equilibrium equation @@ -2547,7 +2547,7 @@ def create_model(name: str, model.set_QK, rule=rule_node_balance) - #************************************************************************** + # ************************************************************************* # limit number of directed arcs per direction @@ -2712,7 +2712,7 @@ def create_model(name: str, model.set_GLL_arc_max, rule=rule_constr_limited_parallel_arcs_per_direction) - #************************************************************************** + # ************************************************************************* # there can only one incoming arc at most, if there are no outgoing arcs @@ -2746,11 +2746,11 @@ def create_model(name: str, else: # more than one incoming arc is possible - #****************************************************************** + # ***************************************************************** # number of (new) incoming directed arcs in a group - #****************************************************************** + # ***************************************************************** b_max_in_gl = 0 @@ -2760,7 +2760,7 @@ def create_model(name: str, temp_constr = ( sum( - #********************************************************** + # ********************************************************* # interfaced groups sum( sum(1 @@ -2771,7 +2771,7 @@ def create_model(name: str, for t in m.set_T_int ) + - #********************************************************** + # ********************************************************* # optional non-interfaced groups sum( sum( @@ -2786,7 +2786,7 @@ def create_model(name: str, if t not in m.set_T_int # not interfaced ) + - #********************************************************** + # ********************************************************* # interfaced arcs sum( m.var_xi_arc_inv_gllj[(g,l_circ,l,j_circ)] @@ -2796,7 +2796,7 @@ def create_model(name: str, if j_circ not in m.set_J_col[(g,l_circ,l)] # individual ) if (g,l_circ,l) in m.set_J else 0 + - #********************************************************** + # ********************************************************* # optional non-interfaced arcs sum( sum(m.var_delta_arc_inv_glljh[(g,l_circ,l,j_dot,h_dot)] @@ -2808,7 +2808,7 @@ def create_model(name: str, if j_dot not in m.set_J_mdt[(g,l_circ,l)] # optional ) if (g,l_circ,l) in m.set_J else 0 + - #********************************************************** + # ********************************************************* # preexisting directed arcs sum( 1 @@ -2816,20 +2816,20 @@ def create_model(name: str, if j_pre_dir not in m.set_J_und[(g,l_circ,l)] # directed ) if (g,l_circ,l) in m.set_J_pre else 0 + - #********************************************************** + # ********************************************************* # mandatory directed arcs sum( 1 for j_mdt_dir in m.set_J_mdt[(g,l_circ,l)] if j_mdt_dir not in m.set_J_und[(g,l_circ,l)] # directed ) if (g,l_circ,l) in m.set_J_mdt else 0 - #********************************************************** + # ********************************************************* for l_circ in m.set_L[g] if l_circ not in m.set_L_exp[g] if l_circ != l ) <= 1 #+ # M_gl*sum( - # #********************************************************** + # # ********************************************************* # # outgoing arcs in interfaced groups, nominal direction # sum(sum(1 # for j in m.set_J_col[(g,l,l_diamond)] @@ -2849,7 +2849,7 @@ def create_model(name: str, # for t in m.set_T_int # ) if (g,l_diamond,l) in m.set_J_col else 0 # + - # #********************************************************** + # # ********************************************************* # # TODO: outgoing arcs in non-interfaced optional groups, nominal # sum(sum(1 # for j in m.set_J_col[(g,l,l_diamond)] @@ -2879,14 +2879,14 @@ def create_model(name: str, # if t not in m.set_T_int # ) if (g,l_diamond,l) in m.set_J_col else 0 # + - # #********************************************************** + # # ********************************************************* # # interfaced individual outgoing arcs, nominal direction # sum(m.var_xi_arc_inv_gllj[(g,l,l_diamond,j)] # for j in m.set_J_int[(g,l,l_diamond)] # interfaced # if j not in m.set_J_col[(g,l,l_diamond)] # individual # ) if (g,l,l_diamond) in m.set_J_int else 0 # + - # #********************************************************** + # # ********************************************************* # # interfaced individual undirected arcs, reverse direction # sum(m.var_xi_arc_inv_gllj[(g,l,l_diamond,j)] # for j in m.set_J_und[(g,l_diamond,l)] # undirected @@ -2894,7 +2894,7 @@ def create_model(name: str, # if j not in m.set_J_col[(g,l_diamond,l)] # individual # ) if (g,l_diamond,l) in m.set_J_und else 0 # + - # #********************************************************** + # # ********************************************************* # # outgoing non-interfaced individual optional arcs # sum( # sum(m.var_delta_arc_inv_glljh[(g,l,l_diamond,j,h)] @@ -2905,7 +2905,7 @@ def create_model(name: str, # if j not in m.set_J_int[(g,l,l_diamond)] # interfaced # ) if (g,l,l_diamond) in m.set_J else 0 # + - # #********************************************************** + # # ********************************************************* # # individual non-interfaced undirected arcs, reverse dir. # sum( # sum(m.var_delta_arc_inv_glljh[(g,l_diamond,l,j,h)] @@ -2916,30 +2916,30 @@ def create_model(name: str, # if j not in m.set_J_int[(g,l_diamond,l)] # interfaced # ) if (g,l_diamond,l) in m.set_J_und else 0 # + - # #********************************************************** + # # ********************************************************* # # preselected outgonig arcs, nominal direction # len(m.set_J_pre[(g,l,l_diamond)] # ) if (g,l,l_diamond) in m.set_J_pre else 0 # + - # #********************************************************** + # # ********************************************************* # # mandatory outgoing arcs, nominal direction # len(m.set_J_mdt[(g,l,l_diamond)] # ) if (g,l,l_diamond) in m.set_J_mdt else 0 # + - # #********************************************************** + # # ********************************************************* # # undirected preselected arcs, reverse direction # sum(1 # for j in m.set_J_pre[(g,l_diamond,l)] # if j in m.set_J_und[(g,l_diamond,l)] # ) if (g,l_diamond,l) in m.set_J_pre else 0 # + - # #********************************************************** + # # ********************************************************* # # undirected mandatory arcs, reverse direction # sum(1 # for j in m.set_J_mdt[(g,l_diamond,l)] # if j in m.set_J_und[(g,l_diamond,l)] # ) if (g,l_diamond,l) in m.set_J_mdt else 0 - # #********************************************************** + # # ********************************************************* # for l_diamond in m.set_L[g] # if l_diamond not in m.set_L_imp[g] # if l_diamond != l @@ -2963,7 +2963,7 @@ def create_model(name: str, rule=rule_constr_max_incoming_directed_arcs ) - #************************************************************************** + # ************************************************************************* # def rule_constr_max_outgoing_directed_arcs(m, g, l): @@ -2974,7 +2974,7 @@ def create_model(name: str, # rule=rule_constr_max_outgoing_directed_arcs # ) -# #************************************************************************** +# # ************************************************************************* # # there can only one outgoing arc at most, if there are no incoming arcs @@ -3089,13 +3089,13 @@ def create_model(name: str, # model.set_GL_not_exp_imp, # rule=rule_constr_max_outgoing_arcs) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # arcs - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # maximum flow amplitude per time interval @@ -3139,7 +3139,7 @@ def create_model(name: str, model.set_QK, # once per time interval rule=rule_constr_arc_max_flow_time_rev) - #************************************************************************** + # ************************************************************************* # maximum nominal flow amplitude (applies to both directions) @@ -3156,7 +3156,7 @@ def create_model(name: str, model.set_GLLJ_sgl, # directed and undirected arcs, new only rule=rule_constr_max_nominal_flow_amplitude) - #************************************************************************** + # ************************************************************************* # one option per arc # note: one constraint per (undirected) arc @@ -3183,7 +3183,7 @@ def create_model(name: str, model.set_GLLJ_sgl, # directed and undirected arcs, new only rule=rule_constr_one_arc_option_per_arc) - #************************************************************************** + # ************************************************************************* # # interface equations (unnecessary if the domain is UnitInterval) @@ -3195,7 +3195,7 @@ def create_model(name: str, # model.set_GLLJ_int, # rule=rule_constr_single_arc_interfaces) - #************************************************************************** + # ************************************************************************* # SOS1 constraints for arc selection @@ -3206,7 +3206,7 @@ def create_model(name: str, weights=model.param_arc_inv_sos1_weights_glljh, # key: GLLJH; alue: weight sos=1) - #************************************************************************** + # ************************************************************************* # one flow direction per time interval in undirected preexisting arcs @@ -3240,7 +3240,7 @@ def create_model(name: str, model.set_QK, # once per time interval rule=rule_constr_one_sns_per_time_interval) - #************************************************************************** + # ************************************************************************* # no flow except in the flow direction, for new undirected arcs @@ -3310,7 +3310,7 @@ def create_model(name: str, model.set_QK, # once per time interval rule=rule_constr_no_flow_except_in_sns_rev) - #************************************************************************** + # ************************************************************************* # SOS1 constraints for flow sense determination (undirected arcs) @@ -3322,7 +3322,7 @@ def create_model(name: str, weights=model.param_arc_sns_sos1_weights_glljqk, sos=1) - #************************************************************************** + # ************************************************************************* # static losses @@ -3350,7 +3350,7 @@ def create_model(name: str, model.set_QK, rule=rule_constr_static_losses_existence) - #************************************************************************** + # ************************************************************************* # static losses placed downstream @@ -3426,7 +3426,7 @@ def create_model(name: str, model.set_QK, rule=rule_constr_static_losses_downstream_und_rev) - #************************************************************************** + # ************************************************************************* # static losses modulated by flow sense for pre-existing arcs @@ -3574,13 +3574,13 @@ def create_model(name: str, model.set_QK, rule=rule_constr_static_losses_sense_new_eq2_rev) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # groups of arcs - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # maximum flow amplitude per time interval, for arcs in arc groups @@ -3623,7 +3623,7 @@ def create_model(name: str, model.set_QK, # all time intervals rule=rule_constr_arc_group_max_flow_time_rev) - #************************************************************************** + # ************************************************************************* # maximum nominal flow amplitude for arc groups @@ -3640,7 +3640,7 @@ def create_model(name: str, model.set_T, # all arc groups rule=rule_constr_arc_group_max_nominal_flow_amplitude) - #************************************************************************** + # ************************************************************************* # one option per arc group @@ -3659,7 +3659,7 @@ def create_model(name: str, model.set_T, # all arc groups rule=rule_constr_one_arc_option_per_arc_group) - #************************************************************************** + # ************************************************************************* # SOS1 constraints for arc group selection @@ -3670,7 +3670,7 @@ def create_model(name: str, weights=model.param_arc_inv_sos1_weights_th, # key: TH; value: weight sos=1) - #************************************************************************** + # ************************************************************************* # one flow direction per time interval, for undirected arcs within groups @@ -3702,7 +3702,7 @@ def create_model(name: str, model.set_QK, # once per time interval rule=rule_constr_one_sns_per_time_interval_group) - #************************************************************************** + # ************************************************************************* # no flow except in the flow direction, for new undirected arcs in groups @@ -3754,7 +3754,7 @@ def create_model(name: str, model.set_QK, # once per time interval rule=rule_constr_no_flow_except_in_sns_group_rev) - #************************************************************************** + # ************************************************************************* # interface equations for arc groups @@ -3768,13 +3768,13 @@ def create_model(name: str, model.set_T_int, rule=rule_constr_arc_group_interfaces) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # converters - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # input signal limits for dimensionable inputs @@ -3859,7 +3859,7 @@ def create_model(name: str, model.set_QK, rule=rule_constr_u_bin_limits) - #************************************************************************** + # ************************************************************************* # outputs @@ -3941,7 +3941,7 @@ def create_model(name: str, model.set_IR_dim_eq, rule=rule_constr_y_amp_pos_neg_match) - #************************************************************************** + # ************************************************************************* # states @@ -4029,16 +4029,16 @@ def create_model(name: str, model.set_IN_dim_eq, rule=rule_constr_x_amp_pos_neg_match) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* return model - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* -#****************************************************************************** -#****************************************************************************** -#****************************************************************************** -#****************************************************************************** -#****************************************************************************** \ No newline at end of file +# ***************************************************************************** +# ***************************************************************************** +# ***************************************************************************** +# ***************************************************************************** +# ***************************************************************************** \ No newline at end of file diff --git a/src/topupopt/problems/esipp/network.py b/src/topupopt/problems/esipp/network.py index 6ce4c2a86f5fac077b9502fadbc958d07bc30225..cfbb71dc1efd698a623895ef3709d2c89e73e28b 100644 --- a/src/topupopt/problems/esipp/network.py +++ b/src/topupopt/problems/esipp/network.py @@ -17,9 +17,10 @@ from math import inf import networkx as nx from ...data.gis.identify import find_unconnected_nodes +from .resource import are_prices_time_invariant -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** class Arcs: """A class for arc technologies in a network.""" @@ -63,16 +64,16 @@ class Arcs: Arcs.validate(self) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def number_options(self): "Return the number of arc options." return len(self.minimum_cost) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def is_isotropic(self, reverse_none_means_isotropic: bool = True) -> bool: @@ -128,24 +129,24 @@ class Arcs: return True - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def has_been_selected(self) -> bool: """Returns True if an option has been selected and False otherwise.""" return True in self.options_selected - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def is_infinite_capacity(self) -> bool: """Returns True if there is one capacity and it is infinite.""" return len(self.capacity) == 1 and self.capacity == (inf,) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def has_proportional_losses(self) -> bool: """Returns False if the efficiency is always one, otherwise True.""" @@ -188,8 +189,8 @@ class Arcs: tuple(1 for _ in range(len(self.efficiency_reverse))) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def has_static_losses(self) -> bool: """Returns False if the static losses are always zero, otherwise True.""" @@ -209,8 +210,8 @@ class Arcs: tuple(0 for _ in range(len(self.static_loss))) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def validate_sizes(self, number_options: int, @@ -260,12 +261,12 @@ class Arcs: 'of options, scenarios and intervals.' ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def validate(self): - #********************************************************************** + # ********************************************************************* # check the types @@ -354,7 +355,7 @@ class Arcs: 'The information about capacities being instantaneous or not'+ ' should be given as a boolean variable.') - #********************************************************************** + # ********************************************************************* # the number of techs. in capacity and minimum_cost should be the same @@ -371,7 +372,7 @@ class Arcs: 'No capacity and minimum cost values were provided. At le' +'ast one option should be provided.') - #********************************************************************** + # ********************************************************************* # if efficiency is not None (i.e., the arc has proportional losses) @@ -519,14 +520,14 @@ class Arcs: raise ValueError( 'Minimum cost values must be positive or zero.') - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** class ArcsWithoutProportionalLosses(Arcs): """A class for arc technologies without proportional losses.""" @@ -553,8 +554,8 @@ class ArcsWithoutProportionalLosses(Arcs): validate=validate ) -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** class ArcsWithoutStaticLosses(Arcs): """A class for arc technologies without static losses.""" @@ -582,8 +583,8 @@ class ArcsWithoutStaticLosses(Arcs): validate=validate ) -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** class ArcsWithoutLosses(Arcs): """A class for arc technologies without losses.""" @@ -609,8 +610,8 @@ class ArcsWithoutLosses(Arcs): validate=validate ) -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** class Network(nx.MultiDiGraph): @@ -674,49 +675,51 @@ class Network(nx.MultiDiGraph): self.nodes_wo_in_dir_arc_limitations = [] self.nodes_wo_out_dir_arc_limitations = [] - - #************************************************************************** - #************************************************************************** + + # ************************************************************************* + # ************************************************************************* # add a new import node def add_import_node(self, node_key, - prices: dict, # ResourcePrice, - prices_are_time_invariant: bool = False): + prices: dict): node_dict = { self.KEY_NODE_TYPE: self.KEY_NODE_TYPE_IMP, self.KEY_NODE_PRICES: prices, - self.KEY_NODE_PRICES_TIME_INVARIANT: prices_are_time_invariant + self.KEY_NODE_PRICES_TIME_INVARIANT: ( + are_prices_time_invariant(prices) + ) } self.add_node( node_key, **node_dict) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # add a new export node def add_export_node(self, node_key, - prices: dict, # ResourcePrice, - prices_are_time_invariant: bool = False): + prices: dict): node_dict = { self.KEY_NODE_TYPE: self.KEY_NODE_TYPE_EXP, self.KEY_NODE_PRICES: prices, - self.KEY_NODE_PRICES_TIME_INVARIANT: prices_are_time_invariant + self.KEY_NODE_PRICES_TIME_INVARIANT: ( + are_prices_time_invariant(prices) + ) } self.add_node( node_key, **node_dict) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # add a new supply/demand node @@ -733,8 +736,8 @@ class Network(nx.MultiDiGraph): node_key, **node_dict) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # add a new waypoint node @@ -749,8 +752,8 @@ class Network(nx.MultiDiGraph): node_key, **node_dict) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # modify an existing network node @@ -843,8 +846,8 @@ class Network(nx.MultiDiGraph): raise ValueError('No such node was found.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # identify importing nodes @@ -858,8 +861,8 @@ class Network(nx.MultiDiGraph): == self.KEY_NODE_TYPE_IMP) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # identify exporting nodes @@ -873,8 +876,8 @@ class Network(nx.MultiDiGraph): == self.KEY_NODE_TYPE_EXP) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # identify waypoint nodes @@ -888,8 +891,8 @@ class Network(nx.MultiDiGraph): == self.KEY_NODE_TYPE_WAY) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # identify source sink nodes @@ -903,8 +906,8 @@ class Network(nx.MultiDiGraph): == self.KEY_NODE_TYPE_SOURCE_SINK) ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # verify if everything is alright @@ -953,8 +956,8 @@ class Network(nx.MultiDiGraph): 'export nodes.' ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* # identify node types @@ -981,8 +984,8 @@ class Network(nx.MultiDiGraph): self.validate() - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def add_directed_arc( self, @@ -1027,8 +1030,8 @@ class Network(nx.MultiDiGraph): self.KEY_ARC_UND: False} ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def add_undirected_arc( self, @@ -1057,8 +1060,8 @@ class Network(nx.MultiDiGraph): self.KEY_ARC_UND: True} ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def get_pseudo_unique_arc_key(self, node_key_start, @@ -1105,8 +1108,8 @@ class Network(nx.MultiDiGraph): iteration += 1 raise ValueError('No unique arc key could be produced.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def modify_network_arc(self, node_key_a, @@ -1125,8 +1128,8 @@ class Network(nx.MultiDiGraph): **data_dict ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def add_infinite_capacity_arc( self, @@ -1183,8 +1186,8 @@ class Network(nx.MultiDiGraph): arcs=arcs ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def add_preexisting_directed_arc( self, @@ -1249,8 +1252,8 @@ class Network(nx.MultiDiGraph): arcs=arcs ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def add_preexisting_undirected_arc( self, @@ -1319,8 +1322,8 @@ class Network(nx.MultiDiGraph): node_key_b=node_key_b, arcs=arcs) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def arc_is_undirected(self, arc_key) -> bool: "Returns True if the arc is undirected and False otherwise." @@ -1341,8 +1344,8 @@ class Network(nx.MultiDiGraph): return False - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def has_tree_topology(self) -> bool: """ @@ -1386,5 +1389,5 @@ class Network(nx.MultiDiGraph): return nx.is_tree(network_view) -#****************************************************************************** -#****************************************************************************** \ No newline at end of file +# ***************************************************************************** +# ***************************************************************************** \ No newline at end of file diff --git a/src/topupopt/problems/esipp/problem.py b/src/topupopt/problems/esipp/problem.py index a85d6f8f87fb38c720c9c91853926871f456d1cc..9f965d1fd3f11878e466ea8a573bf1308adaf2d5 100644 --- a/src/topupopt/problems/esipp/problem.py +++ b/src/topupopt/problems/esipp/problem.py @@ -23,15 +23,17 @@ from ...data.finance.invest import discount_factor from .network import Network, Arcs from .system import EnergySystem + +from .resource import ResourcePrice -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** class InfrastructurePlanningProblem(EnergySystem): """A class for optimisation of infrastructure planning problems.""" - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* SOS1_ARC_WEIGHTS_NONE = None SOS1_ARC_WEIGHTS_COST = 1 @@ -66,8 +68,8 @@ class InfrastructurePlanningProblem(EnergySystem): STATIC_LOSS_MODE_DS ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def __init__(self, name: str, @@ -82,7 +84,7 @@ class InfrastructurePlanningProblem(EnergySystem): prepare_model: bool = True, validate_inputs: bool = True): # TODO: switch to False when everything is more mature - #********************************************************************** + # ********************************************************************* if validate_inputs: @@ -170,7 +172,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.assessment_weights = dict(assessment_weights) - #********************************************************************** + # ********************************************************************* # set the name @@ -182,7 +184,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.optimisation_problem_type = SolverInterface.PROBLEM_MILP - #********************************************************************** + # ********************************************************************* # initialise dynamic systems and networks objects @@ -192,7 +194,7 @@ class InfrastructurePlanningProblem(EnergySystem): converters=converters ) - #********************************************************************** + # ********************************************************************* # modelling options @@ -228,7 +230,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.use_arc_interface_variables = [] - #********************************************************************** + # ********************************************************************* # groups @@ -259,7 +261,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.groups_arc_nnr = {} - #********************************************************************** + # ********************************************************************* # static losses @@ -279,14 +281,14 @@ class InfrastructurePlanningProblem(EnergySystem): self.static_losses_downstream = {} - #********************************************************************** + # ********************************************************************* if prepare_model: self.prepare() - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def _validate_inputs( self, @@ -463,8 +465,8 @@ class InfrastructurePlanningProblem(EnergySystem): number_time_intervals ) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def reset_arc_groups(self): "Clears the dictionaries for the groups of arcs under consideration." @@ -493,8 +495,8 @@ class InfrastructurePlanningProblem(EnergySystem): self.groups_arc_nnr = {} - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def create_arc_group(self, gllj_tuples: tuple, @@ -604,8 +606,8 @@ class InfrastructurePlanningProblem(EnergySystem): return new_t - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def clear_static_loss_configurations(self): """ @@ -625,8 +627,8 @@ class InfrastructurePlanningProblem(EnergySystem): self.static_losses_upstream = {} - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def place_static_losses(self, network_key, arc_key, mode): """ @@ -685,7 +687,7 @@ class InfrastructurePlanningProblem(EnergySystem): return - #************************************************************** + # ************************************************************* # departure node @@ -721,7 +723,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.static_losses_departure_node[ (network_key,*arc_key[0:2])].append(arc_key[2]) - #************************************************************** + # ************************************************************* # arrival node @@ -758,7 +760,7 @@ class InfrastructurePlanningProblem(EnergySystem): (network_key,*arc_key[0:2])].append(arc_key[2]) - #************************************************************** + # ************************************************************* # upstream @@ -794,7 +796,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.static_losses_upstream[ (network_key,*arc_key[0:2])].append(arc_key[2]) - #************************************************************** + # ************************************************************* # downstream @@ -830,7 +832,7 @@ class InfrastructurePlanningProblem(EnergySystem): self.static_losses_downstream[ (network_key,*arc_key[0:2])].append(arc_key[2]) - #************************************************************** + # ************************************************************* else: # the arc does not exist @@ -843,8 +845,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'Either the network key provided is incorrect or the arc key '+ 'lacks the proper size.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def place_static_losses_departure_node(self): """ @@ -914,8 +916,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'import and export nodes. Use an intermediate arc'+ ' to obtain the same outcome.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def place_static_losses_arrival_node(self): """ @@ -987,8 +989,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'import and export nodes. Use an intermediate arc'+ ' to obtain the same outcome.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def place_static_losses_upstream(self): """ @@ -1058,8 +1060,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'import and export nodes. Use an intermediate arc'+ ' to obtain the same outcome.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def place_static_losses_downstream(self): """ @@ -1127,7 +1129,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'import and export nodes. Use an intermediate arc'+ ' to obtain the same outcome.') - # #************************************************************************** + # # ************************************************************************* # def favour_placing_fixed_directed_arc_losses_upstream(self): # """ @@ -1182,12 +1184,12 @@ class InfrastructurePlanningProblem(EnergySystem): # (network_key,*arc_key) # ) - # #************************************************************** + # # ************************************************************* - # #****************************************************************** + # # ***************************************************************** - # #************************************************************************** - # #************************************************************************** + # # ************************************************************************* + # # ************************************************************************* # def do_not_place_fixed_losses_downstream(self, network_key, arc_key): # """ @@ -1219,8 +1221,8 @@ class InfrastructurePlanningProblem(EnergySystem): # # if it does not, ignore - # #************************************************************************** - # #************************************************************************** + # # ************************************************************************* + # # ************************************************************************* # def place_fixed_losses_downstream(self, network_key, arc_key): # """ @@ -1297,8 +1299,8 @@ class InfrastructurePlanningProblem(EnergySystem): # 'Either the network key provided is incorrect or the arc key '+ # 'lacks the proper size.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def use_interface_variables_for_arc_selection(self, network_key, @@ -1364,8 +1366,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'Either the network key provided is incorrect or the arc key '+ 'lacks the proper size.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def do_not_use_interface_variables_for_arc_selection(self, network_key, @@ -1395,8 +1397,8 @@ class InfrastructurePlanningProblem(EnergySystem): # if it does not, ignore - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def do_not_use_sos1_for_arc_selection(self, network_key, @@ -1427,8 +1429,8 @@ class InfrastructurePlanningProblem(EnergySystem): # if it does not, ignore - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def do_not_use_sos1_for_flow_sense(self, network_key, @@ -1459,8 +1461,8 @@ class InfrastructurePlanningProblem(EnergySystem): # if it does not, ignore - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def use_sos1_for_flow_senses( self, @@ -1610,8 +1612,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'Either the network key provided is incorrect or the arc key '+ 'lacks the proper size.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def use_sos1_for_arc_selection( self, @@ -1734,8 +1736,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'Either the network key provided is incorrect or the arc key '+ 'lacks the proper size.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def compute_arc_sos1_weights(self, arcs: Arcs, @@ -1787,8 +1789,8 @@ class InfrastructurePlanningProblem(EnergySystem): return sos1_weights - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def prepare(self): """Sets up the problem model with which instances can be built.""" @@ -1797,8 +1799,8 @@ class InfrastructurePlanningProblem(EnergySystem): self.model = create_model(self.name) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def instantiate(self, pyomo_dict: dict = None, @@ -1827,6 +1829,10 @@ class InfrastructurePlanningProblem(EnergySystem): else: + # make pre-instantiation preparations + + + # generate pyomo-ready dictionary pyomo_dict = self.create_pyomo_dictionary( @@ -1837,8 +1843,8 @@ class InfrastructurePlanningProblem(EnergySystem): self.instance = self.model.create_instance(pyomo_dict) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def optimise(self, solver_name: str, @@ -1899,8 +1905,8 @@ class InfrastructurePlanningProblem(EnergySystem): # TODO: reach this statement, perhaps using a small solver budget - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def create_pyomo_dictionary(self, include_ancillary_sets: bool = False) -> dict: @@ -1908,17 +1914,17 @@ class InfrastructurePlanningProblem(EnergySystem): print('creating the dictionary...') - #********************************************************************** - #********************************************************************** - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* # sets - #********************************************************************** - #********************************************************************** - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* # time @@ -1952,7 +1958,7 @@ class InfrastructurePlanningProblem(EnergySystem): set_QPK = [(q,p,k) for (q,p) in set_QP for k in set_K_q[q]] - #********************************************************************** + # ********************************************************************* # set of networks @@ -2029,7 +2035,7 @@ class InfrastructurePlanningProblem(EnergySystem): if l1 != l2 ) - #********************************************************************** + # ********************************************************************* # set of price segments @@ -2037,14 +2043,14 @@ class InfrastructurePlanningProblem(EnergySystem): (g, l, q, p, k): tuple( s for s in range(self.networks[g].nodes[l][ - Network.KEY_NODE_PRICES][(q,p,k)].number_segments) + Network.KEY_NODE_PRICES][(q,p,k)].number_segments()) ) if not self.networks[g].nodes[l][ Network.KEY_NODE_PRICES_TIME_INVARIANT] else tuple( s for s in range(self.networks[g].nodes[l][ Network.KEY_NODE_PRICES][ - (q,p,k)].number_segments) + (q,p,k)].number_segments()) ) # for g in self.networks.keys() # for l in self.networks[g].nodes @@ -2064,7 +2070,7 @@ class InfrastructurePlanningProblem(EnergySystem): set_GLQPKS_imp = tuple(glqpks for glqpks in set_GLQPKS if glqpks[1] in set_L_imp[glqpks[0]]) - #********************************************************************** + # ********************************************************************* # dynamic systems @@ -2076,7 +2082,7 @@ class InfrastructurePlanningProblem(EnergySystem): set_I_new = self.optional_converters # or [key for key in self.optional_converters] - #********************************************************************** + # ********************************************************************* # inputs @@ -2112,7 +2118,7 @@ class InfrastructurePlanningProblem(EnergySystem): converter_key: converter.externality_inducing_inputs for converter_key, converter in self.converters.items()} - #********************************************************************** + # ********************************************************************* # set of outputs @@ -2140,7 +2146,7 @@ class InfrastructurePlanningProblem(EnergySystem): converter_key: converter.externality_inducing_outputs for converter_key, converter in self.converters.items()} - #********************************************************************** + # ********************************************************************* # states @@ -2172,7 +2178,7 @@ class InfrastructurePlanningProblem(EnergySystem): converter_key: converter.externality_inducing_states for converter_key, converter in self.converters.items()} - #********************************************************************** + # ********************************************************************* # arcs @@ -2352,17 +2358,28 @@ class InfrastructurePlanningProblem(EnergySystem): # TODO: replace this with validation rule in the pyomo model, asap - for (g,u,v) in set_J_stt: + for u in self.networks[g].import_nodes: - for j in set_J_stt[(g,u,v)]: + for v in self.networks[g].export_nodes: - if (u in self.networks[g].import_nodes and - v in self.networks[g].export_nodes): + if (g, u, v) in set_J_stt and len(set_J_stt[(g,u,v)]) != 0: raise ValueError( 'There cannot be arcs with fixed losses between import' +' and export nodes.' ) + + # for (g,u,v) in set_J_stt: + + # for j in set_J_stt[(g,u,v)]: + + # if (u in self.networks[g].import_nodes and + # v in self.networks[g].export_nodes): + + # raise ValueError( + # 'There cannot be arcs with fixed losses between import' + # +' and export nodes.' + # ) # directed arcs: # 1) in the start node (also upstream) @@ -2522,9 +2539,9 @@ class InfrastructurePlanningProblem(EnergySystem): if j not in set_J_col[(g, u, v)] # individual } - #********************************************************************** - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* # ancillary sets @@ -2588,8 +2605,8 @@ class InfrastructurePlanningProblem(EnergySystem): tuple((g,v,u,j) for (g,u,v,j) in set_GLLJ_static_und_red) ) - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # set of arc groups @@ -2631,22 +2648,22 @@ class InfrastructurePlanningProblem(EnergySystem): for t in set_T } - #********************************************************************** - #********************************************************************** - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* # parameters - #********************************************************************** - #********************************************************************** - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* # objective function - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # assessment weight @@ -2708,17 +2725,17 @@ class InfrastructurePlanningProblem(EnergySystem): ) } - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # converters - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # objective function - #********************************************************************** + # ********************************************************************* # minimum cost @@ -2784,11 +2801,11 @@ class InfrastructurePlanningProblem(EnergySystem): for (q,k) in set_QK } - #********************************************************************** + # ********************************************************************* # inputs - #********************************************************************** + # ********************************************************************* # upper bound for input signals @@ -2826,11 +2843,11 @@ class InfrastructurePlanningProblem(EnergySystem): # param_f_amp_u_imqk = {} - #********************************************************************** + # ********************************************************************* # outputs - #********************************************************************** + # ********************************************************************* # upper bounds for outputs (default: none) @@ -2940,7 +2957,7 @@ class InfrastructurePlanningProblem(EnergySystem): for (q,k) in set_QK } - #********************************************************************** + # ********************************************************************* # states @@ -3027,13 +3044,13 @@ class InfrastructurePlanningProblem(EnergySystem): # param_f_amp_x_inqk = {} - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # nodes - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # static balance for nodes (default: 0) @@ -3073,13 +3090,13 @@ class InfrastructurePlanningProblem(EnergySystem): # for (grid, node_key, m, k), a_glmk in dynsys.a_glmk.items() } - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # arcs - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # nominal flow amplitude limit adjustment coefficient (1 if the time intervals are regular) @@ -3251,7 +3268,7 @@ class InfrastructurePlanningProblem(EnergySystem): set_GLLJ_sgl = tuple(param_c_arc_var_gllj.keys()) - #********************************************************************** + # ********************************************************************* # sos1 weights for arc selection @@ -3306,8 +3323,8 @@ class InfrastructurePlanningProblem(EnergySystem): param_arc_sns_sos1_weights_glljqk.keys() ) - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* # parameters for groups of arcs @@ -3351,7 +3368,7 @@ class InfrastructurePlanningProblem(EnergySystem): for h in set_H_t[t] } - #********************************************************************** + # ********************************************************************* # fixed arc losses for preselected arcs @@ -3423,22 +3440,22 @@ class InfrastructurePlanningProblem(EnergySystem): set((g,u,v,j,h) for (g,u,v,j,h,q,k) in param_w_new_glljhqk.keys()) ) - #********************************************************************** - #********************************************************************** - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* + # ********************************************************************* # produce a dictionary with the data for the problem data_dict = {None: { - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** # sets - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** # common sets @@ -3480,7 +3497,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'set_GLL': set_GLL, - #****************************************************************** + # ***************************************************************** # tariff @@ -3492,7 +3509,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'set_GLQPKS_imp': set_GLQPKS_imp, - #****************************************************************** + # ***************************************************************** # converter sets @@ -3536,7 +3553,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'set_R_ext': set_R_ext, - #****************************************************************** + # ***************************************************************** # arc related sets @@ -3574,7 +3591,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'set_H_gllj': set_H_gllj, - #****************************************************************** + # ***************************************************************** # groups of arcs @@ -3640,13 +3657,13 @@ class InfrastructurePlanningProblem(EnergySystem): 'param_arc_inv_sos1_weights_th': param_arc_inv_sos1_weights_th, - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** # ancillary sets - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** 'set_GLLJ': set_GLLJ, @@ -3697,13 +3714,13 @@ class InfrastructurePlanningProblem(EnergySystem): # model.set_GLLJ_static_und_pre, # model.set_GLLJ_static_und_new, - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** # parameters - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** # objective function @@ -3717,7 +3734,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'param_v_max_glqpks': param_v_max_glqpks, - #****************************************************************** + # ***************************************************************** # converters @@ -3791,7 +3808,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'param_f_amp_y_irqk': param_f_amp_y_irqk, - #****************************************************************** + # ***************************************************************** # network @@ -3803,7 +3820,7 @@ class InfrastructurePlanningProblem(EnergySystem): 'param_a_nw_glirqk': param_a_nw_glirqk, - #****************************************************************** + # ***************************************************************** # arc parameters @@ -3833,8 +3850,8 @@ class InfrastructurePlanningProblem(EnergySystem): 'param_arc_sns_sos1_weights_glljqk': param_arc_sns_sos1_weights_glljqk, - #****************************************************************** - #****************************************************************** + # ***************************************************************** + # ***************************************************************** }} @@ -3842,8 +3859,8 @@ class InfrastructurePlanningProblem(EnergySystem): return data_dict - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def assert_solver_problem_compatibility(self, solver_name): @@ -3851,8 +3868,8 @@ class InfrastructurePlanningProblem(EnergySystem): solver_name, self.optimisation_problem_type) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def import_results(self, solved_instance: pyo.ConcreteModel = None, @@ -3864,7 +3881,7 @@ class InfrastructurePlanningProblem(EnergySystem): solved_instance = self.instance - #********************************************************************** + # ********************************************************************* # upload investment results, namely the NPV, NCFs and DFs @@ -3892,7 +3909,7 @@ class InfrastructurePlanningProblem(EnergySystem): # Warning('The NPV results are not consistent with the input data.') - #********************************************************************** + # ********************************************************************* # for each arc subject to optimisation @@ -3912,16 +3929,16 @@ class InfrastructurePlanningProblem(EnergySystem): ) ) - #****************************************************************** + # ***************************************************************** # update the object self.networks[g].edges[(l1,l2,j)][ Network.KEY_ARC_TECH].options_selected[h] = decision - #****************************************************************** + # ***************************************************************** - #********************************************************************** + # ********************************************************************* # for each arc group @@ -3942,12 +3959,305 @@ class InfrastructurePlanningProblem(EnergySystem): self.networks[g].edges[(l1,l2,j)][ Network.KEY_ARC_TECH].options_selected[h] = decision - #****************************************************************** + # ***************************************************************** - #********************************************************************** + # ********************************************************************* - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + +# ***************************************************************************** +# ***************************************************************************** + +def simplify_peak_total_problem( + problem: InfrastructurePlanningProblem + ) -> InfrastructurePlanningProblem: + + # ************************************************************************* + # ************************************************************************* + + # check if the simplification is feasible + if not is_peak_total_problem(problem): + # it is not possible to simplify the problem + return problem + + # ************************************************************************* + # ************************************************************************* + + # identify the peak assessment + ref_found = False + for key, net in problem.networks.items(): + for node_key in net.source_sink_nodes: + q_ref, k_ref = sorted( + ((value, key) + for key, value in net.nodes[node_key][ + Network.KEY_NODE_BASE_FLOW + ].items() + ), + reverse=True + )[0][1] + ref_found = True + break + if ref_found: + break + + # ************************************************************************* + # ************************************************************************* + + # define the peak assessment and the peak interval + q_peak = 'peak' + k_peak = 0 + # define the total assessment and the total interval + q_total = 'total' + k_total = 0 + + # ************************************************************************* + # ************************************************************************* + + # create one peak scenario per polarity within each network + # create one total scenario per polarity within each network + for key, net in problem.networks.items(): + # 1) losses in arcs: + # 1.1) sum the static losses for all time intervals (total assessment) + # 1.2) insert the static losses for the peak assessment + # 1.3) remove all static losses but for the peak assessment + # 1.4) insert the static losses for the total assessment + for arc_key in net.edges(keys=True): + # check if the arc has static losses + if net.edges[arc_key][Network.KEY_ARC_TECH].has_static_losses(): + # 1.1) sum the static losses for all time intervals + loss_sum = { + (h, q_total, k_total): sum( + net.edges[arc_key][Network.KEY_ARC_TECH].static_loss[ + (h, q_ref, k) + ] + for k in range(problem.number_time_intervals[q_ref]) + ) + for h in range( + net.edges[arc_key][ + Network.KEY_ARC_TECH + ].number_options() + ) + } + # 1.2) insert the static losses for the peak assessment + net.edges[arc_key][Network.KEY_ARC_TECH].static_loss.update({ + (h, q_peak, k_peak): ( + net.edges[arc_key][Network.KEY_ARC_TECH].static_loss[ + (h, q_ref, k_ref) + ] + ) + for h in range( + net.edges[arc_key][ + Network.KEY_ARC_TECH].number_options() + ) + }) + # 1.3) remove all static losses but for the peak assessment + for hqk in tuple(net.edges[arc_key][ + Network.KEY_ARC_TECH].static_loss): + if hqk[1:] != (q_peak, k_peak): + net.edges[arc_key][ + Network.KEY_ARC_TECH].static_loss.pop(hqk) + # 1.4) insert the static losses for the total assessment + net.edges[arc_key][Network.KEY_ARC_TECH].static_loss.update( + loss_sum) + + # efficiency + # 1.2) insert the efficiencies for the peak and total assessments + # 1.3) remove all efficiencies but for the peak and total assessm. + + if net.edges[arc_key][ + Network.KEY_ARC_TECH + ].has_proportional_losses(): + # peak assessment efficiency + net.edges[arc_key][Network.KEY_ARC_TECH].efficiency.update({ + (q_peak, k_peak): ( + net.edges[arc_key][Network.KEY_ARC_TECH].efficiency[ + (q_ref, k_ref) + ] + ) + }) + # total assessment efficiency + net.edges[arc_key][Network.KEY_ARC_TECH].efficiency.update({ + (q_total, k_total): ( + net.edges[arc_key][Network.KEY_ARC_TECH].efficiency[ + (q_ref, k_ref) + ] + ) + }) + for qk in tuple(net.edges[arc_key][ + Network.KEY_ARC_TECH].efficiency): + if qk != (q_peak, k_peak) and qk != (q_total, k_total): + net.edges[arc_key][ + Network.KEY_ARC_TECH].efficiency.pop(qk) + + # 2) prices in import/export nodes: + # 2.1) determine the price for the total assessment + # 2.2) insert the prices for the peak assessment + # 2.3) remove all prices but those for the peak assessment + # 2.4) insert the prices for the total assessment + for node_key in net.nodes(): + # 2.1) determine the price for the total assessment + # 2.2) insert the prices for the peak assessment + if node_key in net.import_nodes or node_key in net.export_nodes: + # import node: + # - get the current price + # - insert the peak price = 0 + # export node: + # - get the current price + # - insert the peak price = 0 + total_price = { + (q_total, p, k_total): ( + net.nodes[node_key][Network.KEY_NODE_PRICES][ + (q_ref, p, k_ref) + ] + ) + for p in problem.reporting_periods[q_ref] + } + net.nodes[node_key][Network.KEY_NODE_PRICES].update({ + (q_peak, p, k_peak): ResourcePrice(prices=0) + for p in problem.reporting_periods[q_ref] + }) + else: # other nodes + continue + # 2.3) remove all prices but those for the peak assessment + for qpk in tuple( + net.nodes[node_key][Network.KEY_NODE_PRICES].keys() + ): + if qpk[0] != q_peak and qpk[2] != k_peak: + net.nodes[node_key][Network.KEY_NODE_PRICES].pop(qpk) + # 2.4) insert the prices for the total assessment + net.nodes[node_key][Network.KEY_NODE_PRICES].update(total_price) + + # 3) flows in other nodes: + # 3.1) determine the flow volume for the total assessment + # 3.2) insert the prices and base flows for the peak scenario + # 3.3) remove all but the peak scenario and intervals + # 3.4) insert the flow volume for the total assessment + + for node_key in net.source_sink_nodes: + # 3.1) determine the flow volume for the total assessment + total_flow = { + (q_total, k_total): sum( + net.nodes[node_key][Network.KEY_NODE_BASE_FLOW][(q_ref, k)] + for k in range(problem.number_time_intervals[q_ref]) + ) + } + # 3.2) insert the prices and base flows for the peak scenario + net.nodes[node_key][Network.KEY_NODE_BASE_FLOW].update( + {(q_peak, k_peak): net.nodes[node_key][ + Network.KEY_NODE_BASE_FLOW + ][(q_ref, k_ref)] + } + ) + # 3.3) remove all but the peak scenario and intervals + for qk in tuple(net.nodes[node_key][Network.KEY_NODE_BASE_FLOW]): + if qk != (q_peak, k_peak): + net.nodes[node_key][Network.KEY_NODE_BASE_FLOW].pop(qk) + # 3.4) insert the flow volume for the total assessment + net.nodes[node_key][Network.KEY_NODE_BASE_FLOW].update(total_flow) + + # ************************************************************************* + # ************************************************************************* + + # update the assessments, reporting periods, intervals and segments + + # assessments + problem.assessment_keys = (q_peak, q_total) + problem.number_assessments = len(problem.assessment_keys) + + # reporting periods + problem.reporting_periods = { + q: tuple(problem.reporting_periods[q_ref]) + for q in problem.assessment_keys + } + problem.number_reporting_periods = { + q: len(problem.reporting_periods[q]) + for q in problem.assessment_keys + } + + # time intervals + problem.time_intervals = { + q_peak: [problem.time_intervals[q_ref][k_ref]], + q_total: [sum(problem.time_intervals[q_ref])] + } + problem.number_time_intervals = { + q: len(problem.time_intervals[q]) + for q in problem.assessment_keys + } + + # average time interval + problem.average_time_interval = { + q: mean(problem.time_intervals[q]) + for q in problem.assessment_keys + } + # normalised time interval duration + # problem.normalised_time_interval_duration = { + # (q,k): duration/problem.average_time_interval[q] + # for q in problem.assessment_keys + # for k, duration in enumerate(problem.time_intervals[q]) + # } + problem.normalised_time_interval_duration = { + (q_peak, k_peak): 1, + (q_total, k_total): ( + sum(problem.time_intervals[q_total])/ + 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]) + problem.discount_rates.pop(q_ref) + + # f coefficients + + # ************************************************************************* + # ************************************************************************* + + # return the modified problem + return problem + +# ***************************************************************************** +# ***************************************************************************** + +def is_peak_total_problem(problem: InfrastructurePlanningProblem) -> bool: + """Returns True if the problem only concerns peak capacity and volume.""" + + # conditions: + # 1) maximum congestion occurs simultaneously across the network + # - corollary: there are no dynamic behaviours in the network + # - simplifying assumption: no proportional losses in the network + # 2) the time during which maximum congestion occurs can be determined + # 3) energy prices are constant in time and volume + + # check #1 + + # check #2 + # check #3: energy prices are constant in time and volume + for key, net in problem.networks.items(): + # check import nodes + for imp_node_key in net.import_nodes: + # is an import node, check if it is time invariant + if not net.nodes[imp_node_key][ + Network.KEY_NODE_PRICES_TIME_INVARIANT]: + return False # is not time invariant + # it is time invariant, but is it volume invariant? check any qpk + for qpk in net.nodes[imp_node_key][Network.KEY_NODE_PRICES]: + if not net.nodes[imp_node_key][ + Network.KEY_NODE_PRICES][qpk].is_volume_invariant(): + # it is not volume invariant + return False + # if the entries are time invariant, checking one will do + break + + # # check #4: none of the arcs can have proportional losses + # for key, net in problem.networks.items(): + # # check each edge + # for edge_key in net.edges(keys=True): + # if net.edges[edge_key][ + # Network.KEY_ARC_TECH].has_proportional_losses(): + # return False # has proportional losses, return False + return True # all conditions are true -#****************************************************************************** -#****************************************************************************** \ No newline at end of file +# ***************************************************************************** +# ***************************************************************************** \ No newline at end of file diff --git a/src/topupopt/problems/esipp/resource.py b/src/topupopt/problems/esipp/resource.py index 028ab660ce6315715e83c10fe1bbeb6187321aae..039f8874e44384c5d660d1246287c46429fa1655 100644 --- a/src/topupopt/problems/esipp/resource.py +++ b/src/topupopt/problems/esipp/resource.py @@ -1,10 +1,12 @@ -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** from numbers import Real -#****************************************************************************** -#****************************************************************************** +# ***************************************************************************** +# ***************************************************************************** + +# TODO: change name to ResourceTariff class ResourcePrice: """A class for piece-wise linear resource prices in network problems.""" @@ -16,30 +18,27 @@ class ResourcePrice: # - a flag # accepted inputs: - # 1) prices and volumes as dicts - # - keys can be segment indexes or (segment index, time index) tuples - # - values can be numeric and non-negative or None (last segment only) - # - the number of segments is not - # - the number of intervals does not change - # 2) prices and values as lists with matching sizes + # 1) prices and values as lists with matching sizes # - all elements in the lists need to be numeric # - all prices need to be non-negative # - all volumes need to be positive - # 3) price as a list, volume as None + # 2) price as a list, volume as None # - all elements in the list need to be numeric and non-negative - (self.number_segments, + (self._number_segments, self.prices, self.volumes) = self.validate_list( prices, volumes ) + + self._volume_invariant = self.is_volume_invariant() - #********************************************************************** - #********************************************************************** + # ********************************************************************* + # ********************************************************************* - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def validate_list(self, prices, volumes): """Validates the inputs provided in list format.""" @@ -133,7 +132,7 @@ class ResourcePrice: return number_segments, prices, volumes - elif type(volumes) == type(None): + elif type(volumes) == type(None) or isinstance(volumes, Real): # the prices must be numeric and positive @@ -156,17 +155,14 @@ class ResourcePrice: # done - return number_segments, [prices], [None] + return number_segments, [prices], [volumes] else: - raise TypeError( - 'The volumes should be provided as list or None when the '+ - 'prices are provided as a list.' - ) + raise TypeError('Unrecognised type for volumes.') - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def price_monotonically_increasing_with_volume(self) -> bool: @@ -174,13 +170,13 @@ class ResourcePrice: # if there is only one segment, return false - if self.number_segments == 1: + if self._number_segments == 1: return True # check one interval: - for segment_index in range(self.number_segments-1): + for segment_index in range(self._number_segments-1): # check consecutive segments @@ -194,8 +190,8 @@ class ResourcePrice: return True - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def price_monotonically_decreasing_with_volume(self) -> bool: @@ -203,13 +199,13 @@ class ResourcePrice: # if there is only one segment, return false - if self.number_segments == 1: + if self._number_segments == 1: return True # check one interval: - for segment_index in range(self.number_segments-1): + for segment_index in range(self._number_segments-1): # check consecutive segments @@ -223,15 +219,79 @@ class ResourcePrice: return True - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* def is_volume_capped(self) -> bool: return not (type(self.volumes[-1]) == type(None)) - #************************************************************************** - #************************************************************************** + # ************************************************************************* + # ************************************************************************* + + def is_volume_invariant(self) -> bool: + """Returns True if the prices are the same regardless of the volume.""" + # is a list + if len(set(self.prices)) == 1: + return True # a list with the same price >> volume invariant + else: + return False # a list with more than 1 price >> volume sensitive + + # ************************************************************************* + # ************************************************************************* + + def number_segments(self, redo: bool = False) -> int: + """Returns the number of price segments.""" + if hasattr(self, "_number_segments") and not redo: + return self._number_segments + return len(self.prices) + + # ************************************************************************* + # ************************************************************************* + + def is_equivalent(self, other) -> bool: + """Returns True if a given ResourcePrice is equivalent to another.""" + # resources are equivalent if: + # 1) the prices are the same + # 2) the volume limits are the same + + # the number of segments has to match + if self.number_segments() != other.number_segments(): + return False # the number of segments do not match + # check the prices + if self.prices != other.prices: + return False # prices are different + # prices match, check the volumes + if self.volumes != other.volumes: + return False # volumes are different + return True # all conditions have been met + + # ************************************************************************* + # ************************************************************************* -#****************************************************************************** -#****************************************************************************** \ No newline at end of file +# ***************************************************************************** +# ***************************************************************************** + +def are_prices_time_invariant(resource_prices_qpk: dict) -> bool: + """Returns True if all prices are identical per time interval.""" + # check if there is only one or no (q,p,k) entry + if len(resource_prices_qpk) <= 1: + return True # only one or no entry = is time invariant + # check if the entries for the same period and assessment are time invariant + entries_qp = set([qpk[0:2] for qpk in resource_prices_qpk]) + qpk_qp = { + qp: [qpk for qpk in resource_prices_qpk if qp == qpk[0:2]] + for qp in entries_qp + } + # check if the tariffs per period and assessment are equivalent + for qp, qpk_list in qpk_qp.items(): + for i in range(len(qpk_list)-1): + if not resource_prices_qpk[qpk_list[0]].is_equivalent( + resource_prices_qpk[qpk_list[i+1]] + ): + return False + # all tariffs are equivalent per period and assessment: they are invariant + return True + +# ***************************************************************************** +# ***************************************************************************** \ No newline at end of file diff --git a/tests/examples_esipp_network.py b/tests/examples_esipp_network.py index bb790cf6e3d11242acc7411e57e313b32e023d05..e1ab98fa8a4b5e69ed5466ef3410a5814383e677 100644 --- a/tests/examples_esipp_network.py +++ b/tests/examples_esipp_network.py @@ -1266,7 +1266,7 @@ def examples_arc_technologies(): volumes=[ *[random.random() for k in range(number_time_intervals-1)], None] ) - net.add_import_node(node_key='G', prices=imp_resource_price) + net.add_import_node(node_key='G', prices={(0,0,0): imp_resource_price}) # add export node @@ -1276,7 +1276,7 @@ def examples_arc_technologies(): volumes=[ *[random.random() for k in range(number_time_intervals-1)], None] ) - net.add_export_node(node_key='H', prices=exp_resource_price) + net.add_export_node(node_key='H', prices={(0,0,0): exp_resource_price}) net.add_waypoint_node(node_key='Z') @@ -1401,22 +1401,22 @@ def examples_modifying_nodes(): # add isolated import node net.add_import_node(node_key='I_iso', - prices=resource_price) + prices={(0,0,0): resource_price}) # add import node with outgoing arcs net.add_import_node(node_key='I', - prices=resource_price) + prices={(0,0,0): resource_price}) # add isolated export node net.add_import_node(node_key='E_iso', - prices=resource_price) + prices={(0,0,0): resource_price}) # add export node with incoming arcs net.add_export_node(node_key='E', - prices=resource_price) + prices={(0,0,0): resource_price}) # add isolated normal node @@ -1844,12 +1844,12 @@ def examples_network_disallowed_cases(): # add import node I net.add_import_node(node_key='I', - prices=resource_price) + prices={(0,0,0): resource_price}) # add export node E net.add_export_node(node_key='E', - prices=resource_price) + prices={(0,0,0): resource_price}) # add regular node A @@ -1941,7 +1941,7 @@ def examples_network_disallowed_cases(): # create a new export node net.add_export_node(node_key='E1', - prices=resource_price) + prices={(0,0,0): resource_price}) # create an arc starting in that export node @@ -1964,7 +1964,7 @@ def examples_network_disallowed_cases(): # create a new import node net.add_import_node(node_key='I1', - prices=resource_price) + prices={(0,0,0): resource_price}) # create an arc ending in that import node diff --git a/tests/examples_esipp_resource.py b/tests/examples_esipp_resource.py deleted file mode 100644 index e45105e81c047c0043de591ec00b23deb171cb36..0000000000000000000000000000000000000000 --- a/tests/examples_esipp_resource.py +++ /dev/null @@ -1,357 +0,0 @@ -# imports - -import random - -from src.topupopt.problems.esipp.resource import ResourcePrice - -#****************************************************************************** -#****************************************************************************** - -def examples(): - - # test ResourcePrice objects created with lists - - example_resource_prices_lists() - -#****************************************************************************** -#****************************************************************************** - -def example_resource_prices_lists(): - - #************************************************************************** - - # aspects that were tested: - # i) number of segments (1, multiple or none) - # ii) price variations (increasing, decreasing and stable) - # iii) volume limits - - #************************************************************************** - - # 1) multiple segments, prices increase, volume limits - # 2) multiple segments, prices decrease, volume limits - # 3) multiple segments, prices are stable, volume limits - # 4) multiple segments, prices increase, no volume limit - # 5) multiple segments, prices decrease, no volume limit - # 6) multiple segments, prices are stable, no volume limit - # 7) one segment, prices are stable, volume limits - # 8) one segment, prices are stable, no volume limit - - #************************************************************************** - - # 1) multiple segments, prices increase, volume limits - - prices = [1,2,3] - - volumes = [2,1,3] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 3 - - assert res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert not res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 2) multiple segments, prices decrease, volume limits - - prices = [3,2,1] - - volumes = [2,1,3] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 3 - - assert res_p.is_volume_capped() - - assert not res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 3) multiple segments, prices are stable, volume limits - - prices = [2,2,2] - - volumes = [2,1,3] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 3 - - assert res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 4) multiple segments, prices increase, no volume limit - - prices = [1,2,3] - - volumes = [2,1,None] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 3 - - assert not res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert not res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 5) multiple segments, prices decrease, no volume limit - - prices = [2,2,2] - - volumes = [2,1,None] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 3 - - assert not res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 6) multiple segments, prices are stable, no volume limit - - prices = [2,2,2] - - volumes = [2,1,None] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 3 - - assert not res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 7) one segment, prices are stable, volume limits - - prices = [2] - - volumes = [2] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 1 - - assert res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - - # 8) one segment, prices are stable, no volume limit - - prices = [3] - - volumes = [None] - - res_p = ResourcePrice(prices=prices, volumes=volumes) - - assert res_p.number_segments == 1 - - assert not res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - res_p = ResourcePrice(prices=prices[0], volumes=volumes[0]) - - assert res_p.number_segments == 1 - - assert not res_p.is_volume_capped() - - assert res_p.price_monotonically_increasing_with_volume() - - assert res_p.price_monotonically_decreasing_with_volume() - - #************************************************************************** - #************************************************************************** - - # errors - - #************************************************************************** - - # create object without prices - - error_triggered = False - - try: - _ = ResourcePrice(prices=None, - volumes=volumes) - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with negative prices in lists - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,-3,2], - volumes=[3,4,5]) - except ValueError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object where an intermediate segment has no volume limit - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,4,2], - volumes=[3,None,5]) - except ValueError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with negative volumes in lists - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,3,2], - volumes=[4,-1,2]) - except ValueError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with non-numeric prices in lists - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,'4',2], - volumes=[3,4,5]) - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with non-numeric volumes in lists - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,3,2], - volumes=[4,'3',2]) - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with mismatched price and volume lists - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,3,2], - volumes=[5,7]) - except ValueError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with a price list as an input and an unsupported type - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,3,2], - volumes='hello') - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with negative prices in lists (no volumes are provided) - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,3,-2], - volumes=None) - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with non-numeric prices in lists (no volumes are provided) - - error_triggered = False - - try: - _ = ResourcePrice(prices=[7,3,'a'], - volumes=None) - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with non-numeric prices in lists (no volumes are provided) - - error_triggered = False - - try: - _ = ResourcePrice(prices=5, - volumes=[7,3,4]) - except TypeError: - error_triggered = True - assert error_triggered - - #************************************************************************** - - # create object with negative prices - - error_triggered = False - - try: - _ = ResourcePrice(prices=-3, - volumes=None) - except ValueError: - error_triggered = True - assert error_triggered - - #************************************************************************** - -#****************************************************************************** -#****************************************************************************** \ No newline at end of file diff --git a/tests/test_all.py b/tests/test_all.py index 1a028d48ab290b807c39c04046b706646a926af9..ca908e5e1ecbd574540e923f18403b83dd8e1e98 100644 --- a/tests/test_all.py +++ b/tests/test_all.py @@ -10,7 +10,6 @@ from examples_converter import examples as examples_converter from examples_dynsys import examples as examples_dynsys from examples_esipp_network import examples as examples_esipp_network from examples_esipp_problem import examples as examples_esipp_problem -from examples_esipp_resource import examples as examples_esipp_resource from examples_esipp import examples as examples_esipp from examples_signal import examples as examples_signal @@ -33,9 +32,6 @@ def test_suite(): test_examples_esipp_problem = True # test_examples_esipp_problem = False - test_examples_esipp_resource = True - # test_examples_esipp_resource = False - test_examples_esipp = True # test_examples_esipp = False @@ -46,7 +42,6 @@ def test_suite(): # test_examples_dynsys = True # test_examples_esipp_network = True # test_examples_esipp_problem = True - # test_examples_esipp_resource = True # test_examples_esipp = True # test_examples_gis = True # test_examples_signal = True @@ -194,18 +189,6 @@ def test_suite(): #************************************************************************** - # esipp-resource - - if test_examples_esipp_resource: - - print('\'esipp-resource\': testing about to start...') - - examples_esipp_resource() - - print('\'esipp-resource\': testing complete.') - - #************************************************************************** - # esipp if test_examples_esipp: diff --git a/tests/test_esipp_problem.py b/tests/test_esipp_problem.py new file mode 100644 index 0000000000000000000000000000000000000000..26d15f9244d2a9dfa4e69297dc550aa809e19b57 --- /dev/null +++ b/tests/test_esipp_problem.py @@ -0,0 +1,607 @@ +# imports + +# standard + +import math + +from statistics import mean + +# 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 + +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 + +# ***************************************************************************** +# ***************************************************************************** + +class TestESIPPProblem: + + def build_solve_ipp( + self, + solver: str = 'glpk', + solver_options: dict = None, + use_sos_arcs: bool = False, + arc_sos_weight_key: str = ( + InfrastructurePlanningProblem.SOS1_ARC_WEIGHTS_NONE), + arc_use_real_variables_if_possible: bool = False, + use_sos_sense: bool = False, + sense_sos_weight_key: int = ( + InfrastructurePlanningProblem.SOS1_SENSE_WEIGHT_NOMINAL_HIGHER + ), + sense_use_real_variables_if_possible: bool = False, + sense_use_arc_interfaces: bool = False, + perform_analysis: bool = False, + plot_results: bool = False, + print_solver_output: bool = False, + irregular_time_intervals: bool = False, + networks: 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)} + + # 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 + + # one interval twice as long as the average is worth twice + # one interval half as long as the average is worth half + + # time_weights = [ + # [time_period_duration/average_time_interval_duration + # for time_period_duration in intraperiod_time_interval_duration] + # for p in range(number_periods)] + + time_weights = None # nothing yet + + normalised_time_interval_duration = None # nothing yet + + # create problem object + + ipp = InfrastructurePlanningProblem( + name='problem', + 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 + ) + + # add networks and systems + + for netkey, net in networks.items(): + + ipp.add_network(network_key=netkey, network=net) + + # define arcs as mandatory + + if type(mandatory_arcs) == list: + + for full_arc_key in mandatory_arcs: + + ipp.make_arc_mandatory(full_arc_key[0], full_arc_key[1:]) + + # if make_all_arcs_mandatory: + + # for network_key in ipp.networks: + + # for arc_key in ipp.networks[network_key].edges(keys=True): + + # # preexisting arcs are no good + + # if ipp.networks[network_key].edges[arc_key][ + # Network.KEY_ARC_TECH].has_been_selected(): + + # continue + + # ipp.make_arc_mandatory(network_key, arc_key) + + # set up the use of sos for arc selection + + if use_sos_arcs: + + for network_key in ipp.networks: + + for arc_key in ipp.networks[network_key].edges(keys=True): + + if ipp.networks[network_key].edges[arc_key][ + Network.KEY_ARC_TECH].has_been_selected(): + + continue + + ipp.use_sos1_for_arc_selection( + network_key, + arc_key, + use_real_variables_if_possible=( + arc_use_real_variables_if_possible), + sos1_weight_method=arc_sos_weight_key) + + + # set up the use of sos for flow sense determination + + if use_sos_sense: + + for network_key in ipp.networks: + + for arc_key in ipp.networks[network_key].edges(keys=True): + + if not ipp.networks[network_key].edges[arc_key][ + Network.KEY_ARC_UND]: + + continue + + ipp.use_sos1_for_flow_senses( + network_key, + arc_key, + use_real_variables_if_possible=( + sense_use_real_variables_if_possible + ), + use_interface_variables=sense_use_arc_interfaces, + sos1_weight_method=sense_sos_weight_key) + + elif sense_use_arc_interfaces: # set up the use of arc interfaces w/o sos1 + + for network_key in ipp.networks: + + for arc_key in ipp.networks[network_key].edges(keys=True): + + if ipp.networks[network_key].edges[arc_key][ + Network.KEY_ARC_TECH].has_been_selected(): + + continue + + ipp.use_interface_variables_for_arc_selection( + network_key, + arc_key + ) + + # static losses + + if static_losses_mode == ipp.STATIC_LOSS_MODE_ARR: + + ipp.place_static_losses_arrival_node() + + elif static_losses_mode == ipp.STATIC_LOSS_MODE_DEP: + + ipp.place_static_losses_departure_node() + + elif static_losses_mode == ipp.STATIC_LOSS_MODE_US: + + ipp.place_static_losses_upstream() + + elif static_losses_mode == ipp.STATIC_LOSS_MODE_DS: + + ipp.place_static_losses_downstream() + + else: + + raise ValueError('Unknown static loss modelling mode.') + + # ********************************************************************* + + # groups + + if type(arc_groups_dict) != type(None): + + for key in arc_groups_dict: + + ipp.create_arc_group(arc_groups_dict[key]) + + # ********************************************************************* + + # maximum number of parallel arcs + + for key in max_number_parallel_arcs: + + ipp.set_maximum_number_parallel_arcs( + network_key=key[0], + node_a=key[1], + node_b=key[2], + limit=max_number_parallel_arcs[key]) + + # ********************************************************************* + + if simplify_problem: + + ipp = simplify_peak_total_problem(ipp) + + # ********************************************************************* + + # instantiate (disable the default case v-a-v fixed losses) + + # ipp.instantiate(place_fixed_losses_upstream_if_possible=False) + + ipp.instantiate(initialise_ancillary_sets=init_aux_sets) + + # optimise + + ipp.optimise(solver_name=solver, + solver_options=solver_options, + output_options={}, + print_solver_output=print_solver_output) + + # return the problem object + + return ipp + + # ********************************************************************* + # ********************************************************************* + + # ************************************************************************* + # ************************************************************************* + + def test_single_network_single_arc_problem(self): + + # scenario + q = 0 + # time + number_intervals = 3 + # periods + number_periods = 2 + + # 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() + + # 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, + 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 + ) + + 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 the only feasible solution + 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) + + # ************************************************************************* + # ************************************************************************* + + def test_single_network_single_arc_problem_simpler(self): + + # scenario + q = 0 + # time + number_intervals = 3 + # periods + number_periods = 2 + + # 2 nodes: one import, one regular + mynet = Network() + + # import node + # node_IMP = generate_pseudo_unique_key(mynet.nodes()) + node_IMP = 'thatimpnode' + 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()) + node_A = 'thatnodea' + + 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() + + # 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, + 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=True + ) + + assert is_peak_total_problem(ipp) + assert ipp.results['Problem'][0]['Number of constraints'] == 20 + assert ipp.results['Problem'][0]['Number of variables'] == 19 + assert ipp.results['Problem'][0]['Number of nonzeros'] == 36 + + # ********************************************************************* + # ********************************************************************* + + # validation + + # the arc should be installed since it is the only feasible solution + assert True in ipp.networks['mynet'].edges[(node_IMP, node_A, 0)][ + Network.KEY_ARC_TECH].options_selected + + # capex should be four + assert math.isclose(pyo.value(ipp.instance.var_capex), 4.0, 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) + + # TODO: compare model pretty print with expected one + + # from contextlib import redirect_stdout + # import io + + # ipp.instance.constr_imp_flow_cost.pprint() # only one constraint + # f = io.StringIO() + # with redirect_stdout(f): + # # ipp.instance.pprint() # full model + # ipp.instance.constr_imp_flow_cost.pprint() # only one constraint + + # expected_string = r"""constr_imp_flow_cost : Size=4, Index=constr_imp_flow_cost_index, Active=True + # Key : Lower : Body : Upper : Active + # ('mynet', 'thatimpnode', 'peak', 0, 0) : 0.0 : 0*var_if_glqpks[mynet,thatimpnode,peak,0,0,0] - var_ifc_glqpk[mynet,thatimpnode,peak,0,0] : 0.0 : True + # ('mynet', 'thatimpnode', 'peak', 1, 0) : 0.0 : 0*var_if_glqpks[mynet,thatimpnode,peak,1,0,0] - var_ifc_glqpk[mynet,thatimpnode,peak,1,0] : 0.0 : True + # ('mynet', 'thatimpnode', 'total', 0, 0) : 0.0 : var_if_glqpks[mynet,thatimpnode,total,0,0,0] - var_ifc_glqpk[mynet,thatimpnode,total,0,0] : 0.0 : True + # ('mynet', 'thatimpnode', 'total', 1, 0) : 0.0 : var_if_glqpks[mynet,thatimpnode,total,1,0,0] - var_ifc_glqpk[mynet,thatimpnode,total,1,0] : 0.0 : True + # """ + # assert expected_string == f.getvalue() + + + # from contextlib import redirect_stdout + # import io + # f = io.StringIO() + # with redirect_stdout(f): + # print('foobar') + # print(12) + # 12+3 + # print('Got stdout: "{0}"'.format(f.getvalue())) + + + # ********************************************************************* + # ********************************************************************* + +# ***************************************************************************** +# ***************************************************************************** \ No newline at end of file diff --git a/tests/test_esipp_resource.py b/tests/test_esipp_resource.py new file mode 100644 index 0000000000000000000000000000000000000000..642a3e1aa61ecdbde10a04cd7fbaff43b2428fe6 --- /dev/null +++ b/tests/test_esipp_resource.py @@ -0,0 +1,761 @@ +# imports + +from src.topupopt.problems.esipp.resource import ResourcePrice +from src.topupopt.problems.esipp.resource import are_prices_time_invariant + +# ***************************************************************************** +# ***************************************************************************** + +class TestResourcePrice: + + # ************************************************************************* + # ************************************************************************* + + def test_resources_time_invariant(self): + + # single entry + + resource_prices = { + (0, 0, 0): ResourcePrice(prices=1, volumes=None), + } + + assert are_prices_time_invariant(resource_prices) + + # ********************************************************************* + + # single assessment, two periods, same prices for both periods + + resource_prices = { + (0, 0, 0): ResourcePrice(prices=1, volumes=None), + (0, 0, 1): ResourcePrice(prices=1, volumes=None), + (0, 0, 2): ResourcePrice(prices=1, volumes=None), + (0, 1, 0): ResourcePrice(prices=1, volumes=None), + (0, 1, 1): ResourcePrice(prices=1, volumes=None), + (0, 1, 2): ResourcePrice(prices=1, volumes=None), + } + + assert are_prices_time_invariant(resource_prices) + + # ********************************************************************* + + # single assessment, two periods, same prices per period + + resource_prices = { + (0, 0, 0): ResourcePrice(prices=1, volumes=None), + (0, 0, 1): ResourcePrice(prices=1, volumes=None), + (0, 0, 2): ResourcePrice(prices=1, volumes=None), + (0, 1, 0): ResourcePrice(prices=2, volumes=None), + (0, 1, 1): ResourcePrice(prices=2, volumes=None), + (0, 1, 2): ResourcePrice(prices=2, volumes=None), + } + + assert are_prices_time_invariant(resource_prices) + + # ********************************************************************* + + # single assessment, two periods, different prices in a given period + + resource_prices = { + (0, 0, 0): ResourcePrice(prices=1, volumes=None), + (0, 0, 1): ResourcePrice(prices=1, volumes=None), + (0, 0, 2): ResourcePrice(prices=1, volumes=None), + (0, 1, 0): ResourcePrice(prices=2, volumes=None), + (0, 1, 1): ResourcePrice(prices=2.5, volumes=None), + (0, 1, 2): ResourcePrice(prices=2, volumes=None), + } + + assert not are_prices_time_invariant(resource_prices) + + # ********************************************************************* + + # ************************************************************************* + # ************************************************************************* + + def test_resource_prices_reals(self): + + # 1) single segment, no volume limit, real input + + prices = 3 + + volumes = None + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=False) == 1 + + assert res_p.number_segments(redo=True) == 1 + + assert not res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # 2) single segment, volume limit, real input + + prices = 3 + + volumes = 1.5 + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=False) == 1 + + assert res_p.number_segments(redo=True) == 1 + + assert res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # ************************************************************************* + # ************************************************************************* + + def test_equivalence_single(self): + + # ********************************************************************* + # ********************************************************************* + + # single segment + + # ********************************************************************* + # ********************************************************************* + + # no volume limit + + # single segment, no volume limit, different formats + # prices and volumes match = True + + prices = 3 + volumes = None + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=[prices], volumes=[volumes]) + assert res_p1.is_equivalent(res_p2) + assert res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, no volume limit, different formats + # prices do not match = False + + prices = 3 + volumes = None + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=[prices+1], volumes=[volumes]) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, no volume limit, same format + # prices and volumes match = True + + prices = 3 + volumes = None + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert res_p1.is_equivalent(res_p2) + assert res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, no volume limit, same format + # prices do not match = False + + prices = 3 + volumes = None + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices+1, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + # ********************************************************************* + + # with volume limits + + # single segment, volume limit, different formats + # prices and volumes match = True + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=[prices], volumes=[volumes]) + assert res_p1.is_equivalent(res_p2) + assert res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, different formats: False + # prices do not match = False + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=[prices+1], volumes=[volumes]) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, same format + # prices and volumes match = True + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert res_p1.is_equivalent(res_p2) + assert res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, same format: False + # prices do not match = False + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices+1, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, different formats + # volumes do not match = False + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=[prices], volumes=[volumes+1]) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, same format + # volumes do not match = False + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices, volumes=volumes+1) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, different formats + # volumes do not match = False + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=[prices], volumes=[None]) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # single segment, volume limit, same format + # volumes do not match = False + + prices = 3 + volumes = 1 + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices, volumes=None) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + # ********************************************************************* + + # ************************************************************************* + # ************************************************************************* + + def test_equivalence_multiple_segments(self): + + # ********************************************************************* + # ********************************************************************* + + # multiple segments + + # ********************************************************************* + # ********************************************************************* + + # no volume limit + + # two segments, no volume limit, same format + # prices and volumes match = True + + prices = [1,3] + volumes = [1,None] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert res_p1.is_equivalent(res_p2) + assert res_p2.is_equivalent(res_p1) + + # two segments, no volume limit, same format + # prices do not match = False + + prices = [1,3] + volumes = [1,None] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + prices = [2,3] + volumes = [1,None] + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # with volume limits + + # two segments segment, volume limit, same format + # prices and volumes match = True + + prices = [1,3] + volumes = [1,3] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert res_p1.is_equivalent(res_p2) + assert res_p2.is_equivalent(res_p1) + + # two segments, volume limit, same format: False + # prices do not match = False + + prices = [1,3] + volumes = [1,4] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + prices = [1,4] + volumes = [1,4] + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + + # volumes do not match + + # single segment, volume limit, same format + # volumes do not match = False + + prices = [1,3] + volumes = [1,4] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + prices = [1,3] + volumes = [1,5] + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # single segment, volume limit, same format + # volumes do not match = False + + prices = [1,3] + volumes = [1,4] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + prices = [1,3] + volumes = [1,None] + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + # ********************************************************************* + + # different number of segments + + prices = [1,3] + volumes = [1,4] + res_p1 = ResourcePrice(prices=prices, volumes=volumes) + prices = [1,3,5] + volumes = [1,4,None] + res_p2 = ResourcePrice(prices=prices, volumes=volumes) + assert not res_p1.is_equivalent(res_p2) + assert not res_p2.is_equivalent(res_p1) + + # ********************************************************************* + # ********************************************************************* + + # ************************************************************************* + # ************************************************************************* + + def test_resource_prices_lists(self): + + # ********************************************************************* + + # aspects that were tested: + # i) number of segments (1, multiple or none) + # ii) price variations (increasing, decreasing and stable) + # iii) volume limits + + # ********************************************************************* + + # 1) multiple segments, prices increase, volume limits + # 2) multiple segments, prices decrease, volume limits + # 3) multiple segments, prices are stable, volume limits + # 4) multiple segments, prices increase, no volume limit + # 5) multiple segments, prices decrease, no volume limit + # 6) multiple segments, prices are stable, no volume limit + # 7) one segment, prices are stable, volume limits + # 8) one segment, prices are stable, no volume limit + + # ********************************************************************* + + # 1) multiple segments, prices increase, volume limits + + prices = [1,2,3] + + volumes = [2,1,3] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 3 + + assert res_p.number_segments(redo=False) == 3 + + assert res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert not res_p.price_monotonically_decreasing_with_volume() + + assert not res_p.is_volume_invariant() + + # ********************************************************************* + + # 2) multiple segments, prices decrease, volume limits + + prices = [3,2,1] + + volumes = [2,1,3] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=False) == 3 + + assert res_p.is_volume_capped() + + assert not res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert not res_p.is_volume_invariant() + + # ********************************************************************* + + # 3) multiple segments, prices are stable, volume limits + + prices = [2,2,2] + + volumes = [2,1,3] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 3 + + assert res_p.number_segments(redo=False) == 3 + + assert res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # ********************************************************************* + + # 4) multiple segments, prices increase, no volume limit + + prices = [1,2,3] + + volumes = [2,1,None] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 3 + + assert res_p.number_segments(redo=False) == 3 + + assert not res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert not res_p.price_monotonically_decreasing_with_volume() + + assert not res_p.is_volume_invariant() + + # ********************************************************************* + + # 5) multiple segments, prices decrease, no volume limit + + prices = [2,2,2] + + volumes = [2,1,None] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 3 + + assert res_p.number_segments(redo=False) == 3 + + assert not res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # ********************************************************************* + + # 6) multiple segments, prices are stable, no volume limit + + prices = [2,2,2] + + volumes = [2,1,None] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 3 + + assert res_p.number_segments(redo=False) == 3 + + assert not res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # ********************************************************************* + + # 7) one segment, prices are stable, volume limits + + prices = [2] + + volumes = [2] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 1 + + assert res_p.number_segments(redo=False) == 1 + + assert res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # ********************************************************************* + + # 8) one segment, prices are stable, no volume limit + + prices = [3] + + volumes = [None] + + res_p = ResourcePrice(prices=prices, volumes=volumes) + + assert res_p.number_segments(redo=True) == 1 + + assert res_p.number_segments(redo=False) == 1 + + assert not res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + res_p = ResourcePrice(prices=prices[0], volumes=volumes[0]) + + assert res_p.number_segments(redo=True) == 1 + + assert res_p.number_segments(redo=False) == 1 + + assert not res_p.is_volume_capped() + + assert res_p.price_monotonically_increasing_with_volume() + + assert res_p.price_monotonically_decreasing_with_volume() + + assert res_p.is_volume_invariant() + + # ********************************************************************* + + # errors + + # ********************************************************************* + + # create object without prices + + error_triggered = False + + try: + _ = ResourcePrice(prices=None, + volumes=volumes) + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with negative prices in lists + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,-3,2], + volumes=[3,4,5]) + except ValueError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object where an intermediate segment has no volume limit + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,4,2], + volumes=[3,None,5]) + except ValueError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with negative volumes in lists + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,3,2], + volumes=[4,-1,2]) + except ValueError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with non-numeric prices in lists + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,'4',2], + volumes=[3,4,5]) + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with non-numeric volumes in lists + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,3,2], + volumes=[4,'3',2]) + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with mismatched price and volume lists + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,3,2], + volumes=[5,7]) + except ValueError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with a price list as an input and an unsupported type + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,3,2], + volumes='hello') + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with negative prices in lists (no volumes are provided) + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,3,-2], + volumes=None) + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with non-numeric prices in lists (no volumes are provided) + + error_triggered = False + + try: + _ = ResourcePrice(prices=[7,3,'a'], + volumes=None) + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with non-numeric prices in lists (no volumes are provided) + + error_triggered = False + + try: + _ = ResourcePrice(prices=5, + volumes=[7,3,4]) + except TypeError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + + # create object with negative prices + + error_triggered = False + + try: + _ = ResourcePrice(prices=-3, + volumes=None) + except ValueError: + error_triggered = True + assert error_triggered + + # ********************************************************************* + +# ***************************************************************************** +# ***************************************************************************** \ No newline at end of file