Source code for aristopy.component

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ==============================================================================
#    C O M P O N E N T
# ==============================================================================
"""
* File name: component.py
* Last edited: 2020-06-30
* Created by: Stefan Bruche (TU Berlin)

Components are added to an instance of the EnergySystem class.
The Component class holds basic parameters (e.g., DataFrames for variables and
parameters) and methods (e.g., calculate contributions to the objective
function), that are required in all types of components. *aristopy* has five
basic component types that are inheriting the parameters and methods from the
Component class: Source, Sink, Conversion, Bus, Storage.
The Component class itself contains abstract methods and can, therefore, not be
instantiated itself.
"""
import copy
from collections import OrderedDict
from abc import ABCMeta, abstractmethod
import pandas as pd
from numpy import nan
import operator as oper
import pyomo.environ as pyo
import pyomo.network as network
from aristopy import utils, EnergySystem


# In Python even a class is an object. The metaclass is the class of a class.
# A metaclass defines how a class behaves. A class is an instance of metaclass.
# The default metaclass used to construct a class in Python is 'type'.
# The metaclass 'ABCMeta' enables the use of the decorator 'abstractmethod'.
# A class that has a metaclass derived from 'ABCMeta' cannot be instantiated
# unless all of its abstract methods and properties are overridden.
# Hence, the 'Component' class cannot be instantiated itself and the classes
# inheriting from 'Component' must override all abstract methods of the parent.

