|
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
|
|
|
|