diff --git a/docs/usage.rst b/docs/usage.rst index 58247d9d9..025e68e46 100644 --- a/docs/usage.rst +++ b/docs/usage.rst @@ -1325,13 +1325,6 @@ Besides the `invest` variable, new variables are introduced as well. These are: .. note:: - * You can specify a `discount_rate` for the model. If you do not do so, 0.02 will be used as a default, corresponding - to sort of a social discount rate. If you work with costs in real terms, discounting is obsolete, so define - `discount_rate = 0` in that case. - * You can specify an `interest_rate` for every investment object. If you do not do so, it will be chosen the same - as the model's `discount_rate`. You could use this default to model a perfect competition administered by some sort of - social planner, but even in a social planner setting, you might want to deviate from the `discount_rate` - value and/or discriminate among technologies with different risk profiles and hence different interest requirements. * For storage units, the `initial_content` is not allowed combined with multi-period investments. The storage inflow and outflow are forced to zero until the storage unit is invested into. * You can specify periods of different lengths, but the frequency of your timeindex needs to be consistent. Also, diff --git a/src/oemof/solph/_energy_system.py b/src/oemof/solph/_energy_system.py index 74a220f79..9ab142f22 100644 --- a/src/oemof/solph/_energy_system.py +++ b/src/oemof/solph/_energy_system.py @@ -182,6 +182,8 @@ def __init__( self._extract_periods_matrix() self._extract_end_year_of_optimization() self.use_remaining_value = use_remaining_value + else: + self.end_year_of_optimization = 1 def _extract_periods_years(self): """Map years in optimization to respective period based on time indices diff --git a/src/oemof/solph/_models.py b/src/oemof/solph/_models.py index 5ecbbd474..170d6632a 100644 --- a/src/oemof/solph/_models.py +++ b/src/oemof/solph/_models.py @@ -52,9 +52,6 @@ class Model(po.ConcreteModel): Solph looks for these groups in the given energy system and uses them to create the constraints of the optimization problem. Defaults to `Model.CONSTRAINT_GROUPS` - discount_rate : float or None - The rate used for discounting in a multi-period model. - A 2% discount rate needs to be defined as 0.02. objective_weighting : array like (optional) Weights used for temporal objective function expressions. If nothing is passed, `timeincrement` will be used which @@ -84,17 +81,6 @@ class Model(po.ConcreteModel): rc : `pyomo.core.base.suffix.Suffix` or None Store the reduced costs of the model if pyomo suffix is set to IMPORT - Note - ---- - - * The discount rate is only applicable for a multi-period model. - * If you want to work with costs data in nominal terms, - you should specify a discount rate. - * By default, there is a discount rate of 2% in a multi-period model. - * If you want to provide your costs data in real terms, - just specify `discount_rate = 0`, i.e. effectively there will be - no discounting. - **The following basic sets are created**: @@ -134,7 +120,7 @@ class Model(po.ConcreteModel): InvestNonConvexFlowBlock, ] - def __init__(self, energysystem, discount_rate=None, **kwargs): + def __init__(self, energysystem, **kwargs): super().__init__() # Check root logger. Due to a problem with pyomo the building of the @@ -190,9 +176,7 @@ def __init__(self, energysystem, discount_rate=None, **kwargs): self.dual = None self.rc = None - if discount_rate is not None: - self.discount_rate = discount_rate - elif energysystem.periods is not None: + if energysystem.periods is not None: self._set_discount_rate_with_warning() else: pass diff --git a/src/oemof/solph/components/_generic_storage.py b/src/oemof/solph/components/_generic_storage.py index 33358a469..c76d72d33 100644 --- a/src/oemof/solph/components/_generic_storage.py +++ b/src/oemof/solph/components/_generic_storage.py @@ -420,26 +420,18 @@ class GenericStorageBlock(ScalarBlock): **The following parts of the objective function are created:** - *Standard model* - * :attr: `storage_costs` not 0 .. math:: \sum_{t \in \textrm{TIMEPOINTS} > 0} c_{storage}(t) \cdot E(t) - - *Multi-period model* - - * :attr:`fixed_costs` not None + * :attr:`fixed_costs` not 0 .. math:: \displaystyle \sum_{pp=0}^{year_{max}} E_{nom} - \cdot c_{fixed}(pp) \cdot DF^{-pp} + \cdot c_{fixed}(pp) - where: - - * :math:`DF=(1+dr)` is the discount factor with discount rate :math:`dr`. - * :math:`year_{max}` denotes the last year of the optimization + where :math:`year_{max}` denotes the last year of the optimization horizon, i.e. at the end of the last period. """ # noqa: E501 @@ -591,26 +583,19 @@ def _objective_expression(self): r""" Objective expression for storages with no investment. - Note - ---- - * For standard models, this adds nothing as variable costs are - already added in the Block :py:class:`~.SimpleFlowBlock`. - * For multi-period models, fixed costs may be introduced - and added here. + * Fixed costs (will not have an impact on the actual optimisation). + * Variable costs for storage content. """ m = self.parent_block() fixed_costs = 0 - if m.es.periods is not None: - for n in self.STORAGES: - if valid_sequence(n.fixed_costs, len(m.PERIODS)): - fixed_costs += sum( - n.nominal_storage_capacity - * n.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) - for pp in range(m.es.end_year_of_optimization) - ) + for n in self.STORAGES: + if valid_sequence(n.fixed_costs, len(m.PERIODS)): + fixed_costs += sum( + n.nominal_storage_capacity * n.fixed_costs[pp] + for pp in range(m.es.end_year_of_optimization) + ) self.fixed_costs = Expression(expr=fixed_costs) storage_costs = 0 @@ -1813,7 +1798,7 @@ def _objective_expression(self): ) investment_costs_increment = ( self.invest[n, p] * annuity * present_value_factor - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -1854,7 +1839,7 @@ def _objective_expression(self): investment_costs_increment = ( self.invest[n, p] * annuity * present_value_factor + self.invest_status[n, p] * n.investment.offset[p] - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -1880,9 +1865,7 @@ def _objective_expression(self): m.es.periods_years[p] + lifetime, ) fixed_costs += sum( - self.invest[n, p] - * n.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + self.invest[n, p] * n.investment.fixed_costs[pp] for pp in range( m.es.periods_years[p], range_limit, @@ -1897,9 +1880,7 @@ def _objective_expression(self): m.es.end_year_of_optimization, lifetime - age ) fixed_costs += sum( - n.investment.existing - * n.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + n.investment.existing * n.investment.fixed_costs[pp] for pp in range(range_limit) ) @@ -1971,15 +1952,11 @@ def _evaluate_remaining_value_difference( self.invest[n, p] * (remaining_annuity - original_annuity) * present_value_factor_remaining - ) * (1 + m.discount_rate) ** (-end_year_of_optimization) + ) if nonconvex: return convex_investment_costs + self.invest_status[ n, p - ] * (n.investment.offset[-1] - n.investment.offset[p]) * ( - 1 + m.discount_rate - ) ** ( - -end_year_of_optimization - ) + ] * (n.investment.offset[-1] - n.investment.offset[p]) else: return convex_investment_costs else: diff --git a/src/oemof/solph/components/experimental/_sink_dsm.py b/src/oemof/solph/components/experimental/_sink_dsm.py index 4645a5899..7a0537d3b 100644 --- a/src/oemof/solph/components/experimental/_sink_dsm.py +++ b/src/oemof/solph/components/experimental/_sink_dsm.py @@ -693,22 +693,16 @@ def _objective_expression(self): self.dsm_up[g, t] * m.objective_weighting[t] * g.cost_dsm_up[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) variable_costs += ( - ( - self.dsm_do_shift[g, t] * g.cost_dsm_down_shift[t] - + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] - ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + self.dsm_do_shift[g, t] * g.cost_dsm_down_shift[t] + + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + ) * m.objective_weighting[t] if valid_sequence(g.fixed_costs, len(m.PERIODS)): fixed_costs += sum( max(g.max_capacity_up, g.max_capacity_down) * g.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) for pp in range(m.es.end_year_of_optimization) ) @@ -1398,7 +1392,7 @@ def _objective_expression(self): ) investment_costs_increment = ( self.invest[g, p] * annuity * present_value_factor - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -1424,16 +1418,11 @@ def _objective_expression(self): self.dsm_up[g, t] * m.objective_weighting[t] * g.cost_dsm_up[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) variable_costs += ( - ( - self.dsm_do_shift[g, t] * g.cost_dsm_down_shift[t] - + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] - ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + self.dsm_do_shift[g, t] * g.cost_dsm_down_shift[t] + + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + ) * m.objective_weighting[t] if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime @@ -1443,9 +1432,7 @@ def _objective_expression(self): m.es.periods_years[p] + lifetime, ) fixed_costs += sum( - self.invest[g, p] - * g.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + self.invest[g, p] * g.investment.fixed_costs[pp] for pp in range( m.es.periods_years[p], range_limit, @@ -1460,9 +1447,7 @@ def _objective_expression(self): m.es.end_year_of_optimization, lifetime - age ) fixed_costs += sum( - g.investment.existing - * g.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + g.investment.existing * g.investment.fixed_costs[pp] for pp in range(range_limit) ) @@ -1533,7 +1518,7 @@ def _evaluate_remaining_value_difference( self.invest[g, p] * (remaining_annuity - original_annuity) * present_value_factor_remaining - ) * (1 + m.discount_rate) ** (-end_year_of_optimization) + ) else: return 0 else: @@ -2184,26 +2169,17 @@ def _objective_expression(self): self.dsm_up[g, t] * m.objective_weighting[t] * g.cost_dsm_up[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) variable_costs += ( - ( - sum( - self.dsm_do_shift[g, tt, t] - for tt in m.TIMESTEPS - ) - * g.cost_dsm_down_shift[t] - + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] - ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + sum(self.dsm_do_shift[g, tt, t] for tt in m.TIMESTEPS) + * g.cost_dsm_down_shift[t] + + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + ) * m.objective_weighting[t] if valid_sequence(g.fixed_costs, len(m.PERIODS)): fixed_costs += sum( max(g.max_capacity_up, g.max_capacity_down) * g.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) for pp in range(m.es.end_year_of_optimization) ) @@ -3250,7 +3226,7 @@ def _objective_expression(self): ) investment_costs_increment = ( self.invest[g, p] * annuity * present_value_factor - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -3276,20 +3252,12 @@ def _objective_expression(self): self.dsm_up[g, t] * m.objective_weighting[t] * g.cost_dsm_up[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) ) variable_costs += ( - ( - sum( - self.dsm_do_shift[g, tt, t] - for tt in m.TIMESTEPS - ) - * g.cost_dsm_down_shift[t] - + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] - ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + sum(self.dsm_do_shift[g, tt, t] for tt in m.TIMESTEPS) + * g.cost_dsm_down_shift[t] + + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + ) * m.objective_weighting[t] if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime @@ -3299,9 +3267,7 @@ def _objective_expression(self): m.es.periods_years[p] + lifetime, ) fixed_costs += sum( - self.invest[g, p] - * g.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + self.invest[g, p] * g.investment.fixed_costs[pp] for pp in range( m.es.periods_years[p], range_limit, @@ -3316,9 +3282,7 @@ def _objective_expression(self): m.es.end_year_of_optimization, lifetime - age ) fixed_costs += sum( - g.investment.existing - * g.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + g.investment.existing * g.investment.fixed_costs[pp] for pp in range(range_limit) ) @@ -3389,7 +3353,7 @@ def _evaluate_remaining_value_difference( self.invest[g, p] * (remaining_annuity - original_annuity) * present_value_factor_remaining - ) * (1 + m.discount_rate) ** (-end_year_of_optimization) + ) else: return 0 else: @@ -4367,36 +4331,26 @@ def _objective_expression(self): for g in self.DR: for p, t in m.TIMEINDEX: variable_costs += ( - ( - sum( - self.dsm_up[g, h, t] - + self.balance_dsm_do[g, h, t] - for h in g.delay_time - ) - * g.cost_dsm_up[t] + sum( + self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] + for h in g.delay_time ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + * g.cost_dsm_up[t] + ) * m.objective_weighting[t] variable_costs += ( - ( - sum( - self.dsm_do_shift[g, h, t] - + self.balance_dsm_up[g, h, t] - for h in g.delay_time - ) - * g.cost_dsm_down_shift[t] - + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + sum( + self.dsm_do_shift[g, h, t] + + self.balance_dsm_up[g, h, t] + for h in g.delay_time ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + * g.cost_dsm_down_shift[t] + + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + ) * m.objective_weighting[t] if valid_sequence(g.fixed_costs, len(m.PERIODS)): fixed_costs += sum( max(g.max_capacity_up, g.max_capacity_down) * g.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) for pp in range(m.es.end_year_of_optimization) ) @@ -5744,7 +5698,7 @@ def _objective_expression(self): ) investment_costs_increment = ( self.invest[g, p] * annuity * present_value_factor - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -5767,30 +5721,21 @@ def _objective_expression(self): for p, t in m.TIMEINDEX: variable_costs += ( - ( - sum( - self.dsm_up[g, h, t] - + self.balance_dsm_do[g, h, t] - for h in g.delay_time - ) - * g.cost_dsm_up[t] + sum( + self.dsm_up[g, h, t] + self.balance_dsm_do[g, h, t] + for h in g.delay_time ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + * g.cost_dsm_up[t] + ) * m.objective_weighting[t] variable_costs += ( - ( - sum( - self.dsm_do_shift[g, h, t] - + self.balance_dsm_up[g, h, t] - for h in g.delay_time - ) - * g.cost_dsm_down_shift[t] - + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + sum( + self.dsm_do_shift[g, h, t] + + self.balance_dsm_up[g, h, t] + for h in g.delay_time ) - * m.objective_weighting[t] - * (1 + m.discount_rate) ** (-m.es.periods_years[p]) - ) + * g.cost_dsm_down_shift[t] + + self.dsm_do_shed[g, t] * g.cost_dsm_down_shed[t] + ) * m.objective_weighting[t] if valid_sequence(g.investment.fixed_costs, len(m.PERIODS)): lifetime = g.investment.lifetime @@ -5800,9 +5745,7 @@ def _objective_expression(self): m.es.periods_years[p] + lifetime, ) fixed_costs += sum( - self.invest[g, p] - * g.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + self.invest[g, p] * g.investment.fixed_costs[pp] for pp in range( m.es.periods_years[p], range_limit, @@ -5817,9 +5760,7 @@ def _objective_expression(self): m.es.end_year_of_optimization, lifetime - age ) fixed_costs += sum( - g.investment.existing - * g.investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) + g.investment.existing * g.investment.fixed_costs[pp] for pp in range(range_limit) ) @@ -5890,7 +5831,7 @@ def _evaluate_remaining_value_difference( self.invest[g, p] * (remaining_annuity - original_annuity) * present_value_factor_remaining - ) * (1 + m.discount_rate) ** (-end_year_of_optimization) + ) else: return 0 else: diff --git a/src/oemof/solph/flows/_investment_flow_block.py b/src/oemof/solph/flows/_investment_flow_block.py index 2a09f11a2..072fdba76 100644 --- a/src/oemof/solph/flows/_investment_flow_block.py +++ b/src/oemof/solph/flows/_investment_flow_block.py @@ -945,7 +945,7 @@ def _objective_expression(self): self.invest[i, o, p] * annuity * present_value_factor_remaining - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -990,7 +990,7 @@ def _objective_expression(self): * present_value_factor_remaining + self.invest_status[i, o, p] * m.flows[i, o].investment.offset[p] - ) * (1 + m.discount_rate) ** (-m.es.periods_years[p]) + ) remaining_value_difference = ( self._evaluate_remaining_value_difference( m, @@ -1021,7 +1021,6 @@ def _objective_expression(self): fixed_costs += sum( self.invest[i, o, p] * m.flows[i, o].investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) for pp in range(m.es.periods_years[p], range_limit) ) @@ -1037,7 +1036,6 @@ def _objective_expression(self): fixed_costs += sum( m.flows[i, o].investment.existing * m.flows[i, o].investment.fixed_costs[pp] - * (1 + m.discount_rate) ** (-pp) for pp in range(range_limit) ) @@ -1113,17 +1111,13 @@ def _evaluate_remaining_value_difference( self.invest[i, o, p] * (remaining_annuity - original_annuity) * present_value_factor_remaining - ) * (1 + m.discount_rate) ** (-end_year_of_optimization) + ) if nonconvex: return convex_investment_costs + self.invest_status[ i, o, p ] * ( m.flows[i, o].investment.offset[-1] - m.flows[i, o].investment.offset[p] - ) * ( - 1 + m.discount_rate - ) ** ( - -end_year_of_optimization ) else: return convex_investment_costs diff --git a/src/oemof/solph/flows/_simple_flow_block.py b/src/oemof/solph/flows/_simple_flow_block.py index 0a8c910d4..cea3b4953 100644 --- a/src/oemof/solph/flows/_simple_flow_block.py +++ b/src/oemof/solph/flows/_simple_flow_block.py @@ -433,31 +433,17 @@ def _objective_expression(self): variable_costs = 0 fixed_costs = 0 - if m.es.periods is None: - for i, o in m.FLOWS: - if valid_sequence( - m.flows[i, o].variable_costs, len(m.TIMESTEPS) - ): - for t in m.TIMESTEPS: - variable_costs += ( - m.flow[i, o, t] - * m.objective_weighting[t] - * m.flows[i, o].variable_costs[t] - ) + for i, o in m.FLOWS: + if valid_sequence(m.flows[i, o].variable_costs, len(m.TIMESTEPS)): + for t in m.TIMESTEPS: + variable_costs += ( + m.flow[i, o, t] + * m.objective_weighting[t] + * m.flows[i, o].variable_costs[t] + ) - else: + if m.es.periods is not None: for i, o in m.FLOWS: - if valid_sequence( - m.flows[i, o].variable_costs, len(m.TIMESTEPS) - ): - for p, t in m.TIMEINDEX: - variable_costs += ( - m.flow[i, o, t] - * m.objective_weighting[t] - * m.flows[i, o].variable_costs[t] - * ((1 + m.discount_rate) ** -m.es.periods_years[p]) - ) - # Fixed costs for units with no lifetime limit if ( m.flows[i, o].fixed_costs[0] is not None @@ -468,7 +454,6 @@ def _objective_expression(self): fixed_costs += sum( m.flows[i, o].nominal_value * m.flows[i, o].fixed_costs[pp] - * ((1 + m.discount_rate) ** (-pp)) for pp in range(m.es.end_year_of_optimization) ) @@ -482,7 +467,6 @@ def _objective_expression(self): fixed_costs += sum( m.flows[i, o].nominal_value * m.flows[i, o].fixed_costs[pp] - * ((1 + m.discount_rate) ** (-pp)) for pp in range(range_limit) ) @@ -495,7 +479,6 @@ def _objective_expression(self): fixed_costs += sum( m.flows[i, o].nominal_value * m.flows[i, o].fixed_costs[pp] - * ((1 + m.discount_rate) ** (-pp)) for pp in range(range_limit) ) diff --git a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py index c61952720..d0c823566 100644 --- a/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py +++ b/tests/test_scripts/test_solph/test_multi_period_model/test_multi_period_dispatch_model.py @@ -26,6 +26,9 @@ from oemof.solph import views +@pytest.mark.skip( + reason="Too complex for a unit test. Results cannot be checked easily." +) @pytest.mark.filterwarnings( "ignore:Ensure that your timeindex and timeincrement are" " consistent.:UserWarning"