Source code for aristopy.conversion

#!/usr/bin/env python
# -*- coding: utf-8 -*-
# ==============================================================================
#    C O N V E R S I O N
# ==============================================================================
"""
* File name: conversion.py
* Last edited: 2020-06-14
* Created by: Stefan Bruche (TU Berlin)

A conversion component takes commodities at the inlet and provides other
commodities at the outlet after an internal conversion.
"""
import pyomo.environ as pyo
from aristopy.component import Component
from aristopy import utils


[docs]class Conversion(Component): 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, start_up_cost=0, min_load_rel=None, instances_in_group=1, group_has_existence_order=True, group_has_operation_order=True, use_inter_period_formulation=True ): """ Initialize an instance of the Conversion class. .. note:: See the documentation of the :class:`Component <aristopy.component.Component>` class for a description of all keyword arguments and inherited methods. :param start_up_cost: Costs incurred when the state of the binary operation variable (BI_OP) changes from 0 (OFF) to 1 (ON) from one time step to the next one [€/Start]; (requires keyword argument 'has_operation_binary_var' set to True). |br| *Default: 0* :type start_up_cost: float or int (>=0) :param min_load_rel: Value for the relative minimal part-load of a conversion unit (e.g., 0.5 represents 50% minimal load). Minimal part-loads require the availability of a binary operation variable ('has_operation_binary_var'=True) and currently, fixed capacities need to be specified ('capacity_min'='capacity_max'). |br| *Default: None* :type min_load_rel: float or int (0<=value<=1), or None :param instances_in_group: 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* :type instances_in_group: int (>=0) :param group_has_existence_order: If multiple similar instances are created and arranged in a group ('instances_in_group'>1), the user might want to introduce an order of their binary existence variable values to break the symmetry of the optimization problem. If the flag is set to True a constraint is created for each component of the group that requires the previous component in the group to exist (BI_EX=1) before the own existence variable can take the value 1. |br| *Default: True* :type group_has_existence_order: bool :param group_has_operation_order: If multiple similar instances are created and arranged in a group ('instances_in_group'>1), the user might want to introduce an order of their binary operation variable values to break the symmetry of the optimization problem. If the flag is set to True a constraint is created for each component of the group and every time step of the optimization problem that requires the previous component in the group to be operated (BI_OP=1) before the own operation variable can take the value 1. |br| *Default: True* :type group_has_operation_order: bool :param use_inter_period_formulation: A second binary start-up variable is created (BI_SU_INTER), if the inter-period model formulation is requested (flag=True) and start-up cost are introduced (argument 'start_up_cost'>0). This variable is used in case the model is optimized with aggregated time-series data (multiple periods). The variable links the binary operation variable values of otherwise independent periods, to enforce start-up cost if the operation status (BI_OP) changes from one period to the next (OFF to ON). Note: This formulation introduces additional variables and constraints, and increases both model accuracy and model complexity. |br| *Default: True* :type use_inter_period_formulation: bool """ # Prevent None at inlet & outlet! (Flows are checked in Component init) if inlet is None: raise utils.io_error_message('Conversion', name, 'inlet') if outlet is None: raise utils.io_error_message('Conversion', name, 'outlet') Component.__init__(self, ensys=ensys, name=name, inlet=inlet, outlet=outlet, basic_variable=basic_variable, has_existence_binary_var=has_existence_binary_var, has_operation_binary_var=has_operation_binary_var, time_series_data=time_series_data, scalar_params=scalar_params, additional_vars=additional_vars, user_expressions=user_expressions, capacity=capacity, capacity_min=capacity_min, capacity_max=capacity_max, capacity_per_module=capacity_per_module, maximal_module_number=maximal_module_number, capex_per_capacity=capex_per_capacity, capex_if_exist=capex_if_exist, opex_per_capacity=opex_per_capacity, opex_if_exist=opex_if_exist, opex_operation=opex_operation ) # Check and set the value for the minimal relative part-load if min_load_rel is not None: utils.check_and_set_positive_number(min_load_rel, 'min_load_rel') assert min_load_rel <= 1, 'Maximal value for "min_load_rel" is 1!' if not self.has_bi_op: raise ValueError('Minimal part-loads require the availability ' 'of a binary operation variable ' '("has_operation_binary_var=True").') if self.capacity is None: raise NotImplementedError( 'Minimal part-loads with flexible unit capacity can be ' 'modelled but it requires some effort. This feature has' ' not been implemented yet. Please consider the ' '"user_expressions" attribute to model it yourself.\n ' 'E.g.: Q_CAP_OR_OFF[p, t] == CAP * BI_OP[p, t] * dt ' '--> Linearization required (Glover)!,\n ' 'Q[p, t] >= Q_CAP_OR_OFF[p, t] * min_load_rel * dt') self.min_load_rel = min_load_rel # Check and set additional input arguments self.start_up_cost = utils.check_and_set_positive_number( start_up_cost, 'start_up_cost') self.use_inter_period_formulation = utils.check_and_set_bool( use_inter_period_formulation, 'use_inter_period_formulation') # Multiple instances formed and collected in one group utils.check_positive_int(instances_in_group, 'instances_in_group', zero_is_invalid=False) self.instances_in_group = instances_in_group self.group_has_existence_order = utils.check_and_set_bool( group_has_existence_order, 'group_has_existence_order') if self.instances_in_group > 1 and not self.has_bi_ex and \ self.group_has_existence_order: # is True raise ValueError('Group requires a binary existence variable if an ' 'existence order is requested!') self.group_has_operation_order = utils.check_and_set_bool( group_has_operation_order, 'group_has_operation_order') if self.instances_in_group > 1 and not self.has_bi_op and \ self.group_has_operation_order: # is True raise ValueError('Group requires a binary operation variable if an ' 'operation order is requested!') # Add variables for Start-up if self.start_up_cost != 0: if not self.has_bi_op: raise ValueError('Start-up cost require the availability ' 'of a binary operation variable.') # Else: add Start-up binary variable self.add_var(name=utils.BI_SU, domain='Binary', has_time_set=True) # If inter-period-formulation is requested: add another binary var if self.use_inter_period_formulation: self.add_var(name=utils.BI_SU_INTER, domain='Binary', has_time_set=False, init=0, alternative_set='inter_period_time_set') # Last step: Add the component to the EnergySystem instance self.add_to_energy_system(ensys, name, instances_in_group) def __repr__(self): return '<Conversion: "%s">' % self.name # ========================================================================== # 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] def declare_component_constraints(self, ensys, model): """ Method to declare all component constraints. The following constraint methods are inherited from the Component class and are not documented in this sub-class: * :meth:`con_couple_bi_ex_and_cap <aristopy.component.Component.con_couple_bi_ex_and_cap>` * :meth:`con_cap_min <aristopy.component.Component.con_cap_min>` * :meth:`con_cap_modular <aristopy.component.Component.con_cap_modular>` * :meth:`con_modular_sym_break <aristopy.component.Component.con_modular_sym_break>` * :meth:`con_couple_existence_and_modular <aristopy.component.Component.con_couple_existence_and_modular>` * :meth:`con_bi_var_ex_and_op_relation <aristopy.component.Component.con_bi_var_ex_and_op_relation>` * :meth:`con_couple_op_binary_and_basic_var <aristopy.component.Component.con_couple_op_binary_and_basic_var>` *Method is not intended for public access!* :param ensys: Instance of the EnergySystem class :param model: Pyomo ConcreteModel of the EnergySystem instance """ # Time-independent constraints : # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~ self.con_couple_bi_ex_and_cap() self.con_cap_min() self.con_cap_modular() self.con_modular_sym_break() self.con_couple_existence_and_modular() self.con_existence_sym_break(ensys) # Time-dependent constraints : # ~~~~~~~~~~~~~~~~~~~~~~~~~~~~ self.con_bi_var_ex_and_op_relation(model) self.con_couple_op_binary_and_basic_var(model) self.con_operation_limit(model) self.con_operation_sym_break(ensys, model) self.con_min_load_rel(model) self.con_start_up_cost(model) self.con_start_up_cost_inter(ensys, model)
# ************************************************************************** # Time-independent constraints # **************************************************************************
[docs] def con_existence_sym_break(self, ensys): """ Constraint to state, that the next component in a group can only be built if the previous one already exists (symmetry break constraint for groups consisting of multiple components 'instances_in_group' > 1), if keyword argument 'group_has_existence_order' is set to True. E.g.: |br| ``BI_EX (of conversion_2) <= BI_EX (of conversion_1)`` *Method is not intended for public access!* """ if self.number_in_group > 1 and self.group_has_existence_order: bi_ex = self.variables[utils.BI_EX]['pyomo'] # Get 'bi_ex' of the previous component in the sequence prior = self.group_name + '_{}'.format(self.number_in_group - 1) prior_comp = ensys.components[prior] bi_ex_prior = prior_comp.variables[utils.BI_EX]['pyomo'] def con_existence_sym_break(m): return bi_ex <= bi_ex_prior setattr(self.block, 'con_existence_sym_break', pyo.Constraint(rule=con_existence_sym_break))
# ************************************************************************** # Time-dependent constraints # **************************************************************************
[docs] def con_operation_sym_break(self, ensys, model): """ Constraint to state, that the next component in a group can only be operated if the previous one is already 'ON' (symmetry break constraint for groups consisting of multiple components 'instances_in_group' > 1), if keyword argument 'group_has_operation_order' is set to True. E.g.: |br| ``BI_OP[p, t] (of conversion_2) <= BI_OP[p, t] (of conversion_1)`` *Method is not intended for public access!* """ if self.number_in_group > 1 and self.group_has_operation_order: bi_op = self.variables[utils.BI_OP]['pyomo'] prior = self.group_name + '_{}'.format(self.number_in_group - 1) prior_comp = ensys.components[prior] bi_op_prior = prior_comp.variables[utils.BI_OP]['pyomo'] def con_operation_sym_break(m, p, t): return bi_op[p, t] <= bi_op_prior[p, t] setattr(self.block, 'con_operation_sym_break', pyo.Constraint( model.time_set, rule=con_operation_sym_break))
[docs] def con_operation_limit(self, model): """ The basic variable of a component is limited by its nominal capacity. This usually means, the operation (main commodity) of a conversion (MWh) is limited by its nominal power (MW) multiplied with the number of hours per time step. E.g.: |br| ``Q[p, t] <= CAP * dt`` *Method is not intended for public access!* """ # Only required if component has a capacity variable if self.has_capacity_var: cap = self.variables[utils.CAP]['pyomo'] basic_var = self.variables[self.basic_variable]['pyomo'] has_time_set = self.variables[self.basic_variable]['has_time_set'] dt = self.ensys.hours_per_time_step def con_operation_limit(m, p, t): if has_time_set: return basic_var[p, t] <= cap * dt else: # Exceptional case: Selection of a scalar basic variable return basic_var == cap setattr(self.block, 'con_operation_limit', pyo.Constraint( model.time_set, rule=con_operation_limit))
[docs] def con_min_load_rel(self, model): """ Constraint to set a value for the relative minimal part-load of a conversion unit (e.g., a component can either be switched off (BI_OP=0) or can work between 50% and 100% of its nominal capacity if a value of 0.5 for 'min_load_rel' is specified. The constraint requires the availability of a binary operation variable ('has_operation_binary_var' is True) and currently, fixed capacities need to be specified ('capacity_min'='capacity_max'='capacity'). E.g.: |br| ``Q[p, t] >= capacity * BI_OP[p, t] * min_load_rel * dt`` *Method is not intended for public access!* """ # Only required if minimal part-loads should be modelled. # Availability of binary operation variable and fixed capacity is # checked during component initialization. if self.min_load_rel is not None: bi_op = self.variables['BI_OP']['pyomo'] basic_var = self.variables[self.basic_variable]['pyomo'] cap = self.capacity min_load = self.min_load_rel dt = self.ensys.hours_per_time_step def con_min_load_rel(m, p, t): return basic_var[p, t] >= cap * bi_op[p, t] * min_load * dt setattr(self.block, 'con_min_load_rel', pyo.Constraint( model.time_set, rule=con_min_load_rel))
[docs] def con_start_up_cost(self, model): """ Constraint to determine the status of the binary start-up variable. If the operational status of a component changes from OFF (BI_OP=0) to ON (BI_OP=1) from one time step to the next, the binary start-up variable must take a value of 1. For shut-down and remaining ON / OFF, the status variable can take both values, but the obj. function forces it to be 0. E.g.: |br| ``0 <= BI_OP[t-1] - BI_OP[t] + BI_SU[t]`` *Method is not intended for public access!* """ # Only if the start-up cost value is larger than 0 if self.start_up_cost > 0: bi_op = self.variables[utils.BI_OP]['pyomo'] bi_su = self.variables[utils.BI_SU]['pyomo'] def con_start_up_cost(m, p, t): if t != 0: # not in first time step of a period return 0 <= bi_op[p, t - 1] - bi_op[p, t] + bi_su[p, t] else: return pyo.Constraint.Skip setattr(self.block, 'con_start_up_cost', pyo.Constraint( model.time_set, rule=con_start_up_cost))
[docs] def con_start_up_cost_inter(self, ensys, model): """ Constraint to link the binary operation variables of consecutive periods, to enforce start-up cost if the operation status (BI_OP) changes from one period to the next (OFF to ON). With this, the binary operation status of the last time step in the previous typical period and the operation status of the first time step in the current typical period are compared. Binary variable 'BI_SU_INTER' indicates a potential status change from OFF to ON. E.g.: |br| ``0 <= BI_OP[p_typ-1, t_last] - BI_OP[p_typ, 0] + BI_SU_INTER[p]`` *Method is not intended for public access!* """ # Only if start-up cost are applied, the inter-period formulation is # requested and the data is clustered. if self.start_up_cost > 0 and self.use_inter_period_formulation \ and ensys.is_data_clustered: bi_op = self.variables[utils.BI_OP]['pyomo'] bi_su_inter = self.variables[utils.BI_SU_INTER]['pyomo'] def con_start_up_cost_inter(m, p): # not for first period, because there is no precursor to use if not p == ensys.periods[0]: typ_period = ensys.periods_order[p] prev_typ_period = ensys.periods_order[p-1] last_ts_idx = model.time_set.last()[1] return 0 <= bi_op[prev_typ_period, last_ts_idx] - bi_op[ typ_period, 0] + bi_su_inter[p] else: return pyo.Constraint.Skip setattr(self.block, 'con_start_up_cost_inter', pyo.Constraint( ensys.periods, rule=con_start_up_cost_inter))
# ========================================================================== # 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 """ # Call method in "Component" class and calculate CAPEX and OPEX super().get_objective_function_contribution(ensys, model) # START-UP COST : # ~~~~~~~~~~~~~~~ if self.start_up_cost > 0: bi_su = self.variables[utils.BI_SU]['pyomo'] # only avail. if '>0' start_cost_intra = -1 * ensys.pvf * self.start_up_cost * sum( bi_su[p, t] * ensys.period_occurrences[p] for p, t in model.time_set) / ensys.number_of_years if self.use_inter_period_formulation and ensys.is_data_clustered: bi_su_inter = self.variables[utils.BI_SU_INTER]['pyomo'] start_cost_inter = -1 * ensys.pvf * self.start_up_cost * pyo.\ summation(bi_su_inter) / ensys.number_of_years else: start_cost_inter = 0 self.comp_obj_dict['start_up_cost'] = \ start_cost_intra + start_cost_inter 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 """ comp_dict = super().serialize() return comp_dict