1
|
|
|
# -*- coding: utf-8 -*- |
2
|
|
|
|
3
|
|
|
"""Optional classes to be added to a network class. |
4
|
|
|
|
5
|
|
|
SPDX-FileCopyrightText: Uwe Krien <[email protected]> |
6
|
|
|
SPDX-FileCopyrightText: Simon Hilpert |
7
|
|
|
SPDX-FileCopyrightText: Cord Kaldemeyer |
8
|
|
|
SPDX-FileCopyrightText: Stephan Günther |
9
|
|
|
SPDX-FileCopyrightText: Patrik Schönfeldt |
10
|
|
|
SPDX-FileCopyrightText: jmloenneberga |
11
|
|
|
SPDX-FileCopyrightText: Johannes Kochems |
12
|
|
|
SPDX-FileCopyrightText: Malte Fritz |
13
|
|
|
SPDX-FileCopyrightText: Jonas Freißmann |
14
|
|
|
|
15
|
|
|
SPDX-License-Identifier: MIT |
16
|
|
|
|
17
|
|
|
""" |
18
|
|
|
from warnings import warn |
19
|
|
|
|
20
|
|
|
from oemof.tools import debugging |
21
|
|
|
|
22
|
|
|
from oemof.solph._plumbing import sequence |
23
|
|
|
|
24
|
|
|
|
25
|
|
|
class Investment: |
26
|
|
|
"""Defines an Investment object holding all the specifications needed |
27
|
|
|
for investment modeling. |
28
|
|
|
|
29
|
|
|
Parameters |
30
|
|
|
---------- |
31
|
|
|
maximum : float, :math:`P_{invest,max}(p)` or :math:`E_{invest,max}(p)` |
32
|
|
|
Maximum of the additional invested capacity; |
33
|
|
|
defined per period p for a multi-period model. |
34
|
|
|
minimum : float, :math:`P_{invest,min}(p)` or :math:`E_{invest,min}(p)` |
35
|
|
|
Minimum of the additional invested capacity. If `nonconvex` is `True`, |
36
|
|
|
`minimum` defines the threshold for the invested capacity; |
37
|
|
|
defined per period p for a multi-period model. |
38
|
|
|
ep_costs : float, :math:`c_{invest,var}` |
39
|
|
|
Equivalent periodical costs or investment expenses for the investment |
40
|
|
|
|
41
|
|
|
* For a standard model: equivalent periodical costs for the investment |
42
|
|
|
per flow capacity, i.e. annuities for investments already calculated. |
43
|
|
|
* For a multi-period model: Investment expenses for the respective |
44
|
|
|
period (in nominal terms). Annuities are calculated within the |
45
|
|
|
objective term, also considering age and lifetime. |
46
|
|
|
existing : float, :math:`P_{exist}` or :math:`E_{exist}` |
47
|
|
|
Existing / installed capacity. The invested capacity is added on top |
48
|
|
|
of this value. Hence, existing capacities come at no additional costs. |
49
|
|
|
Not applicable if `nonconvex` is set to `True`. |
50
|
|
|
nonconvex : bool |
51
|
|
|
If `True`, a binary variable for the status of the investment is |
52
|
|
|
created. This enables additional fix investment costs (*offset*) |
53
|
|
|
independent of the invested flow capacity. Therefore, use the `offset` |
54
|
|
|
parameter. |
55
|
|
|
offset : float, :math:`c_{invest,fix}` |
56
|
|
|
Additional fixed investment costs. Only applicable if `nonconvex` is |
57
|
|
|
set to `True`. |
58
|
|
|
overall_maximum : float, :math:`P_{overall,max}` or :math:`E_{overall,max}` |
59
|
|
|
Overall maximum capacity investment, i.e. the amount of capacity |
60
|
|
|
that can be totally installed at maximum in any period (taking into |
61
|
|
|
account decommissionings); only applicable for multi-period models |
62
|
|
|
overall_minimum : float :math:`P_{overall,min}` or :math:`E_{overall,min}` |
63
|
|
|
Overall minimum capacity investment that needs to be installed |
64
|
|
|
in the last period of the optimization (taking into account |
65
|
|
|
decommissionings); only applicable for multi-period models |
66
|
|
|
lifetime : int, :math:`l` |
67
|
|
|
Units lifetime, given in years; only applicable for multi-period |
68
|
|
|
models |
69
|
|
|
age : int, :math:`a` |
70
|
|
|
Units start age, given in years at the beginning of the optimization; |
71
|
|
|
only applicable for multi-period models |
72
|
|
|
fixed_costs : float or list of float, :math:`c_{fixed}(p)` |
73
|
|
|
Fixed costs in each period (given in nominal terms); |
74
|
|
|
only applicable for multi-period models |
75
|
|
|
|
76
|
|
|
|
77
|
|
|
For the variables, constraints and parts of the objective function, which |
78
|
|
|
are created, see |
79
|
|
|
:py:class:`~oemof.solph.blocks.investment_flow.InvestmentFlow`, |
80
|
|
|
:py:class:`~oemof.solph.components.generic_storage.GenericInvestmentStorageBlock` |
81
|
|
|
:py:class:`~oemof.solph.custom.sink_dsm.SinkDSMOemofInvestmentBlock`, |
82
|
|
|
:py:class:`~oemof.solph.custom.sink_dsm.SinkDSMDLRInvestmentBlock` and |
83
|
|
|
:py:class:`~oemof.solph.custom.sink_dsm.SinkDSMDIWInvestmentBlock`. |
84
|
|
|
|
85
|
|
|
""" # noqa: E501 |
86
|
|
|
|
87
|
|
|
def __init__( |
88
|
|
|
self, |
89
|
|
|
maximum=float("+inf"), |
90
|
|
|
minimum=0, |
91
|
|
|
ep_costs=0, |
92
|
|
|
existing=0, |
93
|
|
|
nonconvex=False, |
94
|
|
|
offset=0, |
95
|
|
|
overall_maximum=None, |
96
|
|
|
overall_minimum=None, |
97
|
|
|
lifetime=None, |
98
|
|
|
age=0, |
99
|
|
|
fixed_costs=None, |
100
|
|
|
custom_attributes=None, |
101
|
|
|
): |
102
|
|
|
if custom_attributes is None: |
103
|
|
|
custom_attributes = {} |
104
|
|
|
self.maximum = sequence(maximum) |
105
|
|
|
self.minimum = sequence(minimum) |
106
|
|
|
self.ep_costs = sequence(ep_costs) |
107
|
|
|
self.existing = existing |
108
|
|
|
self.nonconvex = nonconvex |
109
|
|
|
self.offset = sequence(offset) |
110
|
|
|
self.overall_maximum = overall_maximum |
111
|
|
|
self.overall_minimum = overall_minimum |
112
|
|
|
self.lifetime = lifetime |
113
|
|
|
self.age = age |
114
|
|
|
self.fixed_costs = sequence(fixed_costs) |
115
|
|
|
|
116
|
|
|
for attribute in custom_attributes.keys(): |
117
|
|
|
value = custom_attributes.get(attribute) |
118
|
|
|
setattr(self, attribute, value) |
119
|
|
|
|
120
|
|
|
self._check_invest_attributes() |
121
|
|
|
self._check_invest_attributes_maximum() |
122
|
|
|
self._check_invest_attributes_offset() |
123
|
|
|
self._check_age_and_lifetime() |
124
|
|
|
self._check_invest_attributes_nonconvex() |
125
|
|
|
self._check_nonconvex() |
126
|
|
|
|
127
|
|
|
def _check_invest_attributes(self): |
128
|
|
|
"""Throw an error if existing is other than 0 and nonconvex is True""" |
129
|
|
|
if (self.existing != 0) and (self.nonconvex is True): |
130
|
|
|
e1 = ( |
131
|
|
|
"Values for 'offset' and 'existing' are given in" |
132
|
|
|
" investement attributes. \n These two options cannot be " |
133
|
|
|
"considered at the same time." |
134
|
|
|
) |
135
|
|
|
raise AttributeError(e1) |
136
|
|
|
|
137
|
|
|
def _check_invest_attributes_maximum(self): |
138
|
|
|
"""Throw an error if maximum is infinite and nonconvex is True""" |
139
|
|
|
if (self.maximum[0] == float("+inf")) and (self.nonconvex is True): |
140
|
|
|
e2 = ( |
141
|
|
|
"Please provide a maximum investment value in case of" |
142
|
|
|
" nonconvex investment (nonconvex=True), which is in the" |
143
|
|
|
" expected magnitude." |
144
|
|
|
" \nVery high maximum values (> 10e8) as maximum investment" |
145
|
|
|
" limit might lead to numeric issues, so that no investment" |
146
|
|
|
" is done, although it is the optimal solution!" |
147
|
|
|
) |
148
|
|
|
raise AttributeError(e2) |
149
|
|
|
|
150
|
|
|
def _check_invest_attributes_offset(self): |
151
|
|
|
"""Throw an error if offset is given without nonconvex=True""" |
152
|
|
|
if (self.offset[0] != 0) and (self.nonconvex is False): |
153
|
|
|
e3 = ( |
154
|
|
|
"If `nonconvex` is `False`, the `offset` parameter will be" |
155
|
|
|
" ignored." |
156
|
|
|
) |
157
|
|
|
raise AttributeError(e3) |
158
|
|
|
|
159
|
|
|
def _check_age_and_lifetime(self): |
160
|
|
|
"""Throw an error if age is chosen greater or equal to lifetime; |
161
|
|
|
only applicable for multi-period models |
162
|
|
|
""" |
163
|
|
|
if self.lifetime is not None: |
164
|
|
|
if self.age >= self.lifetime: |
165
|
|
|
e4 = ( |
166
|
|
|
"A unit's age must be smaller than its " |
167
|
|
|
"expected lifetime." |
168
|
|
|
) |
169
|
|
|
raise AttributeError(e4) |
170
|
|
|
|
171
|
|
|
def _check_invest_attributes_nonconvex(self): |
172
|
|
|
"""Throw an error if nonconvex is not of type boolean.""" |
173
|
|
|
if not isinstance(self.nonconvex, bool): |
174
|
|
|
e5 = ( |
175
|
|
|
"The `nonconvex` parameter of the `Investment` class has to be" |
176
|
|
|
+ f" of type boolean, not {type(self.nonconvex)}." |
177
|
|
|
) |
178
|
|
|
raise AttributeError(e5) |
179
|
|
|
|
180
|
|
|
def _check_nonconvex(self): |
181
|
|
|
"""Checking for unnecessary setting of nonconvex""" |
182
|
|
|
if self.nonconvex: |
183
|
|
|
if (self.minimum.min() == 0) and (self.offset.min() == 0): |
184
|
|
|
msg = ( |
185
|
|
|
"It is not necessary to set the investment to `nonconvex` " |
186
|
|
|
"if `minimum` and `offset` are 0.\n" |
187
|
|
|
"This can lead to the `invest_status` variable becoming " |
188
|
|
|
"1, even if the `nominal_capacity` is optimized to 0." |
189
|
|
|
) |
190
|
|
|
warn(msg, debugging.SuspiciousUsageWarning) |
191
|
|
|
|
192
|
|
|
|
193
|
|
|
class NonConvex: |
194
|
|
|
"""Defines a NonConvex object holding all the specifications for NonConvex |
195
|
|
|
Flows, i.e. Flows with binary variables associated to them. |
196
|
|
|
|
197
|
|
|
Parameters |
198
|
|
|
---------- |
199
|
|
|
startup_costs : numeric (iterable or scalar) |
200
|
|
|
Costs associated with a start of the flow (representing a unit). |
201
|
|
|
shutdown_costs : numeric (iterable or scalar) |
202
|
|
|
Costs associated with the shutdown of the flow (representing a unit). |
203
|
|
|
activity_costs : numeric (iterable or scalar) |
204
|
|
|
Costs associated with the active operation of the flow, independently |
205
|
|
|
from the actual output. |
206
|
|
|
inactivity_costs : numeric (iterable or scalar) |
207
|
|
|
Costs associated with not operating the flow. |
208
|
|
|
minimum_uptime : numeric or list of numeric (1 or positive integer) |
209
|
|
|
Minimum number of time steps that a flow must be greater then its |
210
|
|
|
minimum flow after startup. Be aware that minimum up and downtimes |
211
|
|
|
can contradict each other and may lead to infeasible problems. |
212
|
|
|
minimum_downtime : numeric or list of numeric (1 or positive integer) |
213
|
|
|
Minimum number of time steps a flow is forced to zero after |
214
|
|
|
shutting down. Be aware that minimum up and downtimes can |
215
|
|
|
contradict each other and may to infeasible problems. |
216
|
|
|
maximum_startups : numeric (0 or positive integer) |
217
|
|
|
Maximum number of start-ups in the optimization timeframe. |
218
|
|
|
maximum_shutdowns : numeric (0 or positive integer) |
219
|
|
|
Maximum number of shutdowns in the optimization timeframe. |
220
|
|
|
initial_status : numeric (0 or 1) |
221
|
|
|
Integer value indicating the status of the flow in the first time step |
222
|
|
|
(0 = off, 1 = on). For minimum up and downtimes, the initial status |
223
|
|
|
is set for the respective values in the beginning e.g. if a |
224
|
|
|
minimum uptime of four timesteps is defined and the initial status is |
225
|
|
|
set to one, the initial status is fixed for the four first timesteps |
226
|
|
|
of the optimization period. Otherwise if the initial status is set to |
227
|
|
|
zero and the first timesteps are fixed for the number of minimum |
228
|
|
|
downtime steps. |
229
|
|
|
negative_gradient_limit : numeric (iterable, scalar or None) |
230
|
|
|
the normed *upper bound* on the positive difference |
231
|
|
|
(`flow[t-1] < flow[t]`) of two consecutive flow values. |
232
|
|
|
positive_gradient_limit : numeric (iterable, scalar or None) |
233
|
|
|
the normed *upper bound* on the negative difference |
234
|
|
|
(`flow[t-1] > flow[t]`) of two consecutive flow values. |
235
|
|
|
""" |
236
|
|
|
|
237
|
|
|
def __init__( |
238
|
|
|
self, |
239
|
|
|
initial_status=0, |
240
|
|
|
minimum_uptime=0, |
241
|
|
|
minimum_downtime=0, |
242
|
|
|
maximum_startups=None, |
243
|
|
|
maximum_shutdowns=None, |
244
|
|
|
startup_costs=None, |
245
|
|
|
shutdown_costs=None, |
246
|
|
|
activity_costs=None, |
247
|
|
|
inactivity_costs=None, |
248
|
|
|
negative_gradient_limit=None, |
249
|
|
|
positive_gradient_limit=None, |
250
|
|
|
custom_attributes=None, |
251
|
|
|
): |
252
|
|
|
if custom_attributes is None: |
253
|
|
|
custom_attributes = {} |
254
|
|
|
|
255
|
|
|
self.initial_status = initial_status |
256
|
|
|
self.minimum_uptime = sequence(minimum_uptime) |
257
|
|
|
self.minimum_downtime = sequence(minimum_downtime) |
258
|
|
|
self.maximum_startups = maximum_startups |
259
|
|
|
self.maximum_shutdowns = maximum_shutdowns |
260
|
|
|
|
261
|
|
|
self.startup_costs = sequence(startup_costs) |
262
|
|
|
self.shutdown_costs = sequence(shutdown_costs) |
263
|
|
|
self.activity_costs = sequence(activity_costs) |
264
|
|
|
self.inactivity_costs = sequence(inactivity_costs) |
265
|
|
|
self.negative_gradient_limit = sequence(negative_gradient_limit) |
266
|
|
|
self.positive_gradient_limit = sequence(positive_gradient_limit) |
267
|
|
|
|
268
|
|
|
for attribute, value in custom_attributes.items(): |
269
|
|
|
setattr(self, attribute, value) |
270
|
|
|
|
271
|
|
|
if initial_status == 0: |
272
|
|
|
self.first_flexible_timestep = self.minimum_downtime[0] |
273
|
|
|
else: |
274
|
|
|
self.first_flexible_timestep = self.minimum_uptime[0] |
275
|
|
|
|