[docs]class Component(metaclass=ABCMeta): def __init__(self, ensys, name, inlet, outlet, basic_variable, has_existence_binary_var=False, has_operation_binary_var=False, time_series_data=None, scalar_params=None, additional_vars=None, user_expressions=None, capacity=None, capacity_min=None, capacity_max=None, capacity_per_module=None, maximal_module_number=None, capex_per_capacity=0, capex_if_exist=0, opex_per_capacity=0, opex_if_exist=0, opex_operation=0 ): """ Initialize an instance of the Component class. Note that an instance of the class Component itself can not be instantiated since it holds abstract methods. The abstract methods are later overwritten by the child classes to enable instantiation. :param ensys: EnergySystem instance the component is added to. :type ensys: instance of aristopy's EnergySystem class :param name: Unique name of the component or the component group. :type name: str :param inlet: The inlet holds a set of aristopy's Flow instances, transporting commodities, entering the component. The commodities are creating variables inside of the component. The variable name is consistent with the name of the commodity, if not specified differently in the Flow instance. |br| Example: inlet=aristopy.Flow('ABC', var_name='XYZ') => commodity 'ABC' is entering the component, and is represented by a newly an internally added variable with the name 'XYZ'. :type inlet: (list of) instance(s) of aristopy's Flow class, or None :param outlet: See description of keyword argument 'inlet'. :type outlet: (list of) instance(s) of aristopy's Flow class, or None :param basic_variable: Components may have multiple variables, but every component has only one basic variable. It is used to restrict capacities, set operation rates, and calculate CAPEX and OPEX (if available). Usually, the basic variable points to a commodity variable on the inlet or the outlet of the component. In this case, users need to use string inputs 'inlet_variable' or 'outlet_variable', respectively. If any other variable should be used as the basic variable (e.g., added via keyword argument 'additional_vars'), users need to set the variable name directly, as specified during declaration. :type basic_variable: str :param has_existence_binary_var: States if the component has a binary variable that indicates its existence status. If the parameter is set to True, a scalar pyomo binary variable is added to the pyomo block model of the component (name specified in utils-file, default: 'BI_EX'). It can be used to enable minimal component capacities ('capacity_min'), or capacity-independent CAPEX and OPEX ('capex_if_exist', 'opex_if_exist'). |br| *Default: False* :type has_existence_binary_var: bool :param has_operation_binary_var: States if the component has a binary variable that indicates its operational status. If the parameter is set to True, a pyomo binary variable, utilizing the global EnergySystem time set, is added to the pyomo block model of the component (name specified in utils-file, default: 'BI_OP'). It can be used to enable load-depending conversion rates (via 'user_expressions'), or minimal part-loads ('min_load_rel'), and start-up cost ('start_up_cost') (the last two only for Conversion). |br| *Default: False* :type has_operation_binary_var: bool :param time_series_data: Keyword argument for adding time series data to the component by using instances of aristopy’s Series class. This data can be used for manual scripting in the 'user_expressions'. |br| *Default: None* :type time_series_data: (list of) instance(s) of aristopy's Series class, or None :param scalar_params: Keyword argument for adding scalar parameters to the component by using a Python dict {'parameter_name': value}. The scalar parameters can be used for manual scripting in the 'user_expressions'. |br| *Default: None* :type scalar_params: dict, or None :param additional_vars: Keyword argument for adding variables to the component, that are not automatically created, e.g., by attaching Flows to the inlet or outlet. The variables are provided by adding a set of aristopy's Var instances. |br| Example: additional_vars=aristopy.Var('ABC', domain='Reals', ub=42) |br| *Default: None* :type additional_vars: (list of) instance(s) of aristopy's Var class, or None :param user_expressions: Keyword argument for adding expression-like strings, which are converted into pyomo constraints during model declaration. User expressions can be applied for various ends, e.g., for specifying commodity conversion rates, or limiting capacities. The options for mathematical operators are: 'sum', 'sin', 'cos', 'exp', 'log', '==', '>=', '<=', '**', '*', '/', '+', '-', '(', ')'. The expressions can handle variables that are created with standard names (see Globals in utils-file, e.g., CAP), and the variables and parameters added by the user via keyword arguments: 'inlet', 'outlet', 'time_series_data', 'scalar_params', 'additional_vars'. |br| Example: user_expressions=['Q == 0.5*F + 2*BI_OP', 'CAP <= 42'] |br| *Default: None* :type user_expressions: (list of) str, or None :param capacity: States the fixed capacity value of the component, if the component exists. Hence, if the parameter 'has_existence_binary_var' is set to True, the value for 'capacity' is only applied if the component is selected in the optimal design (BI_EX=1). |br| *Default: None* :type capacity: float or int (>=0), or None :param capacity_min: States the minimal component capacity, if the component exists. Hence, if the parameter 'has_existence_binary_var' is set to True, the value for 'capacity_min' is only applied if the component is selected in the optimal design (BI_EX=1). |br| *Default: None* :type capacity_min: float or int (>=0), or None :param capacity_max: States the maximal component capacity. This value introduces an upper bound for the capacity variable. It also serves as an over-estimator and is required if the parameters 'has_existence_binary_var' or 'has_operation_binary_var' is True. |br| *Default: None* :type capacity_max: float or int (>=0), or None :param capacity_per_module: If a component is modular, its capacity can be modeled with parameter 'capacity_per_module'. If a value is introduced, an additional binary variable is created (default name: 'BI_MODULE_EX'), which indicates the existence of each module. |br| *Default: None* :type capacity_per_module: float or int (>=0), or None :param maximal_module_number: This keyword argument works in combination with parameter 'capacity_per_module' and indicates the maximal number of modules to be installed in the optimal design. |br| *Default: None* :type maximal_module_number: int (>0), or None :param capex_per_capacity: Parameter to calculate the capital investment cost associated with the *CAPACITY* of a component. The final value for capacity-related CAPEX is obtained by multiplying 'capex_per_capacity' with the capacity variable value (CAP). |br| *Default: 0* :type capex_per_capacity: float or int (>=0) :param capex_if_exist: Parameter to calculate the capital investment cost associated with the *EXISTENCE* of a component. The final value for existence-related CAPEX is obtained by multiplying 'capex_if_exist' with the binary existence variable value (BI_EX). |br| *Default: 0* :type capex_if_exist: float or int (>=0) :param opex_per_capacity: Parameter to calculate the annual operational cost associated with the *CAPACITY* of a component. The final value for capacity-related OPEX is obtained by multiplying 'opex_per_capacity' with the capacity variable value (CAP). |br| *Default: 0* :type opex_per_capacity: float or int (>=0) :param opex_if_exist: Parameter to calculate the annual operational cost associated with the *EXISTENCE* of a component. The final value for existence-related OPEX is obtained by multiplying 'opex_if_exist' with the binary existence variable value (BI_EX). |br| *Default: 0* :type opex_if_exist: float or int (>=0) :param opex_operation: Parameter to calculate the annual operational cost associated with the *OPERATION* of a component (associated with its basic variable). The final value for operation-related OPEX is obtained by summarizing the products of 'opex_operation' with the time-dependent values of the basic variable. |br| *Default: 0* :type opex_operation: float or int (>=0) """ # Check and set general attributes: if not isinstance(ensys, EnergySystem): raise TypeError('Input "ensys" requires an EnergySystem instance.') self.ensys = ensys assert isinstance(name, str), 'The component "name" should be a string!' self.name = name # might be changed by "add_to_energy_system" self.group_name = name self.number_in_group = 1 # dito self.block = None # pyomo simple Block object self.log = None # Init: Local logger of the component instance # V A R I A B L E S: # ------------------ # Initialize an empty pandas DataFrame to store the component variables. self.variables = pd.DataFrame(index=[ 'domain', 'has_time_set', 'alternative_set', 'init', 'ub', 'lb', 'pyomo']) # Holds a copy of the 'variables' DataFrame for later reset. It is # stored while variables are edited, imported or relaxed (if requested). self.variables_copy = pd.DataFrame(index=[ 'domain', 'has_time_set', 'alternative_set', 'init', 'ub', 'lb']) # Check and set input values for capacities: self.capacity, self.capacity_min, self.capacity_max, \ self.capacity_per_module, self.maximal_module_number = \ utils.check_and_set_capacities( capacity, capacity_min, capacity_max, capacity_per_module, maximal_module_number) # Specify a capacity variable (with standard name -> see: utils.CAP) if # capacities are stated and add it to the 'variables' DataFrame. # Set 'capacity_max' as upper bound for the capacity variable. # Lower bound is only used if component is built (depends on the status # of the existence binary variable if specified) --> see constraints! self.has_capacity_var = True if self.capacity_min is not None or \ self.capacity_max is not None else False if self.has_capacity_var: self.add_var(utils.CAP, has_time_set=False, ub=self.capacity_max) if self.capacity_per_module is not None: # add binary variables for the existence of modules of a component self.add_var(name=utils.BI_MODULE_EX, has_time_set=False, alternative_set=range( 1, self.maximal_module_number+1), # 1 to incl. end domain='Binary') # Add binary variables for existence and operation with standard names # (see: utils.BI_EX, and utils.BI_OP, respectively) if required: self.has_bi_ex = has_existence_binary_var if self.has_bi_ex: self.add_var(name=utils.BI_EX, domain='Binary', has_time_set=False) self.has_bi_op = has_operation_binary_var if self.has_bi_op: self.add_var(name=utils.BI_OP, domain='Binary', has_time_set=True) if self.capacity_max is None and (self.has_bi_op or self.has_bi_ex): raise ValueError('An over-estimator is required if a binary ' 'operation or existence variable is specified. ' 'Hence, please enter a value for "capacity_max"!') # C O M P O N E N T C O N N E C T I O N S: # ------------------------------------------ # Check and add inlet and outlets (list of aristopy Flows) self.inlet = utils.check_and_set_flows(inlet) self.outlet = utils.check_and_set_flows(outlet) # List of component commodities (present at inlet and outlet) self.commodities = [] # init # Dicts for commodities and their respective variable names self.inlet_commod_and_var_names = {} # @inlet: {commod: var_name} self.outlet_commod_and_var_names = {} # @outlet:{commod: var_name} for flow in self.inlet: # Add commodity to the commodities list if not available: if flow.commodity not in self.commodities: self.commodities.append(flow.commodity) # Add variable to variables DataFrame if not available: if flow.var_name not in self.variables: self.add_var(flow.var_name) # Only add commodity it if it is not already declared at inlet: if self.inlet_commod_and_var_names.get(flow.commodity) is None: self.inlet_commod_and_var_names[flow.commodity] = flow.var_name for flow in self.outlet: # Check variable name at outlet has not already been used at inlet if flow.var_name in self.inlet_commod_and_var_names.values(): raise ValueError( 'Commodity "%s" is found at inlet and outlet of ' 'component "%s" with the same variable name "%s". ' 'Please use different variable names for both sides!' % (flow.commodity, name, flow.var_name)) # Add commodity to the commodities list if not available: if flow.commodity not in self.commodities: self.commodities.append(flow.commodity) # Add variable to variables DataFrame if not available: if flow.var_name not in self.variables: self.add_var(flow.var_name) # Only add commodity it if it is not already declared at outlet: if self.outlet_commod_and_var_names.get(flow.commodity) is None: self.outlet_commod_and_var_names[flow.commodity] = flow.var_name # Dictionary used for plotting. Updated during EnergySystem declaration. self.var_connections = {} # {var_name: [connected_arc_names]} # ADDITIONAL VARIABLES can be added by the 'additional_vars' argument # via instances of aristopy's Var class (done after inlet/outlet init, # to raise an error, if variable name already exists). add_var_list = utils.check_add_vars_input(additional_vars) for var in add_var_list: self.add_var(name=var.name, domain=var.domain, has_time_set=var.has_time_set, ub=var.ub, lb=var.lb, alternative_set=var.alternative_set, init=var.init) # Every component has a basic variable. It is used to restrict # capacities, set operation rates and calculate capex and opex. Usually, # the basic variable is referring to a commodity on the inlet or the # outlet of the component. But it can also be set via 'additional_vars'. if basic_variable == 'inlet_variable': self.basic_variable = self.inlet[0].var_name elif basic_variable == 'outlet_variable': self.basic_variable = self.outlet[0].var_name elif basic_variable in self.variables: self.basic_variable = basic_variable else: raise ValueError('Name of the basic variable for component "%s" is ' 'unknown. Please use defaults "inlet_variable" or ' '"outlet_variable" to use the variable name of a ' 'connected Flow, or manually add the variable with' ' the "additional_vars" keyword.' % name) # P A R A M E T E R S: # -------------------- # Initialize an empty pandas DataFrame to store the component parameters self.parameters = pd.DataFrame(index=['tsam_weight', 'has_time_set', 'values', 'full_resolution', 'aggregated']) # Add scalar parameters from dict 'scalar_params' {param_name: value} if scalar_params is not None: utils.check_scalar_params_dict(scalar_params) for key, val in scalar_params.items(): self.add_param(name=key, data=val) # Add time series data from keyword argument 'time_series_data'. # Check that it only holds instances of aristopy's Series class. series_list = utils.check_and_set_time_series_data(time_series_data) for series in series_list: # Make sure time series has correct length and index: data = utils.check_and_convert_time_series(ensys, series.data) self.add_param(series.name, data, series.weighting_factor) # Check and set the input values for the cost parameters and prevent # invalid parameter combinations. self.capex_per_capacity = utils.check_and_set_positive_number( capex_per_capacity, 'capex_per_capacity') self.capex_if_exist = utils.check_and_set_positive_number( capex_if_exist, 'capex_if_exist') self.opex_per_capacity = utils.check_and_set_positive_number( opex_per_capacity, 'opex_per_capacity') self.opex_if_exist = utils.check_and_set_positive_number( opex_if_exist, 'opex_if_exist') self.opex_operation = utils.check_and_set_positive_number( opex_operation, 'opex_operation') if not self.has_capacity_var and ( self.capex_per_capacity > 0 or self.opex_per_capacity > 0): raise ValueError('Make sure there is a capacity restriction (e.g. ' '"capacity", "capacity_max") if you use capacity ' 'related CAPEX or OPEX.') if not self.has_bi_ex and ( self.capex_if_exist > 0 or self.opex_if_exist > 0): raise ValueError('Make sure there is an existence binary variable ' 'if you use existence related CAPEX or OPEX.') # Dictionary to store the contributions of the component to the global # objective function value: self.comp_obj_dict = { 'capex_capacity': 0, 'capex_exist': 0, 'opex_capacity': 0, 'opex_exist': 0, 'opex_operation': 0, 'commodity_cost': 0, 'commodity_revenues': 0, 'start_up_cost': 0} # U S E R E X P R E S S I O N S: # -------------------------------- # Add 'user_expressions' (type: list of strings, or empty list) self.user_expressions = utils.check_and_set_user_expr(user_expressions) # 'user_expression_dict' holds converted expr -> filled in declaration self.user_expressions_dict = {} def __repr__(self): return '<Component: "%s">' % self.name
[docs] def pprint(self): """ Access the pretty print functionality of the pyomo model Block """ if isinstance(self.block, pyo.Block): self.block.pprint()
# ========================================================================== # A D D C O M P O N E N T T O T H E E N E R G Y S Y S T E M # ==========================================================================
[docs] def add_to_energy_system(self, ensys, group, instances_in_group=1): """ Add the component to the specified instance of the EnergySystem class. This implies, creation of copies with unique names if multiple identical components should be instantiated ('instances_in_group' > 1), adding the component to the dictionary 'components' of the EnergySystem, and initializing a Logger instance for each component. *Method is not intended for public access!* :param ensys: EnergySystem instance the component is added to. :param group: Name (str) of the group where the component is added to (corresponds to initialization keyword attribute 'name'). :param instances_in_group: (int > 0) States the number of similar component instances that are simultaneously created and arranged in a group. That means, the user has the possibility to instantiate multiple component instances (only for Conversion!) with identical specifications. These components work independently, but may have an order for their binary existence and/or operation variables (see: 'group_has_existence_order', 'group_has_operation_order'). If a number larger than 1 is provided, the names of the components are extended with integers starting from 1 (e.g., 'conversion_1', ...). |br| *Default: 1* """ # Reset flags if new components are added ensys.is_model_declared, ensys.is_data_clustered = False, False for nbr in range(1, instances_in_group + 1): if instances_in_group > 1: # Create a DEEP-copy of self! Shallow copy creates problems # since the Component object holds complicating elements # (e.g., pandas DataFrames) instance = copy.deepcopy(self) # We always need the reference on the original EnergySystem # instance and not on a newly created copy of it! instance.ensys = self.ensys instance.name = group + '_{}'.format(nbr) # overwrite name instance.number_in_group = nbr # overwrite number_in_group else: instance = self # reference self in 'instance' # If component name already exists raise an error. if instance.name in ensys.components: raise ValueError('Component name "%s" already exists!' % instance.name) else: # Add component with its name and all attributes and methods to # the dictionary 'components' of the EnergySystem instance ensys.components[instance.name] = instance # Define an own Logger instance for every added component instance.log = instance.ensys.logger.get_logger(instance) instance.log.info('Add component "%s" to the EnergySystem' % instance.name)
# ========================================================================== # M A N I P U L A T I O N O F V A R I A B L E S # ==========================================================================
[docs] def add_var(self, name, domain='NonNegativeReals', has_time_set=True, alternative_set=None, ub=None, lb=None, init=None): """ Method for adding a variable to the pandas DataFrame 'variables'. *Method is not intended for public access!* :param name: (str) Name / identifier of the added variable. :param domain: (str) A super-set of the values the variable can take on. Possible values are: 'Reals', 'NonNegativeReals', 'Binary'. |br| *Default: 'NonNegativeReals'* :param has_time_set: (bool) Is True, if the time set of the EnergySystem instance is also a set of the added variable. |br| * Default: True :param alternative_set: Alternative variable sets can be added here via iterable Python objects (e.g. list). |br| *Default: None* :param ub: (float, int, None) Upper variable bound. |br| *Default: None* :param lb: (float, int, None) Lower variable bound. |br| *Default: None* :param init: A function or Python object that provides starting values for the added variable. |br| *Default: None* """ # Make sure the variable name is unique in the Dataframe if name in self.variables: raise ValueError('Variable with name "%s" already found in the ' 'DataFrame. Please use a different name.' % name) # Specify bounds in the DataFrame according to the variable domain if domain == 'NonNegativeReals': lb = 0 elif domain == 'Binary': lb = 0 ub = 1 # Append data to DataFrame series = pd.Series({'has_time_set': has_time_set, 'alternative_set': alternative_set, 'domain': domain, 'init': init, 'ub': ub, 'lb': lb, 'pyomo': None}) self.variables[name] = series
[docs] def store_var_copy(self, name, var): """ Method to store the current specifications of a provided variable in the pandas DataFrame "variables_copy". *Method is not intended for public access!* :param name: (str) Name / identifier of the stored variable. :param var: (pandas Series) Variable data to store. """ series = pd.Series({'has_time_set': var['has_time_set'], 'alternative_set': var['alternative_set'], 'domain': var['domain'], 'init': var['init'], 'ub': var['ub'], 'lb': var['lb']}) # Only store it if it is not already in the DataFrame => otherwise # a more original version might be overwritten unintentionally. if name not in self.variables_copy: self.variables_copy[name] = series
[docs] def relax_integrality(self, include_existence=True, include_modules=True, include_time_dependent=True, store_previous_variables=True): """ Method to relax the integrality of the binary variables. This means binary variables are declared to be 'NonNegativeReals' with an upper bound of 1. This method encompasses the resetting of the DataFrame "variables" and the pyomo variables itself (if already constructed). The relaxation can be performed for the binary existence variable, the module existence binary variables and time-dependent binary variables. :param include_existence: State whether the integrality of the binary existence variables should be relaxed (if available). |br| *Default: True* :type include_existence: bool :param include_modules: State whether the integrality of the binary modules existence variables should be relaxed (if available). |br| *Default: True* :type include_modules: bool :param include_time_dependent: State whether the integrality of the time-dependent binary variables should be relaxed. (if available). |br| *Default: True* :type include_time_dependent: bool :param store_previous_variables: State whether the representation of the variables before applying the relaxation should be stored in the DataFrame "variables_copy". This representation can be used by method "reset_variables" to undo changes. |br| *Default: True* :type store_previous_variables: bool """ # Check the input: utils.check_and_set_bool(include_existence, 'include_existence') utils.check_and_set_bool(include_modules, 'include_modules') utils.check_and_set_bool(include_time_dependent, 'include_time_dependent') utils.check_and_set_bool(store_previous_variables, 'store_previous_variables') def relax_variable(var): # Manipulate "variables" (also possible if model not declared yet) var['domain'] = 'NonNegativeReals' var['ub'] = 1 if self.ensys.is_model_declared: # model / variables are declared domain = getattr(pyo, 'NonNegativeReals') var_pyomo = var['pyomo'] if var_pyomo is not None and not var_pyomo.is_indexed(): var_pyomo.domain = domain var_pyomo.setub(1) # additional step required for persistent model if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var(var_pyomo) elif var_pyomo is not None and var_pyomo.is_indexed(): for idx in var_pyomo: var_pyomo[idx].domain = domain var_pyomo[idx].setub(1) # additional step required for persistent model if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var(var_pyomo[idx]) # Loop through the DataFrame "variables" for v in self.variables.columns: # Include the global existence variable: if include_existence and v == utils.BI_EX: if store_previous_variables: self.store_var_copy(v, self.variables[v]) relax_variable(self.variables[v]) # self.log.debug('Relax integrality of variable: "%s"' % v) # Include the module existence variables: elif include_modules and v == utils.BI_MODULE_EX: if store_previous_variables: self.store_var_copy(v, self.variables[v]) relax_variable(self.variables[v]) # self.log.debug('Relax integrality of variable: "%s"' % v) # Include time-dependent variables elif include_time_dependent and self.variables[v]['domain'] \ == 'Binary' and self.variables[v]['has_time_set']: if store_previous_variables: self.store_var_copy(v, self.variables[v]) relax_variable(self.variables[v])
# self.log.debug('Relax integrality of variable: "%s"' % v)
[docs] def edit_variable(self, name, store_previous_variables=True, **kwargs): """ Method on component level for manipulating the specifications of already defined component variables (e.g., change variable domain, add variable bounds, etc.). :param name: Name / identifier of the edited variable. :type name: str :param store_previous_variables: State whether the representation of the variables before applying the edit_variable method should be stored in DataFrame "variables_copy" of each component. This representation can be used by mehtod "reset_variables" to undo changes. |br| *Default: True* :type store_previous_variables: bool :param kwargs: Additional keyword arguments for editing. Options are: 'ub' and 'lb' to add an upper or lower variable bound, 'domain' to set the variable domain, and 'has_time_set' to define if the variable should inherit the global time set of the EnergySystem. """ # Check the method input (done locally because might be called # directly from component). utils.check_edit_var_input(name, store_previous_variables, **kwargs) # Check if the variable with given 'name' is available for editing: if name not in self.variables.columns: self.log.warn('No variable "%s" available for editing' % name) else: self.log.info('Edit variable: "%s"' % name) # Store the previous variable setting if requested if store_previous_variables: self.store_var_copy(name=name, var=self.variables[name]) # Write keyword arguments in "variables" DataFrame for key, val in kwargs.items(): self.variables[name][key] = val # Edit the pyomo variable if it has already been constructed if self.ensys.is_model_declared: var_pyomo = self.variables[name]['pyomo'] edit_domain, edit_lb, edit_ub = False, False, False # init if 'domain' in kwargs.keys(): domain = getattr(pyo, kwargs['domain']) edit_domain = True if 'lb' in kwargs.keys(): lb = kwargs['lb'] edit_lb = True if 'ub' in kwargs.keys(): ub = kwargs['ub'] edit_ub = True if 'has_time_set' in kwargs.keys(): self.log.warn('Note: The model needs to be declared again ' 'if the set of variable should be edited.') if not var_pyomo.is_indexed(): if edit_domain: var_pyomo.domain = domain if edit_lb: var_pyomo.setlb(lb) if edit_ub: var_pyomo.setub(ub) if self.ensys.is_persistent_model_declared and ( edit_domain or edit_lb or edit_ub): self.ensys.solver.update_var(var_pyomo) else: # indexed variable for idx in var_pyomo: if edit_domain: var_pyomo[idx].domain = domain if edit_lb: var_pyomo[idx].setlb(lb) if edit_ub: var_pyomo[idx].setub(ub) if self.ensys.is_persistent_model_declared and ( edit_domain or edit_lb or edit_ub): self.ensys.solver.update_var(var_pyomo[idx])
[docs] def reset_variables(self): """ Method to reset the variables to the state, that is stored in the DataFrame "variables_copy". This includes the resetting of the DataFrame "variables" and the pyomo variables itself (if already constructed). """ # Loop through variables in DataFrame "variables_copy" for var_copy_name in self.variables_copy: # The variable should be declared in the DataFrame "variables" if var_copy_name in self.variables: # Reset the "variables" DataFrame to values in "variables_copy" for attr in ['domain', 'has_time_set', 'alternative_set', 'init', 'ub', 'lb']: self.variables[var_copy_name][attr] = \ self.variables_copy[var_copy_name][attr] # Reset the pyomo variable if it has already been constructed if self.ensys.is_model_declared: var_pyomo = self.variables[var_copy_name]['pyomo'] var_copy_dict = self.variables_copy[var_copy_name] domain = getattr(pyo, var_copy_dict['domain']) # Reset domain & unfix var (works globally on (non)-indexed) var_pyomo.domain = domain var_pyomo.unfix() # Set lower and upper bounds and update persistent solver if not var_pyomo.is_indexed(): var_pyomo.setlb(var_copy_dict['lb']) var_pyomo.setub(var_copy_dict['ub']) if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var(var_pyomo) else: # variable is indexed --> might take a while, especially # if a persistent model needs to be updated idx by idx for idx in var_pyomo: var_pyomo[idx].setlb(var_copy_dict['lb']) var_pyomo[idx].setub(var_copy_dict['ub']) if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var(var_pyomo[idx]) # Delete the variable from the "variables_copy" after resetting self.variables_copy.drop(var_copy_name, axis=1, inplace=True)
# ========================================================================== # E X P O R T AND I M P O R T C O N F I G U R A T I O N # ==========================================================================
[docs] def export_component_configuration(self): """ This method exports the component configuration data (results of the optimization) as a pandas Series. The features are (if exist): * the binary existence variable (utils.BI_EX), * the binary existence variables of modules (utils.BI_MODULE_EX) * the component capacity variable (of main commodity, utils.CAP) :return: The configuration of the modelled component instance. :rtype: pandas Series """ bi_ex_val, bi_mod_ex_val, cap_val = None, None, None if self.has_bi_ex: bi_ex_val = self.variables[utils.BI_EX]['pyomo'].value if self.capacity_per_module is not None: var = self.variables[utils.BI_MODULE_EX]['pyomo'] bi_mod_ex_val = {i: var[i].value for i in range( 1, self.maximal_module_number + 1)} # equiv. to pyomo.RangeSet if self.has_capacity_var: cap_val = self.variables[utils.CAP]['pyomo'].value series = pd.Series({utils.BI_EX: bi_ex_val, utils.BI_MODULE_EX: bi_mod_ex_val, utils.CAP: cap_val}) # Replace numpy NaNs that might occur here and there with None series = series.replace({nan: None}) return series
[docs] def import_component_configuration(self, data, fix_existence=True, fix_modules=True, fix_capacity=True, store_previous_variables=True): """ Method to load a pandas Series with configuration specifications (binary existence variables and capacity variable values). The values are used to fix a specific component configuration (for example from other model runs). :param data: The configuration of the modelled component instance. :type data: pandas Series :param fix_existence: Specify whether the imported (global) binary component existence variable should be fixed (if available). |br| *Default: True* :type fix_existence: bool :param fix_modules: Specify whether the imported binary existence variable of the component modules should be fixed (if available). |br| *Default: True* :type fix_modules: bool :param fix_capacity: Specify whether the imported component capacity variable (of the main commodity) should be fixed or not. |br| *Default: True* :type fix_capacity: bool :param store_previous_variables: State whether the representation of the variables before applying the configuration import should be stored in DataFrame "variables_copy" of each component. This representation can be used by method "reset_variables" to undo changes. |br| *Default: True* :type store_previous_variables: bool """ # Check input: if not isinstance(data, pd.Series): raise TypeError('The data needs to be imported as a pandas Series!') utils.check_and_set_bool(fix_existence, 'fix_existence'), utils.check_and_set_bool(fix_modules, 'fix_modules') utils.check_and_set_bool(fix_capacity, 'fix_capacity'), utils.check_and_set_bool(store_previous_variables, 'store_previous_variables') # Note: Importing seems to work without rounding => if not: round values # EXISTENCE VARIABLE: Fix if required and available in model and data if fix_existence and self.has_bi_ex and data[utils.BI_EX] is not None: # Backup of variables in "variables_copy" if requested for resetting if store_previous_variables: self.store_var_copy(name=utils.BI_EX, var=self.variables[utils.BI_EX]) # Always possible to set bounds (also if not declared) self.variables[utils.BI_EX]['lb'] = data[utils.BI_EX] self.variables[utils.BI_EX]['ub'] = data[utils.BI_EX] if self.ensys.is_model_declared: # model / variables are declared self.variables[utils.BI_EX]['pyomo'].fix(data[utils.BI_EX]) # additional step required for persistent model if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var( self.variables[utils.BI_EX]['pyomo']) # MODULE EXISTENCE VARIABLE: Fix if required and available if fix_modules and self.capacity_per_module is not None \ and data[utils.BI_MODULE_EX] is not None: # Backup of variables in "variables_copy" if requested for resetting if store_previous_variables: self.store_var_copy(name=utils.BI_MODULE_EX, var=self.variables[utils.BI_MODULE_EX]) # Always possible to set bounds (also if not declared) self.variables[utils.BI_MODULE_EX]['lb'] = data[utils.BI_MODULE_EX] self.variables[utils.BI_MODULE_EX]['ub'] = data[utils.BI_MODULE_EX] if self.ensys.is_model_declared: for i in range(1, self.maximal_module_number + 1): self.variables[utils.BI_MODULE_EX]['pyomo'][i].fix( data[utils.BI_MODULE_EX][i]) # additional step required for persistent model if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var( self.variables[utils.BI_MODULE_EX]['pyomo'][i]) # CAPACITY VARIABLE: Fix if required and available in model and data if fix_capacity and self.has_capacity_var \ and data[utils.CAP] is not None: # Backup of variables in "variables_copy" if requested for resetting if store_previous_variables: self.store_var_copy(name=utils.CAP, var=self.variables[utils.CAP]) # Always possible to set bounds (also if not declared) self.variables[utils.CAP]['lb'] = data[utils.CAP] self.variables[utils.CAP]['ub'] = data[utils.CAP] if self.ensys.is_model_declared: # model / variables are declared self.variables[utils.CAP]['pyomo'].fix(data[utils.CAP]) # additional step required for persistent model if self.ensys.is_persistent_model_declared: self.ensys.solver.update_var( self.variables[utils.CAP]['pyomo'])
# ========================================================================== # P A R A M E T E R S AND T I M E S E R I E S # ==========================================================================
[docs] def add_param(self, name, data, tsam_weight=1): """ Method for adding a parameter to the pandas DataFrame 'parameters'. *Method is not intended for public access!* :param name: (str) Name / identifier of the added parameter. :param data: (float, int, list, dict, numpy array, pandas Series) Data. :param tsam_weight: (int or float) Weighting factor to use in case the provided parameter is a series and an aggregation is requested. |br| *Default: 1* """ # Make sure the parameter name is unique in the Dataframe if name in self.parameters: raise ValueError('Parameter with name "%s" already found in the ' 'DataFrame. Please use a different name.' % name) # If input is integer or float, flag 'has_time_set' is set to False. # Else, (input: numpy array, list, dict or pandas series) flag -> True has_time_set = False if isinstance(data, (int, float)) else True # Append data to DataFrame series = pd.Series({'tsam_weight': tsam_weight, 'has_time_set': has_time_set, 'values': data, 'full_resolution': data, 'aggregated': None}) self.parameters[name] = series
[docs] def set_time_series_data(self, use_clustered_data): """ Sets the time series data in the 'parameters' DataFrame of a component depending on whether a calculation with aggregated time series data should be performed or with the original data (without clustering). *Method is not intended for public access!* :param use_clustered_data: Use aggregated data (True), or original data (False). :type use_clustered_data: bool """ for param in self.parameters: param_dict = self.parameters[param] if param_dict['has_time_set']: if use_clustered_data: # Use aggregated data if time series aggr. is requested param_dict['values'] = param_dict['aggregated'] else: # else use full resolution data with reformatted index (p,t) data = param_dict['full_resolution'] idx = pd.MultiIndex.from_product([[0], data.index]) param_dict['values'] = pd.Series(data.values, index=idx)
[docs] def get_time_series_data_for_aggregation(self): """ Collect all time series data and their respective weights and merge them in two dictionaries. The returned dictionaries are used for the time series aggregation in the "cluster" method of the EnergySystem instance. *Method is not intended for public access!* :returns: (dict) time series data, (dict) time series weights """ data, weights = {}, {} for param in self.parameters: if self.parameters[param]['has_time_set']: # is True unique_name = self.group_name + '_' + param data[unique_name] = self.parameters[param]['full_resolution'] weights[unique_name] = self.parameters[param]['tsam_weight'] return data, weights
[docs] def set_aggregated_time_series_data(self, data): """ Store the aggregated time series data in the 'parameters' DataFrame. *Method is not intended for public access!* :param data: (pandas DataFrame) DataFrame with the aggregated time series data of all components. """ # Find time series data of a comp. (identifier starts with group name # and the parameter name is in the parameters DataFrame) for series_name in data: if series_name.startswith(self.group_name) and \ series_name[len(self.group_name)+1:] in self.parameters: # Get the original parameter name by slicing the 'unique_name' param_name = series_name[len(self.group_name)+1:] # Store aggregated time series data in parameters dict of comp. self.parameters[param_name]['aggregated'] = data[series_name]
# ========================================================================== # M O D E L B L O C K D E C L A R A T I O N # ==========================================================================
[docs] def declare_component_model_block(self, model): """ Create a pyomo Block and store it in attribute 'block' of the component. The pyomo Block is the container for all pyomo objects of the component. *Method is not intended for public access!* :param model: Pyomo ConcreteModel of the EnergySystem instance """ setattr(model, self.name, pyo.Block()) self.block = getattr(model, self.name)
# ========================================================================== # V A R I A B L E D E C L A R A T I O N # ==========================================================================
[docs] def declare_component_variables(self, model): """ Create all pyomo variables that are stored in DataFrame 'variables'. *Method is not intended for public access!* :param model: Pyomo ConcreteModel of the EnergySystem instance """ for var_name in self.variables: # Get the pandas series from DataFrame and the variable specs var_dict = self.variables[var_name] domain = getattr(pyo, var_dict['domain']) # If iterable object (dict, or list) in 'lb' and 'ub' field # -> use a lambda function to specify the variable bounds if hasattr(var_dict['lb'], '__iter__') \ and hasattr(var_dict['ub'], '__iter__'): bounds = (lambda m, i: (var_dict['lb'][i], var_dict['ub'][i])) else: bounds = (var_dict['lb'], var_dict['ub']) init = var_dict['init'] # Differentiation between variables with and without time_set if var_dict['has_time_set']: setattr(self.block, var_name, pyo.Var( model.time_set, domain=domain, bounds=bounds, initialize=init)) # e.g. for 'BI_MODULE_EX' or for 'SOC' with 'intra_period_time_set' elif var_dict['alternative_set'] is not None: try: # Try to find the set in the concrete model instance and # use it for the variable declaration alt_set = getattr(model, var_dict['alternative_set']) setattr(self.block, var_name, pyo.Var( alt_set, domain=domain, bounds=bounds, initialize=init)) except AttributeError: # attr. is a string but it is not found self.log.error('Pyomo model does not have the attribute ' '"%s"' % var_dict['alternative_set']) raise except TypeError: # provided attribute is not a string try: # Assume the entry in the dict is the desired set setattr(self.block, var_name, pyo.Var( var_dict['alternative_set'], domain=domain, bounds=bounds, initialize=init)) except Exception: # e.g. "object is not iterable" self.log.error('Something went wrong in the declaration' ' of variable "%s" with the set "%s"' % ( var_name, var_dict['alternative_set'])) raise # built variable without any set else: setattr(self.block, var_name, pyo.Var( domain=domain, bounds=bounds, initialize=init)) # Store variable in self.variables[var_name]['pyomo'] pyomo_var = getattr(self.block, var_name) self.variables[var_name]['pyomo'] = pyomo_var
# ========================================================================== # P O R T D E C L A R A T I O N # ==========================================================================
[docs] def declare_component_ports(self): """ Create all ports from the dictionaries 'inlet_commod_and_var_names' and 'outlet_commod_and_var_names', and assign port variables. Note: The ports are currently always created with extensive behaviour! *Method is not intended for public access!* """ # Create inlet ports for commod, var_name in self.inlet_commod_and_var_names.items(): # Declare port port_name = 'inlet_' + commod setattr(self.block, port_name, network.Port()) # Add variable to port port = getattr(self.block, port_name) port.add(getattr(self.block, var_name), commod, network.Port.Extensive, include_splitfrac=False) # Create outlet ports for commod, var_name in self.outlet_commod_and_var_names.items(): # Declare port port_name = 'outlet_' + commod setattr(self.block, port_name, network.Port()) # Add variable to port port = getattr(self.block, port_name) port.add(getattr(self.block, var_name), commod, network.Port.Extensive, include_splitfrac=False)
# ========================================================================== # U S E R C O N S T R A I N T D E C L A R A T I O N # ==========================================================================
[docs] def declare_component_user_constraints(self, model): """ Create all constraints, that are introduced by manual scripting in the keyword argument 'user_expressions'. *Method is not intended for public access!* :param model: Pyomo ConcreteModel of the EnergySystem instance """ reserved_chars = ['*', '/', '+', '-', '(', ')', '==', '>=', '<=', '**', 'sum', 'sin', 'cos', 'exp', 'log'] for expr_name, expr in self.user_expressions_dict.items(): has_time_dependency = False # init for i, name in enumerate(expr): if name not in reserved_chars: try: # Convert it to float if it is possible without an error expr[i] = float(name) except ValueError: # If it is not a float or a reserved char, it should be # a variable or parameter. Check if the name is # available in DataFrames 'variables' or 'parameters'. if name in self.variables.columns: # write a tuple on position of expression: # Pos 0: The variable (simple or indexed) # Pos 1: The bool value if the var has a time set expr[i] = (self.variables[name].loc['pyomo'], self.variables[name].loc['has_time_set']) # Set flag 'has_time_dependency' to True if at least # one variable in expression has a time dependency if self.variables[name].loc['has_time_set']: has_time_dependency = True elif name in self.parameters.columns: # write a tuple on position of expression: # Pos 0: The parameter (scalar or time series) # Pos 1: The bool value if the param has a time set expr[i] = (self.parameters[name].loc['values'], self.parameters[name].loc['has_time_set']) if self.parameters[name].loc['has_time_set']: has_time_dependency = True else: raise ValueError('Variable or parameter "{}" has ' 'not been declared. Hence, ' 'constructing block "{}" failed.' .format(name, self.name)) # Create an empty DataFrame (with or without index) to store the # expressions that are simplified in the next step df_expr = pd.DataFrame( index=(pd.MultiIndex.from_tuples(model.time_set) if has_time_dependency else pd.MultiIndex.from_tuples([(0, 0)])), columns=range(len(expr))) # Add data from the expression to the new DataFrame for i, name in enumerate(expr): if name in reserved_chars or isinstance(name, float): df_expr[i] = name elif isinstance(name, tuple) and not name[1]: # name is parameter or variable and has no time set df_expr[i] = [name[0] for _ in range(len(df_expr.index))] elif isinstance(name, tuple) and name[1]: # name is parameter or variable and has a time set df_expr[i] = [name[0][j] for j in df_expr.index] # Do the simplification process and get two python dictionaries with # left hand side (lhs) and right hand side (rhs) expressions for the # whole set (time slice) and the operation sign ('==', '>=', '<='). # The simplification function also requires a list with weights for # all time steps. It is applied in case time-dependent variables are # summarized ("sum" operation). Only important if data is clustered. time_step_weights = [self.ensys.period_occurrences[p] for p, _ in model.time_set] lhs, op, rhs, dropped_idx = utils.simplify_user_constraint( df_expr, time_step_weights) # Overwrite the 'has_time_dependency' flag if index was dropped # while constructing the constraint because of 'sum' operation. if has_time_dependency and dropped_idx: has_time_dependency = False # Construct the user constraint using lhs, op, rhs: def built_user_constraint_rule(m, p=None, t=None): # If the constraint has no index (set) lhs and rhs hold only one # entry at index (0, 0) --> "p", "t" are overwritten with zeros p = p if p is not None else 0 t = t if t is not None else 0 # Do the final lhs/rhs operation and return the expression # Note: looping over rows of pandas DataFrames takes much longer if op == '<=': return oper.le(lhs[(p, t)], rhs[(p, t)]) elif op == '==': return oper.eq(lhs[(p, t)], rhs[(p, t)]) elif op == '>=': return oper.ge(lhs[(p, t)], rhs[(p, t)]) # Add user constraint to the component block setattr(self.block, expr_name, pyo.Constraint( model.time_set if has_time_dependency else [None], rule=built_user_constraint_rule))
# ========================================================================== # C O N V E N T I O N A L C O N S T R A I N T D E C L A R A T I O N # ==========================================================================
[docs] @abstractmethod def declare_component_constraints(self, ensys, model): """ Abstract method to create (conventional) component constraints. """ raise NotImplementedError
# ************************************************************************** # Time-independent constraints # **************************************************************************
[docs] def con_couple_bi_ex_and_cap(self): """ Constraint to couple the global existence binary variable with the capacity variable. If the component does not exist, the capacity must take a value of 0. Note: The availability of the required 'capacity_max' parameter is checked during initialization. E.g.: |br| ``CAP <= BI_EX * capacity_max`` *Method is not intended for public access!* """ if self.has_bi_ex: bi_ex = self.variables[utils.BI_EX]['pyomo'] cap = self.variables[utils.CAP]['pyomo'] cap_max = self.capacity_max def con_couple_bi_ex_and_cap(m): return cap <= bi_ex * cap_max setattr(self.block, 'con_couple_bi_ex_and_cap', pyo.Constraint(rule=con_couple_bi_ex_and_cap))
[docs] def con_cap_min(self): """ Constraint to set the minimum capacity of a component (based on its basic variable). If a binary existence variable is declared, the minimal capacity is only enforced if the component exists. E.g.: |br| ``CAP >= capacity_min * BI_EX`` or |br| ``CAP >= capacity_min`` |br| *Method is not intended for public access!* """ if self.capacity_min is not None: cap = self.variables[utils.CAP]['pyomo'] cap_min = self.capacity_min def con_cap_min(m): if self.has_bi_ex: bi_ex = self.variables[utils.BI_EX]['pyomo'] return cap >= cap_min * bi_ex else: return cap >= cap_min setattr(self.block, 'con_cap_min', pyo.Constraint(rule=con_cap_min))
[docs] def con_cap_modular(self): """ Constraint to calculate the nominal capacity of a component from the product of the capacity per module and the number of existing modules. E.g.: |br| ``CAP == capacity_per_module * summation(BI_MODULE_EX)`` *Method is not intended for public access!* """ if self.capacity_per_module is not None: cap = self.variables[utils.CAP]['pyomo'] bi_mod = self.variables[utils.BI_MODULE_EX]['pyomo'] cap_per_mod = self.capacity_per_module def con_cap_modular(m): return cap == cap_per_mod * pyo.summation(bi_mod) setattr(self.block, 'con_cap_modular', pyo.Constraint( rule=con_cap_modular))
[docs] def con_modular_sym_break(self): """ Constraint to state, that the next module can only be built if the previous one already exists (symmetry break constraint for components consisting of multiple modules). E.g.: |br| ``BI_MODULE_EX[2] <= BI_MODULE_EX[1]`` *Method is not intended for public access!* """ if self.capacity_per_module is not None: bi_mod = self.variables[utils.BI_MODULE_EX]['pyomo'] def con_modular_sym_break(m, nr): if nr != 1: return bi_mod[nr] <= bi_mod[nr-1] else: return pyo.Constraint.Skip setattr(self.block, 'con_modular_sym_break', pyo.Constraint(pyo.RangeSet(self.maximal_module_number), rule=con_modular_sym_break))
[docs] def con_couple_existence_and_modular(self): """ Constraint to couple the global existence binary variable with the binary existence status of the first module. All other modules are indirectly coupled via symmetry breaks. E.g.: |br| ``BI_EX >= BI_MODULE_EX[1]`` *Method is not intended for public access!* """ if self.has_bi_ex and self.capacity_per_module is not None: bi_ex = self.variables[utils.BI_EX]['pyomo'] bi_mod = self.variables[utils.BI_MODULE_EX]['pyomo'] def con_couple_existence_and_modular(m): if self.maximal_module_number >= 1: return bi_ex >= bi_mod[1] else: return pyo.Constraint.Skip setattr(self.block, 'con_couple_existence_and_modular', pyo.Constraint(rule=con_couple_existence_and_modular))
# ************************************************************************** # Time-dependent constraints # **************************************************************************
[docs] def con_bi_var_ex_and_op_relation(self, model): """ Constraint to set a relationship between the binary variables for existence and operation. A component can only be operated if it exists. E.g.: |br| ``BI_OP[p, t] <= BI_EX`` *Method is not intended for public access!* """ # Only required if both binary variables are considered if self.has_bi_ex and self.has_bi_op: bi_ex = self.variables[utils.BI_EX]['pyomo'] bi_op = self.variables[utils.BI_OP]['pyomo'] def con_bi_var_ex_and_op_relation(m, p, t): return bi_op[p, t] <= bi_ex setattr(self.block, 'con_bi_var_ex_and_op_relation', pyo.Constraint(model.time_set, rule=con_bi_var_ex_and_op_relation))
[docs] @abstractmethod def con_operation_limit(self, model): """ Constraint to limit the operation (value of the basic variable) of a component (MWh) by its nominal power (MW) multiplied with the number of hours per time step (not for Storage because it is already a capacity!). E.g.: |br| ``Q[p, t] <= CAP * dt`` (Conversion, Sink, Source) |br| ``SOC[p, t] <= CAP`` (Storage) |br| ``Q_IN[p, t] <= CAP * dt`` (Bus) *Method is not intended for public access!* """ raise NotImplementedError
[docs] def con_couple_op_binary_and_basic_var(self, model): """ Constraint to couple the binary operation variable (is available), with the basic variable of the component. Therefore, the "capacity_max" parameter serves for overestimation (BigM). E.g.: |br| ``Q[p, t] <= capacity_max * BI_OP[p, t] * dt`` *Method is not intended for public access!* """ if self.has_bi_op: bi_op = self.variables[utils.BI_OP]['pyomo'] basic_var = self.variables[self.basic_variable]['pyomo'] cap_max = self.capacity_max dt = self.ensys.hours_per_time_step def con_couple_op_binary_and_basic_var(m, p, t): return basic_var[p, t] <= cap_max * bi_op[p, t] * dt setattr(self.block, 'con_couple_op_binary_and_basic_var', pyo.Constraint(model.time_set, rule=con_couple_op_binary_and_basic_var))
# ========================================================================== # O B J E C T I V E F U N C T I O N C O N T R I B U T I O N # ==========================================================================
[docs] def get_objective_function_contribution(self, ensys, model): """ Calculate the objective function contributions of the component and add the values to the component dictionary "comp_obj_dict". *Method is not intended for public access!* :param ensys: Instance of the EnergySystem class :param model: Pyomo ConcreteModel of the EnergySystem instance """ # The following part is the same for all components: # Alias of the components' objective function dictionary obj = self.comp_obj_dict # Get general required variables: basic_var = self.variables[self.basic_variable]['pyomo'] # CAPEX : # ~~~~~~~ # CAPEX depending on capacity of component if self.capex_per_capacity > 0: cap = self.variables[utils.CAP]['pyomo'] obj['capex_capacity'] = -1 * self.capex_per_capacity * cap # CAPEX depending on existence of component if self.capex_if_exist > 0: bi_ex = self.variables[utils.BI_EX]['pyomo'] obj['capex_exist'] = -1 * self.capex_if_exist * bi_ex # OPEX : # ~~~~~~ # OPEX depending on capacity of component if self.opex_per_capacity > 0: cap = self.variables[utils.CAP]['pyomo'] obj['opex_capacity'] = -1 * ensys.pvf * self.opex_per_capacity * cap # OPEX depending on existence of component if self.opex_if_exist > 0: bi_ex = self.variables[utils.BI_EX]['pyomo'] obj['opex_exist'] = -1 * ensys.pvf * self.opex_if_exist * bi_ex # OPEX for operation (related to basic variable!) if self.opex_operation > 0: obj['opex_operation'] = -1 * ensys.pvf * self.opex_operation * sum( basic_var[p, t] * ensys.period_occurrences[p] for p, t in model.time_set) / ensys.number_of_years return sum(self.comp_obj_dict.values())
# ========================================================================== # S E R I A L I Z E # ==========================================================================
[docs] def serialize(self): """ This method collects all relevant input data and optimization results from the Component instance, and returns them in an ordered dictionary. :return: OrderedDict """ return OrderedDict([ ('model_class', self.__class__.__name__), ('group_name', self.group_name), ('number_in_group', self.number_in_group), ('variables', self.get_variable_values()), ('parameters', self.get_parameter_values()), ('commodities', self.commodities), ('inlet_commod_and_var_names', self.inlet_commod_and_var_names), ('outlet_commod_and_var_names', self.outlet_commod_and_var_names), ('basic_variable', self.basic_variable), ('var_connections', self.var_connections), ('comp_obj_dict', self.get_obj_values()) ])
def get_variable_values(self): var_dict = {} for var in self.variables.loc['pyomo']: var_dict[var.local_name] = str(var.get_values()) return var_dict def get_parameter_values(self): para_dict = {} for para in self.parameters: if hasattr(self.parameters[para]['values'], '__iter__'): # Usually data is stored as a pandas Series --> convert to dict para_dict[para] = str(self.parameters[para]['values'].to_dict()) else: # float or integer cannot be converted to dict --> use directly para_dict[para] = str(self.parameters[para]['values']) return para_dict def get_obj_values(self): # pyo.value(XYZ, exception=False) produces ERROR output for expressions. # It's probably a bug in Pyomo. So I use a workaround for now and walk # through the expression and apply pyo.value(XYZ, exception=False) on # all included variables one by one. from pyomo.core.expr import current as EXPR obj_values = {} for key, val in self.comp_obj_dict.items(): if isinstance(val, (int, float)): obj_values[key] = val elif val.is_expression_type(): # if pyomo expression obj_values[key], found_invalid = 0, False # init for v in EXPR.identify_variables(val): if pyo.value(v, exception=False) is None: found_invalid = True break if not found_invalid: obj_values[key] = pyo.value(val, exception=False) else: # should be single pyomo var obj_values[key] = pyo.value(val, exception=False) return obj_values