Skip to content

fine

Modules:

Classes:

  • Component

    The Component class includes the general methods and arguments for the components which are add-able to

  • ComponentModel

    The ComponentModel class provides the general methods used for modeling the components.

  • Conversion

    A Conversion component converts commodities into each other.

  • ConversionDynamic

    Extension of the conversion class with more specific ramping behavior.

  • ConversionPartLoad

    A ConversionPartLoad component maps the (nonlinear) part-load behavior of a Conversion component.

  • EnergySystemModel

    EnergySystemModel class.

  • ImplementedSolvers

    Implemented solvers.

  • LinearOptimalPowerFlow

    A LinearOptimalPowerFlow component shows the behavior of a Transmission component but additionally models a

  • Sink

    A Sink component can transfer a commodity over the energy system boundary out of the system.

  • Source

    A Source component can transfer a commodity over the energy system boundary into the system.

  • Storage

    A Storage component can store a commodity and thus transfers it between time steps.

  • Transmission

    A Transmission component can transmit a commodity between locations of the energy system.

Functions:

Component

Component(
    esM,
    name,
    dimension,
    hasCapacityVariable,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    yearlyFullLoadHoursMin=None,
    yearlyFullLoadHoursMax=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
    pwlcfParameters=None,
)

The Component class includes the general methods and arguments for the components which are add-able to the energy system model (e.g. storage component, source component, transmission component). Every of these components inherits from the Component class.

Create an instance of the Component class.

Required arguments:

:param esM: energy system model to which the component should be added. Used for unit checks. :type esM: EnergySystemModel instance from the FINE package

:param name: name of the component. Has to be unique (i.e. no other components with that name can already exist in the EnergySystemModel instance to which the component is added). :type name: string

:param hasCapacityVariable: specifies if the component should be modeled with a capacity or not. Examples:

* An electrolyzer has a capacity given in GW_electric -> hasCapacityVariable is True.
* In the energy system, biogas can, from a model perspective, be converted into methane (and then
  used in conventional power plants which emit CO2) by getting CO2 from the environment. Thus,
  using biogas in conventional power plants is, from a balance perspective, CO2 free. This
  conversion is purely theoretical and does not require a capacity -> hasCapacityVariable
  is False.
* A electricity cable has a capacity given in GW_electric -> hasCapacityVariable is True.
* If the transmission capacity of a component is unlimited -> hasCapacityVariable is False.
* A wind turbine has a capacity given in GW_electric -> hasCapacityVariable is True.
* Emitting CO2 into the environment is not per se limited by a capacity ->
  hasCapacityVariable is False.

:type hasCapacityVariable: boolean

Default arguments:

:param capacityVariableDomain: describes the mathematical domain of the capacity variables, if they are specified. By default, the domain is specified as 'continuous' and thus declares the variables as positive (>=0) real values. The second input option that is available for this parameter is 'discrete', which declares the variables as positive (>=0) integer values. |br| * the default value is 'continuous' :type capacityVariableDomain: string ('continuous' or 'discrete')

:param capacityPerPlantUnit: capacity of one plant of the component (in the specified physicalUnit of the plant). The default is 1, thus the number of plants is equal to the installed capacity. This parameter should be specified when using a 'discrete' capacityVariableDomain. It can be specified when using a 'continuous' variable domain. |br| * the default value is 1 :type capacityPerPlantUnit: dict of strictly positive float or strictly positive float

:param hasIsBuiltBinaryVariable: specifies if binary decision variables should be declared for

* each eligible location of the component, which indicates if the component is built at that location or
  not (dimension=1dim).
* each eligible connection of the transmission component, which indicates if the component is built
  between two locations or not (dimension=2dim).

The binary variables can be used to enforce one-time investment cost or capacity-independent
annual operation cost. If a minimum capacity is specified and this parameter is set to True,
the minimum capacities are only considered if a component is built (i.e. if a component is built
at that location, it has to be built with a minimum capacity of XY GW, otherwise it is set to 0 GW).
|br| * the default value is False

:type hasIsBuiltBinaryVariable: boolean

:param bigM: the bigM parameter is only required when the hasIsBuiltBinaryVariable parameter is set to True. In that case, it is set as a strictly positive float, otherwise it can remain a None value. If not None and the ifBuiltBinaryVariables parameter is set to True, the parameter enforces an artificial upper bound on the maximum capacities which should, however, never be reached. The value should be chosen as small as possible but as large as necessary so that the optimal values of the designed capacities are well below this value after the optimization. |br| * the default value is None :type bigM: None or strictly positive float

:param locationalEligibility:

* Pandas Series that indicates if a component can be built at a location (=1) or not (=0)
  (dimension=1dim) or
* Pandas Series or DataFrame that indicates if a component can be built between two
  locations (=1) or not (=0) (dimension=2dim).

If not specified and a maximum or fixed capacity or time series is given, the parameter will be
set based on these inputs. If the parameter is specified, a consistency check is done to ensure
that the parameters indicate the same locational eligibility. If the parameter is not specified,
and also no other of the parameters is specified, it is assumed that the component is eligible in
each location and all values are set to 1.
This parameter is the key part for ensuring small built times of the optimization problem by avoiding the
declaration of unnecessary variables and constraints.
|br| * the default value is None

:type locationalEligibility:

* None or
* Pandas Series with values equal to 0 and 1. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with values equal to 0 and 1. The column and row indices of the DataFrame have
  to equal the in the energy system model specified locations.

:param capacityMin: if specified, indicates the minimum capacities. The type of this parameter depends on the dimension of the component: If dimension=1dim, it has to be a Pandas Series. If dimension=2dim, it has to be a Pandas Series or DataFrame. If binary decision variables are declared, capacityMin is only used if the component is built. |br| * the default value is None :type capacityMin:

* None or
* float or
* int or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations. or
* Dict with investment periods as keys and one of the options above as values.

:param capacityMax: if specified, indicates the maximum capacities. The type of this parameter depends on the dimension of the component: If dimension=1dim, it has to be a Pandas Series. If dimension=2dim, it has to be a Pandas Series or DataFrame. |br| * the default value is None :type capacityMax:

* None or
* float or
* int or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations. or
* Dict with investment periods as keys and one of the options above as values.

:param partLoadMin: If specified, it defines the lowest relative operation rate a component must maintain during operation. To still allow the component to be completely turned off, a binary variable is introduced for each time step. This enables the model to choose between zero operation or operation at or above the specified minimum load. Note: Adding these binary variables turns the problem into a MILP, which can significantly increase computational time. |br| * the default value is None :type partLoadMin: * None or * Float value in range ]0;1] * Dict with keys of investment periods and float values in range ]0;1]

:param sharedPotentialID: if specified, indicates that the component has to share its maximum potential capacity with other components (e.g. due to space limitations). The shares of how much of the maximum potential is used have to add up to less than 100%. |br| * the default value is None :type sharedPotentialID: string

:param linkedQuantityID: if specified, indicates that the components with the same ID are built with the same number. (e.g. if a vehicle with an engine is built also a storage needs to be built) |br| * the default value is None :type linkedQuantityID: string

:param capacityFix: if specified, indicates the fixed capacities. The type of this parameter depends on the dimension of the component: * If dimension=1dim, it has to be a Pandas Series. * If dimension=2dim, it has to be a Pandas Series or DataFrame. |br| * the default value is None :type capacityFix: * None or * float or * int or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations (dimension=1dim) or connections between these locations in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. or * Dict with investment periods as keys and one of the options above as values.

:param commissioningMin: if specified, indicates the minimum commissioning for the respective investment period. The type of this parameter depends on the dimension of the component: * If dimension=1dim, it has to be a Pandas Series. * If dimension=2dim, it has to be a Pandas Series or DataFrame. If binary decision variables are declared, commissioningMin is only used if the component is built. |br| * the default value is None :type commissioningMin:

* None or
* float or
* int or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations. or
* Dict with investment periods as keys and one of the options above as values.

:param commissioningMax: if specified, indicates the maximum commissioning for the respective investment period. The type of this parameter depends on the dimension of the component: * If dimension=1dim, it has to be a Pandas Series. * If dimension=2dim, it has to be a Pandas Series or DataFrame. |br| * the default value is None :type commissioningMax:

* None or
* float or
* int or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations. or
* Dict with investment periods as keys and one of the options above as values.

:param commissioningFix: if specified, indicates the fixed commissioning for the respective investment period. The type of this parameter depends on the dimension of the component: * If dimension=1dim, it has to be a Pandas Series. * If dimension=2dim, it has to be a Pandas Series or DataFrame. |br| * the default value is None :type commissioningFix: * None or * float or * int or * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations (dimension=1dim) or connections between these locations in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. or * Dict with investment periods as keys and one of the options above as values.

:param isBuiltFix: if specified, indicates fixed decisions in which or between which locations the component is built (i.e. sets the isBuilt binary variables). The type of this parameter depends on the dimension of the component: * If dimension=1dim, it has to be a Pandas Series. * If dimension=2dim, it has to be a Pandas Series or DataFrame. |br| * the default value is None :type isBuiltFix: * None or * Pandas Series with values equal to 0 and 1. The indices of the series have to equal the in the energy system model specified locations (dimension=1dim) or connections between these locations in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or * Pandas DataFrame with values equal to 0 and 1. The row and column indices of the DataFrame have to equal the in the energy system model specified locations.

:param investPerCapacity: describes the investment costs for one unit of the capacity. The invest of a component is obtained by multiplying the commissioned capacities of the component (in the physicalUnit of the component) with the investPerCapacity factor and is distributed over the components technical lifetime. The value has to match the unit costUnit/physicalUnit (e.g. Euro/kW). The investPerCapacity can either be given as

* a float or a Pandas Series with location specific values (dimension=1dim). The cost unit in which the
  parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
  1e6 Euro). The value has to match the unit
  costUnit/physicalUnit (e.g. Euro/kW, 1e6 Euro/GW) or
* a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
  in which the parameter is given has to match the one specified in the energy system model divided by
  the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km). The value has to match the unit
  costUnit/(lengthUnit * physicalUnit) (e.g. Euro/(kW * m), 1e6 Euro/(GW * km))
* a dictionary with years as keys (past years which had stock commissioning and investment periods which
  will be optimized) and one of the two options above as values.
  e.g. {2020: 1000, 2025: 800, 2030: 750}

|br| * the default value is 0

:type investPerCapacity:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.
* Dict with years as keys (past years with stock commissioning and investment periods which will be
  optimized) and one of the two options above as values.

:param investIfBuilt: a capacity-independent invest which only arises in a location if a component is built at that location. The investIfBuilt can either be given as

* a float or a Pandas Series with location specific values (dimension=1dim). The cost unit in which
  the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
  1e6 Euro) or
* a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
  in which the parameter is given has to match the one specified in the energy system model divided by
  the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km)
* a dictionary with years as keys (past years which had stock commissioning and investment periods which
  will be optimized) and one of the two options above as values.
  e.g. {2020: 1000, 2025: 800, 2030: 750}

|br| * the default value is 0

:type investIfBuilt:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.
* Dict with years as keys (past years with stock commissioning and investment periods which will be
  optimized) and one of the two options above as values.

:param opexPerCapacity: describes the operational cost for one unit of capacity. The annual operational cost, which are only a function of the capacity of the component (in the physicalUnit of the component) and not of the specific operation itself, are obtained by multiplying the commissioned capacity of the component at a location with the opexPerCapacity factor and is distributed over the components technical lifetime. The opexPerCapacity factor can either be given as

* a float or a Pandas Series with location specific values (dimension=1dim). The cost unit in which the
  parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
  1e6 Euro). The value has to match the unit
  costUnit/physicalUnit (e.g. Euro/kW, 1e6 Euro/GW)  or
* a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
  in which the parameter is given has to match the one specified in the energy system model divided by
  the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km). The value has to match the unit
  costUnit/(lengthUnit * physicalUnit) (e.g. Euro/(kW * m), 1e6 Euro/(GW * km))
* a dict with years as keys (past years which had stock commissioning and investment periods which
  will be optimized) and one of the two options above as value.
  e.g. {2020: 1000, 2025: 800, 2030: 750}

|br| * the default value is 0

:type opexPerCapacity:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.
* Dict with years as keys (past years with stock commissioning and investment periods which will be
  optimized) and one of the two options above as values.

:param opexIfBuilt: a capacity-independent annual operational cost which only arises in a location if a component is commissioned at that location. The costs are than distributed over the components technical lifetime.The opexIfBuilt can either be given as

* a float or a Pandas Series with location specific values (dimension=1dim) . The cost unit in which
  the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
  1e6 Euro) or
* a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
  in which the parameter is given has to match the one specified in the energy system model divided by
  the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km).
* a dict with years as keys (past years which had stock commissioning and investment periods which
  will be optimized) and one of the two options above as value.
  e.g. {2020: 1000, 2025: 800, 2030: 750}

|br| * the default value is 0

:type opexIfBuilt:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.
* Dict with years as keys (past years with stock commissioning and investment periods which will be
  optimized) and one of the two options above as values.

:param QPcostScale: describes the absolute deviation of the minimum or maximum cost value from the average or weighted average cost value. For further information see Lopion et al. (2019): "Cost Uncertainties in Energy System Optimization Models: A Quadratic Programming Approach for Avoiding Penny Switching Effects". |br| * the default value is 0, i.e. the problem is not quadratic. :type QPcostScale:

* float between 0 and 1
* Pandas Series with positive (0 <= QPcostScale <= 1) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (0 <= QPcostScale <= 1) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.
* Dict with years as keys (past years with stock commissioning and investment period which will be
  optimized) and one of the options above as value

:param interestRate: interest rate which is considered for computing the annuities of the invest of the component (depreciates the invests over the economic lifetime). A value of 0.08 corresponds to an interest rate of 8%. The interest rate is currently constant for all investment periods. Warning: The interest must be greater than 0 if annuityPerpetuity is used in the energy system model. |br| * the default value is 0.08 :type interestRate:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.

:param economicLifetime: economic lifetime of the component which is considered for computing the annuities of the invest of the component (aka depreciation time). The economic lifetime is currently constant over the pathway of investment periods. |br| * the default value is 10 :type economicLifetime:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.

:param technicalLifetime: technical lifetime of the component which is considered for computing the stocks. The technical lifetime is currently constant over the pathway of investment periods. |br| * the default value is None :type technicalLifetime:

* None or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
* Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
  to equal the in the energy system model specified locations.

:param yearlyFullLoadHoursMin: if specified, indicates the minimum yearly full load hours. |br| * the default value is None :type yearlyFullLoadHoursMin:

* None or
* Float with positive (>=0) value or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim).
* Dict with years as keys and one of the two options above as values.

:param yearlyFullLoadHoursMax: if specified, indicates the maximum yearly full load hours. |br| * the default value is None :type yearlyFullLoadHoursMax:

* None or
* Float with positive (>=0) value or
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
  energy system model specified locations (dimension=1dim) or connections between these locations
  in the format of 'loc1' + '_' + 'loc2' (dimension=2dim).
* Dict with years as keys and one of the two options above as values.

:param stockCommissioning: if specified, indictates historical commissioned capacities. The parameter describes, how much capacity was commissioned per location in which past investment period. The past investment period is not part of the optimized investment periods.

* e.g. if startYear is 2020:
  {2016:pandas.series(index=["loc1","loc2"],data=[4,3]).
  2018: pandas.series(index=["loc1","loc2"],data=[1,2])}
* e.g. if startYear is 0:
  {-4:pandas.series(index=["loc1","loc2"],data=[4,3]).
  -2: pandas.series(index=["loc1","loc2"],data=[1,2])}

Warning: Commissioning years older than the technical lifetime from startYear will be ignored.
|br| * the default value is None

:type stockCommissioning:

* None or
* Dict with past years as keys and pandas.Series with index of locations as values

:param modelingClass: to the Component connected modeling class. |br| * the default value is ModelingClass :type modelingClass: a class inheriting from ComponentModeling

:param floorTechnicalLifetime: if a technical lifetime is not a multiple of the interval, this parameters decides if the technical lifetime is floored to the interval or ceiled to the next interval, by default True. The costs will then be applied to the corrected interval.

:param pwlcfParameters: parameters used for piecewise linear cost function module. Can be used to approximate non-linear cost functions for endogenous technology learning (etl) or economies of scale (eos). Enables a standardized endogenous technological learning approach with a fixed learning rate. In that case, the learning is conducted in each investment period and connected throughout. Alternatively enables an economies of scale approach. In that case, the cost scaling is indepent in each investment period.

Example: For etl, the cost reduce with the total cumulative installed capacity via a learning curve approach which is linearized.
pwlcfParameters = {
    "etlParameters": {
        "initCost": 1,
        "learningRate": 0.18,
        "initCapacity": 10,
        "maxCapacity": 50,
        "noSegments": 4,
    }
Example: For eos, the cost of a specific component (at one location and in one investment period) decreases with increased plant size.
pwlcfParameters = {
    "eosParameters": pd.DataFrame(data=np.array([[0,1,2,3],[0,1000, 1800, 2400],[0, 10, 18, 24]]).T, columns=["capacity", "totalInvest", "totalOpex"])
}

:type pwlcfParameters: dict

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Get

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Set

  • setTimeSeriesData

    Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Sets

Source code in fine/component.py
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
463
464
465
466
467
468
469
470
471
472
473
474
475
476
477
478
479
480
481
482
483
484
485
486
487
488
489
490
491
492
493
494
495
496
497
498
499
500
501
502
503
504
505
506
507
508
509
510
511
512
513
514
515
516
517
518
519
520
521
522
523
524
525
526
527
528
529
530
531
532
533
534
535
536
537
538
539
540
541
542
543
544
545
546
547
548
549
550
551
552
553
554
555
556
557
558
559
560
561
562
563
564
565
566
567
568
569
570
571
572
573
574
575
576
577
578
579
580
581
582
583
584
585
586
587
588
589
590
591
592
593
594
595
596
597
598
599
600
601
602
603
604
605
606
607
608
609
610
611
612
613
614
615
616
617
618
619
620
621
622
623
624
625
626
627
628
629
630
631
632
633
634
635
636
637
638
639
640
641
642
643
644
645
646
647
648
649
650
651
652
653
654
655
656
657
658
659
660
661
662
663
664
665
666
667
668
669
670
671
672
673
674
675
676
677
678
679
680
681
682
683
684
685
686
687
688
689
690
691
692
693
694
695
696
697
698
699
700
701
702
703
704
705
706
707
708
709
710
711
712
713
714
715
716
717
718
719
720
721
722
723
def __init__(
    self,
    esM,
    name,
    dimension,
    hasCapacityVariable,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    yearlyFullLoadHoursMin=None,
    yearlyFullLoadHoursMax=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
    pwlcfParameters=None,
):
    """Create an instance of the Component class.

    **Required arguments:**

    :param esM: energy system model to which the component should be added. Used for unit checks.
    :type esM: EnergySystemModel instance from the FINE package

    :param name: name of the component. Has to be unique (i.e. no other components with that name can
        already exist in the EnergySystemModel instance to which the component is added).
    :type name: string

    :param hasCapacityVariable: specifies if the component should be modeled with a capacity or not. Examples:

        * An electrolyzer has a capacity given in GW_electric -> hasCapacityVariable is True.
        * In the energy system, biogas can, from a model perspective, be converted into methane (and then
          used in conventional power plants which emit CO2) by getting CO2 from the environment. Thus,
          using biogas in conventional power plants is, from a balance perspective, CO2 free. This
          conversion is purely theoretical and does not require a capacity -> hasCapacityVariable
          is False.
        * A electricity cable has a capacity given in GW_electric -> hasCapacityVariable is True.
        * If the transmission capacity of a component is unlimited -> hasCapacityVariable is False.
        * A wind turbine has a capacity given in GW_electric -> hasCapacityVariable is True.
        * Emitting CO2 into the environment is not per se limited by a capacity ->
          hasCapacityVariable is False.

    :type hasCapacityVariable: boolean

    **Default arguments:**

    :param capacityVariableDomain: describes the mathematical domain of the capacity variables, if they are
        specified. By default, the domain is specified as 'continuous' and thus declares the variables as positive
        (>=0) real values. The second input option that is available for this parameter is 'discrete', which
        declares the variables as positive (>=0) integer values.
        |br| * the default value is 'continuous'
    :type capacityVariableDomain: string ('continuous' or 'discrete')

    :param capacityPerPlantUnit: capacity of one plant of the component (in the specified physicalUnit of
        the plant). The default is 1, thus the number of plants is equal to the installed capacity.
        This parameter should be specified when using a 'discrete' capacityVariableDomain.
        It can be specified when using a 'continuous' variable domain.
        |br| * the default value is 1
    :type capacityPerPlantUnit: dict of strictly positive float or strictly positive float

    :param hasIsBuiltBinaryVariable: specifies if binary decision variables should be declared for

        * each eligible location of the component, which indicates if the component is built at that location or
          not (dimension=1dim).
        * each eligible connection of the transmission component, which indicates if the component is built
          between two locations or not (dimension=2dim).

        The binary variables can be used to enforce one-time investment cost or capacity-independent
        annual operation cost. If a minimum capacity is specified and this parameter is set to True,
        the minimum capacities are only considered if a component is built (i.e. if a component is built
        at that location, it has to be built with a minimum capacity of XY GW, otherwise it is set to 0 GW).
        |br| * the default value is False
    :type hasIsBuiltBinaryVariable: boolean

    :param bigM: the bigM parameter is only required when the hasIsBuiltBinaryVariable parameter is set to
        True. In that case, it is set as a strictly positive float, otherwise it can remain a None value.
        If not None and the ifBuiltBinaryVariables parameter is set to True, the parameter enforces an
        artificial upper bound on the maximum capacities which should, however, never be reached. The value
        should be chosen as small as possible but as large as necessary so that the optimal values of the
        designed capacities are well below this value after the optimization.
        |br| * the default value is None
    :type bigM: None or strictly positive float

    :param locationalEligibility:

        * Pandas Series that indicates if a component can be built at a location (=1) or not (=0)
          (dimension=1dim) or
        * Pandas Series or DataFrame that indicates if a component can be built between two
          locations (=1) or not (=0) (dimension=2dim).

        If not specified and a maximum or fixed capacity or time series is given, the parameter will be
        set based on these inputs. If the parameter is specified, a consistency check is done to ensure
        that the parameters indicate the same locational eligibility. If the parameter is not specified,
        and also no other of the parameters is specified, it is assumed that the component is eligible in
        each location and all values are set to 1.
        This parameter is the key part for ensuring small built times of the optimization problem by avoiding the
        declaration of unnecessary variables and constraints.
        |br| * the default value is None
    :type locationalEligibility:

        * None or
        * Pandas Series with values equal to 0 and 1. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with values equal to 0 and 1. The column and row indices of the DataFrame have
          to equal the in the energy system model specified locations.

    :param capacityMin: if specified, indicates the minimum capacities. The type of this parameter depends on the
        dimension of the component: If dimension=1dim, it has to be a Pandas Series. If dimension=2dim, it has to
        be a Pandas Series or DataFrame. If binary decision variables are declared, capacityMin is only used
        if the component is built.
        |br| * the default value is None
    :type capacityMin:

        * None or
        * float or
        * int or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations. or
        * Dict with investment periods as keys and one of the options above as values.

    :param capacityMax: if specified, indicates the maximum capacities. The type of this parameter depends on the
        dimension of the component: If dimension=1dim, it has to be a Pandas Series. If dimension=2dim, it has to
        be a Pandas Series or DataFrame.
        |br| * the default value is None
    :type capacityMax:

        * None or
        * float or
        * int or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations. or
        * Dict with investment periods as keys and one of the options above as values.

    :param partLoadMin: If specified, it defines the lowest relative operation
        rate a component must maintain during operation. To still allow the component
        to be completely turned off, a binary variable is introduced for each time
        step. This enables the model to choose between zero operation or operation
        at or above the specified minimum load.
        Note: Adding these binary variables turns the problem into a MILP, which
        can significantly increase computational time.
        |br| * the default value is None
    :type partLoadMin:
        * None or
        * Float value in range ]0;1]
        * Dict with keys of investment periods and float values in range ]0;1]

    :param sharedPotentialID: if specified, indicates that the component has to share its maximum
        potential capacity with other components (e.g. due to space limitations). The shares of how
        much of the maximum potential is used have to add up to less than 100%.
        |br| * the default value is None
    :type sharedPotentialID: string

    :param linkedQuantityID: if specified, indicates that the components with the same ID are built with the same number.
        (e.g. if a vehicle with an engine is built also a storage needs to be built)
        |br| * the default value is None
    :type linkedQuantityID: string

    :param capacityFix: if specified, indicates the fixed capacities. The type of this parameter
        depends on the dimension of the component:
        * If dimension=1dim, it has to be a Pandas Series.
        * If dimension=2dim, it has to be a Pandas Series or DataFrame.
        |br| * the default value is None
    :type capacityFix:
        * None or
        * float or
        * int or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations. or
        * Dict with investment periods as keys and one of the options above as values.

    :param commissioningMin: if specified, indicates the minimum commissioning for the respective
        investment period. The type of this parameter depends on the dimension of the component:
        * If dimension=1dim, it has to be a Pandas Series.
        * If dimension=2dim, it has to be a Pandas Series or DataFrame.
        If binary decision variables are declared, commissioningMin is only used
        if the component is built.
        |br| * the default value is None
    :type commissioningMin:

        * None or
        * float or
        * int or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations. or
        * Dict with investment periods as keys and one of the options above as values.

    :param commissioningMax: if specified, indicates the maximum commissioning for the respective
        investment period. The type of this parameter depends on the dimension of the component:
        * If dimension=1dim, it has to be a Pandas Series.
        * If dimension=2dim, it has to be a Pandas Series or DataFrame.
        |br| * the default value is None
    :type commissioningMax:

        * None or
        * float or
        * int or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations. or
        * Dict with investment periods as keys and one of the options above as values.

    :param commissioningFix: if specified, indicates the fixed commissioning for the respective
        investment period. The type of this parameter depends on the dimension of the component:
        * If dimension=1dim, it has to be a Pandas Series.
        * If dimension=2dim, it has to be a Pandas Series or DataFrame.
        |br| * the default value is None
    :type commissioningFix:
        * None or
        * float or
        * int or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations. or
        * Dict with investment periods as keys and one of the options above as values.

    :param isBuiltFix: if specified, indicates fixed decisions in which or between which locations the component is
        built (i.e. sets the isBuilt binary variables). The type of this parameter
        depends on the dimension of the component:
        * If dimension=1dim, it has to be a Pandas Series.
        * If dimension=2dim, it has to be a Pandas Series or DataFrame.
        |br| * the default value is None
    :type isBuiltFix:
        * None or
        * Pandas Series with values equal to 0 and 1. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with values equal to 0 and 1. The row and column indices of the DataFrame
          have to equal the in the energy system model specified locations.

    :param investPerCapacity: describes the investment costs for one unit of the capacity. The
        invest of a component is obtained by multiplying the commissioned capacities
        of the component (in the physicalUnit of the component) with the investPerCapacity factor
        and is distributed over the components technical lifetime.
        The value has to match the unit costUnit/physicalUnit (e.g. Euro/kW).
        The investPerCapacity can either be given as

        * a float or a Pandas Series with location specific values (dimension=1dim). The cost unit in which the
          parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
          1e6 Euro). The value has to match the unit
          costUnit/physicalUnit (e.g. Euro/kW, 1e6 Euro/GW) or
        * a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
          in which the parameter is given has to match the one specified in the energy system model divided by
          the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km). The value has to match the unit
          costUnit/(lengthUnit * physicalUnit) (e.g. Euro/(kW * m), 1e6 Euro/(GW * km))
        * a dictionary with years as keys (past years which had stock commissioning and investment periods which
          will be optimized) and one of the two options above as values.
          e.g. {2020: 1000, 2025: 800, 2030: 750}

        |br| * the default value is 0
    :type investPerCapacity:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.
        * Dict with years as keys (past years with stock commissioning and investment periods which will be
          optimized) and one of the two options above as values.

    :param investIfBuilt: a capacity-independent invest which only arises in a location if a component
        is built at that location. The investIfBuilt can either be given as

        * a float or a Pandas Series with location specific values (dimension=1dim). The cost unit in which
          the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
          1e6 Euro) or
        * a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
          in which the parameter is given has to match the one specified in the energy system model divided by
          the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km)
        * a dictionary with years as keys (past years which had stock commissioning and investment periods which
          will be optimized) and one of the two options above as values.
          e.g. {2020: 1000, 2025: 800, 2030: 750}

        |br| * the default value is 0
    :type investIfBuilt:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.
        * Dict with years as keys (past years with stock commissioning and investment periods which will be
          optimized) and one of the two options above as values.

    :param opexPerCapacity: describes the operational cost for one unit of capacity. The annual operational cost,
        which are only a function of the capacity of the component (in the physicalUnit of the component) and not
        of the specific operation itself, are obtained by multiplying the commissioned capacity of the component
        at a location with the opexPerCapacity factor and is distributed over the components technical lifetime.
        The opexPerCapacity factor can either be given as

        * a float or a Pandas Series with location specific values (dimension=1dim). The cost unit in which the
          parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
          1e6 Euro). The value has to match the unit
          costUnit/physicalUnit (e.g. Euro/kW, 1e6 Euro/GW)  or
        * a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
          in which the parameter is given has to match the one specified in the energy system model divided by
          the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km). The value has to match the unit
          costUnit/(lengthUnit * physicalUnit) (e.g. Euro/(kW * m), 1e6 Euro/(GW * km))
        * a dict with years as keys (past years which had stock commissioning and investment periods which
          will be optimized) and one of the two options above as value.
          e.g. {2020: 1000, 2025: 800, 2030: 750}

        |br| * the default value is 0
    :type opexPerCapacity:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.
        * Dict with years as keys (past years with stock commissioning and investment periods which will be
          optimized) and one of the two options above as values.

    :param opexIfBuilt: a capacity-independent annual operational cost which only arises in a location
        if a component is commissioned at that location. The costs are than distributed over the components
        technical lifetime.The opexIfBuilt can either be given as

        * a float or a Pandas Series with location specific values (dimension=1dim) . The cost unit in which
          the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar,
          1e6 Euro) or
        * a float or a Pandas Series or DataFrame with location specific values (dimension=2dim). The cost unit
          in which the parameter is given has to match the one specified in the energy system model divided by
          the specified lengthUnit (e.g. Euro/m, Dollar/m, 1e6 Euro/km).
        * a dict with years as keys (past years which had stock commissioning and investment periods which
          will be optimized) and one of the two options above as value.
          e.g. {2020: 1000, 2025: 800, 2030: 750}

        |br| * the default value is 0
    :type opexIfBuilt:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.
        * Dict with years as keys (past years with stock commissioning and investment periods which will be
          optimized) and one of the two options above as values.

    :param QPcostScale: describes the absolute deviation of the minimum or maximum cost value from
        the average or weighted average cost value. For further information see
        Lopion et al. (2019): "Cost Uncertainties in Energy System Optimization Models:
        A Quadratic Programming Approach for Avoiding Penny Switching Effects".
        |br| * the default value is 0, i.e. the problem is not quadratic.
    :type QPcostScale:

        * float between 0 and 1
        * Pandas Series with positive (0 <= QPcostScale <= 1) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (0 <= QPcostScale <= 1) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.
        * Dict with years as keys (past years with stock commissioning and investment period which will be
          optimized) and one of the options above as value

    :param interestRate: interest rate which is considered for computing the annuities of the invest
        of the component (depreciates the invests over the economic lifetime).
        A value of 0.08 corresponds to an interest rate of 8%.
        The interest rate is currently constant for all investment periods.
        Warning: The interest must be greater than 0 if annuityPerpetuity is used in the energy system model.
        |br| * the default value is 0.08
    :type interestRate:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.

    :param economicLifetime: economic lifetime of the component which is considered for computing the
        annuities of the invest of the component (aka depreciation time).
        The economic lifetime is currently constant over the pathway of investment periods.
        |br| * the default value is 10
    :type economicLifetime:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.

    :param technicalLifetime: technical lifetime of the component which is considered for computing the
        stocks. The technical lifetime is currently constant over the pathway of investment periods.
        |br| * the default value is None
    :type technicalLifetime:

        * None or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim) or
        * Pandas DataFrame with positive (>=0) values. The row and column indices of the DataFrame have
          to equal the in the energy system model specified locations.

    :param yearlyFullLoadHoursMin: if specified, indicates the minimum yearly full load hours.
        |br| * the default value is None
    :type yearlyFullLoadHoursMin:

        * None or
        * Float with positive (>=0) value or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim).
        * Dict with years as keys and one of the two options above as values.

    :param yearlyFullLoadHoursMax: if specified, indicates the maximum yearly full load hours.
        |br| * the default value is None
    :type yearlyFullLoadHoursMax:

        * None or
        * Float with positive (>=0) value or
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the
          energy system model specified locations (dimension=1dim) or connections between these locations
          in the format of 'loc1' + '_' + 'loc2' (dimension=2dim).
        * Dict with years as keys and one of the two options above as values.

    :param stockCommissioning: if specified, indictates historical commissioned capacities.
        The parameter describes, how much capacity was commissioned per location in which past
        investment period. The past investment period is not part of the optimized investment periods.

        * e.g. if startYear is 2020:
          {2016:pandas.series(index=["loc1","loc2"],data=[4,3]).
          2018: pandas.series(index=["loc1","loc2"],data=[1,2])}
        * e.g. if startYear is 0:
          {-4:pandas.series(index=["loc1","loc2"],data=[4,3]).
          -2: pandas.series(index=["loc1","loc2"],data=[1,2])}

        Warning: Commissioning years older than the technical lifetime from startYear will be ignored.
        |br| * the default value is None
    :type stockCommissioning:

        * None or
        * Dict with past years as keys and pandas.Series with index of locations as values

    :param modelingClass: to the Component connected modeling class.
        |br| * the default value is ModelingClass
    :type modelingClass: a class inheriting from ComponentModeling

    :param floorTechnicalLifetime: if a technical lifetime is not a multiple of the interval, this
        parameters decides if the technical lifetime is floored to the interval or ceiled to the next interval,
        by default True. The costs will then be applied to the corrected interval.

    :param pwlcfParameters: parameters used for piecewise linear cost function module. Can be used to approximate non-linear cost functions for endogenous technology learning (etl) or economies of scale (eos).
            Enables a standardized endogenous technological learning approach with a fixed learning rate. In that case, the learning is conducted in each investment period and connected throughout.
            Alternatively enables an economies of scale approach. In that case, the cost scaling is indepent in each investment period.

        Example: For etl, the cost reduce with the total cumulative installed capacity via a learning curve approach which is linearized.
        pwlcfParameters = {
            "etlParameters": {
                "initCost": 1,
                "learningRate": 0.18,
                "initCapacity": 10,
                "maxCapacity": 50,
                "noSegments": 4,
            }
        Example: For eos, the cost of a specific component (at one location and in one investment period) decreases with increased plant size.
        pwlcfParameters = {
            "eosParameters": pd.DataFrame(data=np.array([[0,1,2,3],[0,1000, 1800, 2400],[0, 10, 18, 24]]).T, columns=["capacity", "totalInvest", "totalOpex"])
        }
    :type pwlcfParameters: dict
    """
    # Set general component data
    utils.isEnergySystemModelInstance(esM)
    self.name = name
    self.dimension = dimension
    self.modelingClass = ComponentModel

    self.hasCapacityVariable = hasCapacityVariable
    self.capacityVariableDomain = capacityVariableDomain
    self.capacityPerPlantUnit = capacityPerPlantUnit
    self.processedCapacityPerPlantUnit = (
        utils.checkAndSetInvestmentPeriodParameters(
            "capacityPerPlantUnit",
            capacityPerPlantUnit,
            esM,
        )
    )

    self.hasIsBuiltBinaryVariable = hasIsBuiltBinaryVariable
    self.bigM = bigM

    # Set design variable modeling parameters
    utils.checkDesignVariableModelingParameters(
        esM,
        capacityVariableDomain,
        hasCapacityVariable,
        self.processedCapacityPerPlantUnit,
        hasIsBuiltBinaryVariable,
        bigM,
    )

    self.partLoadMin = partLoadMin

    # Set economic data
    self.economicLifetime = utils.checkAndSetCostParameter(
        esM, name, economicLifetime, dimension, locationalEligibility
    )
    technicalLifetime = utils.checkTechnicalLifetime(
        esM, technicalLifetime, economicLifetime
    )
    self.technicalLifetime = utils.checkAndSetCostParameter(
        esM, name, technicalLifetime, dimension, locationalEligibility
    )
    utils.checkEconomicAndTechnicalLifetime(
        self.economicLifetime, self.technicalLifetime
    )
    self.floorTechnicalLifetime = utils.checkFlooringParameter(
        floorTechnicalLifetime, self.technicalLifetime, esM.investmentPeriodInterval
    )
    self.ipTechnicalLifetime = utils.checkAndSetLifetimeInvestmentPeriod(
        esM, name, self.technicalLifetime
    )
    self.ipEconomicLifetime = utils.checkAndSetLifetimeInvestmentPeriod(
        esM, name, self.economicLifetime
    )

    self.stockYears, self.processedStockYears = utils.checkStockYears(
        stockCommissioning,
        esM.startYear,
        esM.investmentPeriodInterval,
        self.ipTechnicalLifetime,
    )
    # invest per capacity
    self.investPerCapacity = investPerCapacity
    self.processedInvestPerCapacity = (
        utils.checkAndSetInvestmentPeriodCostParameter(
            esM,
            name,
            investPerCapacity,
            dimension,
            locationalEligibility,
            self.processedStockYears + esM.investmentPeriods,
        )
    )
    # invest if built
    self.investIfBuilt = investIfBuilt
    self.processedInvestIfBuilt = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        investIfBuilt,
        dimension,
        locationalEligibility,
        self.processedStockYears + esM.investmentPeriods,
    )
    # opex per capacity
    self.opexPerCapacity = opexPerCapacity
    self.processedOpexPerCapacity = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        opexPerCapacity,
        dimension,
        locationalEligibility,
        self.processedStockYears + esM.investmentPeriods,
    )
    # opex if built
    self.opexIfBuilt = opexIfBuilt
    self.processedOpexIfBuilt = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        opexIfBuilt,
        dimension,
        locationalEligibility,
        self.processedStockYears + esM.investmentPeriods,
    )
    # QP costscale
    self.QPcostScale = QPcostScale
    self.processedQPcostScale = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        QPcostScale,
        dimension,
        locationalEligibility,
        self.processedStockYears + esM.investmentPeriods,
    )
    # interest rate
    self.interestRate = utils.checkAndSetCostParameter(
        esM, name, interestRate, dimension, locationalEligibility
    )

    self.CCF = utils.getCapitalChargeFactor(
        self.interestRate,
        self.economicLifetime,
        self.processedStockYears + esM.investmentPeriods,
    )

    # Set location-specific design parameters
    self.locationalEligibility = locationalEligibility
    self.sharedPotentialID = sharedPotentialID
    if str(type(self))[-14:-2] != "Transmission":
        self.capacityMin = capacityMin
        self.capacityMax = capacityMax
        self.capacityFix = capacityFix
        self.commissioningMin = commissioningMin
        self.commissioningMax = commissioningMax
        self.commissioningFix = commissioningFix
    (
        self.processedCapacityMin,
        self.processedCapacityMax,
        self.processedCapacityFix,
    ) = utils.checkAndSetBounds(
        esM, name, "capacity", capacityMin, capacityMax, capacityFix
    )
    (
        self.processedCommissioningMin,
        self.processedCommissioningMax,
        self.processedCommissioningFix,
    ) = utils.checkAndSetBounds(
        esM,
        name,
        "commissioning",
        commissioningMin,
        commissioningMax,
        commissioningFix,
    )

    self.linkedQuantityID = linkedQuantityID

    # Set yearly full load hour parameters
    self.yearlyFullLoadHoursMin = yearlyFullLoadHoursMin
    self.yearlyFullLoadHoursMax = yearlyFullLoadHoursMax
    self.processedYearlyFullLoadHoursMin = utils.checkAndSetFullLoadHoursParameter(
        esM, name, yearlyFullLoadHoursMin, dimension, locationalEligibility
    )
    self.processedYearlyFullLoadHoursMax = utils.checkAndSetFullLoadHoursParameter(
        esM, name, yearlyFullLoadHoursMax, dimension, locationalEligibility
    )

    self.isBuiltFix = isBuiltFix

    utils.checkLocationSpecficDesignInputParams(self, esM)

    # Set quadratic capacity bounds and residual cost scale (1-cost scale)
    self.QPbound = utils.getQPbound(
        self.processedStockYears + esM.investmentPeriods,
        self.processedQPcostScale,
        self.processedCapacityMax,
        self.processedCapacityMin,
    )
    self.QPcostDev = utils.getQPcostDev(
        self.processedStockYears + esM.investmentPeriods, self.processedQPcostScale
    )

    # stock commissioning
    self.stockCommissioning = stockCommissioning
    self.processedStockCommissioning = utils.checkAndSetStock(
        self, esM, stockCommissioning
    )
    self.stockCapacityStartYear = utils.setStockCapacityStartYear(
        self, esM, dimension
    )

    # check the capacity development with stock for mismatches
    utils.checkCapacityDevelopmentWithStock(
        esM.investmentPeriods,
        self.processedCapacityMax,
        self.processedCapacityFix,
        self.processedStockCommissioning,
        self.ipTechnicalLifetime,
        self.floorTechnicalLifetime,
    )

    self.pwlcfParameters = pwlcfParameters
    self.pwlcf = None
    if pwlcfParameters and not all(
        param is None for param in pwlcfParameters.values()
    ):
        pwlcfModule = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModule
        self.pwlcf = pwlcfModule(self, esM, **pwlcfParameters)

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation abstractmethod

getDataForTimeSeriesAggregation(ip)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Get all time series data of a component for time series aggregation.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/component.py
@abstractmethod
def getDataForTimeSeriesAggregation(self, ip):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Get
    all time series data of a component for time series aggregation.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    raise NotImplementedError

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData abstractmethod

setAggregatedTimeSeriesData(data, ip)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Set aggregated time series data after applying time series aggregation.

:param data: time series data :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/component.py
@abstractmethod
def setAggregatedTimeSeriesData(self, data, ip):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Set
    aggregated time series data after applying time series aggregation.

    :param data: time series data
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    raise NotImplementedError

setTimeSeriesData abstractmethod

setTimeSeriesData(hasTSA)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Sets the time series data of a component (either the full time series if hasTSA is false or the aggregated time series if hasTSA is True).

:param hasTSA: indicates if time series aggregation should be considered for modeling :type hasTSA: boolean

Source code in fine/component.py
@abstractmethod
def setTimeSeriesData(self, hasTSA):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Sets
    the time series data of a component (either the full time series if hasTSA is false or the aggregated
    time series if hasTSA is True).

    :param hasTSA: indicates if time series aggregation should be considered for modeling
    :type hasTSA: boolean
    """
    raise NotImplementedError

ComponentModel

ComponentModel()

The ComponentModel class provides the general methods used for modeling the components. Every model class of the several component technologies inherits from the ComponentModel class. Within the ComponentModel class, general valid sets, variables and constraints are declared.

Create a ComponentModel class instance.

Methods:

Source code in fine/component.py
def __init__(self):
    """Create a ComponentModel class instance."""
    self.abbrvName = ""
    self.dimension = ""
    self.componentsDict = {}
    self._capacityVariablesOptimum = {}
    self._commissioningVariablesOptimum = {}
    self._decommissioningVariablesOptimum = {}
    self._isBuiltVariablesOptimum = {}
    self._optSummary = {}

additionalMinPartLoad

additionalMinPartLoad(
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opVarBinName,
    capVarName,
    isOperationCommisYearDepending=False,
)

Set, if applicable, the minimal part load of a component.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def additionalMinPartLoad(
    self,
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opVarBinName,
    capVarName,
    isOperationCommisYearDepending=False,
):
    """Set, if applicable, the minimal part load of a component.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    opVarBin = getattr(pyM, opVarBinName + "_" + abbrvName, None)

    # only create constraint when partLoadMin specified
    if opVarBin is not None:
        capVar = getattr(pyM, capVarName + "_" + abbrvName)
        commisVar = getattr(pyM, "commis_" + abbrvName)
        constrSetMinPartLoad = getattr(
            pyM, constrSetName + "partLoadMin_" + abbrvName
        )

        def getPartLoadMin(compDict, compName, ip):
            return getattr(compDict[compName], "processedPartLoadMin")[ip]

        def getBigM(compDict, compName):
            return getattr(compDict[compName], "bigM")

        if not pyM.hasSegmentation:
            if isOperationCommisYearDepending:

                def opMinPartLoad(pyM, loc, compName, commis, ip, p, t):
                    processedPartLoadMin = getPartLoadMin(compDict, compName, ip)
                    bigM = getBigM(compDict, compName)
                    return (
                        opVar[loc, compName, commis, ip, p, t]
                        >= processedPartLoadMin
                        * commisVar[loc, compName, commis]
                        * esM.hoursPerTimeStep
                        - (1 - opVarBin[loc, compName, commis, ip, p, t]) * bigM
                    )
            else:

                def opMinPartLoad(pyM, loc, compName, ip, p, t):
                    processedPartLoadMin = getPartLoadMin(compDict, compName, ip)
                    bigM = getBigM(compDict, compName)
                    return (
                        opVar[loc, compName, ip, p, t]
                        >= processedPartLoadMin
                        * capVar[loc, compName, ip]
                        * esM.hoursPerTimeStep
                        - (1 - opVarBin[loc, compName, ip, p, t]) * bigM
                    )
        elif isOperationCommisYearDepending:

            def opMinPartLoad(pyM, loc, compName, commis, ip, p, t):
                processedPartLoadMin = getPartLoadMin(compDict, compName, ip)
                bigM = getBigM(compDict, compName)
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    >= processedPartLoadMin
                    * commisVar[loc, compName, commis]
                    * esM.hoursPerSegment[ip][p, t]
                    - (1 - opVarBin[loc, compName, commis, ip, p, t]) * bigM
                )
        else:

            def opMinPartLoad(pyM, loc, compName, ip, p, t):
                processedPartLoadMin = getPartLoadMin(compDict, compName, ip)
                bigM = getBigM(compDict, compName)
                return (
                    opVar[loc, compName, ip, p, t]
                    >= processedPartLoadMin
                    * capVar[loc, compName, ip]
                    * esM.hoursPerSegment[ip][p, t]
                    - (1 - opVarBin[loc, compName, ip, p, t]) * bigM
                )

        setattr(
            pyM,
            constrName + "partLoadMin_2_" + abbrvName,
            pyomo.Constraint(
                constrSetMinPartLoad, pyM.intraYearTimeSet, rule=opMinPartLoad
            ),
        )

bigM

bigM(pyM)

Enforce the consideration of the binary design variables of a component.

.. math::

\\text{M}^{comp} \\cdot bin^{comp}_{loc,ip} \\geq commis^{comp}_{loc,ip}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def bigM(self, pyM):
    r"""Enforce the consideration of the binary design variables of a component.

    .. math::

        \\text{M}^{comp} \\cdot bin^{comp}_{loc,ip} \\geq commis^{comp}_{loc,ip}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    commisVar = getattr(pyM, "commis_" + abbrvName)
    commisBinVar = getattr(pyM, "commisBin_" + abbrvName)
    commisBinVarSet = getattr(pyM, "designDecisionVarSet_" + abbrvName)

    def bigM(pyM, loc, compName, ip):
        comp = compDict[compName]
        if ip not in comp.processedStockYears:
            # set bigM for investment periods
            M = (
                comp.processedCapacityMax[ip][loc]
                if comp.processedCapacityMax[ip] is not None
                else comp.bigM
            )
            return (
                commisVar[loc, compName, ip] <= commisBinVar[loc, compName, ip] * M
            )
        # set binary variables fix for stock years
        hasStockCommissioning = (
            self.componentsDict[compName].processedStockCommissioning[ip].loc[loc]
            > 0
        )
        if hasStockCommissioning:
            return commisBinVar[loc, compName, ip] == 1
        return commisBinVar[loc, compName, ip] == 0

    setattr(
        pyM, "ConstrBigM_" + abbrvName, pyomo.Constraint(commisBinVarSet, rule=bigM)
    )

binaryOperation

binaryOperation(
    pyM,
    constrName,
    constrSetName,
    binaryParameterName,
    opVarName,
    opVarBinName,
    isOperationCommisYearDepending=False,
)

Create binary operation constraints for component operation.

Defines two constraints linking a continuous operation variable to its corresponding binary variable using the Big-M formulation. Handles both standard and commissioning year-dependent cases.

The binaryOperation1 constraint is used to force the binary variable to one if the continuous variable is greater than zero.

The binaryOperation2 constraint ensures that the continuous variable is greater than zero whenever the binary variable is one. This is used for the upTimeMin and downTimeMin feature.

Source code in fine/component.py
def binaryOperation(
    self,
    pyM,
    constrName,
    constrSetName,
    binaryParameterName,
    opVarName,
    opVarBinName,
    isOperationCommisYearDepending=False,
):
    """Create binary operation constraints for component operation.

    Defines two constraints linking a continuous operation variable
    to its corresponding binary variable using the Big-M formulation.
    Handles both standard and commissioning year-dependent cases.

    The binaryOperation1 constraint is used to force the binary variable
    to one if the continuous variable is greater than zero.

    The binaryOperation2 constraint ensures that the continuous
    variable is greater than zero whenever the binary variable is one.
    This is used for the upTimeMin and downTimeMin feature.
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    opVarBin = getattr(pyM, opVarBinName + "_" + abbrvName, None)

    # only create constraint when binary operation variable is specified
    if opVarBin is not None:
        constrSetBinary = getattr(
            pyM, constrSetName + binaryParameterName + "_" + abbrvName
        )

        def getBigM(compName):
            return getattr(compDict[compName], "bigM")

        # First constraint
        if isOperationCommisYearDepending:

            def binOperation1(pyM, loc, compName, commis, ip, p, t):
                return opVar[loc, compName, commis, ip, p, t] <= opVarBin[
                    loc, compName, commis, ip, p, t
                ] * getBigM(compName)
        else:

            def binOperation1(pyM, loc, compName, ip, p, t):
                return opVar[loc, compName, ip, p, t] <= opVarBin[
                    loc, compName, ip, p, t
                ] * getBigM(compName)

        setattr(
            pyM,
            constrName + "binaryOperation1_" + abbrvName,
            pyomo.Constraint(
                constrSetBinary, pyM.intraYearTimeSet, rule=binOperation1
            ),
        )

        # Second constraint
        if isOperationCommisYearDepending:

            def binOperation2(pyM, loc, compName, commis, ip, p, t):
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    >= opVarBin[loc, compName, commis, ip, p, t] * 1e-4
                )
        else:

            def binOperation2(pyM, loc, compName, ip, p, t):
                return (
                    opVar[loc, compName, ip, p, t]
                    >= opVarBin[loc, compName, ip, p, t] * 1e-4
                )

        setattr(
            pyM,
            constrName + "binaryOperation2_" + abbrvName,
            pyomo.Constraint(
                constrSetBinary, pyM.intraYearTimeSet, rule=binOperation2
            ),
        )

capToNbInt

capToNbInt(pyM)

Determine the components' capacities from the number of installed units.

.. math::

cap^{comp}_{loc} = \\text{capPerUnit}^{comp} \\cdot nbInt^{comp}_{loc}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def capToNbInt(self, pyM):
    r"""Determine the components' capacities from the number of installed units.

    .. math::

        cap^{comp}_{loc} = \\text{capPerUnit}^{comp} \\cdot nbInt^{comp}_{loc}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    capVar, nbIntVar = (
        getattr(pyM, "cap_" + abbrvName),
        getattr(pyM, "nbInt_" + abbrvName),
    )
    nbIntVarSet = getattr(pyM, "discreteDesignDimensionVarSet_" + abbrvName)

    def capToNbInt(pyM, loc, compName, ip):
        return (
            capVar[loc, compName, ip]
            == nbIntVar[loc, compName, ip]
            * compDict[compName].processedCapacityPerPlantUnit[ip]
        )

    setattr(
        pyM,
        "ConstrCapToNbInt_" + abbrvName,
        pyomo.Constraint(nbIntVarSet, rule=capToNbInt),
    )

capToNbReal

capToNbReal(pyM)

Determine the components' capacities from the number of installed units.

.. math::

cap^{comp}_{loc} = \\text{capPerUnit}^{comp} \\cdot nbReal^{comp}_{loc}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def capToNbReal(self, pyM):
    r"""Determine the components' capacities from the number of installed units.

    .. math::

        cap^{comp}_{loc} = \\text{capPerUnit}^{comp} \\cdot nbReal^{comp}_{loc}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    capVar, nbRealVar = (
        getattr(pyM, "cap_" + abbrvName),
        getattr(pyM, "nbReal_" + abbrvName),
    )
    nbRealVarSet = getattr(pyM, "continuousDesignDimensionVarSet_" + abbrvName)

    def capToNbReal(pyM, loc, compName, ip):
        return (
            capVar[loc, compName, ip]
            == nbRealVar[loc, compName, ip]
            * compDict[compName].processedCapacityPerPlantUnit[ip]
        )

    setattr(
        pyM,
        "ConstrCapToNbReal_" + abbrvName,
        pyomo.Constraint(nbRealVarSet, rule=capToNbReal),
    )

capacityMinDec

capacityMinDec(pyM)

Enforce the consideration of minimum capacities for components with design decision variables.

Minimal capacity which needs to be reached for every investment period with commissioning. As the commisBinVar is coupled with commissioning var, constraint only sets minimal Capacity if component is commissioned. Therefore decommissioning of the component is possible without any constraints.

.. math::

\\text{capMin}^{comp}_{loc} \\cdot commisBin^{comp}_{loc,ip} \\leq  cap^{comp}_{loc,ip}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def capacityMinDec(self, pyM):
    r"""Enforce the consideration of minimum capacities for components with design decision variables.

    Minimal capacity which needs to be reached for every investment period with commissioning.
    As the commisBinVar is coupled with commissioning var, constraint only sets minimal Capacity if component is commissioned.
    Therefore decommissioning of the component is possible without any constraints.

    .. math::

        \\text{capMin}^{comp}_{loc} \\cdot commisBin^{comp}_{loc,ip} \\leq  cap^{comp}_{loc,ip}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisBinVar = getattr(pyM, "commisBin_" + abbrvName)
    commisBinVarSet = getattr(pyM, "designDecisionVarSet_" + abbrvName)

    def capacityMinDec(pyM, loc, compName, ip):
        if ip not in compDict[compName].processedStockYears:
            return (
                capVar[loc, compName, ip]
                >= compDict[compName].processedCapacityMin[ip][loc]
                * commisBinVar[loc, compName, ip]
                if compDict[compName].processedCapacityMin[ip] is not None
                else pyomo.Constraint.Skip
            )
        # constraint not required for stock years
        return pyomo.Constraint.Skip

    setattr(
        pyM,
        "ConstrCapacityMinDec_" + abbrvName,
        pyomo.Constraint(commisBinVarSet, rule=capacityMinDec),
    )

declareBinOpVarSet

declareBinOpVarSet(
    esM,
    pyM,
    binaryOperationParameter=["partLoadMin"],
    binaryOperationSetName="operationBinVarSet",
)

Declare binary operation variables.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareBinOpVarSet(
    self,
    esM,
    pyM,
    binaryOperationParameter=["partLoadMin"],
    binaryOperationSetName="operationBinVarSet",
):
    """Declare binary operation variables.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    # check if any component has binary operation variables
    def _identifyBinaryOperationComponents(compDict, compName):
        return any(
            getattr(compDict[compName], x, None) is not None
            for x in binaryOperationParameter
        )

    binaryOperationComponents = [
        compName
        for (_, compName, _) in varSet
        if _identifyBinaryOperationComponents(compDict, compName)
    ]

    # if components with binary operations exist, set up the corresponding set
    if len(binaryOperationComponents) > 0:

        def declareOpBinVarSet(pyM):
            return (
                (loc, compName, ip)
                for compName, comp in compDict.items()
                if compName in binaryOperationComponents
                for loc in comp.processedLocationalEligibility.index
                for ip in esM.investmentPeriods
                if comp.processedLocationalEligibility[loc] == 1
            )

        setattr(
            pyM,
            binaryOperationSetName + "_" + abbrvName,
            pyomo.Set(dimen=3, initialize=declareOpBinVarSet),
        )

declareBinaryDesignDecisionVars

declareBinaryDesignDecisionVars(pyM, relaxIsBuiltBinary)

Declare binary variables [-] indicating if a component is considered at a location or not [-].

If a isBuiltFix parameter is given, the bounds are set to enforce

.. math:: bin^{comp}{loc} = \text{binFix}^{comp}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareBinaryDesignDecisionVars(self, pyM, relaxIsBuiltBinary):
    r"""Declare binary variables [-] indicating if a component is considered at a location or not [-].

    If a isBuiltFix parameter is given, the bounds are set to enforce

    .. math::
        bin^{comp}_{loc} = \\text{binFix}^{comp}_{loc}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    abbrvName, compDict = self.abbrvName, self.componentsDict

    def binDomain(pyM, loc, compName, ip):
        """Return the minimal necessary domain for the binary variable depending on the given conditions, e.g., if values are already fixed or if binary variables should be relaxed."""
        if relaxIsBuiltBinary:
            # If binary variables are relaxed, value can take all non negative reals (between 0 and 1)
            return pyomo.NonNegativeReals

        if ip < 0:
            return pyomo.Binary
        if (compDict[compName].isBuiltFix is not None) or (
            compDict[compName].processedCapacityFix[ip] is not None
        ):
            # If isBuiltFix or capacityFix is given, binary variable is already fixed.
            return pyomo.NonNegativeReals
        return pyomo.Binary

    def binBounds(pyM, loc, compName, ip):
        """Return bounds with the minimal necessary freedom for the binary variables (e.g., (0, 0) or (1, 1))."""
        if ip < 0:
            return None, None
        if compDict[compName].isBuiltFix is not None:
            # If isBuiltFix is given, binary variable is set to isBuiltFix
            return (
                compDict[compName].isBuiltFix[loc],
                compDict[compName].isBuiltFix[loc],
            )
        if (
            compDict[compName].processedCapacityFix[ip] is not None
            and loc in compDict[compName].processedCapacityFix[ip].index
        ):
            # If capacityFix is given, binary variable is set to 1
            return (
                (1, 1)
                if compDict[compName].processedCapacityFix[ip][loc] > 0
                else (0, 0)
            )
        # Binary Variable between 0 and 1
        return (0, 1)

    if relaxIsBuiltBinary:
        setattr(
            pyM,
            "commisBin_" + abbrvName,
            pyomo.Var(
                getattr(pyM, "designDecisionVarSet_" + abbrvName),
                domain=binDomain,
                bounds=(0, 1),
            ),
        )
    else:
        setattr(
            pyM,
            "commisBin_" + abbrvName,
            pyomo.Var(
                getattr(pyM, "designDecisionVarSet_" + abbrvName),
                domain=binDomain,
                bounds=binBounds,
            ),
        )

declareCapacityVars

declareCapacityVars(pyM)

Declare capacity variables.

.. math::

\\text{capMin}^{comp}_{loc} \\leq cap^{comp}_{loc} \\leq \\text{capMax}^{comp}_{loc}

If a capacityFix parameter is given, the bounds are set to enforce

.. math:: \text{cap}^{comp}{loc} = \text{capFix}^{comp}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareCapacityVars(self, pyM):
    r"""Declare capacity variables.

    .. math::

        \\text{capMin}^{comp}_{loc} \\leq cap^{comp}_{loc} \\leq \\text{capMax}^{comp}_{loc}

    If a capacityFix parameter is given, the bounds are set to enforce

    .. math::
        \\text{cap}^{comp}_{loc} = \\text{capFix}^{comp}_{loc}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    abbrvName = self.abbrvName

    def capBounds(pyM, loc, compName, ip):
        """Set the lower and upper capacity bounds."""
        comp = self.componentsDict[compName]
        if (
            comp.processedCapacityFix[ip] is not None
            and loc in comp.processedCapacityFix[ip].index
        ):
            # in utils.py there are checks to ensure that capacityFix is between min and max
            return (
                comp.processedCapacityFix[ip][loc],
                comp.processedCapacityFix[ip][loc],
            )
        # the upper bound is only set if the parameter is given and no binary design variable exists
        # In the case of the binary design variable, the bigM-constraint will suffice as upper bound.
        if (comp.processedCapacityMin[ip] is not None) and (
            not comp.hasIsBuiltBinaryVariable
        ):
            capLowerBound = comp.processedCapacityMin[ip][loc]
        else:
            capLowerBound = 0

        if comp.processedCapacityMax[ip] is not None:
            capUpperBound = comp.processedCapacityMax[ip][loc]
        else:
            capUpperBound = None

        return (capLowerBound, capUpperBound)

    setattr(
        pyM,
        "cap_" + abbrvName,
        pyomo.Var(
            getattr(pyM, "designDimensionVarSet_" + abbrvName),
            domain=pyomo.NonNegativeReals,
            bounds=capBounds,
        ),
    )

declareCommissioningVarSet

declareCommissioningVarSet(pyM, esM)

Declare set for commissioning variables in the pyomo object for a modeling class.

The commissioning variable must be set for past investment periods (stock commissioning) and future/optimized investment periods

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareCommissioningVarSet(self, pyM, esM):
    """Declare set for commissioning variables in the pyomo object for a modeling class.

    The commissioning variable must be set for past investment periods
    (stock commissioning) and future/optimized investment periods

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    def declareCommisVarSet(pyM):
        return (
            (loc, compName, ip)
            for compName, comp in compDict.items()
            for loc in comp.processedLocationalEligibility.index
            for ip in comp.processedStockYears + esM.investmentPeriods
            if comp.processedLocationalEligibility[loc] == 1
            and comp.hasCapacityVariable
        )

    setattr(
        pyM,
        "designCommisVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareCommisVarSet),
    )

declareCommissioningVars

declareCommissioningVars(pyM, esM)

Declare commissioning variable for capacity development of component.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareCommissioningVars(self, pyM, esM):
    """Declare commissioning variable for capacity development of component.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """

    def commisBounds(pyM, loc, compName, ip):
        """Set the lower and upper commissioning bounds."""
        comp = self.componentsDict[compName]
        if ip < 0:
            return None, None
        if (
            comp.processedCommissioningFix[ip] is not None
            and loc in comp.processedCommissioningFix[ip].index
        ):
            # in utils.py there are checks to ensure that CommissioningFix is between min and max
            return (
                comp.processedCommissioningFix[ip][loc],
                comp.processedCommissioningFix[ip][loc],
            )
        # the upper bound is only set if the parameter is given and no binary design variable exists
        # In the case of the binary design variable, the bigM-constraint will suffice as upper bound.
        if (
            comp.processedCommissioningMin[ip] is not None
            and not comp.hasIsBuiltBinaryVariable
        ):
            commisLowerBound = comp.processedCommissioningMin[ip][loc]
        else:
            commisLowerBound = 0

        if comp.processedCommissioningMax[ip] is not None:
            commisUpperBound = comp.processedCommissioningMax[ip][loc]
        else:
            commisUpperBound = None

        return commisLowerBound, commisUpperBound

    abbrvName = self.abbrvName
    setattr(
        pyM,
        "commis_" + abbrvName,
        pyomo.Var(
            getattr(pyM, "designCommisVarSet_" + abbrvName),
            domain=pyomo.NonNegativeReals,
            bounds=commisBounds,
        ),
    )

declareComponentConstraints abstractmethod

declareComponentConstraints(esM, pyM)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Declare constraints of components in the componentModel class.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
@abstractmethod
def declareComponentConstraints(self, esM, pyM):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises).
    Declare constraints of components in the componentModel class.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    raise NotImplementedError

declareContinuousDesignVarSet

declareContinuousDesignVarSet(pyM)

Declare set for continuous number of installed components in the pyomo object for a modeling class.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareContinuousDesignVarSet(self, pyM):
    """Declare set for continuous number of installed components in the pyomo object for a modeling class.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    abbrvName = self.abbrvName

    def declareContinuousDesignVarSet(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in getattr(
                pyM, "designDimensionVarSet_" + abbrvName
            )
        )

    setattr(
        pyM,
        "continuousDesignDimensionVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareContinuousDesignVarSet),
    )

declareDecommissioningVars

declareDecommissioningVars(pyM, esM)

Declare decommissioning variable for capacity development of component.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareDecommissioningVars(self, pyM, esM):
    """Declare decommissioning variable for capacity development of component.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    abbrvName = self.abbrvName
    setattr(
        pyM,
        "decommis_" + abbrvName,
        pyomo.Var(
            getattr(pyM, "designDimensionVarSet_" + abbrvName),
            domain=pyomo.NonNegativeReals,
        ),
    )

declareDesignDecisionVarSet

declareDesignDecisionVarSet(pyM)

Declare set for design decision variables in the pyomo object for a modeling class.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareDesignDecisionVarSet(self, pyM):
    """Declare set for design decision variables in the pyomo object for a modeling class.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    def declareDesignDecisionVarSet(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in getattr(pyM, "designCommisVarSet_" + abbrvName)
            if compDict[compName].hasIsBuiltBinaryVariable
        )

    setattr(
        pyM,
        "designDecisionVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareDesignDecisionVarSet),
    )

declareDesignVarSet

declareDesignVarSet(pyM, esM)

Declare set for capacity variables in the pyomo object for a modeling class.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declareDesignVarSet(self, pyM, esM):
    """Declare set for capacity variables in the pyomo object for a modeling class.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    def declareDesignVarSet(pyM):
        return (
            (loc, compName, ip)
            for compName, comp in compDict.items()
            for loc in comp.processedLocationalEligibility.index
            for ip in esM.investmentPeriods
            if comp.processedLocationalEligibility[loc] == 1
            and comp.hasCapacityVariable
        )

    setattr(
        pyM,
        "designDimensionVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareDesignVarSet),
    )

declareDiscreteDesignVarSet

declareDiscreteDesignVarSet(pyM)

Declare set for discrete number of installed components in the pyomo object for a modeling class.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareDiscreteDesignVarSet(self, pyM):
    """Declare set for discrete number of installed components in the pyomo object for a modeling class.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    def declareDiscreteDesignVarSet(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in getattr(
                pyM, "designDimensionVarSet_" + abbrvName
            )
            if compDict[compName].capacityVariableDomain == "discrete"
        )

    setattr(
        pyM,
        "discreteDesignDimensionVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareDiscreteDesignVarSet),
    )

declareIntNumbersVars

declareIntNumbersVars(pyM)

Declare variables representing the (discrete/integer) number of installed components [-].

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareIntNumbersVars(self, pyM):
    """Declare variables representing the (discrete/integer) number of installed components [-].

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    abbrvName = self.abbrvName
    setattr(
        pyM,
        "nbInt_" + abbrvName,
        pyomo.Var(
            getattr(pyM, "discreteDesignDimensionVarSet_" + abbrvName),
            domain=pyomo.NonNegativeIntegers,
        ),
    )

declareLocationComponentSet

declareLocationComponentSet(pyM)

Declare set with location and component in the pyomo object for a modeling class.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareLocationComponentSet(self, pyM):
    """Declare set with location and component in the pyomo object for a modeling class.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    def initLocationComponentSet(pyM):
        return (
            (loc, compName)
            for compName, comp in compDict.items()
            for loc in comp.processedLocationalEligibility.index
            if comp.processedLocationalEligibility[loc] == 1
            and comp.hasCapacityVariable
        )

    setattr(
        pyM,
        "DesignLocationComponentVarSet_" + abbrvName,
        pyomo.Set(dimen=2, initialize=initLocationComponentSet),
    )

declareOpConstrSet1

declareOpConstrSet1(pyM, constrSetName, rateMax, rateFix)

Declare set of locations and components for which hasCapacityVariable is set to True and neither the maximum nor the fixed operation rate is given.

Source code in fine/component.py
def declareOpConstrSet1(self, pyM, constrSetName, rateMax, rateFix):
    """Declare set of locations and components for which hasCapacityVariable is set to True and neither the
    maximum nor the fixed operation rate is given.
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareOpConstrSet1(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if compDict[compName].hasCapacityVariable
            and getattr(compDict[compName], rateMax)[ip] is None
            and getattr(compDict[compName], rateFix)[ip] is None
        )

    setattr(
        pyM,
        constrSetName + "1_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareOpConstrSet1),
    )

declareOpConstrSet2

declareOpConstrSet2(pyM, constrSetName, rateFix)

Declare set of locations and components for which hasCapacityVariable is set to True and a fixed operation rate is given.

Source code in fine/component.py
def declareOpConstrSet2(self, pyM, constrSetName, rateFix):
    """Declare set of locations and components for which hasCapacityVariable is set to True and a fixed
    operation rate is given.
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareOpConstrSet2(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if compDict[compName].hasCapacityVariable
            and getattr(compDict[compName], rateFix)[ip] is not None
        )

    setattr(
        pyM,
        constrSetName + "2_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareOpConstrSet2),
    )

declareOpConstrSet3

declareOpConstrSet3(pyM, constrSetName, rateMax)

Declare set of locations and components for which hasCapacityVariable is set to True and a maximum operation rate is given.

Source code in fine/component.py
def declareOpConstrSet3(self, pyM, constrSetName, rateMax):
    """Declare set of locations and components for which  hasCapacityVariable is set to True and a maximum
    operation rate is given.
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareOpConstrSet3(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if compDict[compName].hasCapacityVariable
            and getattr(compDict[compName], rateMax)[ip] is not None
        )

    setattr(
        pyM,
        constrSetName + "3_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareOpConstrSet3),
    )

declareOpConstrSet4

declareOpConstrSet4(pyM, constrSetName, rateMin)

Declare set of locations and components for which hasCapacityVariable is set to True and a minimum operation rate is given.

Source code in fine/component.py
def declareOpConstrSet4(self, pyM, constrSetName, rateMin):
    """Declare set of locations and components for which  hasCapacityVariable is set to True and a minimum
    operation rate is given.
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareOpConstrSet4(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if compDict[compName].hasCapacityVariable
            and getattr(compDict[compName], rateMin)[ip] is not None
        )

    setattr(
        pyM,
        constrSetName + "4_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareOpConstrSet4),
    )

declareOpConstrSetMinPartLoad

declareOpConstrSetMinPartLoad(pyM, constrSetName)

Declare set of locations and components for which partLoadMin is not None.

Source code in fine/component.py
def declareOpConstrSetMinPartLoad(self, pyM, constrSetName):
    """Declare set of locations and components for which partLoadMin is not None."""
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareOpConstrSetMinPartLoad(pyM):
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if getattr(compDict[compName], "processedPartLoadMin") is not None
        )

    setattr(
        pyM,
        constrSetName + "partLoadMin_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareOpConstrSetMinPartLoad),
    )

declareOpVarSet

declareOpVarSet(esM, pyM)

Declare operation related sets (operation variables and mapping sets) in the pyomo object for a modeling class.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareOpVarSet(self, esM, pyM):
    """Declare operation related sets (operation variables and mapping sets) in the pyomo object for a
    modeling class.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    # Set for operation variables
    def declareOpVarSet(pyM):
        return (
            (loc, compName, ip)
            for compName, comp in compDict.items()
            for loc in comp.processedLocationalEligibility.index
            for ip in esM.investmentPeriods
            if comp.processedLocationalEligibility[loc] == 1
        )

    setattr(
        pyM,
        "operationVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareOpVarSet),
    )

    if self.dimension == Dimension.ONE:
        # Dictionary which lists all components of the modeling class at one location
        setattr(
            pyM,
            "operationVarDict_" + abbrvName,
            {
                ip: {
                    loc: {
                        compName
                        for compName in compDict
                        if (loc, compName, ip)
                        in getattr(pyM, "operationVarSet_" + abbrvName)
                    }
                    for loc in esM.locations
                }
                for ip in esM.investmentPeriods
            },
        )
    elif self.dimension == Dimension.TWO:
        # Dictionaries which list all outgoing and incoming components at a location
        setattr(
            pyM,
            "operationVarDictOut_" + abbrvName,
            {
                ip: {
                    loc: {
                        loc_: {
                            compName
                            for compName in compDict
                            if (loc + "_" + loc_, compName, ip)
                            in getattr(pyM, "operationVarSet_" + abbrvName)
                        }
                        for loc_ in esM.locations
                    }
                    for loc in esM.locations
                }
                for ip in esM.investmentPeriods
            },
        )
        setattr(
            pyM,
            "operationVarDictIn_" + abbrvName,
            {
                ip: {
                    loc: {
                        loc_: {
                            compName
                            for compName in compDict
                            if (loc_ + "_" + loc, compName, ip)
                            in getattr(pyM, "operationVarSet_" + abbrvName)
                        }
                        for loc_ in esM.locations
                    }
                    for loc in esM.locations
                }
                for ip in esM.investmentPeriods
            },
        )

declareOperationBinaryVars

declareOperationBinaryVars(
    pyM,
    opVarBinName="op_bin",
    opBinSetName="operationBinVarSet",
)

Declare binary operation variables.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareOperationBinaryVars(
    self,
    pyM,
    opVarBinName="op_bin",
    opBinSetName="operationBinVarSet",
):
    """Declare binary operation variables.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    abbrvName = self.abbrvName

    # only setup binary operation variables if binary operation is declared, otherwise pyomo
    # always declares binary models which increases run time.

    if hasattr(pyM, opBinSetName + "_" + abbrvName):
        # declare binary operation variables
        setattr(
            pyM,
            opVarBinName + "_" + abbrvName,
            pyomo.Var(
                getattr(pyM, opBinSetName + "_" + abbrvName),
                pyM.intraYearTimeSet,
                domain=pyomo.Binary,
            ),
        )

declareOperationModeSets

declareOperationModeSets(
    pyM, constrSetName, rateMax, rateFix, rateMin=None
)

Declare operating mode sets.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param constrSetName: name of the constraint set. :type constrSetName: string

:param rateMax: attribute of the considered component which stores the maximum operation rate data. :type rateMax: string

:param rateMax: attribute of the considered component which stores the minimum operation rate data. :type rateMax: string

:param rateFix: attribute of the considered component which stores the fixed operation rate data. :type rateFix: string

Source code in fine/component.py
def declareOperationModeSets(
    self, pyM, constrSetName, rateMax, rateFix, rateMin=None
):
    """Declare operating mode sets.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param constrSetName: name of the constraint set.
    :type constrSetName: string

    :param rateMax: attribute of the considered component which stores the maximum operation rate data.
    :type rateMax: string

    :param rateMax: attribute of the considered component which stores the minimum operation rate data.
    :type rateMax: string

    :param rateFix: attribute of the considered component which stores the fixed operation rate data.
    :type rateFix: string
    """
    self.declareOpConstrSet1(pyM, constrSetName, rateMax, rateFix)
    self.declareOpConstrSet2(pyM, constrSetName, rateFix)
    self.declareOpConstrSet3(pyM, constrSetName, rateMax)
    if rateMin:
        self.declareOpConstrSet4(pyM, constrSetName, rateMin)

    self.declareOpConstrSetMinPartLoad(pyM, constrSetName)

declareOperationVars

declareOperationVars(
    pyM,
    esM,
    opVarName,
    opRateFixName="processedOperationRateFix",
    opRateMaxName="processedOperationRateMax",
    isOperationCommisYearDepending=False,
    flexibleConversion=False,
    relevanceThreshold=None,
)

Declare operation variables.

The following operation modes are directly handled during variable creation as bounds instead of constraints.

operation mode 4: If operationRateFix is given for components without a capacity variable, the variables are fixed with operationRateFix, i.e. the operation [commodityUnit*h] is equal to a time series.

.. math:: op^{comp,opType}{loc,p,t} = \text{opRateFix}^{comp,opType}

operation mode 5: If operationRateMax is given for components without a capacity variable, the variables are bounded by operationRateMax, i.e. the operation [commodityUnit*h] is limited by a time series.

.. math:: op^{comp,opType}{loc,p,t} \leq \text{opRateMax}^{comp,opType}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold. |br| * the default value is None :type relevanceThreshold: float (>=0) or None

:param isOperationCommisYearDepending: defines whether the operation variable is depending on the year of commissioning of the component. E.g. relevant if the commodity conversion, for example the efficiency, varies over the transformation pathway :type isOperationCommisYearDepending: str

Source code in fine/component.py
def declareOperationVars(
    self,
    pyM,
    esM,
    opVarName,
    opRateFixName="processedOperationRateFix",
    opRateMaxName="processedOperationRateMax",
    isOperationCommisYearDepending=False,
    flexibleConversion=False,
    relevanceThreshold=None,
):
    r"""Declare operation variables.

    The following operation modes are directly handled during variable creation as bounds instead of constraints.

    operation mode 4: If operationRateFix is given for components without a capacity variable,
    the variables are fixed with operationRateFix, i.e. the operation [commodityUnit*h] is equal to a time series.

    .. math::
        op^{comp,opType}_{loc,p,t} = \\text{opRateFix}^{comp,opType}_{loc,p,t}

    operation mode 5: If operationRateMax is given for components without a capacity variable,
    the variables are bounded by operationRateMax, i.e. the operation [commodityUnit*h] is limited by a time series.

    .. math::
        op^{comp,opType}_{loc,p,t} \\leq \\text{opRateMax}^{comp,opType}_{loc,p,t}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold.
        |br| * the default value is None
    :type relevanceThreshold: float (>=0) or None

    :param isOperationCommisYearDepending: defines whether the operation variable is depending on the year
        of commissioning of the component. E.g. relevant if the commodity conversion, for example the efficiency,
        varies over the transformation pathway
    :type isOperationCommisYearDepending: str
    """
    abbrvName, compDict = self.abbrvName, self.componentsDict

    def opBounds(pyM, loc, compName, ip, p, t):  # noqa: PLR0911
        if not getattr(compDict[compName], "hasCapacityVariable"):
            if not pyM.hasSegmentation:
                if getattr(compDict[compName], opRateMaxName)[ip] is not None:
                    rate = getattr(compDict[compName], opRateMaxName)[ip]
                    if rate is not None:
                        if relevanceThreshold is not None:
                            validThreshold = 0 < relevanceThreshold
                            if validThreshold and (
                                rate[loc][p, t] < relevanceThreshold
                            ):
                                return (0, 0)
                        return (0, rate[loc][p, t])
                    return None
                if getattr(compDict[compName], opRateFixName)[ip] is not None:
                    rate = getattr(compDict[compName], opRateFixName)[ip]
                    if rate is not None:
                        if relevanceThreshold is not None:
                            validThreshold = 0 < relevanceThreshold
                            if validThreshold and (
                                rate[loc][p, t] < relevanceThreshold
                            ):
                                return (0, 0)
                        return (rate[loc][p, t], rate[loc][p, t])
                    return None
                return (0, None)
            if getattr(compDict[compName], opRateMaxName)[ip] is not None:
                rate = getattr(compDict[compName], opRateMaxName)[ip]
                if rate is not None:
                    if relevanceThreshold is not None:
                        validThreshold = 0 < relevanceThreshold
                        if validThreshold and (
                            rate[loc][p, t] < relevanceThreshold
                        ):
                            return (0, 0)
                    return (
                        0,
                        rate[loc][p, t]
                        * esM.timeStepsPerSegment[ip].to_dict()[p, t],
                    )
                return None
            if getattr(compDict[compName], opRateFixName)[ip] is not None:
                rate = getattr(compDict[compName], opRateFixName)[ip]
                if rate is not None:
                    if relevanceThreshold is not None:
                        validThreshold = 0 < relevanceThreshold
                        if validThreshold and (
                            rate[loc][p, t] < relevanceThreshold
                        ):
                            return (0, 0)
                    return (
                        rate[loc][p, t]
                        * esM.timeStepsPerSegment[ip].to_dict()[p, t],
                        rate[loc][p, t]
                        * esM.timeStepsPerSegment[ip].to_dict()[p, t],
                    )
                return None
            return (0, None)
        return (0, None)

    if isOperationCommisYearDepending:
        # if the operation is depending on the year of commissioning, e.g. due to variable efficiencies over the
        # transformation pathway, the operation is additionally depending on commis
        def opBounds_commisDepending(pyM, loc, compName, commis, ip, p, t):
            return opBounds(pyM, loc, compName, ip, p, t)

        setattr(
            pyM,
            opVarName + "_" + abbrvName,
            pyomo.Var(
                getattr(pyM, "operationCommisVarSet_" + abbrvName),
                pyM.intraYearTimeSet,
                domain=pyomo.NonNegativeReals,
                bounds=opBounds_commisDepending,
            ),
        )
    elif flexibleConversion:
        setattr(
            pyM,
            opVarName + "_" + abbrvName,
            pyomo.Var(
                getattr(pyM, "operationFlexVarSet_" + abbrvName),
                pyM.intraYearTimeSet,
                domain=pyomo.NonNegativeReals,
            ),
        )
    else:
        setattr(
            pyM,
            opVarName + "_" + abbrvName,
            pyomo.Var(
                getattr(pyM, "operationVarSet_" + abbrvName),
                pyM.intraYearTimeSet,
                domain=pyomo.NonNegativeReals,
                bounds=opBounds,
            ),
        )

declarePathwaySets

declarePathwaySets(pyM, esM)

Declare set for capacity development in the pyomo object for a modeling class.

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def declarePathwaySets(self, pyM, esM):
    """Declare set for capacity development in the pyomo object for a modeling class.

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName

    def initDesignDevelopmentSet(pyM):
        return (
            (loc, compName, ip)
            for compName, comp in compDict.items()
            for loc in comp.processedLocationalEligibility.index
            for ip in esM.investmentPeriods[:-1]
            if comp.processedLocationalEligibility[loc] == 1
            and comp.hasCapacityVariable
        )

    setattr(
        pyM,
        "designDevelopmentVarSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=initDesignDevelopmentSet),
    )

declareRealNumbersVars

declareRealNumbersVars(pyM)

Declare variables representing the (continuous) number of installed components [-].

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def declareRealNumbersVars(self, pyM):
    """Declare variables representing the (continuous) number of installed components [-].

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    abbrvName = self.abbrvName
    setattr(
        pyM,
        "nbReal_" + abbrvName,
        pyomo.Var(
            getattr(pyM, "continuousDesignDimensionVarSet_" + abbrvName),
            domain=pyomo.NonNegativeReals,
        ),
    )

declareSets abstractmethod

declareSets(esM, pyM)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Declare sets of components and constraints in the componentModel class.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
@abstractmethod
def declareSets(self, esM, pyM):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises).
    Declare sets of components and constraints in the componentModel class.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    raise NotImplementedError

declareVariables abstractmethod

declareVariables(esM, pyM, relevanceThreshold)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Declare variables of components in the componentModel class.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold. |br| * the default value is None :type relevanceThreshold: float (>=0) or None

Source code in fine/component.py
@abstractmethod
def declareVariables(self, esM, pyM, relevanceThreshold):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises).
    Declare variables of components in the componentModel class.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold.
        |br| * the default value is None
    :type relevanceThreshold: float (>=0) or None
    """
    raise NotImplementedError

declareYearlyFullLoadHoursMaxSet

declareYearlyFullLoadHoursMaxSet(pyM)

Declare set of locations and components for which maximum yearly full load hours are given.

Source code in fine/component.py
def declareYearlyFullLoadHoursMaxSet(self, pyM):
    """Declare set of locations and components for which maximum yearly full load hours are given."""
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareYearlyFullLoadHoursMaxSet():
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if compDict[compName].processedYearlyFullLoadHoursMax[ip] is not None
        )

    setattr(
        pyM,
        "yearlyFullLoadHoursMaxSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareYearlyFullLoadHoursMaxSet()),
    )

declareYearlyFullLoadHoursMinSet

declareYearlyFullLoadHoursMinSet(pyM)

Declare set of locations and components for which minimum yearly full load hours are given.

Source code in fine/component.py
def declareYearlyFullLoadHoursMinSet(self, pyM):
    """Declare set of locations and components for which minimum yearly full load hours are given."""
    compDict, abbrvName = self.componentsDict, self.abbrvName
    varSet = getattr(pyM, "operationVarSet_" + abbrvName)

    def declareYearlyFullLoadHoursMinSet():
        return (
            (loc, compName, ip)
            for loc, compName, ip in varSet
            if compDict[compName].processedYearlyFullLoadHoursMin[ip] is not None
        )

    setattr(
        pyM,
        "yearlyFullLoadHoursMinSet_" + abbrvName,
        pyomo.Set(dimen=3, initialize=declareYearlyFullLoadHoursMinSet()),
    )

decommissioningConstraint

decommissioningConstraint(pyM, esM)

Declase the decommissioning after the technical lifetime from investment period of commissioning.

.. math::

decommis^{comp}_{loc,ip} = commis^{comp}_{loc,ip-\\mathrm{ipTechnicalLifetime}}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def decommissioningConstraint(self, pyM, esM):
    r"""Declase the decommissioning after the technical lifetime from investment
    period of commissioning.

    .. math::

        decommis^{comp}_{loc,ip} = commis^{comp}_{loc,ip-\\mathrm{ipTechnicalLifetime}}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    abbrvName = self.abbrvName
    commisVar = getattr(pyM, "commis_" + abbrvName)
    decommisVar = getattr(pyM, "decommis_" + abbrvName)
    decommisConstrSet = getattr(pyM, "designDimensionVarSet_" + abbrvName)

    def capacityDecommissioning(pyM, loc, compName, ip):
        tech_lifetime = self.componentsDict[compName].ipTechnicalLifetime[loc]

        # commissioning date is depending whether technical lifetime ceiled or floored to next interval
        # if technical lifetime is already a multiple of the interval, nothing happens
        if self.componentsDict[compName].floorTechnicalLifetime:
            comm_date = ip - math.floor(tech_lifetime)
        else:
            comm_date = ip - math.ceil(tech_lifetime)
        # if the commissioning date is within the investment periods, the
        # decommissioning and commissioning variables are linked
        if comm_date in esM.investmentPeriods:
            return (
                decommisVar[loc, compName, ip]
                == commisVar[loc, compName, comm_date]
            )
        # else the decommissioning is depending on the stockcommissioning
        # or set to 0
        procStockCommissioning = self.componentsDict[
            compName
        ].processedStockCommissioning
        if procStockCommissioning is not None:
            return (
                decommisVar[loc, compName, ip]
                == self.componentsDict[compName].processedStockCommissioning[
                    comm_date
                ][loc]
            )
        return decommisVar[loc, compName, ip] == 0

    setattr(
        pyM,
        "DecommConstrCapacityDevelopment_" + abbrvName,
        pyomo.Constraint(decommisConstrSet, rule=capacityDecommissioning),
    )

designBinFix

designBinFix(pyM)

Set, if applicable, the installed capacities of a component.

.. math::

bin^{comp}_{(loc_1,loc_2),ip} = \\text{binFix}^{comp}_{(loc_1,loc_2)}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def designBinFix(self, pyM):
    r"""Set, if applicable, the installed capacities of a component.

    .. math::

        bin^{comp}_{(loc_1,loc_2),ip} = \\text{binFix}^{comp}_{(loc_1,loc_2)}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    commisBinVar = getattr(pyM, "commisBin_" + abbrvName)
    commisBinVarSet = getattr(pyM, "designDecisionVarSet_" + abbrvName)

    def designBinFix(pyM, loc, compName, ip):
        return (
            commisBinVar[loc, compName, ip] == compDict[compName].isBuiltFix[loc]
            if compDict[compName].isBuiltFix is not None
            else pyomo.Constraint.Skip
        )

    setattr(
        pyM,
        "ConstrDesignBinFix_" + abbrvName,
        pyomo.Constraint(commisBinVarSet, rule=designBinFix),
    )

designDevelopmentConstraint

designDevelopmentConstraint(pyM, esM)

Link the capacity development between investment periods.

For stochastic: The capacity design must be equal between the different years.

.. math::

cap^{comp}_{loc,ip+1} =  cap^{comp}_{loc,ip}

For the development pathway, the capacity of an investment period is composed of the capacity of the previous investment periods and the commissioning and decommissioning in the current investment period.

.. math::

cap^{comp}_{loc,ip+1} =  cap^{comp}_{loc,ip} + commis^{comp}_{loc,ip} - decommis^{comp}_{loc,ip}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def designDevelopmentConstraint(self, pyM, esM):
    """Link the capacity development between investment periods.

    For stochastic: The capacity design must be equal between the different years.

    .. math::

        cap^{comp}_{loc,ip+1} =  cap^{comp}_{loc,ip}

    For the development pathway, the capacity of an investment period is composed
    of the capacity of the previous investment periods and the commissioning and
    decommissioning in the current investment period.

    .. math::

        cap^{comp}_{loc,ip+1} =  cap^{comp}_{loc,ip} + commis^{comp}_{loc,ip} - decommis^{comp}_{loc,ip}


    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    abbrvName = self.abbrvName
    commisConstrSet = getattr(pyM, "designDevelopmentVarSet_" + abbrvName)
    if esM.stochasticModel:
        capVar = getattr(pyM, "cap_" + abbrvName)

        def capacityDevelopmentStochastic(pyM, loc, compName, ip):
            # all investment periods must have the same capacity
            return capVar[loc, compName, ip + 1] == capVar[loc, compName, ip]

        setattr(
            pyM,
            "ConstrCapacityDevelopment_" + abbrvName,
            pyomo.Constraint(
                commisConstrSet,
                rule=capacityDevelopmentStochastic,
            ),
        )
    else:
        capVar = getattr(pyM, "cap_" + abbrvName)
        commisVar = getattr(pyM, "commis_" + abbrvName)
        decommisVar = getattr(pyM, "decommis_" + abbrvName)

        def capacityDevelopmentPerfectForesight(pyM, loc, compName, ip):
            return (
                capVar[loc, compName, ip + 1]
                == capVar[loc, compName, ip]
                + commisVar[loc, compName, ip + 1]
                - decommisVar[loc, compName, ip + 1]
            )

        setattr(
            pyM,
            "ConstrCapacityDevelopment_" + abbrvName,
            pyomo.Constraint(
                commisConstrSet, rule=capacityDevelopmentPerfectForesight
            ),
        )

getCommodityBalanceContribution abstractmethod

getCommodityBalanceContribution(
    pyM, commod, loc, ip, p, t
)

Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises). Get contribution to a commodity balance.

Source code in fine/component.py
@abstractmethod
def getCommodityBalanceContribution(self, pyM, commod, loc, ip, p, t):
    """Abstract method which has to be implemented by subclasses (otherwise a NotImplementedError raises).
    Get contribution to a commodity balance.
    """
    raise NotImplementedError

getEconomicsDesign

getEconomicsDesign(
    pyM,
    esM,
    factorNames,
    lifetimeAttr,
    varName,
    divisorName="",
    QPfactorNames=[],
    QPdivisorNames=[],
    getOptValue=False,
    getOptValueCostType=TAC,
)

Set design dependent cost equations for the individual components. The equations will be set for all components of a modeling class and all locations.

Required arguments

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

:param factorNames: Strings of the parameters that have to be multiplied within the equation. (e.g. ['processedInvestPerCapacity'] to multiply the capacity variable with the investment per each capacity unit). :type factorNames: list of strings

:param varName: String of the variable that has to be multiplied within the equation (e.g. 'cap' for capacity variable). :type varName: string

:param divisorName: String of the variable that is used as a divisor within the equation (e.g. 'CCF'). If the divisorName is an empty string, there is no division within the equation. |br| * the default value is "". :type divisorName: string

:param QPfactorNames: Strings of the parameters that have to be multiplied when quadratic programming is used. (e.g. ['processedQPcostScale']) :type QPfactorNames: list of strings

:param QPdivisorNames: Strings of the parameters that have to be used as divisors when quadratic programming is used. (e.g. ['QPbound']) :type QPdivisorNames: list of strings

:param getOptValue: Boolean that defines the output of the function:

- True: Return the optimal cost values.
- False: Return the cost equation.

|br| * the default value is False.

:type getoptValue: boolean

:param getOptValueCostType: the cost type can either be TAC (total anualized costs) or NPV (net present value) |br| * the default value is None. :type getOptValueCostType: string

Source code in fine/component.py
def getEconomicsDesign(
    self,
    pyM,
    esM,
    factorNames,
    lifetimeAttr,
    varName,
    divisorName="",
    QPfactorNames=[],
    QPdivisorNames=[],
    getOptValue=False,
    getOptValueCostType=CostType.TAC,
):
    """Set design dependent cost equations for the individual components. The equations will be set
    for all components of a modeling class and all locations.

    **Required arguments**

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package

    :param factorNames: Strings of the parameters that have to be multiplied within the equation.
        (e.g. ['processedInvestPerCapacity'] to multiply the capacity variable with the investment per each capacity unit).
    :type factorNames: list of strings

    :param varName: String of the variable that has to be multiplied within the equation (e.g. 'cap' for capacity variable).
    :type varName: string

    :param divisorName: String of the variable that is used as a divisor within the equation (e.g. 'CCF').
        If the divisorName is an empty string, there is no division within the equation.
        |br| * the default value is "".
    :type divisorName: string

    :param QPfactorNames: Strings of the parameters that have to be multiplied when quadratic programming is used. (e.g. ['processedQPcostScale'])
    :type QPfactorNames: list of strings

    :param QPdivisorNames: Strings of the parameters that have to be used as divisors when quadratic programming is used. (e.g. ['QPbound'])
    :type QPdivisorNames: list of strings

    :param getOptValue: Boolean that defines the output of the function:

        - True: Return the optimal cost values.
        - False: Return the cost equation.

        |br| * the default value is False.
    :type getoptValue: boolean

    :param getOptValueCostType: the cost type can either be TAC (total anualized costs) or NPV (net present value)
        |br| * the default value is None.
    :type getOptValueCostType: string
    """
    try:
        getOptValueCostType = CostType(getOptValueCostType)
    except ValueError as exc:
        raise ValueError("The cost types must be 'TAC' or 'NPV'.") from exc

    var = getattr(pyM, varName + "_" + self.abbrvName)
    if esM.stochasticModel:
        if getOptValue:
            cost_results = {}
            for ip in esM.investmentPeriods:
                cost_results[ip] = pd.DataFrame()
            for loc, compName, ip in var:
                if ip not in esM.investmentPeriods:
                    continue
                cost_results[ip].loc[compName, loc] = self.getLocEconomicsDesign(
                    pyM,
                    esM,
                    factorNames,
                    varName,
                    loc,
                    compName,
                    ip,
                    divisorName,
                    QPfactorNames,
                    QPdivisorNames,
                    getOptValue,
                )
            return cost_results
        return sum(
            self.getLocEconomicsDesign(
                pyM,
                esM,
                factorNames,
                varName,
                loc,
                compName,
                ip,
                divisorName,
                QPfactorNames,
                QPdivisorNames,
                getOptValue,
            )
            for loc, compName, ip in var
        )
    # Components can have different investPerCapacity in different years.
    # The capex contribution however only depends on the capex of the
    # commissioning year. Therefore, we initialize a dataframe with index and
    # columns of the investment periods. The rows describe the commissioning
    # years, e.g. a component build in year 2 but with a lifetime of three
    # years would have entries for df.loc[2,2:5]. Afterwards we
    # sum the contributions per column, multiply it with the annuity
    # present value factor to get the npv of the component for
    # different investPerCapacity and several ip for commissioning

    # initialize dict with (loc,comp) as key and df as values
    costContribution = {}
    locCompNamesCombinations = list(set([(x[0], x[1]) for x in var.get_values()]))
    componentYears = {}

    for loc, compName in locCompNamesCombinations:
        # get all years of component with location (also stock years)
        componentYears[compName] = (
            esM.getComponentAttribute(compName, "processedStockYears")
            + esM.investmentPeriods
        )

        costContribution[(loc, compName)] = {
            (y, i): 0
            for y in componentYears[compName]
            for i in esM.investmentPeriods
        }

    # fill the dataframes (per location and compName) with the cost
    # contributions depending on the commissioning year (index) and the
    # investment period (columns)
    for loc, compName, commisYear in var:
        ipEconomicLifetime = getattr(
            esM.getComponent(compName), "ipEconomicLifetime"
        )[loc]
        ipTechnicalLifetime = getattr(
            esM.getComponent(compName), "ipTechnicalLifetime"
        )[loc]

        (fullCostIntervals, costInLastEconInterval, costInLastTechInterval) = (
            utils.getParametersForUnevenLifetimes(compName, loc, lifetimeAttr, esM)
        )

        # calculation of the annuity
        annuity = self.getLocEconomicsDesign(
            pyM,
            esM,
            factorNames,
            varName,
            loc,
            compName,
            commisYear,
            divisorName,
            QPfactorNames,
            QPdivisorNames,
            getOptValue,
        )

        # write costs into dataframe
        # a) costs for complete intervals
        for i in range(commisYear, commisYear + fullCostIntervals):
            costContribution[(loc, compName)][(commisYear, i)] = (
                annuity
                * utils.annuityPresentValueFactor(
                    esM, compName, loc, esM.investmentPeriodInterval
                )
            )

        # b) costs for last economic interval
        # example: interval 5, economic lifetime 7, technical lifetime 10
        # last interval has costs only in year 5 and 6
        if costInLastEconInterval:
            # calculate portion of interval with economic lifetime
            # example: interval 5, economic lifetime 7 leads to partlyCostInLastEconomicInterval of 0.4
            partlyCostInLastEconomicInterval = (
                ipEconomicLifetime % 1
            ) * esM.investmentPeriodInterval
            costContribution[(loc, compName)][
                (commisYear, commisYear + fullCostIntervals)
            ] = annuity * utils.annuityPresentValueFactor(
                esM, compName, loc, partlyCostInLastEconomicInterval
            )

        # c) costs for last technical interval due to additionally required capacity after technical lifetime is over
        # example: interval 5, economic lifetime 5, technical lifetime 7 and is ceiled to 10
        # extra costs for years 8 and 9
        if costInLastTechInterval and ipTechnicalLifetime % 1 != 0:
            partlyCostInLastTechnicalInterval = (
                1 - (ipTechnicalLifetime % 1)
            ) * esM.investmentPeriodInterval
            if commisYear + math.ceil(ipTechnicalLifetime) - 1 in [
                k[1] for k in costContribution[(loc, compName)].keys()
            ]:
                costContribution[(loc, compName)][
                    (
                        commisYear,
                        commisYear + math.ceil(ipTechnicalLifetime) - 1,
                    )
                ] = costContribution[(loc, compName)][
                    (
                        commisYear,
                        commisYear + math.ceil(ipTechnicalLifetime) - 1,
                    )
                ] + annuity * (
                    utils.annuityPresentValueFactor(
                        esM,
                        compName,
                        loc,
                        partlyCostInLastTechnicalInterval,
                    )
                    / (1 + esM.getComponent(compName).interestRate[loc])
                    ** (
                        esM.investmentPeriodInterval
                        - partlyCostInLastTechnicalInterval
                    )
                )

    # create dictionary with ip as key and cost contribution as value
    if getOptValue:
        cost_results = {ip: pd.DataFrame() for ip in esM.investmentPeriods}
        for loc, compName in locCompNamesCombinations:
            for ip in esM.investmentPeriods:
                cContrSum = sum(
                    [
                        costContribution[(loc, compName)].get((y, ip), 0)
                        for y in componentYears[compName]
                    ]
                )
                if getOptValueCostType == CostType.NPV:
                    cost_results[ip].loc[compName, loc] = (
                        cContrSum * utils.discountFactor(esM, ip, compName, loc)
                    )
                elif getOptValueCostType == CostType.TAC:
                    cost_results[ip].loc[compName, loc] = (
                        cContrSum
                        / utils.annuityPresentValueFactor(
                            esM, compName, loc, esM.investmentPeriodInterval
                        )
                    )
        return cost_results
    if esM.annuityPerpetuity:
        # the last investment period gets the perpetuity cost
        # contribution, implying the system design and operation
        # will remain constant after the time frame of the
        # transformation pathway.
        for loc, compName in costContribution.keys():  # noqa: PLC0206
            for y in componentYears[compName]:
                costContribution[(loc, compName)][
                    (y, esM.investmentPeriods[-1])
                ] = costContribution[(loc, compName)][
                    (y, esM.investmentPeriods[-1])
                ] / (
                    utils.annuityPresentValueFactor(
                        esM, compName, loc, esM.investmentPeriodInterval
                    )
                    * esM.getComponent(compName).interestRate[loc]
                )
    return sum(
        sum(
            [
                costContribution[(loc, compName)].get((y, ip), 0)
                for y in componentYears[compName]
            ]
        )
        * utils.discountFactor(esM, ip, compName, loc)
        for loc, compName, ip in var
        if ip in esM.investmentPeriods
    )

getEconomicsOperation

getEconomicsOperation(
    pyM,
    esM,
    fncType,
    factorNames,
    varName,
    dictName,
    getOptValue=False,
    getOptValueCostType=TAC,
)

Set time-dependent equations for the individual components. The equations will be set for all components of a modeling class and all locations as well as for each considered time step. In case of a two-dimensional component (e.g. a transmission component), the equations will be set for all possible connections between the defined locations.

Required arguments:

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: EnergySystemModel instance representing the energy system in which the components should be modeled. :type esM: esM - EnergySystemModel class instance

:param fncType: Function type, either "TD" or "TimeSeries" :type fncType: string

:param factorNames: Strings of the time-dependent parameters that have to be multiplied within the equation. (e.g. ['opexPerOperation'] to multiply the operation variable with the costs for each operation). :type factorNames: list of strings

:param varName: String of the variable that has to be multiplied within the equation (e.g. 'op' for operation variable). :type varName: string

:param dictName: String of the variable set (e.g. 'operationVarDict') :type dictName: string

Default arguments:

:param getOptValue: Boolean that defines the output of the function:

- True: Return the optimal value.
- False: Return the equation.

|br| * the default value is False.

:type getoptValue: boolean

:param getOptValueCostType: the cost type can either be TAC (total annualized costs) or NPV (net present value) |br| * the default value is None. :type getOptValueCostType: string

Source code in fine/component.py
def getEconomicsOperation(
    self,
    pyM,
    esM,
    fncType,
    factorNames,
    varName,
    dictName,
    getOptValue=False,
    getOptValueCostType=CostType.TAC,
):
    """Set time-dependent equations for the individual components. The equations will be set for all components of a modeling class
    and all locations as well as for each considered time step.
    In case of a two-dimensional component (e.g. a transmission component), the equations will be set for all possible connections between the
    defined locations.

    **Required arguments:**

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: EnergySystemModel instance representing the energy system in which the components should be modeled.
    :type esM: esM - EnergySystemModel class instance

    :param fncType: Function type, either "TD" or "TimeSeries"
    :type fncType: string

    :param factorNames: Strings of the time-dependent parameters that have to be multiplied within the equation.
        (e.g. ['opexPerOperation'] to multiply the operation variable with the costs for each operation).
    :type factorNames: list of strings

    :param varName: String of the variable that has to be multiplied within the equation (e.g. 'op' for operation variable).
    :type varName: string

    :param dictName: String of the variable set (e.g. 'operationVarDict')
    :type dictName: string

    **Default arguments:**

    :param getOptValue: Boolean that defines the output of the function:

        - True: Return the optimal value.
        - False: Return the equation.

        |br| * the default value is False.
    :type getoptValue: boolean

    :param getOptValueCostType: the cost type can either be TAC (total annualized costs) or NPV (net present value)
        |br| * the default value is None.
    :type getOptValueCostType: string
    """
    try:
        getOptValueCostType = CostType(getOptValueCostType)
    except ValueError as exc:
        raise ValueError(
            "getOptValueCostType must be either 'TAC' or 'NPV'"
        ) from exc
    try:
        fncType = FncType(fncType)
    except ValueError as exc:
        raise ValueError("fncType must be either 'TD' or 'TimeSeries'") from exc
    if fncType == FncType.TIME_SERIES:
        factorName = factorNames[0]  # noqa: F841

    var = getattr(pyM, varName + "_" + self.abbrvName)
    locCompIpCombinations = list(set([(x[0], x[1], x[2]) for x in var]))
    locCompNamesCombinations = list(set([(x[0], x[1]) for x in var.get_values()]))

    if esM.stochasticModel:
        if getOptValue:
            cost_results = {}
            for ip in esM.investmentPeriods:
                cost_results[ip] = pd.DataFrame()
            for loc, compName, ip in locCompIpCombinations:
                if ip not in esM.investmentPeriods:
                    continue
                cost_results[ip].loc[compName, loc] = self.getLocEconomicsOperation(
                    pyM,
                    esM,
                    fncType,
                    factorNames,
                    varName,
                    loc,
                    compName,
                    ip,
                    getOptValue,
                )
            return cost_results
        return sum(
            self.getLocEconomicsOperation(
                pyM,
                esM,
                fncType,
                factorNames,
                varName,
                loc,
                compName,
                ip,
                getOptValue,
            )
            for loc, compName, ip in locCompIpCombinations
        )
    # Components can have different investPerCapacity in different
    # years. The capex contribution however only depends on the capex
    # of the commissioning year. Therefore, we initialize a
    # dataframe with index and columns of the investment periods.
    # The rows describe the commissioning years,
    # e.g. a component build in year 2 but with a lifetime of three
    # years would have entries for df.loc[2,2:5]. Afterwards we
    # sum the contributions per column, multiply it with the annuity
    # present value factor to get the npv of the component for
    # different investPerCapacity and several ip for commissioning

    # initialize dict with (loc,comp) as key and df as values
    costContribution = {}
    componentYears = {}
    for loc, compName in locCompNamesCombinations:
        # get all years of component with location (also stock years)
        componentYears[compName] = (
            esM.getComponentAttribute(compName, "processedStockYears")
            + esM.investmentPeriods
        )
        costContribution[(loc, compName)] = {
            (y, i): 0
            for y in componentYears[compName]
            for i in esM.investmentPeriods
        }

    # fill the dataframes (per location and compName) with the cost
    # contributions depending on the commissioning year (index) and the
    # investment period (columns)

    locCompIpCombinations = list(set([(x[0], x[1], x[2]) for x in var]))
    for loc, compName, year in locCompIpCombinations:
        costContribution[(loc, compName)][(year, year)] = (
            self.getLocEconomicsOperation(
                pyM,
                esM,
                fncType,
                factorNames,
                varName,
                loc,
                compName,
                year,
                getOptValue,
            )
        )

    # create dictionary with ip as key and a dataframe with
    # cost contribution per component+location as value
    if getOptValue:
        cost_results = {ip: pd.DataFrame() for ip in esM.investmentPeriods}
        for loc, compName in locCompNamesCombinations:
            for ip in esM.investmentPeriods:
                cContrSum = sum(
                    [
                        costContribution[(loc, compName)].get((y, ip), 0)
                        for y in componentYears[compName]
                    ]
                )
                if getOptValueCostType == CostType.NPV:
                    cost_results[ip].loc[compName, loc] = (
                        cContrSum
                        * utils.annuityPresentValueFactor(
                            esM, compName, loc, esM.investmentPeriodInterval
                        )
                        * utils.discountFactor(esM, ip, compName, loc)
                    )
                elif getOptValueCostType == CostType.TAC:
                    cost_results[ip].loc[compName, loc] = cContrSum
        return cost_results
    if esM.annuityPerpetuity:
        # the last investment period gets the perpetuity cost
        # contribution, implying the system design and operation
        # will remain constant after the time frame of the
        # transformation pathway.
        for loc, compName in costContribution.keys():  # noqa: PLC0206
            for y in componentYears[compName]:
                costContribution[(loc, compName)][
                    (y, esM.investmentPeriods[-1])
                ] = costContribution[(loc, compName)][
                    (y, esM.investmentPeriods[-1])
                ] / (
                    utils.annuityPresentValueFactor(
                        esM, compName, loc, esM.investmentPeriodInterval
                    )
                    * esM.getComponent(compName).interestRate[loc]
                )
    return sum(
        sum(
            [
                costContribution[(loc, compName)].get((y, ip), 0)
                for y in componentYears[compName]
            ]
        )
        * utils.annuityPresentValueFactor(
            esM, compName, loc, esM.investmentPeriodInterval
        )
        * utils.discountFactor(esM, ip, compName, loc)
        for loc, compName, ip in locCompIpCombinations
        if ip in esM.investmentPeriods
    )

getLocEconomicsDesign

getLocEconomicsDesign(
    pyM,
    esM,
    factorNames,
    varName,
    loc,
    compName,
    ip,
    divisorName="",
    QPfactorNames=[],
    QPdivisorNames=[],
    getOptValue=False,
)

Set time-independent equation specified for one component in one location in one investment period.

Required arguments:

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

:param factorNames: Strings of the parameters that have to be multiplied within the equation. (e.g. ['processedInvestPerCapacity'] to multiply the capacity variable with the investment per each capacity unit). :type factorNames: list of strings

:param varName: String of the variable that has to be multiplied within the equation (e.g. 'cap' for capacity variable). :type varName: string

:param loc: String of the location for which the equation should be set up. :type loc: string

:param compName: String of the component name for which the equation should be set up. :type compName: string

Default arguments:

:param ip: investment period :type ip: int

:param divisorName: String of the variable that is used as a divisor within the equation (e.g. 'CCF'). If the divisorName is an empty string, there is no division within the equation. |br| * the default value is ''. :type divisorName: string

:param QPfactorNames: Strings of the parameters that have to be multiplied when quadratic programming is used. (e.g. ['processedQPcostScale']) :type QPfactorNames: list of strings

:param QPdivisorNames: Strings of the parameters that have to be used as divisors when quadratic programming is used. (e.g. ['QPbound']) :type QPdivisorNames: list of strings

:param getOptValue: Boolean that defines the output of the function:

- True: Return the optimal value.
- False: Return the equation.

|br| * the default value is False.

:type getoptValue: boolean

Source code in fine/component.py
def getLocEconomicsDesign(  # noqa: PLR0911
    self,
    pyM,
    esM,
    factorNames,
    varName,
    loc,
    compName,
    ip,
    divisorName="",
    QPfactorNames=[],
    QPdivisorNames=[],
    getOptValue=False,
):
    """Set time-independent equation specified for one component in one location in one investment period.

    **Required arguments:**

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package

    :param factorNames: Strings of the parameters that have to be multiplied within the equation.
        (e.g. ['processedInvestPerCapacity'] to multiply the capacity variable with the investment per each capacity unit).
    :type factorNames: list of strings

    :param varName: String of the variable that has to be multiplied within the equation (e.g. 'cap' for capacity variable).
    :type varName: string

    :param loc: String of the location for which the equation should be set up.
    :type loc: string

    :param compName: String of the component name for which the equation should be set up.
    :type compName: string

    **Default arguments:**

    :param ip: investment period
    :type ip: int

    :param divisorName: String of the variable that is used as a divisor within the equation (e.g. 'CCF').
        If the divisorName is an empty string, there is no division within the equation.
        |br| * the default value is ''.
    :type divisorName: string

    :param QPfactorNames: Strings of the parameters that have to be multiplied when quadratic programming is used. (e.g. ['processedQPcostScale'])
    :type QPfactorNames: list of strings

    :param QPdivisorNames: Strings of the parameters that have to be used as divisors when quadratic programming is used. (e.g. ['QPbound'])
    :type QPdivisorNames: list of strings

    :param getOptValue: Boolean that defines the output of the function:

        - True: Return the optimal value.
        - False: Return the equation.

        |br| * the default value is False.
    :type getoptValue: boolean
    """
    # negative ip (historical data) older than technical lifetime
    # round or ceil technical lifetime to interval
    if self.componentsDict[compName].floorTechnicalLifetime:
        roundedTechnicalLifetime = math.floor(
            self.componentsDict[compName].ipTechnicalLifetime[loc]
        )
    else:
        roundedTechnicalLifetime = math.ceil(
            self.componentsDict[compName].ipTechnicalLifetime[loc]
        )
    if ip < -roundedTechnicalLifetime:
        return 0
    # years where component could have commissioning as it is within the technical
    # lifetime, but does not have commissioning
    if ip < 0 and self.componentsDict[compName].processedStockCommissioning is None:
        return 0
    if (
        ip < 0
        and self.componentsDict[compName].processedStockCommissioning is not None
    ):
        if self.componentsDict[compName].processedStockCommissioning[ip][loc] == 0:
            return 0

    var = getattr(pyM, varName + "_" + self.abbrvName)
    factors = [
        getattr(self.componentsDict[compName], factorName)[ip][loc]
        for factorName in factorNames
    ]
    divisor = (
        getattr(self.componentsDict[compName], divisorName)[ip][loc]
        if not divisorName == ""
        else 1
    )

    factor = 1.0 / divisor
    for factor_ in factors:
        factor *= factor_

    _var = var[loc, compName, ip]

    if self.componentsDict[compName].processedQPcostScale[ip][loc] == 0:
        if not getOptValue:
            return factor * _var
        return factor * _var.value
    QPfactors = [
        getattr(self.componentsDict[compName], QPfactorName)[ip][loc]
        for QPfactorName in QPfactorNames
    ]
    QPdivisors = [
        getattr(self.componentsDict[compName], QPdivisorName)[ip][loc]
        for QPdivisorName in QPdivisorNames
    ]
    QPfactor = 1
    for QPfactor_ in QPfactors:
        QPfactor *= QPfactor_
    for QPdivisor in QPdivisors:
        QPfactor /= QPdivisor
    if not getOptValue:
        return factor * _var + QPfactor * _var * _var
    return factor * _var.value + QPfactor * _var.value * _var.value

getLocEconomicsOperation

getLocEconomicsOperation(
    pyM,
    esM,
    fncType,
    factorNames,
    varName,
    loc,
    compName,
    ip,
    getOptValue=False,
)

Set time-dependent cost functions for the individual components. The equations will be set for all components of a modeling class and all locations as well as for each considered time step.

Required arguments:

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: EnergySystemModel instance representing the energy system in which the components should be modeled. :type esM: esM - EnergySystemModel class instance

:param fncType: Function type, either "TD" or "TimeSeries :type fncType: string

:param factorName: String of the time-dependent parameter that have to be multiplied within the equation. (e.g. 'commodityCostTimeSeries' to multiply the operation variable with the costs for each operation). :type factorNames: string

:param varName: String of the variable that has to be multiplied within the equation (e.g. 'op' for operation variable). :type varName: string

:param dictName: String of the variable set (e.g. 'operationVarDict') :type dictName: string

:param loc: String of the location for which the equation should be set up. :type loc: string

:param compName: String of the component name for which the equation should be set up. :type compName: string

:param ip: investment period of transformation path analysis. :type ip: int

Default arguments:

:param getOptValue: Boolean that defines the output of the function:

- True: Return the optimal value.
- False: Return the equation.

|br| * the default value is False.

:type getoptValue: boolean

Source code in fine/component.py
def getLocEconomicsOperation(
    self,
    pyM,
    esM,
    fncType,
    factorNames,
    varName,
    loc,
    compName,
    ip,
    getOptValue=False,
):
    """Set time-dependent cost functions for the individual components. The equations will be set for all components
    of a modeling class and all locations as well as for each considered time step.

    **Required arguments:**

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: EnergySystemModel instance representing the energy system in which the components should be modeled.
    :type esM: esM - EnergySystemModel class instance

    :param fncType: Function type,  either "TD" or "TimeSeries
    :type fncType: string

    :param factorName: String of the time-dependent parameter that have to be multiplied within the equation.
        (e.g. 'commodityCostTimeSeries' to multiply the operation variable with the costs for each operation).
    :type factorNames: string

    :param varName: String of the variable that has to be multiplied within the equation (e.g. 'op' for operation variable).
    :type varName: string

    :param dictName: String of the variable set (e.g. 'operationVarDict')
    :type dictName: string

    :param loc: String of the location for which the equation should be set up.
    :type loc: string

    :param compName: String of the component name for which the equation should be set up.
    :type compName: string

    :param ip: investment period of transformation path analysis.
    :type ip: int

    **Default arguments:**

    :param getOptValue: Boolean that defines the output of the function:

        - True: Return the optimal value.
        - False: Return the equation.

        |br| * the default value is False.
    :type getoptValue: boolean
    """
    var = getattr(pyM, varName + "_" + self.abbrvName)

    # create new timeSet for current investment period
    timeSet_pt = [(p, t) for ip0, p, t in pyM.timeSet if ip0 == ip]

    # get factor
    if fncType == FncType.TD:
        factors = [
            getattr(self.componentsDict[compName], factorName)[ip][loc]
            for factorName in factorNames
        ]
        # TODO in no function, there is more than one factor, therefore the
        # use case of the following calculation is questioned
        # are the costs per operation calculated correctly for conversions?
        # Shouldnt there be a multiplication with the efficiency?
        factorVal = 1.0
        for factor_ in factors:
            factorVal *= factor_
        # write pd series with constant value for factornames
        mIdx = pd.MultiIndex.from_tuples(timeSet_pt, names=["Period", "TimeStep"])
        factor = pd.Series(factorVal, index=mIdx)
    elif fncType == FncType.TIME_SERIES:
        # if there is not time series, there is not cost contribution
        if getattr(self.componentsDict[compName], factorNames[0])[ip] is None:
            return 0
        factor = getattr(self.componentsDict[compName], factorNames[0])[ip][loc]

    if esM.stochasticModel:
        if not getOptValue:
            return (
                sum(
                    factor[p, t]
                    * var[loc, compName, ip, p, t]
                    * esM.periodOccurrences[ip][p]
                    for p, t in timeSet_pt
                )
                / esM.numberOfYears
            )
        return (
            sum(
                factor[p, t]
                * var[loc, compName, ip, p, t].value
                * esM.periodOccurrences[ip][p]
                for p, t in timeSet_pt
            )
            / esM.numberOfYears
        )
    if not getOptValue:
        return (
            sum(
                factor[p, t]
                * var[loc, compName, ip, p, t]
                * esM.periodOccurrences[ip][p]
                for p, t in timeSet_pt
            )
            / esM.numberOfYears
        )
    return (
        sum(
            factor[p, t]
            * var[loc, compName, ip, p, t].value
            * esM.periodOccurrences[ip][p]
            for p, t in timeSet_pt
        )
        / esM.numberOfYears
    )

getObjectiveFunctionContribution

getObjectiveFunctionContribution(esM, pyM)

Get contribution to the objective function.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

Source code in fine/component.py
def getObjectiveFunctionContribution(self, esM, pyM):
    """Get contribution to the objective function.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel
    """
    capexCap = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedInvestPerCapacity", "QPcostDev"],
        QPfactorNames=["processedQPcostScale", "processedInvestPerCapacity"],
        lifetimeAttr="ipEconomicLifetime",
        varName="commis",
        divisorName="CCF",
        QPdivisorNames=["QPbound", "CCF"],
    )
    capexDec = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedInvestIfBuilt"],
        lifetimeAttr="ipEconomicLifetime",
        varName="commisBin",
        divisorName="CCF",
    )
    opexCap = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedOpexPerCapacity", "QPcostDev"],
        QPfactorNames=["processedQPcostScale", "processedOpexPerCapacity"],
        lifetimeAttr="ipTechnicalLifetime",
        varName="commis",
        QPdivisorNames=["QPbound"],
    )
    opexDec = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedOpexIfBuilt"],
        lifetimeAttr="ipTechnicalLifetime",
        varName="commisBin",
    )

    return capexCap + capexDec + opexCap + opexDec

getOptimalValues

getOptimalValues(name='all', ip=0)

Return optimal values of the components.

:param name: name of the variables of which the optimal values should be returned:

* 'capacityVariablesOptimum',
* 'isBuiltVariablesOptimum',
* 'operationVariablesOptimum',
* 'commissioningVariablesOptimum'
* 'decommissioningVariablesOptimum'
* 'all' or another input: all variables are returned.

:type name: string

:param ip: investment period of transformation path analysis. |br| * the default value is 0 :type ip: int

:returns: a dictionary with the optimal values of the components :rtype: dict

Source code in fine/component.py
def getOptimalValues(self, name="all", ip=0):
    """Return optimal values of the components.

    :param name: name of the variables of which the optimal values should be returned:

        * 'capacityVariablesOptimum',
        * 'isBuiltVariablesOptimum',
        * 'operationVariablesOptimum',
        * 'commissioningVariablesOptimum'
        * 'decommissioningVariablesOptimum'
        * 'all' or another input: all variables are returned.

    :type name: string

    :param ip: investment period of transformation path analysis.
        |br| * the default value is 0
    :type ip: int

    :returns: a dictionary with the optimal values of the components
    :rtype: dict
    """
    timeDependentMapping = {
        "capacityVariablesOptimum": False,
        "isBuiltVariablesOptimum": False,
        "operationVariablesOptimum": True,
        "commissioningVariablesOptimum": False,
        "decommissioningVariablesOptimum": False,
    }

    if name in timeDependentMapping:
        return {
            "values": getattr(self, f"_{name}")[ip],
            "timeDependent": timeDependentMapping[name],
            "dimension": self.dimension,
        }
    return {
        valName: {
            "values": getattr(self, f"_{valName}")[ip],
            "timeDependent": timeDependentMapping[valName],
            "dimension": self.dimension,
        }
        for valName in timeDependentMapping
    }

getSharedPotentialContribution

getSharedPotentialContribution(pyM, key, loc, ip)

Get the share which the components of the modeling class have on a shared maximum potential at a location.

Source code in fine/component.py
def getSharedPotentialContribution(self, pyM, key, loc, ip):
    """Get the share which the components of the modeling class have on a shared maximum potential at a location."""
    compDict, abbrvName = self.componentsDict, self.abbrvName
    capVar = getattr(pyM, "cap_" + abbrvName)
    capVarSet = getattr(pyM, "designDimensionVarSet_" + abbrvName)
    return sum(
        capVar[loc, compName, ip] / compDict[compName].processedCapacityMax[ip][loc]
        for compName in compDict
        if compDict[compName].sharedPotentialID == key
        and (loc, compName, ip) in capVarSet
    )

hasOpVariablesForLocationCommodity abstractmethod

hasOpVariablesForLocationCommodity(esM, loc, commod)

Check if operation variables exist in the modeling class at a location which are connected to a commodity.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: esM - EnergySystemModel class instance

:param loc: name of the regarded location (locations are defined in the EnergySystemModel instance) :type loc: string

:param commod: name of the regarded commodity (commodities are defined in the EnergySystemModel instance) :param commod: string

Source code in fine/component.py
@abstractmethod
def hasOpVariablesForLocationCommodity(self, esM, loc, commod):
    """Check if operation variables exist in the modeling class at a location which are connected to a commodity.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: esM - EnergySystemModel class instance

    :param loc: name of the regarded location (locations are defined in the EnergySystemModel instance)
    :type loc: string

    :param commod: name of the regarded commodity (commodities are defined in the EnergySystemModel instance)
    :param commod: string
    """
    raise NotImplementedError

operationMode1

operationMode1(
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    factorName=None,
    *,
    isOperationCommisYearDepending=False,
)

Define operation mode 1. The operation [commodityUnith] is limited by the installed capacity in:\n * [commodityUnith] (for storages) or in * [commodityUnit] multiplied by the hours per time step (else).\n An additional factor can limited the operation further.

.. math::

op^{comp,opType}_{loc,ip,p,t} \\leq \\tau^{hours} \\cdot \\text{opFactor}^{opType} \\cdot cap^{comp}_{loc,ip}
Source code in fine/component.py
def operationMode1(
    self,
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    factorName=None,
    *,
    isOperationCommisYearDepending=False,
):
    r"""Define operation mode 1. The operation [commodityUnit*h] is limited by the installed capacity in:\n
    * [commodityUnit*h] (for storages) or in
    * [commodityUnit] multiplied by the hours per time step (else).\n
    An additional factor can limited the operation further.

    .. math::

        op^{comp,opType}_{loc,ip,p,t} \\leq \\tau^{hours} \\cdot \\text{opFactor}^{opType} \\cdot cap^{comp}_{loc,ip}

    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    constrSet1 = getattr(pyM, constrSetName + "1_" + abbrvName)

    if not pyM.hasSegmentation:
        factor1 = esM.hoursPerTimeStep
        if isOperationCommisYearDepending:

            def op1(pyM, loc, compName, commis, ip, p, t):
                factor2 = (
                    1
                    if factorName is None
                    else getattr(compDict[compName], factorName)
                )
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    <= factor1 * factor2 * commisVar[loc, compName, commis]
                )
        else:

            def op1(pyM, loc, compName, ip, p, t):
                factor2 = (
                    1
                    if factorName is None
                    else getattr(compDict[compName], factorName)
                )
                return (
                    opVar[loc, compName, ip, p, t]
                    <= factor1 * factor2 * capVar[loc, compName, ip]
                )

        setattr(
            pyM,
            constrName + "1_" + abbrvName,
            pyomo.Constraint(constrSet1, pyM.intraYearTimeSet, rule=op1),
        )
    else:
        if isOperationCommisYearDepending:

            def op1(pyM, loc, compName, commis, ip, p, t):
                factor1 = esM.hoursPerSegment[ip].to_dict()
                factor2 = (
                    1
                    if factorName is None
                    else getattr(compDict[compName], factorName)
                )
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    <= factor1[p, t] * factor2 * commisVar[loc, compName, commis]
                )  # factor not dependent on ip

        else:

            def op1(pyM, loc, compName, ip, p, t):
                factor1 = esM.hoursPerSegment[ip].to_dict()
                factor2 = (
                    1
                    if factorName is None
                    else getattr(compDict[compName], factorName)
                )
                return (
                    opVar[loc, compName, ip, p, t]
                    <= factor1[p, t] * factor2 * capVar[loc, compName, ip]
                )  # factor not dependent on ip

        setattr(
            pyM,
            constrName + "1_" + abbrvName,
            pyomo.Constraint(constrSet1, pyM.intraYearTimeSet, rule=op1),
        )

operationMode2

operationMode2(
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opRateName="processedOperationRateFix",
    *,
    isOperationCommisYearDepending=False,
)

Define operation mode 2.

The operation [commodityUnith] is equal to the installed capacity multiplied with a time series in:\n * [commodityUnith] (for storages) or in * [commodityUnit] multiplied by the hours per time step (else).\n

.. math::

op^{comp,opType}_{loc,ip,p,t} \\leq \\tau^{hours} \\cdot \\text{opRateMax}^{comp,opType}_{loc,ip,p,t} \\cdot cap^{comp}_{loc,ip}
Source code in fine/component.py
def operationMode2(
    self,
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opRateName="processedOperationRateFix",
    *,
    isOperationCommisYearDepending=False,
):
    r"""Define operation mode 2.

    The operation [commodityUnit*h] is equal to the installed capacity multiplied
    with a time series in:\n
    * [commodityUnit*h] (for storages) or in
    * [commodityUnit] multiplied by the hours per time step (else).\n

    .. math::

        op^{comp,opType}_{loc,ip,p,t} \\leq \\tau^{hours} \\cdot \\text{opRateMax}^{comp,opType}_{loc,ip,p,t} \\cdot cap^{comp}_{loc,ip}

    """
    # additions for perfect foresight
    # operationRate is the same for all ip
    compDict, abbrvName = self.componentsDict, self.abbrvName
    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    constrSet2 = getattr(pyM, constrSetName + "2_" + abbrvName)

    if not pyM.hasSegmentation:
        factor = esM.hoursPerTimeStep
        if isOperationCommisYearDepending:

            def op2(pyM, loc, compName, commis, ip, p, t):
                rate = getattr(compDict[compName], opRateName)[ip]
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    == commisVar[loc, compName, commis] * rate[loc][p, t] * factor
                )  # rate independent from ip

        else:

            def op2(pyM, loc, compName, ip, p, t):
                rate = getattr(compDict[compName], opRateName)[ip]
                return (
                    opVar[loc, compName, ip, p, t]
                    == capVar[loc, compName, ip] * rate[loc][p, t] * factor
                )  # rate independent from ip

        setattr(
            pyM,
            constrName + "2_" + abbrvName,
            pyomo.Constraint(constrSet2, pyM.intraYearTimeSet, rule=op2),
        )
    else:
        if isOperationCommisYearDepending:

            def op2(pyM, loc, compName, commis, ip, p, t):
                factor = esM.hoursPerSegment[ip].to_dict()
                rate = getattr(compDict[compName], opRateName)[ip]
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    == commisVar[loc, compName, commis]
                    * rate[loc][p, t]
                    * factor[p, t]
                )

        else:

            def op2(pyM, loc, compName, ip, p, t):
                factor = esM.hoursPerSegment[ip].to_dict()
                rate = getattr(compDict[compName], opRateName)[ip]
                return (
                    opVar[loc, compName, ip, p, t]
                    == capVar[loc, compName, ip] * rate[loc][p, t] * factor[p, t]
                )

        setattr(
            pyM,
            constrName + "2_" + abbrvName,
            pyomo.Constraint(constrSet2, pyM.intraYearTimeSet, rule=op2),
        )

operationMode3

operationMode3(
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opRateName="processedOperationRateMax",
    *,
    isOperationCommisYearDepending=False,
    relevanceThreshold=None,
)

Define operation mode 3.

The operation [commodityUnith] is limited by an installed capacity multiplied with a time series in:\n * [commodityUnith] (for storages) or in * [commodityUnit] multiplied by the hours per time step (else).\n

.. math:: op^{comp,opType}{loc,ip,p,t} = \tau^{hours} \cdot \text{opRateFix}^{comp,opType}} \cdot cap^{comp}_{loc,ip

:param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold. |br| * the default value is None :type relevanceThreshold: float (>=0) or None

Source code in fine/component.py
def operationMode3(
    self,
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opRateName="processedOperationRateMax",
    *,
    isOperationCommisYearDepending=False,
    relevanceThreshold=None,
):
    r"""Define operation mode 3.

    The operation [commodityUnit*h] is limited by an installed capacity multiplied
    with a time series in:\n
    * [commodityUnit*h] (for storages) or in
    * [commodityUnit] multiplied by the hours per time step (else).\n

    .. math::
        op^{comp,opType}_{loc,ip,p,t} = \\tau^{hours} \\cdot \\text{opRateFix}^{comp,opType}_{loc,ip,p,t} \\cdot cap^{comp}_{loc,ip}

    :param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold.
        |br| * the default value is None
    :type relevanceThreshold: float (>=0) or None

    """
    # operationRate is the same for all ip
    compDict, abbrvName = self.componentsDict, self.abbrvName
    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    constrSet3 = getattr(pyM, constrSetName + "3_" + abbrvName)

    if not pyM.hasSegmentation:
        factor = esM.hoursPerTimeStep
        if isOperationCommisYearDepending:

            def op3(pyM, loc, compName, commis, ip, p, t):
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, commis, ip, p, t] == 0
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    <= commisVar[loc, compName, commis] * rate[loc][p, t] * factor
                )

        else:

            def op3(pyM, loc, compName, ip, p, t):
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, ip, p, t] == 0
                return (
                    opVar[loc, compName, ip, p, t]
                    <= capVar[loc, compName, ip] * rate[loc][p, t] * factor
                )

        setattr(
            pyM,
            constrName + "3_" + abbrvName,
            pyomo.Constraint(constrSet3, pyM.intraYearTimeSet, rule=op3),
        )
    else:
        if isOperationCommisYearDepending:

            def op3(pyM, loc, compName, commis, ip, p, t):
                factor = esM.hoursPerSegment[ip].to_dict()
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, commis, ip, p, t] == 0
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    <= commisVar[loc, compName, commis]
                    * rate[loc][p, t]
                    * factor[p, t]
                )  # rate and factor independent from ip

        else:

            def op3(pyM, loc, compName, ip, p, t):
                factor = esM.hoursPerSegment[ip].to_dict()
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, ip, p, t] == 0
                return (
                    opVar[loc, compName, ip, p, t]
                    <= capVar[loc, compName, ip] * rate[loc][p, t] * factor[p, t]
                )  # rate and factor independent from ip

        setattr(
            pyM,
            constrName + "3_" + abbrvName,
            pyomo.Constraint(constrSet3, pyM.intraYearTimeSet, rule=op3),
        )

operationMode4

operationMode4(
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opRateName="processedOperationRateMin",
    *,
    isOperationCommisYearDepending=False,
    relevanceThreshold=None,
)

Define operation mode 4.

The operation [commodityUnith] is limited by an installed capacity multiplied with a time series in:\n * [commodityUnith] (for storages) or in * [commodityUnit] multiplied by the hours per time step (else).\n

.. math:: op^{comp,opType}{loc,ip,p,t} = \tau^{hours} \cdot \text{opRateFix}^{comp,opType}} \cdot cap^{comp}_{loc,ip

:param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold. |br| * the default value is None :type relevanceThreshold: float (>=0) or None

Source code in fine/component.py
def operationMode4(
    self,
    pyM,
    esM,
    constrName,
    constrSetName,
    opVarName,
    opRateName="processedOperationRateMin",
    *,
    isOperationCommisYearDepending=False,
    relevanceThreshold=None,
):
    r"""Define operation mode 4.

    The operation [commodityUnit*h] is limited by an installed capacity
    multiplied with a time series in:\n
    * [commodityUnit*h] (for storages) or in
    * [commodityUnit] multiplied by the hours per time step (else).\n

    .. math::
        op^{comp,opType}_{loc,ip,p,t} = \\tau^{hours} \\cdot \\text{opRateFix}^{comp,opType}_{loc,ip,p,t} \\cdot cap^{comp}_{loc,ip}

    :param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold.
        |br| * the default value is None
    :type relevanceThreshold: float (>=0) or None

    """
    # operationRate is the same for all ip
    compDict, abbrvName = self.componentsDict, self.abbrvName
    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    constrSet4 = getattr(pyM, constrSetName + "4_" + abbrvName)

    if not pyM.hasSegmentation:
        factor = esM.hoursPerTimeStep
        if isOperationCommisYearDepending:

            def op4(pyM, loc, compName, commis, ip, p, t):
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, commis, ip, p, t] == 0
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    >= commisVar[loc, compName, commis] * rate[loc][p, t] * factor
                )

        else:

            def op4(pyM, loc, compName, ip, p, t):
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, ip, p, t] == 0
                return (
                    opVar[loc, compName, ip, p, t]
                    >= capVar[loc, compName, ip] * rate[loc][p, t] * factor
                )

        setattr(
            pyM,
            constrName + "4_" + abbrvName,
            pyomo.Constraint(constrSet4, pyM.intraYearTimeSet, rule=op4),
        )
    else:
        if isOperationCommisYearDepending:

            def op4(pyM, loc, compName, commis, ip, p, t):
                factor = esM.hoursPerSegment[ip].to_dict()
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, commis, ip, p, t] == 0
                return (
                    opVar[loc, compName, commis, ip, p, t]
                    >= commisVar[loc, compName, commis]
                    * rate[loc][p, t]
                    * factor[p, t]
                )  # rate and factor independent from ip

        else:

            def op4(pyM, loc, compName, ip, p, t):
                factor = esM.hoursPerSegment[ip].to_dict()
                rate = getattr(compDict[compName], opRateName)[ip]
                if relevanceThreshold is not None:
                    validTreshold = 0 < relevanceThreshold
                    if validTreshold and (rate[loc][p, t] <= relevanceThreshold):
                        # operationRate is lower than threshold --> set to 0
                        return opVar[loc, compName, ip, p, t] == 0
                return (
                    opVar[loc, compName, ip, p, t]
                    >= capVar[loc, compName, ip] * rate[loc][p, t] * factor[p, t]
                )  # rate and factor independent from ip

        setattr(
            pyM,
            constrName + "4_" + abbrvName,
            pyomo.Constraint(constrSet4, pyM.intraYearTimeSet, rule=op4),
        )

setOptimalValues

setOptimalValues(
    esM, pyM, indexColumns, plantUnit, unitApp=""
)

Set the optimal values for the considered components and return a summary of them. The function is called after optimization was successful and an optimal solution was found. Each sub class of the component class calls this function for setting the common optimal values, e.g. investment and maintenance costs proportional to optimal capacity expansion.

Required arguments

:param esM: EnergySystemModel instance representing the energy system in which the components are modeled. :type esM: EnergySystemModel instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param ip: investment period of transformation path analysis. :type ip: int

:param indexColumns: set of strings with the columns indices of the summary. The indices represent the locations or connections between the locations are used to call the optimal values of the variables of the components in the model class. :type indexColumns: set

:param plantUnit: attribute of the component that describes the unit of the plants to which maximum capacity limitations, cost parameters and the operation time series refer to. Depending on the considered component, possible inputs are "commodityUnit" (e.g. for transmission components) or "physicalUnit" (e.g. for conversion components). :type plantUnit: string

Default arguments

:param unitApp: string which appends the capacity unit in the optimization summary. For example, for the StorageModel class, the parameter is set to '\*h'. |br| * the default value is ''. :type unitApp: string

:return: summary of the optimized values. :rtype: pandas DataFrame

Source code in fine/component.py
3749
3750
3751
3752
3753
3754
3755
3756
3757
3758
3759
3760
3761
3762
3763
3764
3765
3766
3767
3768
3769
3770
3771
3772
3773
3774
3775
3776
3777
3778
3779
3780
3781
3782
3783
3784
3785
3786
3787
3788
3789
3790
3791
3792
3793
3794
3795
3796
3797
3798
3799
3800
3801
3802
3803
3804
3805
3806
3807
3808
3809
3810
3811
3812
3813
3814
3815
3816
3817
3818
3819
3820
3821
3822
3823
3824
3825
3826
3827
3828
3829
3830
3831
3832
3833
3834
3835
3836
3837
3838
3839
3840
3841
3842
3843
3844
3845
3846
3847
3848
3849
3850
3851
3852
3853
3854
3855
3856
3857
3858
3859
3860
3861
3862
3863
3864
3865
3866
3867
3868
3869
3870
3871
3872
3873
3874
3875
3876
3877
3878
3879
3880
3881
3882
3883
3884
3885
3886
3887
3888
3889
3890
3891
3892
3893
3894
3895
3896
3897
3898
3899
3900
3901
3902
3903
3904
3905
3906
3907
3908
3909
3910
3911
3912
3913
3914
3915
3916
3917
3918
3919
3920
3921
3922
3923
3924
3925
3926
3927
3928
3929
3930
3931
3932
3933
3934
3935
3936
3937
3938
3939
3940
3941
3942
3943
3944
3945
3946
3947
3948
3949
3950
3951
3952
3953
3954
3955
3956
3957
3958
3959
3960
3961
3962
3963
3964
3965
3966
3967
3968
3969
3970
3971
3972
3973
3974
3975
3976
3977
3978
3979
3980
3981
3982
3983
3984
3985
3986
3987
3988
3989
3990
3991
3992
3993
3994
3995
3996
3997
3998
3999
4000
4001
4002
4003
4004
4005
4006
4007
4008
4009
4010
4011
4012
4013
4014
4015
4016
4017
4018
4019
4020
4021
4022
4023
4024
4025
4026
4027
4028
4029
4030
4031
4032
4033
4034
4035
4036
4037
4038
4039
4040
4041
4042
4043
4044
4045
4046
4047
4048
4049
4050
4051
4052
4053
4054
4055
4056
4057
4058
4059
4060
4061
4062
4063
4064
4065
4066
4067
4068
4069
4070
4071
4072
4073
4074
4075
4076
4077
4078
4079
4080
4081
4082
4083
4084
4085
4086
4087
4088
4089
4090
4091
4092
4093
4094
4095
4096
4097
4098
4099
4100
4101
4102
4103
4104
4105
4106
4107
4108
4109
4110
4111
4112
4113
4114
4115
4116
4117
4118
4119
4120
4121
4122
4123
4124
4125
4126
4127
4128
4129
4130
4131
4132
4133
4134
4135
4136
4137
4138
4139
4140
4141
4142
4143
4144
4145
4146
4147
4148
4149
4150
4151
4152
4153
4154
4155
4156
4157
4158
4159
4160
4161
4162
4163
4164
4165
4166
4167
4168
4169
4170
4171
4172
4173
4174
4175
4176
4177
4178
4179
4180
4181
4182
4183
4184
4185
4186
4187
4188
4189
4190
4191
4192
4193
4194
4195
4196
4197
4198
4199
4200
4201
4202
4203
4204
4205
4206
4207
4208
4209
4210
4211
4212
4213
4214
4215
4216
4217
4218
4219
4220
4221
4222
4223
4224
4225
4226
4227
4228
4229
4230
4231
4232
4233
4234
4235
4236
4237
4238
4239
4240
4241
4242
4243
4244
4245
4246
4247
4248
4249
4250
4251
4252
4253
4254
4255
4256
4257
def setOptimalValues(self, esM, pyM, indexColumns, plantUnit, unitApp=""):
    r"""Set the optimal values for the considered components and return a summary of them.
    The function is called after optimization was successful and an optimal solution was found.
    Each sub class of the component class calls this function for setting the common optimal values,
    e.g. investment and maintenance costs proportional to optimal capacity expansion.

    **Required arguments**

    :param esM: EnergySystemModel instance representing the energy system in which the components are modeled.
    :type esM: EnergySystemModel instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :param indexColumns: set of strings with the columns indices of the summary. The indices represent the locations
        or connections between the locations are used to call the optimal values of the variables of the components
        in the model class.
    :type indexColumns: set

    :param plantUnit: attribute of the component that describes the unit of the plants to which maximum capacity
        limitations, cost parameters and the operation time series refer to. Depending on the considered component,
        possible inputs are "commodityUnit" (e.g. for transmission components) or "physicalUnit" (e.g. for
        conversion components).
    :type plantUnit: string

    **Default arguments**

    :param unitApp: string which appends the capacity unit in the optimization summary.
        For example, for the StorageModel class, the parameter is set to '\\*h'.
        |br| * the default value is ''.
    :type unitApp: string

    :return: summary of the optimized values.
    :rtype: pandas DataFrame
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    capVar = getattr(esM.pyM, "cap_" + abbrvName)
    binVar = getattr(esM.pyM, "commisBin_" + abbrvName)
    commisVar = getattr(esM.pyM, "commis_" + abbrvName)
    decommisVar = getattr(esM.pyM, "decommis_" + abbrvName)

    props = [
        "capacity",
        "commissioning",
        "decommissioning",
        "isBuilt",
        "capexCap",
        "capexIfBuilt",
        "opexCap",
        "opexIfBuilt",
        "TAC",
        "NPVcontribution",
        "invest",
        "investLifetimeExtension",
        "revenueLifetimeShorteningResale",
    ]
    units = [
        "[-]",
        "[-]",
        "[-]",
        "[-]",
        "[" + esM.costUnit + "/a]",
        "[" + esM.costUnit + "/a]",
        "[" + esM.costUnit + "/a]",
        "[" + esM.costUnit + "/a]",
        "[" + esM.costUnit + "/a]",
        "[" + esM.costUnit + "]",
        "[" + esM.costUnit + "]",
        "[" + esM.costUnit + "]",
        "[" + esM.costUnit + "]",
    ]
    tuples = [
        (compName, prop, unit)
        for compName in compDict.keys()
        for prop, unit in zip(props, units)
    ]
    tuples = list(
        map(
            lambda x: (
                (
                    x[0],
                    x[1],
                    "[" + getattr(compDict[x[0]], plantUnit) + unitApp + "]",
                )
                if x[1] in ["capacity", "commissioning", "decommissioning"]
                else x
            ),
            tuples,
        )
    )
    mIndex = pd.MultiIndex.from_tuples(
        tuples, names=["Component", "Property", "Unit"]
    )

    # get the results for all components
    resultsNPV_cx = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedInvestPerCapacity", "QPcostDev"],
        QPfactorNames=["processedQPcostScale", "processedInvestPerCapacity"],
        lifetimeAttr="ipEconomicLifetime",
        varName="commis",
        divisorName="CCF",
        QPdivisorNames=["QPbound", "CCF"],
        getOptValue=True,
        getOptValueCostType=CostType.NPV,
    )

    resultsTAC_cx = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedInvestPerCapacity", "QPcostDev"],
        QPfactorNames=["processedQPcostScale", "processedInvestPerCapacity"],
        lifetimeAttr="ipEconomicLifetime",
        varName="commis",
        divisorName="CCF",
        QPdivisorNames=["QPbound", "CCF"],
        getOptValue=True,
        getOptValueCostType=CostType.TAC,
    )

    resultsNPV_ox = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedOpexPerCapacity", "QPcostDev"],
        QPfactorNames=["processedQPcostScale", "processedOpexPerCapacity"],
        lifetimeAttr="ipTechnicalLifetime",
        varName="commis",
        QPdivisorNames=["QPbound"],
        getOptValue=True,
        getOptValueCostType=CostType.NPV,
    )

    resultsTAC_ox = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedOpexPerCapacity", "QPcostDev"],
        QPfactorNames=["processedQPcostScale", "processedOpexPerCapacity"],
        lifetimeAttr="ipTechnicalLifetime",
        varName="commis",
        QPdivisorNames=["QPbound"],
        getOptValue=True,
        getOptValueCostType=CostType.TAC,
    )

    # Get NPV contribution for investmentIfBuilt
    resultsNPV_cx_bin = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedInvestIfBuilt"],
        lifetimeAttr="ipEconomicLifetime",
        varName="commisBin",
        divisorName="CCF",
        getOptValue=True,
        getOptValueCostType=CostType.NPV,
    )

    # Calculate the annualized investment costs cx (CAPEX)
    # Get TAC for investmentIfBuilt
    resultsTAC_cx_bin = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedInvestIfBuilt"],
        lifetimeAttr="ipEconomicLifetime",
        varName="commisBin",
        divisorName="CCF",
        getOptValue=True,
        getOptValueCostType=CostType.TAC,
    )

    # Get NPV cost contribution for the annualized operational costs if built ox (OPEX)
    resultsNPV_ox_bin = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedOpexIfBuilt"],
        lifetimeAttr="ipTechnicalLifetime",
        varName="commisBin",
        getOptValue=True,
        getOptValueCostType=CostType.NPV,
    )

    # Calculate the annualized operational costs if built ox (OPEX)
    resultTAC_ox_bin = self.getEconomicsDesign(
        pyM,
        esM,
        factorNames=["processedOpexIfBuilt"],
        lifetimeAttr="ipTechnicalLifetime",
        varName="commisBin",
        getOptValue=True,
        getOptValueCostType=CostType.TAC,
    )

    optSummary = {}
    for ip in esM.investmentPeriods:
        optSummary_ip = pd.DataFrame(
            index=mIndex, columns=sorted(indexColumns)
        ).sort_index()

        # Get and set optimal variable values for capacities
        values = capVar.get_values()
        capOptVal = utils.formatOptimizationOutput(
            values, VarType.DESIGN, Dimension.ONE, ip
        )
        capOptVal_ = utils.formatOptimizationOutput(
            values, VarType.DESIGN, self.dimension, ip, compDict=compDict
        )
        self._capacityVariablesOptimum[esM.investmentPeriodNames[ip]] = capOptVal_
        # Get and set optimal variable values for commissioning
        commisValues = commisVar.get_values()
        commisOptVal = utils.formatOptimizationOutput(
            commisValues, VarType.DESIGN, Dimension.ONE, ip
        )
        commisOptVal_ = utils.formatOptimizationOutput(
            commisValues, VarType.DESIGN, self.dimension, ip, compDict=compDict
        )
        self._commissioningVariablesOptimum[esM.investmentPeriodNames[ip]] = (
            commisOptVal_
        )
        # Get and set optimal variable values for decommissioning
        decommisValues = decommisVar.get_values()
        decommisOptVal = utils.formatOptimizationOutput(
            decommisValues, VarType.DESIGN, Dimension.ONE, ip
        )
        decommisOptVal_ = utils.formatOptimizationOutput(
            decommisValues, VarType.DESIGN, self.dimension, ip, compDict=compDict
        )
        self._decommissioningVariablesOptimum[esM.investmentPeriodNames[ip]] = (
            decommisOptVal_
        )

        if capOptVal is not None:
            # Check if the installed capacities are close to a bigM val
            # ue for components with design decision variables but
            # ignores cases where bigM was substituted by capacityMax parameter (see bigM constraint
            for compName, comp in compDict.items():
                if (
                    comp.hasIsBuiltBinaryVariable
                    and (comp.processedCapacityMax is None)
                    and capOptVal.loc[compName].max() >= comp.bigM * 0.9
                    and esM.verboseLogLevel < 2
                ):
                    warnings.warn(
                        "the capacity of component "
                        + compName
                        + " is in one or more locations close "
                        + "or equal to the chosen Big M. Consider rerunning the simulation with a higher"
                        + " Big M."
                    )

            # Calculate the investment costs i (proportional to commissioning)
            i = commisOptVal.apply(
                lambda commis: (
                    commis
                    * compDict[commis.name].processedInvestPerCapacity[ip]
                    * compDict[commis.name].QPcostDev[ip]
                    + (
                        compDict[commis.name].processedInvestPerCapacity[ip]
                        * compDict[commis.name].processedQPcostScale[ip]
                        / (compDict[commis.name].QPbound[ip])
                        * commis
                        * commis
                    )
                ),
                axis=1,
            )

            # Get NPV contribution for investment
            npv_cx = resultsNPV_cx[ip]

            # Calculate the annualized investment costs cx (CAPEX)
            # Get TAC for investment
            tac_cx = resultsTAC_cx[ip]

            # Get NPV cost contribution for the annualized operational costs ox (OPEX)
            npv_ox = resultsNPV_ox[ip]

            # Calculate the annualized operational costs ox (OPEX)
            tac_ox = resultsTAC_ox[ip]

            # Fill the optimization summary with the calculated values for invest, CAPEX and OPEX
            # (due to capacity expansion).
            optSummary_ip.loc[
                [
                    (
                        ix,
                        "capacity",
                        "[" + getattr(compDict[ix], plantUnit) + unitApp + "]",
                    )
                    for ix in capOptVal.index
                ],
                capOptVal.columns,
            ] = capOptVal.values

            optSummary_ip.loc[
                [(ix, "invest", "[" + esM.costUnit + "]") for ix in i.index],
                i.columns,
            ] = i.values

            optSummary_ip.loc[
                [
                    (ix, "capexCap", "[" + esM.costUnit + "/a]")
                    for ix in tac_cx.index
                ],
                tac_cx.columns,
            ] = tac_cx.values
            optSummary_ip.loc[
                [
                    (ix, "opexCap", "[" + esM.costUnit + "/a]")
                    for ix in tac_ox.index
                ],
                tac_ox.columns,
            ] = tac_ox.values

            # add additional costs for lifetime extension or scrapping bonus if lifetime is floored or ceiled to next interval
            for component in i.index:
                for loc in i.columns:
                    # only relevant if there is any invest
                    if np.isnan(i.loc[component, loc]):
                        val_investLifetimeExtension = 0
                        val_revenueLifetimeShorteningResale = 0
                    else:
                        techLifetime = compDict[component].technicalLifetime[loc]
                        econLifetime = compDict[component].economicLifetime[loc]
                        sameInterval = math.floor(
                            compDict[component].ipTechnicalLifetime[loc]
                        ) == math.floor(compDict[component].ipEconomicLifetime[loc])

                        # investLifetimeExtension
                        if (
                            esM.numberOfInvestmentPeriods > 1
                            and (techLifetime % esM.investmentPeriodInterval != 0)
                            and not compDict[component].floorTechnicalLifetime
                        ):
                            intervalPart = 1 - (
                                compDict[component].ipTechnicalLifetime[loc] % 1
                            )
                            val_investLifetimeExtension = (
                                i.loc[component, loc]
                                * intervalPart
                                / compDict[component].ipEconomicLifetime[loc]
                            )
                        else:
                            val_investLifetimeExtension = 0

                        # revenueLifetimeShorteningResale
                        if (
                            esM.numberOfInvestmentPeriods > 1
                            and econLifetime % esM.investmentPeriodInterval != 0
                            and compDict[component].floorTechnicalLifetime
                            and sameInterval
                        ):
                            intervalPart = (
                                compDict[component].ipEconomicLifetime[loc] % 1
                            )
                            val_revenueLifetimeShorteningResale = (
                                i.loc[component, loc]
                                * intervalPart
                                / compDict[component].ipEconomicLifetime[loc]
                            )
                        else:
                            val_revenueLifetimeShorteningResale = 0

                    # write values into optimization summary
                    optSummary_ip.loc[
                        (
                            component,
                            "investLifetimeExtension",
                            "[" + esM.costUnit + "]",
                        ),
                        loc,
                    ] = val_investLifetimeExtension

                    optSummary_ip.loc[
                        (
                            component,
                            "revenueLifetimeShorteningResale",
                            "[" + esM.costUnit + "]",
                        ),
                        loc,
                    ] = val_revenueLifetimeShorteningResale

        # Get and set optimal variable values for binary investment decisions (isBuiltBinary).
        values = binVar.get_values()
        binCapOptVal = utils.formatOptimizationOutput(
            values, VarType.DESIGN, Dimension.ONE, ip
        )
        binCapOptVal_ = utils.formatOptimizationOutput(
            values, VarType.DESIGN, self.dimension, ip=ip, compDict=compDict
        )
        self._isBuiltVariablesOptimum[esM.investmentPeriodNames[ip]] = binCapOptVal_

        if binCapOptVal is not None:
            # Calculate the investment costs i (fix value if component is built)
            i_bin = binCapOptVal.apply(
                lambda dec: dec * compDict[dec.name].processedInvestIfBuilt[ip],
                axis=1,
            )

            # Get NPV contribution for investmentIfBuilt
            npv_cx_bin = resultsNPV_cx_bin[ip]

            # Calculate the annualized investment costs cx (CAPEX)
            # Get TAC for investmentIfBuilt
            tac_cx_bin = resultsTAC_cx_bin[ip]

            npv_ox_bin = resultsNPV_ox_bin[ip]

            # Calculate the annualized operational costs if built ox (OPEX)
            tac_ox_bin = resultTAC_ox_bin[ip]

            # Fill the optimization summary with the calculated values for invest, CAPEX and OPEX
            # (due to isBuilt decisions).
            optSummary_ip.loc[
                [(ix, "isBuilt", "[-]") for ix in binCapOptVal.index],
                binCapOptVal.columns,
            ] = binCapOptVal.values
            optSummary_ip.loc[
                [(ix, "invest", "[" + esM.costUnit + "]") for ix in i_bin.index],
                i_bin.columns,
            ] += i_bin.values
            optSummary_ip.loc[
                [
                    (ix, "capexIfBuilt", "[" + esM.costUnit + "/a]")
                    for ix in tac_cx_bin.index
                ],
                tac_cx_bin.columns,
            ] = tac_cx_bin.values
            optSummary_ip.loc[
                [
                    (ix, "opexIfBuilt", "[" + esM.costUnit + "/a]")
                    for ix in tac_ox_bin.index
                ],
                tac_ox_bin.columns,
            ] = tac_ox_bin.values

        # Get and set optimal values for commissioning and decommissioning
        # not applicable for singleyear optimization, hence dropped from summary
        # get commissioning and decommissioning results

        # either decommissioning or capacity exists
        # (years can have decommissioning, leading to no left capacity)
        if decommisOptVal is not None or capOptVal is not None:
            # Fill in the optimization summary for commissioning and decommissioning
            # commissioning
            optSummary_ip.loc[
                [
                    (
                        ix,
                        "commissioning",
                        "[" + getattr(compDict[ix], plantUnit) + unitApp + "]",
                    )
                    for ix in commisOptVal.index
                ],
                commisOptVal.columns,
            ] = commisOptVal.values
            # decommissioning
            optSummary_ip.loc[
                [
                    (
                        ix,
                        "decommissioning",
                        "[" + getattr(compDict[ix], plantUnit) + unitApp + "]",
                    )
                    for ix in decommisOptVal.index
                ],
                decommisOptVal.columns,
            ] = decommisOptVal.values

        # Summarize all annualized contributions to the total annual cost
        optSummary_ip.loc[optSummary_ip.index.get_level_values(1) == "TAC"] = (
            optSummary_ip.loc[
                (optSummary_ip.index.get_level_values(1) == "capexCap")
                | (optSummary_ip.index.get_level_values(1) == "opexCap")
                | (optSummary_ip.index.get_level_values(1) == "capexIfBuilt")
                | (
                    optSummary_ip.index.get_level_values(1)
                    == "processedOpexIfBuilt"
                )
            ]
            .groupby(level=0)
            .sum()
            .values
        )

        npv = pd.DataFrame()
        if capOptVal is not None:
            npv = npv.add(npv_cx, fill_value=0)
            npv = npv.add(npv_ox, fill_value=0)
        if binCapOptVal is not None:
            npv = npv.add(npv_cx_bin, fill_value=0)
            npv = npv.add(npv_ox_bin, fill_value=0)

        optSummary_ip.loc[
            [
                (
                    ix,
                    "NPVcontribution",
                    "[" + esM.costUnit + "]",
                )
                for ix in npv.index
            ],
            npv.columns,
        ] = npv.values
        optSummary[esM.investmentPeriodNames[ip]] = optSummary_ip

    return optSummary

stockCapacityConstraint

stockCapacityConstraint(pyM, esM)

Set the stock capacity constraint. The stock capacity is the sum of the stock commissioning, which do not exceed its technical lifetime.

For stochastic, the stock of past investment periods is not only valid for ip=0 but for all investment periods. .. math::

cap^{comp}_{loc,ip} =  stockCap^{comp}_{loc} + commis^{comp}_{loc,ip} - decommis^{comp}_{loc,0}

For capacity development, the stock is only considered for the first investment periods.

.. math::

cap^{comp}_{loc,0} =  stockCap^{comp}_{loc} + commis^{comp}_{loc,0} - decommis^{comp}_{loc,0}

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param esM: energy system model containing general information. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/component.py
def stockCapacityConstraint(self, pyM, esM):
    """Set the stock capacity constraint. The stock capacity is the sum of the stock
    commissioning, which do not exceed its technical lifetime.

    For stochastic, the stock of past investment periods is not only valid for ip=0 but for all investment periods.
    .. math::

        cap^{comp}_{loc,ip} =  stockCap^{comp}_{loc} + commis^{comp}_{loc,ip} - decommis^{comp}_{loc,0}

    For capacity development, the stock is only considered for the first investment periods.

    .. math::

        cap^{comp}_{loc,0} =  stockCap^{comp}_{loc} + commis^{comp}_{loc,0} - decommis^{comp}_{loc,0}

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param esM: energy system model containing general information.
    :type esM: EnergySystemModel instance from the FINE package
    """
    abbrvName = self.abbrvName
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    decommisVar = getattr(pyM, "decommis_" + abbrvName)
    locCompConstrSet = getattr(pyM, "DesignLocationComponentVarSet_" + abbrvName)
    locCompIpConstrSet = getattr(pyM, "designDimensionVarSet_" + abbrvName)

    if esM.stochasticModel:

        def initialStochastic(pyM, loc, compName, ip):
            stock_cap = self.componentsDict[compName].stockCapacityStartYear[loc]
            return (
                capVar[loc, compName, ip]
                == stock_cap
                + commisVar[loc, compName, ip]
                - decommisVar[loc, compName, 0]
            )

        setattr(
            pyM,
            "InitialYear_" + abbrvName,
            pyomo.Constraint(locCompIpConstrSet, rule=initialStochastic),
        )
    else:

        def initialYear(pyM, loc, compName):
            stock_cap = self.componentsDict[compName].stockCapacityStartYear[loc]
            return (
                capVar[loc, compName, 0]
                == stock_cap
                + commisVar[loc, compName, 0]
                - decommisVar[loc, compName, 0]
            )

        setattr(
            pyM,
            "InitialYear_" + abbrvName,
            pyomo.Constraint(locCompConstrSet, rule=initialYear),
        )

stockCommissioningConstraint

stockCommissioningConstraint(pyM, esM)

Set commissioning variable for past investment periods. For past investment periods, where no stock commissioning is specified the commissioning variable is set to zero.

Source code in fine/component.py
def stockCommissioningConstraint(self, pyM, esM):
    """Set commissioning variable for past investment periods. For past investment periods,
    where no stock commissioning is specified the commissioning variable is set to zero.
    """
    commisConstrSet = getattr(pyM, "designCommisVarSet_" + self.abbrvName)
    commisVar = getattr(pyM, "commis_" + self.abbrvName)

    def stockCommissioning(pyM, loc, compName, ip):
        if (
            ip in esM.investmentPeriods
        ):  # initialize stock commissioning only for stock years
            return pyomo.Constraint.Skip
        if (
            self.componentsDict[compName].processedStockCommissioning is None
        ):  # set 0 if there is no stock
            return commisVar[loc, compName, ip] == 0
        return (
            commisVar[loc, compName, ip]
            == self.componentsDict[compName].processedStockCommissioning[ip][loc]
        )

    setattr(
        pyM,
        "StockCommissioning_" + self.abbrvName,
        pyomo.Constraint(commisConstrSet, rule=stockCommissioning),
    )

yearlyFullLoadHoursMax

yearlyFullLoadHoursMax(
    pyM,
    esM,
    constrSetName,
    constrName,
    opVarName,
    isOperationCommisYearDepending=False,
)

Limit the annual full load hours to a maximum value.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: esM - EnergySystemModel class instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param constrName: name for the constraint in esM.pyM :type constrName: str

:param constrSetName: name of the constraint set :type constrSetName: str

:param opVarName: name of the operation variables :type opVarName: str

:param isOperationCommisYearDepending: defines whether the operation variable is depending on the year of commissioning of the component. E.g. relevant if the commodity conversion, for example the efficiency, varies over the transformation pathway :type isOperationCommisYearDepending: str

Source code in fine/component.py
def yearlyFullLoadHoursMax(
    self,
    pyM,
    esM,
    constrSetName,
    constrName,
    opVarName,
    isOperationCommisYearDepending=False,
):
    """Limit the annual full load hours to a maximum value.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: esM - EnergySystemModel class instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param constrName: name for the constraint in esM.pyM
    :type constrName: str

    :param constrSetName: name of the constraint set
    :type constrSetName: str

    :param opVarName: name of the operation variables
    :type opVarName: str

    :param isOperationCommisYearDepending: defines whether the operation variable is depending on the year of commissioning of the component. E.g. relevant if the commodity conversion, for example the efficiency, varies over the transformation pathway
    :type isOperationCommisYearDepending: str
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    yearlyFullLoadHoursMaxSet = getattr(pyM, constrSetName + "_" + abbrvName)
    if isOperationCommisYearDepending:

        def yearlyFullLoadHoursMaxConstraint(pyM, loc, compName, commis, ip):
            full_load_hours = (
                sum(
                    opVar[loc, compName, commis, ip, p, t]
                    * esM.periodOccurrences[ip][p]
                    for p, t in pyM.intraYearTimeSet
                )
                / esM.numberOfYears
            )
            return (
                full_load_hours
                <= commisVar[loc, compName, commis]
                * compDict[compName].processedYearlyFullLoadHoursMax[ip][loc]
            )

    else:

        def yearlyFullLoadHoursMaxConstraint(pyM, loc, compName, ip):
            full_load_hours = (
                sum(
                    opVar[loc, compName, ip, p, t] * esM.periodOccurrences[ip][p]
                    for p, t in pyM.intraYearTimeSet
                )
                / esM.numberOfYears
            )
            return (
                full_load_hours
                <= capVar[loc, compName, ip]
                * compDict[compName].processedYearlyFullLoadHoursMax[ip][loc]
            )

    setattr(
        pyM,
        constrName + "_" + abbrvName,
        pyomo.Constraint(
            yearlyFullLoadHoursMaxSet, rule=yearlyFullLoadHoursMaxConstraint
        ),
    )

yearlyFullLoadHoursMin

yearlyFullLoadHoursMin(
    pyM,
    esM,
    constrSetName,
    constrName,
    opVarName,
    isOperationCommisYearDepending=False,
)

Limit the annual full load hours to a minimum value.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: esM - EnergySystemModel class instance

:param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model. :type pyM: pyomo ConcreteModel

:param constrName: name for the constraint in esM.pyM :type constrName: str

:param constrSetName: name of the constraint set :type constrSetName: str

:param opVarName: name of the operation variables :type opVarName: str

:param isOperationCommisYearDepending: defines whether the operation variable is depending on the year of commissioning of the component. E.g. relevant if the commodity conversion, for example the efficiency, varies over the transformation pathway :type isOperationCommisYearDepending: str

Source code in fine/component.py
def yearlyFullLoadHoursMin(
    self,
    pyM,
    esM,
    constrSetName,
    constrName,
    opVarName,
    isOperationCommisYearDepending=False,
):
    """Limit the annual full load hours to a minimum value.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: esM - EnergySystemModel class instance

    :param pyM: pyomo ConcreteModel which stores the mathematical formulation of the model.
    :type pyM: pyomo ConcreteModel

    :param constrName: name for the constraint in esM.pyM
    :type constrName: str

    :param constrSetName: name of the constraint set
    :type constrSetName: str

    :param opVarName: name of the operation variables
    :type opVarName: str

    :param isOperationCommisYearDepending: defines whether the operation variable is depending on the year of commissioning of the component. E.g. relevant if the commodity conversion, for example the efficiency, varies over the transformation pathway
    :type isOperationCommisYearDepending: str
    """
    compDict, abbrvName = self.componentsDict, self.abbrvName
    opVar = getattr(pyM, opVarName + "_" + abbrvName)
    capVar = getattr(pyM, "cap_" + abbrvName)
    commisVar = getattr(pyM, "commis_" + abbrvName)
    yearlyFullLoadHoursMinSet = getattr(pyM, constrSetName + "_" + abbrvName)
    if isOperationCommisYearDepending:
        # for technologies which have operations depending on the commissioning year, e.g. by variable commodity conversion factors
        def yearlyFullLoadHoursMinConstraint(pyM, loc, compName, commis, ip):
            full_load_hours = (
                sum(
                    opVar[loc, compName, commis, ip, p, t]
                    * esM.periodOccurrences[ip][p]
                    for p, t in pyM.intraYearTimeSet
                )
                / esM.numberOfYears
            )
            return (
                full_load_hours
                >= commisVar[loc, compName, commis]
                * compDict[compName].processedYearlyFullLoadHoursMin[ip][loc]
            )

    else:

        def yearlyFullLoadHoursMinConstraint(pyM, loc, compName, ip):
            full_load_hours = (
                sum(
                    opVar[loc, compName, ip, p, t] * esM.periodOccurrences[ip][p]
                    for p, t in pyM.intraYearTimeSet
                )
                / esM.numberOfYears
            )
            return (
                full_load_hours
                >= capVar[loc, compName, ip]
                * compDict[compName].processedYearlyFullLoadHoursMin[ip][loc]
            )

    setattr(
        pyM,
        constrName + "_" + abbrvName,
        pyomo.Constraint(
            yearlyFullLoadHoursMinSet, rule=yearlyFullLoadHoursMinConstraint
        ),
    )

Conversion

Conversion(
    esM,
    name,
    physicalUnit,
    commodityConversionFactors,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    linkedConversionCapacityID=None,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMin=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    yearlyFullLoadHoursMin=None,
    yearlyFullLoadHoursMax=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
    commissioningDependentCcf=False,
    emissionFactors=None,
    flowShares=None,
    pwlcfParameters=None,
    rampUpMax=None,
    rampDownMax=None,
    useTemporalCyclicConstraints=True,
)

Bases: Component

A Conversion component converts commodities into each other.

Create an instance of the Conversion class, with capacities given in the physical unit of the plants.

The Conversion component specific input arguments are described below. The general component input arguments are described in the Component class.

Required arguments:

:param physicalUnit: reference physical unit of the plants to which maximum capacity limitations, cost parameters and the operation time series refer to. :type physicalUnit: string

:param commodityConversionFactors: conversion factors with which commodities are converted into each other with one unit of operation (dictionary). Each commodity which is converted in this component is indicated by a string in this dictionary. The conversion factor related to this commodity is given as a float (constant), pandas.Series or pandas.DataFrame (time-variable). A negative value indicates that the commodity is consumed. A positive value indicates that the commodity is produced. Check unit consistency when specifying this parameter!

Examples: * An electrolyzer converts, simply put, electricity into hydrogen with an electrical efficiency of 70%. The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity is given in GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue -> the commodityConversionFactors are defined as {'electricity':-1,'hydrogen':0.7}. * A fuel cell converts, simply put, hydrogen into electricity with an efficiency of 60%. The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity is given in GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue -> the commodityConversionFactors are defined as {'electricity':1,'hydrogen':-1/0.6}.

If a transformation pathway analysis is performed the conversion factors can also be varied
over the transformation pathway. Therefore, two different options are available:

1. Variation with operation year (for example to incorporate weather changes for a heat pump).

Example: {2020: {'electricity':-1,'heat':pd.Series(data=[2.5, 2.8, 2.5, ...])}, 2025: {'electricity':-1,'heat':pd.Series(data=[2.7, 2.4, 2.9, ...])}, ...} 2. Variation with commissioning and operation year (for example to incorporate efficiency changes dependent on the installation year). Please note that this implementation massively increases the complexity of the optimization problem.

Example: {(2020, 2020): {'electricity':-1,'heat':pd.Series(data=[2.5, 2.8, 2.5, ...])}, (2020, 2025): {'electricity':-1,'heat':pd.Series(data=[2.7, 2.4, 2.9, ...])}, (2025, 2025): {'electricity':-1,'heat':pd.Series(data=[3.7, 3.4, 3.9, ...])}, ...}

If a conversion component can decide between multiple in- or outputs which one to use
(e.g. a chp plant) a flexible conversion component can be specified. This enables the
component to substitute in- or output commodities within a commodity group. To allow
this behavior an additional level needs to be specified:

* A CHP plant can decide between the production of heat or electricity (or a mix of both).
    When electricity is produced the conversion factor is 0.2 and for heat 0.5:
    {'gas': -1, 'out': {electricity: 0.2, heat: 0.5}}

Location-dependent (time-invariant) conversion factors can be provided as pandas.Series
indexed by the energy system model locations.

Example: {'electricity': -1, 'heat': pd.Series({'DE': 0.9, 'FR': 0.8})}

:type commodityConversionFactors:

* dictionary, assigns commodities (string) to a conversion factors
    (float/int, pandas.Series indexed by locations, or pandas.DataFrame
    with locations as columns and time steps as index)
* dictionary with investment periods as key and one of the first option  as value
* dictionary with tuple of (commissioning year, investment period) as key and one
    of the first option above as value

Example: { 2025: {'electricity': -1, 'hydrogen': pd.DataFrame( {'ElectrolyzerLocation': [0.5, 0.6, 0.7, ...], 'IndustryLocation': [1.0, 0.9, 0.8, ...]}, index=esM.totalTimeSteps)}, 2030: {'electricity': -1, 'hydrogen': pd.DataFrame( {'ElectrolyzerLocation': [0.6, 0.7, 0.8, ...], 'IndustryLocation': [0.9, 0.8, 0.7, ...]}, index=esM.totalTimeSteps)} }

Default arguments:

:param linkedConversionCapacityID: if specifies, indicates that all conversion components with the same ID have to have the same capacity. |br| * the default value is None :type linkedConversionCapacityID: string

:param operationRateMin: if specified, indicates a minimum operation rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the physicalUnit of the plant for each time step. |br| * the default value is None :type operationRateMin: * None * pandas DataFrame with positive (>=0). The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param operationRateMax: if specified, indicates a maximum operation rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the physicalUnit of the plant for each time step. |br| * the default value is None :type operationRateMax: * None * pandas DataFrame with positive (>=0). The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param operationRateFix: if specified, indicates a fixed operation rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the physicalUnit of the plant for each time step. |br| * the default value is None :type operationRateFix: * None * Pandas DataFrame with positive (>=0). The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param tsaWeight: weight with which the time series of the component should be considered when applying time series aggregation. |br| * the default value is 1 :type tsaWeight: positive (>= 0) float

:param opexPerOperation: describes the cost for one unit of the operation. The cost which is directly proportional to the operation of the component is obtained by multiplying the opexPerOperation parameter with the annual sum of the operational time series of the components. The opexPerOperation can either be given as a float or a Pandas Series with location specific values. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro). |br| * the default value is 0 :type opexPerOperation: * Pandas Series with positive (>=0) entries. The indices of the series have to equal the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param commissioningDependentCcf: specifies if commodity conversion factors are dependent on commissioning or operation year. If set to False, the factors are only dependent on the year of operation and no new operation variables are introduced. If set to True, the factors are dependent on commissioning year and new operation variables are introduced for every commissioning year. |br| * the default value is False :type commissioningDependentCcf:bool

:param emissionFactors: can be used to specify emissions for flexible conversion components. This parameter can only be specified if the component is a flexible conversion component (see explanations on commodity conversion factors above). When specified, the emission factors indicate what emissions are produced when a particular commodity is used by the component. Note: For non-flexible conversion components emissions must be specified as commodity conversion factors.

Example: The CO2 emissions of a power plant are dependent on the type of fuel is used (e.g. coal has
higher emissions than gas): {'co2': {'coal': 3, 'gas': 1}}

:type emissionFactors: dict with emission commodities as key and a dict as value. The inner dict holds the emission factors which are dependent on the utilized commodity.

:param flowShares: can be used to constrain the operation of flexible conversion components (see explanations on commodity conversion factors above). When used, the flow shares must be specified as 'min', 'max', or 'fix' values that limit the commodity specific operation rate of a flexible conversion component relative to the overall rate of that component (e.g. if the flow share max for H2 is set to 0.75 and the overall operation rate is 4 MW, then the H2 operation rate must be smaller or equal to 3 MW). Flow shares can be set up for all, some, or none of the modeled investment periods, and can either apply to all regions (if defined as int) or depend on individual regions (if defined as pandas series). Flow shares must be between 0 and 1 if specified.

Example: In the first investment period in Location1 only a small share of 10 % of the modeled gas heaters
are able to burn hydrogen instead of natural gas. In the second investment period more hydrogen ready
heaters are available and 50% of the heaters can burn hydrogen instead of natural gas (10 % of
those heaters can only burn hydrogen). In Location2 only 5 % can burn hydrogen in first period and 40 % can
burn hydrogen in second period:
flowShares = {
    0: {
        'max': {'hydrogen': pd.Series([0.1, 0.05], index=['loc1', 'loc2'])}
    },
    1: {
        'max': {'hydrogen': pd.Series([0.1, 0.05], index=['loc1', 'loc2'])},
        'min': {'hydrogen': pd.Series([0.1], index=['loc1'])}
    }
}

:type flowShares: dict

:param rampUpMax: A maximum ramping rate to limit the increase in the operation of the component as share of the installed capacity. The maximum ramping is defined per hour and not per hoursPerTimeStep. |br| * the default value is None :type rampUpMax: None or float value in range ]0.0,1.0]

:param rampDownMax: A maximum ramping rate to limit the decrease in the operation of the component as share of the installed capacity. The maximum ramping is defined per hour and not per hoursPerTimeStep. |br| * the default value is None :type rampDownMax: None or float value in range ]0.0,1.0]

:param useTemporalCyclicConstraints: If True, the temporal cyclic constraints are used. This means that the operation of the first time steps are mathematically linked to the operation of the last time steps. |br| * the default value is True :type useTemporalCyclicConstraints: boolean

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

Source code in fine/conversion.py
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
def __init__(
    self,
    esM,
    name,
    physicalUnit,
    commodityConversionFactors,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    linkedConversionCapacityID=None,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMin=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    yearlyFullLoadHoursMin=None,
    yearlyFullLoadHoursMax=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
    commissioningDependentCcf=False,
    emissionFactors=None,
    flowShares=None,
    pwlcfParameters=None,
    rampUpMax=None,
    rampDownMax=None,
    useTemporalCyclicConstraints=True,
):
    # TODO: allow that the time series data or min/max/fixCapacity/eligibility is only specified for
    # TODO: eligible locations
    """Create an instance of the Conversion class, with capacities given in the physical unit of the plants.

    The Conversion component specific input arguments are described below. The general component
    input arguments are described in the Component class.

    **Required arguments:**

    :param physicalUnit: reference physical unit of the plants to which maximum capacity limitations,
        cost parameters and the operation time series refer to.
    :type physicalUnit: string

    :param commodityConversionFactors: conversion factors with which commodities are converted into each
        other with one unit of operation (dictionary). Each commodity which is converted in this component
        is indicated by a string in this dictionary. The conversion factor related to this commodity is
        given as a float (constant), pandas.Series or pandas.DataFrame (time-variable). A negative value
        indicates that the commodity is consumed. A positive value indicates that the commodity is produced.
        Check unit consistency when specifying this parameter!

    Examples:
        * An electrolyzer converts, simply put, electricity into hydrogen with an electrical efficiency
            of 70%. The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity is
            given in GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue
            -> the commodityConversionFactors are defined as {'electricity':-1,'hydrogen':0.7}.
        * A fuel cell converts, simply put, hydrogen into electricity with an efficiency of 60%.
            The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity is given in
            GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue
            -> the commodityConversionFactors are defined as {'electricity':1,'hydrogen':-1/0.6}.

        If a transformation pathway analysis is performed the conversion factors can also be varied
        over the transformation pathway. Therefore, two different options are available:

        1. Variation with operation year (for example to incorporate weather changes for a heat pump).

    Example:
           {2020: {'electricity':-1,'heat':pd.Series(data=[2.5, 2.8, 2.5, ...])},
           2025: {'electricity':-1,'heat':pd.Series(data=[2.7, 2.4, 2.9, ...])},
           ...}
        2. Variation with commissioning and operation year (for example to incorporate efficiency
           changes dependent on the installation year). Please note that this implementation massively
           increases the complexity of the optimization problem.

    Example:
           {(2020, 2020): {'electricity':-1,'heat':pd.Series(data=[2.5, 2.8, 2.5, ...])},
           (2020, 2025): {'electricity':-1,'heat':pd.Series(data=[2.7, 2.4, 2.9, ...])},
           (2025, 2025): {'electricity':-1,'heat':pd.Series(data=[3.7, 3.4, 3.9, ...])},
           ...}

        If a conversion component can decide between multiple in- or outputs which one to use
        (e.g. a chp plant) a flexible conversion component can be specified. This enables the
        component to substitute in- or output commodities within a commodity group. To allow
        this behavior an additional level needs to be specified:

        * A CHP plant can decide between the production of heat or electricity (or a mix of both).
            When electricity is produced the conversion factor is 0.2 and for heat 0.5:
            {'gas': -1, 'out': {electricity: 0.2, heat: 0.5}}

        Location-dependent (time-invariant) conversion factors can be provided as pandas.Series
        indexed by the energy system model locations.

    Example:
           {'electricity': -1,
            'heat': pd.Series({'DE': 0.9, 'FR': 0.8})}

    :type commodityConversionFactors:

        * dictionary, assigns commodities (string) to a conversion factors
            (float/int, pandas.Series indexed by locations, or pandas.DataFrame
            with locations as columns and time steps as index)
        * dictionary with investment periods as key and one of the first option  as value
        * dictionary with tuple of (commissioning year, investment period) as key and one
            of the first option above as value

    Example:
            {
                2025: {'electricity': -1,
                    'hydrogen': pd.DataFrame(
                        {'ElectrolyzerLocation': [0.5, 0.6, 0.7, ...],
                            'IndustryLocation':     [1.0, 0.9, 0.8, ...]},
                        index=esM.totalTimeSteps)},
                2030: {'electricity': -1,
                    'hydrogen': pd.DataFrame(
                        {'ElectrolyzerLocation': [0.6, 0.7, 0.8, ...],
                            'IndustryLocation':     [0.9, 0.8, 0.7, ...]},
                        index=esM.totalTimeSteps)}
            }

    **Default arguments:**

    :param linkedConversionCapacityID: if specifies, indicates that all conversion components with the
        same ID have to have the same capacity.
        |br| * the default value is None
    :type linkedConversionCapacityID: string

    :param operationRateMin: if specified, indicates a minimum operation rate for each location and each time
        step, if required also for each investment period, by a positive float. If hasCapacityVariable is set
        to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a
        utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as
        absolute values in form of the physicalUnit of the plant for each time step.
        |br| * the default value is None
    :type operationRateMin:
        * None
        * pandas DataFrame with positive (>=0). The row indices have
          to match the in the energy system model specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param operationRateMax: if specified, indicates a maximum operation rate for each location and each time
        step, if required also for each investment period, by a positive float. If hasCapacityVariable is set
        to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a
        utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as
        absolute values in form of the physicalUnit of the plant for each time step.
        |br| * the default value is None
    :type operationRateMax:
        * None
        * pandas DataFrame with positive (>=0). The row indices have
          to match the in the energy system model specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param operationRateFix: if specified, indicates a fixed operation rate for each location and each time
        step, if required also for each investment period, by a positive float. If hasCapacityVariable is set
        to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a
        utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as
        absolute values in form of the physicalUnit of the plant for each time step.
        |br| * the default value is None
    :type operationRateFix:
        * None
        * Pandas DataFrame with positive (>=0). The row indices have
          to match the in the energy system model specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param tsaWeight: weight with which the time series of the component should be considered when applying
        time series aggregation.
        |br| * the default value is 1
    :type tsaWeight: positive (>= 0) float

    :param opexPerOperation: describes the cost for one unit of the operation. The cost which is
        directly proportional to the operation of the component is obtained by multiplying
        the opexPerOperation parameter with the annual sum of the operational time series of the components.
        The opexPerOperation can either be given as a float or a Pandas Series with location specific values.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro).
        |br| * the default value is 0
    :type opexPerOperation:
        * Pandas Series with positive (>=0) entries. The indices of the series have to equal the in the energy
          system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param commissioningDependentCcf: specifies if commodity conversion factors are dependent on commissioning
        or operation year. If set to False, the factors are only dependent on the year of operation and
        no new operation variables are introduced. If set to True, the factors are dependent on commissioning
        year and new operation variables are introduced for every commissioning year.
        |br| * the default value is False
    :type commissioningDependentCcf:bool

    :param emissionFactors: can be used to specify emissions for flexible conversion components.
        This parameter can only be specified if the component is a flexible conversion component (see explanations
        on commodity conversion factors above). When specified, the emission factors indicate what emissions
        are produced when a particular commodity is used by the component.
        Note: For non-flexible conversion components emissions must be specified as commodity conversion factors.

        Example: The CO2 emissions of a power plant are dependent on the type of fuel is used (e.g. coal has
        higher emissions than gas): {'co2': {'coal': 3, 'gas': 1}}
    :type emissionFactors: dict with emission commodities as key and a dict as value. The inner dict holds the
        emission factors which are dependent on the utilized commodity.

    :param flowShares: can be used to constrain the operation of flexible conversion components (see explanations
        on commodity conversion factors above). When used, the flow shares must be specified as 'min', 'max', or
        'fix' values that limit the commodity specific operation rate of a flexible conversion component relative
        to the overall rate of that component (e.g. if the flow share max for H2 is set to 0.75 and the overall
        operation rate is 4 MW, then the H2 operation rate must be smaller or equal to 3 MW). Flow shares can be
        set up for all, some, or none of the modeled investment periods, and can either apply to all regions
        (if defined as int) or depend on individual regions (if defined as pandas series). Flow shares must be
        between 0 and 1 if specified.

        Example: In the first investment period in Location1 only a small share of 10 % of the modeled gas heaters
        are able to burn hydrogen instead of natural gas. In the second investment period more hydrogen ready
        heaters are available and 50% of the heaters can burn hydrogen instead of natural gas (10 % of
        those heaters can only burn hydrogen). In Location2 only 5 % can burn hydrogen in first period and 40 % can
        burn hydrogen in second period:
        flowShares = {
            0: {
                'max': {'hydrogen': pd.Series([0.1, 0.05], index=['loc1', 'loc2'])}
            },
            1: {
                'max': {'hydrogen': pd.Series([0.1, 0.05], index=['loc1', 'loc2'])},
                'min': {'hydrogen': pd.Series([0.1], index=['loc1'])}
            }
        }
    :type flowShares: dict

    :param rampUpMax: A maximum ramping rate to limit the increase in the operation of the component as share of the installed capacity.
        The maximum ramping is defined per hour and not per hoursPerTimeStep.
        |br| * the default value is None
    :type rampUpMax: None or float value in range ]0.0,1.0]

    :param rampDownMax: A maximum ramping rate to limit the decrease in the operation of the component as share of the installed capacity.
        The maximum ramping is defined per hour and not per hoursPerTimeStep.
        |br| * the default value is None
    :type rampDownMax: None or float value in range ]0.0,1.0]

    :param useTemporalCyclicConstraints: If True, the temporal cyclic constraints are used.
        This means that the operation of the first time steps are mathematically linked to the operation of the last time steps.
        |br| * the default value is True
    :type useTemporalCyclicConstraints: boolean

    """
    Component.__init__(
        self,
        esM,
        name,
        dimension=Dimension.ONE,
        hasCapacityVariable=hasCapacityVariable,
        capacityVariableDomain=capacityVariableDomain,
        capacityPerPlantUnit=capacityPerPlantUnit,
        hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable,
        bigM=bigM,
        locationalEligibility=locationalEligibility,
        capacityMin=capacityMin,
        capacityMax=capacityMax,
        partLoadMin=partLoadMin,
        sharedPotentialID=sharedPotentialID,
        linkedQuantityID=linkedQuantityID,
        capacityFix=capacityFix,
        commissioningMin=commissioningMin,
        commissioningMax=commissioningMax,
        commissioningFix=commissioningFix,
        isBuiltFix=isBuiltFix,
        investPerCapacity=investPerCapacity,
        investIfBuilt=investIfBuilt,
        opexPerCapacity=opexPerCapacity,
        opexIfBuilt=opexIfBuilt,
        QPcostScale=QPcostScale,
        interestRate=interestRate,
        economicLifetime=economicLifetime,
        technicalLifetime=technicalLifetime,
        floorTechnicalLifetime=floorTechnicalLifetime,
        yearlyFullLoadHoursMin=yearlyFullLoadHoursMin,
        yearlyFullLoadHoursMax=yearlyFullLoadHoursMax,
        stockCommissioning=stockCommissioning,
        pwlcfParameters=pwlcfParameters,
    )

    # opexPerOperation
    self.opexPerOperation = opexPerOperation
    self.processedOpexPerOperation = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        opexPerOperation,
        Dimension.ONE,
        locationalEligibility,
        esM.investmentPeriods,
    )

    # check for operationRateMax and operationRateFix
    if operationRateMax is not None and operationRateFix is not None:
        operationRateMax = None
        if esM.verboseLogLevel < 2:
            warnings.warn(
                "If operationRateFix is specified, the operationRateMax parameter is not required.\n"
                + "The operationRateMax time series was set to None."
            )
    if operationRateMin is not None and operationRateFix is not None:
        operationRateMin = None
        if esM.verboseLogLevel < 2:
            warnings.warn(
                "If operationRateFix is specified, the operationRateMin parameter is not required.\n"
                + "The operationRateMin time series was set to None."
            )
    # if both operationRateMin and operationRateMax are given, check if operationRateMin <= operationRateMax
    if operationRateMin is not None and operationRateMax is not None:
        if not (operationRateMax >= operationRateMin).all():
            # raise error
            raise ValueError(
                "The operationRateMin time series has to be smaller or equal to the operationRateMax time series."
            )

    # operationRateMin
    self.operationRateMin = operationRateMin
    self.fullOperationRateMin = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateMin, locationalEligibility
    )
    self.aggregatedOperationRateMin = {}
    self.processedOperationRateMin = {}

    # operationRateMax
    self.operationRateMax = operationRateMax
    self.fullOperationRateMax = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateMax, locationalEligibility
    )
    self.aggregatedOperationRateMax = {}
    self.processedOperationRateMax = {}

    # operationRateFix
    self.operationRateFix = operationRateFix
    self.fullOperationRateFix = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateFix, locationalEligibility
    )
    self.aggregatedOperationRateFix = {}
    self.processedOperationRateFix = {}

    utils.checkOperationRateForCapacityVariable(
        name,
        self.hasCapacityVariable,
        self.fullOperationRateMin,
        self.fullOperationRateMax,
        self.fullOperationRateFix,
    )

    self.rampUpMax = rampUpMax
    self.rampDownMax = rampDownMax
    self.useTemporalCyclicConstraints = useTemporalCyclicConstraints
    utils.checkRampRates(
        esM,
        name,
        self.rampUpMax,
        self.rampDownMax,
    )

    # partLoadMin
    self.processedPartLoadMin = utils.checkAndSetPartLoadMin(
        esM,
        name,
        self.partLoadMin,
        self.fullOperationRateMax,
        self.fullOperationRateFix,
        self.bigM,
        self.hasCapacityVariable,
        fullOperationMin=self.fullOperationRateMin,
    )

    # commodity conversions factors
    self.commissioningDependentCcf = commissioningDependentCcf
    self.commodityConversionFactors = commodityConversionFactors
    (self.isIpDepending, self.isCommisDepending, self.flexibleConversion) = (
        utils.checkConversionFactorProperties(self, esM, commissioningDependentCcf)
    )
    (
        self.fullCommodityConversionFactors,
        self.processedCommodityConversionFactors,
        self.preprocessedCommodityConversionFactors,
    ) = utils.checkAndSetCommodityConversionFactor(self, esM)
    self.aggregatedCommodityConversionFactors = dict.fromkeys(
        self.fullCommodityConversionFactors.keys()
    )

    self.emissionFactors = emissionFactors
    self.processedEmissionFactors = utils.checkEmissionFactors(self, esM)

    self.flowShares = flowShares
    self.processedFlowShares = utils.checkAndSetFlowShares(self, esM)

    utils.isPositiveNumber(tsaWeight)
    self.tsaWeight = tsaWeight
    utils.checkCommodityUnits(esM, physicalUnit)
    if linkedConversionCapacityID is not None:
        utils.isString(linkedConversionCapacityID)
    self.physicalUnit = physicalUnit
    self.modelingClass = ConversionModel
    self.linkedConversionCapacityID = linkedConversionCapacityID

    if any(x is not None for x in self.fullOperationRateFix.values()):
        operationTimeSeries = self.fullOperationRateFix
    elif any(x is not None for x in self.fullOperationRateMax.values()):
        operationTimeSeries = self.fullOperationRateMax
    elif any(x is not None for x in self.fullOperationRateMin.values()):
        operationTimeSeries = self.fullOperationRateMin
    else:
        operationTimeSeries = None

    self.processedLocationalEligibility = utils.setLocationalEligibility(
        esM,
        self.locationalEligibility,
        self.processedCapacityMax,
        self.processedCapacityFix,
        self.isBuiltFix,
        self.hasCapacityVariable,
        operationTimeSeries,
    )

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/conversion.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMin:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMin,
            "_operationRateMin_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )

    if not self.isCommisDepending:
        for commod in self.fullCommodityConversionFactors[ip]:
            weightDict, data = self.prepareTSAInput(
                self.fullCommodityConversionFactors[ip][commod],
                "_commodityConversionFactorTimeSeries" + str(commod) + "_",
                self.tsaWeight,
                weightDict,
                data,
                ip,
            )
    # for components with conversion time-series depending on commissioning years,
    # the time-series of all commissioning years relevant for the investment period must be considered
    else:
        relevantCommissioningYears = [
            x for (x, y) in self.fullCommodityConversionFactors.keys() if y == ip
        ]
        for commisYear in relevantCommissioningYears:
            # divide the weight by the number of relevant commissioning years to
            # prevent a too high total weight of the commodity conversion time-series
            for commod in self.fullCommodityConversionFactors[(commisYear, ip)]:
                weightDict, data = self.prepareTSAInput(
                    self.fullCommodityConversionFactors[(commisYear, ip)][commod],
                    "_commodityConversionFactorTimeSeries"
                    + str(commod)
                    + str(commisYear).replace("-", "minus")
                    + "_",
                    self.tsaWeight / len(relevantCommissioningYears),
                    weightDict,
                    data,
                    ip,
                )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the conversion component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/conversion.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the conversion component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMin[ip] = self.getTSAOutput(
        self.fullOperationRateMin, "_operationRateMin_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )

    # get the aggregated commodity conversion factors
    # The procedure changes depending on whether the commodity conversion factors depend on the year of
    # commissioning or only on the year of operation (investment period).
    if not self.isCommisDepending:
        if self.fullCommodityConversionFactors[ip] != {}:
            self.aggregatedCommodityConversionFactors[ip] = {}
            for commod in self.fullCommodityConversionFactors[ip]:
                self.aggregatedCommodityConversionFactors[ip][commod] = (
                    self.getTSAOutput(
                        self.fullCommodityConversionFactors[ip][commod],
                        "_commodityConversionFactorTimeSeries" + str(commod) + "_",
                        data,
                        ip,
                    )
                )
    else:
        # if depending on the commissioning year, iterate over the relevant commissioning years for the
        # operation of the investment period ip and get the time-series
        relevantCommissioningYears = [
            x for (x, y) in self.fullCommodityConversionFactors.keys() if y == ip
        ]
        for commisYear in relevantCommissioningYears:
            if self.fullCommodityConversionFactors[(commisYear, ip)] != {}:
                self.aggregatedCommodityConversionFactors[(commisYear, ip)] = {}
                for commod in self.fullCommodityConversionFactors[(commisYear, ip)]:
                    self.aggregatedCommodityConversionFactors[(commisYear, ip)][
                        commod
                    ] = self.getTSAOutput(
                        self.fullCommodityConversionFactors[(commisYear, ip)][
                            commod
                        ],
                        "_commodityConversionFactorTimeSeries"
                        + str(commod)
                        + str(commisYear).replace("-", "minus")
                        + "_",
                        data,
                        ip,
                    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/conversion.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    # processedOperationMax
    self.processedOperationRateMax = (
        self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
    )
    # processedOperationFix
    self.processedOperationRateFix = (
        self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
    )
    # processedOperationMin
    self.processedOperationRateMin = (
        self.aggregatedOperationRateMin if hasTSA else self.fullOperationRateMin
    )
    # processedCommodityConversions
    # timeInfo can either be ip or (commis,ip)
    for timeInfo in self.fullCommodityConversionFactors.keys():
        if self.fullCommodityConversionFactors[timeInfo] != {}:
            for commod in self.fullCommodityConversionFactors[timeInfo]:
                self.processedCommodityConversionFactors[timeInfo][commod] = (
                    self.aggregatedCommodityConversionFactors[timeInfo][commod]
                    if hasTSA
                    else self.fullCommodityConversionFactors[timeInfo][commod]
                )

ConversionDynamic

ConversionDynamic(
    esM,
    name,
    physicalUnit,
    commodityConversionFactors,
    downTimeMin=None,
    upTimeMin=None,
    useTemporalCyclicConstraints=True,
    **kwargs,
)

Bases: Conversion

Extension of the conversion class with more specific ramping behavior.

Create a ConversionDynamic class instance. The ConversionDynamic component specific input arguments are described below. The Conversion specific input arguments are described in the Conversion class and the general component input arguments are described in the Component class.

Default arguments:

:param downTimeMin: if specified, indicates minimal down time of the component [hours]. |br| * the default value is None :type downTimeMin: None or integer value in range [0,numberOfTimeSteps*hoursPerTimeStep]

:param upTimeMin: if specified, indicates minimal up time of the component [hours]. |br| * the default value is None :type upTimeMin: None or integer value in range [0,numberOfTimeSteps*hoursPerTimeStep]

:param useTemporalCyclicConstraints: If True, the temporal cyclic constraints are used. This means that the operation of the first time steps are mathematically linked to the operation of the last time steps. |br| * the default value is True :type useTemporalCyclicConstraints: boolean

:param kwargs: All other keyword arguments of the conversion class can be defined as well. :type kwargs: Check Conversion Class documentation.

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate and fixed operation rate depending on whether a time series

Source code in fine/subclasses/conversionDynamic.py
def __init__(
    self,
    esM,
    name,
    physicalUnit,
    commodityConversionFactors,
    downTimeMin=None,
    upTimeMin=None,
    useTemporalCyclicConstraints=True,
    **kwargs,
):
    r"""Create a ConversionDynamic class instance.
    The ConversionDynamic component specific input arguments are described below. The Conversion
    specific input arguments are described in the Conversion class and the general component
    input arguments are described in the Component class.

    **Default arguments:**

    :param downTimeMin: if specified, indicates minimal down time of the component [hours].
        |br| * the default value is None
    :type downTimeMin: None or integer value in range [0,numberOfTimeSteps*hoursPerTimeStep]

    :param upTimeMin: if specified, indicates minimal up time of the component [hours].
        |br| * the default value is None
    :type upTimeMin: None or integer value in range [0,numberOfTimeSteps*hoursPerTimeStep]

    :param useTemporalCyclicConstraints: If True, the temporal cyclic constraints are used.
        This means that the operation of the first time steps are mathematically linked to the operation of the last time steps.
        |br| * the default value is True
    :type useTemporalCyclicConstraints: boolean

    :param **kwargs: All other keyword arguments of the conversion class can be defined as well.
    :type **kwargs: Check Conversion Class documentation.
    """
    Conversion.__init__(
        self, esM, name, physicalUnit, commodityConversionFactors, **kwargs
    )

    self.modelingClass = ConversionDynamicModel
    self.downTimeMin = downTimeMin
    self.upTimeMin = upTimeMin
    self.useTemporalCyclicConstraints = useTemporalCyclicConstraints
    utils.checkConversionDynamicSpecficDesignInputParams(self, esM)

    if self.isCommisDepending:
        raise ValueError(
            "Currently commissioning-depending constraints are not possible"
        )

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/conversion.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMin:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMin,
            "_operationRateMin_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )

    if not self.isCommisDepending:
        for commod in self.fullCommodityConversionFactors[ip]:
            weightDict, data = self.prepareTSAInput(
                self.fullCommodityConversionFactors[ip][commod],
                "_commodityConversionFactorTimeSeries" + str(commod) + "_",
                self.tsaWeight,
                weightDict,
                data,
                ip,
            )
    # for components with conversion time-series depending on commissioning years,
    # the time-series of all commissioning years relevant for the investment period must be considered
    else:
        relevantCommissioningYears = [
            x for (x, y) in self.fullCommodityConversionFactors.keys() if y == ip
        ]
        for commisYear in relevantCommissioningYears:
            # divide the weight by the number of relevant commissioning years to
            # prevent a too high total weight of the commodity conversion time-series
            for commod in self.fullCommodityConversionFactors[(commisYear, ip)]:
                weightDict, data = self.prepareTSAInput(
                    self.fullCommodityConversionFactors[(commisYear, ip)][commod],
                    "_commodityConversionFactorTimeSeries"
                    + str(commod)
                    + str(commisYear).replace("-", "minus")
                    + "_",
                    self.tsaWeight / len(relevantCommissioningYears),
                    weightDict,
                    data,
                    ip,
                )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the conversion component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/conversion.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the conversion component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMin[ip] = self.getTSAOutput(
        self.fullOperationRateMin, "_operationRateMin_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )

    # get the aggregated commodity conversion factors
    # The procedure changes depending on whether the commodity conversion factors depend on the year of
    # commissioning or only on the year of operation (investment period).
    if not self.isCommisDepending:
        if self.fullCommodityConversionFactors[ip] != {}:
            self.aggregatedCommodityConversionFactors[ip] = {}
            for commod in self.fullCommodityConversionFactors[ip]:
                self.aggregatedCommodityConversionFactors[ip][commod] = (
                    self.getTSAOutput(
                        self.fullCommodityConversionFactors[ip][commod],
                        "_commodityConversionFactorTimeSeries" + str(commod) + "_",
                        data,
                        ip,
                    )
                )
    else:
        # if depending on the commissioning year, iterate over the relevant commissioning years for the
        # operation of the investment period ip and get the time-series
        relevantCommissioningYears = [
            x for (x, y) in self.fullCommodityConversionFactors.keys() if y == ip
        ]
        for commisYear in relevantCommissioningYears:
            if self.fullCommodityConversionFactors[(commisYear, ip)] != {}:
                self.aggregatedCommodityConversionFactors[(commisYear, ip)] = {}
                for commod in self.fullCommodityConversionFactors[(commisYear, ip)]:
                    self.aggregatedCommodityConversionFactors[(commisYear, ip)][
                        commod
                    ] = self.getTSAOutput(
                        self.fullCommodityConversionFactors[(commisYear, ip)][
                            commod
                        ],
                        "_commodityConversionFactorTimeSeries"
                        + str(commod)
                        + str(commisYear).replace("-", "minus")
                        + "_",
                        data,
                        ip,
                    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/subclasses/conversionDynamic.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate and fixed operation rate depending on whether a time series
    analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    super().setTimeSeriesData(hasTSA)

ConversionPartLoad

ConversionPartLoad(
    esM,
    name,
    physicalUnit,
    commodityConversionFactors,
    commodityConversionFactorsPartLoad,
    nSegments=None,
    **kwargs,
)

Bases: Conversion

A ConversionPartLoad component maps the (nonlinear) part-load behavior of a Conversion component. It uses the open source module PWLF to generate piecewise linear functions upon a continuous function or discrete data points. The formulation of the optimization is done by using special ordered sets (SOS) constraints. When using ConversionPartLoad it is recommended to check the piecewise linearization visually to verify that the accuracy meets the desired requirements. The ConversionPartLoad class inherits from the Conversion class.

Create an ConversionPartLoad class instance. Capacities are given in the physical unit of the plants. The ConversionPartLoad component specific input arguments are described below. Other specific input arguments are described in the Conversion class and the general component input arguments are described in the Component class.

Required arguments:

:param commodityConversionFactorsPartLoad: A dictionary containing key-value pairs, where each key represents a commodity (e.g., "electricity", "hydrogen") and each value provides the conversion factors that vary with the operation load. These conversion factors dictate the efficiency or rate at which one commodity is transformed into another under different operational conditions. The (nonlinear) part load behavior, which is the relationship between the conversion factors (or efficiency) and the operational load, can be described either using a lambda function for a direct mathematical relationship or a Pandas DataFrame. If a Pandas DataFrame is used, it should contain two columns: one for the x-axis, which represents the operation level (nominal load), and one for the y-axis, which represents the corresponding conversion factor (efficiency) at the corresponding operation level. A negative value indicates that the commodity is consumed. A positive value indicates that the commodity is produced.

Example: * An electrolyzer converts, simply put, electricity into hydrogen with an electrical efficiency depending on the operation level. The physicalUnit is given as GW_electric, the unit for the 'electricity' commodity isgiven in GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue. Here, electricity consumption is represented by a negative value (-1), and hydrogen production efficiency is detailed in a DataFrame with operation levels and corresponding efficiencies.

    # Efficiency Curve of Electrolyzer
    Operation_level = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95]
    Efficiency = [0.1, 0.15, 0.5, 0.7, 0.7, 0.65, 0.63, 0.62, 0.61, 0.60]
    d = {"x": Operation_level, "y": Efficiency}
    partLoadData = pd.DataFrame(d)

    # Definition of commodityConversionFactorsPartLoad
    -> the commodityConversionFactorsPartLoad are defined as {'electricity':-1,'hydrogen':partLoadData}.

Default arguments:

:param nSegments: Number of line segments used for piecewise linearization and generation of point variable (nSegment+1) and segment (nSegment) variable sets. By default, the nSegments is None. For this case, the number of line segments is set to 5. The user can set nSegments by choosing an integer (>=0). It is recommended to choose values between 3 and 7 since the computational cost rises dramatically with increasing nSegments. When specifying nSegements='optimizeSegmentNumbers', an optimal number of line segments is automatically chosen by a bayesian optimization algorithm. |br| * the default value is None :type nSegments: None or integer or string

:param kwargs: All other keyword arguments of the conversion class can be defined as well. :type kwargs: * Check Conversion Class documentation.

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

Source code in fine/subclasses/conversionPartLoad.py
def __init__(
    self,
    esM,
    name,
    physicalUnit,
    commodityConversionFactors,
    commodityConversionFactorsPartLoad,
    nSegments=None,
    **kwargs,
):
    """Create an ConversionPartLoad class instance. Capacities are given in the physical unit
    of the plants.
    The ConversionPartLoad component specific input arguments are described below.
    Other specific input arguments are described in the Conversion class
    and the general component input arguments are described in the Component class.

    **Required arguments:**

    :param commodityConversionFactorsPartLoad: A dictionary containing key-value pairs, where each key represents
    a commodity (e.g., "electricity", "hydrogen") and each value provides the conversion factors that vary with
    the operation load. These conversion factors dictate the efficiency or rate at which one commodity is transformed
    into another under different operational conditions. The (nonlinear) part load behavior, which is the relationship
    between the conversion factors (or efficiency) and the operational load, can be described either using a lambda function
    for a direct mathematical relationship or a Pandas DataFrame. If a Pandas DataFrame is used, it should contain two columns:
    one for the x-axis, which represents the operation level (nominal load), and one for the y-axis, which represents the
    corresponding conversion factor (efficiency) at the corresponding operation level. A negative value indicates that the
    commodity is consumed. A positive value indicates that the commodity is produced.

    Example:
        * An electrolyzer converts, simply put, electricity into hydrogen with an electrical efficiency
            depending on the operation level. The physicalUnit is given as GW_electric, the unit for the 'electricity'
            commodity isgiven in GW_electric and the 'hydrogen' commodity is given in GW_hydrogen_lowerHeatingValue.
            Here, electricity consumption is represented by a negative value (-1), and hydrogen production efficiency
            is detailed in a DataFrame with operation levels and corresponding efficiencies.

            # Efficiency Curve of Electrolyzer
            Operation_level = [0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 0.95]
            Efficiency = [0.1, 0.15, 0.5, 0.7, 0.7, 0.65, 0.63, 0.62, 0.61, 0.60]
            d = {"x": Operation_level, "y": Efficiency}
            partLoadData = pd.DataFrame(d)

            # Definition of commodityConversionFactorsPartLoad
            -> the commodityConversionFactorsPartLoad are defined as {'electricity':-1,'hydrogen':partLoadData}.

    **Default arguments:**

    :param nSegments: Number of line segments used for piecewise linearization and generation of point variable (nSegment+1) and
        segment (nSegment) variable sets.
        By default, the nSegments is None. For this case, the number of line segments is set to 5.
        The user can set nSegments by choosing an integer (>=0). It is recommended to choose values between 3 and 7 since
        the computational cost rises dramatically with increasing nSegments.
        When specifying nSegements='optimizeSegmentNumbers', an optimal number of line segments is automatically chosen by a
        bayesian optimization algorithm.
        |br| * the default value is None
    :type nSegments: None or integer or string

    :param **kwargs: All other keyword arguments of the conversion class can be defined as well.
    :type **kwargs:
        * Check Conversion Class documentation.

    """
    Conversion.__init__(
        self, esM, name, physicalUnit, commodityConversionFactors, **kwargs
    )

    self.modelingClass = ConversionPartLoadModel

    # TODO: Make compatible with conversion
    utils.checkNumberOfConversionFactors(commodityConversionFactors)

    if isinstance(commodityConversionFactorsPartLoad, dict):
        # TODO: Multiple conversionPartLoads
        utils.checkNumberOfConversionFactors(commodityConversionFactorsPartLoad)
        utils.checkCommodities(esM, set(commodityConversionFactorsPartLoad.keys()))
        checkCommodityConversionFactorsPartLoad(
            commodityConversionFactorsPartLoad.values()
        )
        self.commodityConversionFactorsPartLoad = commodityConversionFactorsPartLoad
        self.discretizedPartLoad, self.nSegments = getDiscretizedPartLoad(
            commodityConversionFactorsPartLoad, nSegments
        )

    elif isinstance(commodityConversionFactorsPartLoad, tuple):
        utils.checkNumberOfConversionFactors(
            commodityConversionFactorsPartLoad[0].keys()
        )
        self.discretizedPartLoad = commodityConversionFactorsPartLoad[0]
        self.nSegments = commodityConversionFactorsPartLoad[1]

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/conversion.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMin:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMin,
            "_operationRateMin_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )

    if not self.isCommisDepending:
        for commod in self.fullCommodityConversionFactors[ip]:
            weightDict, data = self.prepareTSAInput(
                self.fullCommodityConversionFactors[ip][commod],
                "_commodityConversionFactorTimeSeries" + str(commod) + "_",
                self.tsaWeight,
                weightDict,
                data,
                ip,
            )
    # for components with conversion time-series depending on commissioning years,
    # the time-series of all commissioning years relevant for the investment period must be considered
    else:
        relevantCommissioningYears = [
            x for (x, y) in self.fullCommodityConversionFactors.keys() if y == ip
        ]
        for commisYear in relevantCommissioningYears:
            # divide the weight by the number of relevant commissioning years to
            # prevent a too high total weight of the commodity conversion time-series
            for commod in self.fullCommodityConversionFactors[(commisYear, ip)]:
                weightDict, data = self.prepareTSAInput(
                    self.fullCommodityConversionFactors[(commisYear, ip)][commod],
                    "_commodityConversionFactorTimeSeries"
                    + str(commod)
                    + str(commisYear).replace("-", "minus")
                    + "_",
                    self.tsaWeight / len(relevantCommissioningYears),
                    weightDict,
                    data,
                    ip,
                )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the conversion component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/conversion.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the conversion component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMin[ip] = self.getTSAOutput(
        self.fullOperationRateMin, "_operationRateMin_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )

    # get the aggregated commodity conversion factors
    # The procedure changes depending on whether the commodity conversion factors depend on the year of
    # commissioning or only on the year of operation (investment period).
    if not self.isCommisDepending:
        if self.fullCommodityConversionFactors[ip] != {}:
            self.aggregatedCommodityConversionFactors[ip] = {}
            for commod in self.fullCommodityConversionFactors[ip]:
                self.aggregatedCommodityConversionFactors[ip][commod] = (
                    self.getTSAOutput(
                        self.fullCommodityConversionFactors[ip][commod],
                        "_commodityConversionFactorTimeSeries" + str(commod) + "_",
                        data,
                        ip,
                    )
                )
    else:
        # if depending on the commissioning year, iterate over the relevant commissioning years for the
        # operation of the investment period ip and get the time-series
        relevantCommissioningYears = [
            x for (x, y) in self.fullCommodityConversionFactors.keys() if y == ip
        ]
        for commisYear in relevantCommissioningYears:
            if self.fullCommodityConversionFactors[(commisYear, ip)] != {}:
                self.aggregatedCommodityConversionFactors[(commisYear, ip)] = {}
                for commod in self.fullCommodityConversionFactors[(commisYear, ip)]:
                    self.aggregatedCommodityConversionFactors[(commisYear, ip)][
                        commod
                    ] = self.getTSAOutput(
                        self.fullCommodityConversionFactors[(commisYear, ip)][
                            commod
                        ],
                        "_commodityConversionFactorTimeSeries"
                        + str(commod)
                        + str(commisYear).replace("-", "minus")
                        + "_",
                        data,
                        ip,
                    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/conversion.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    # processedOperationMax
    self.processedOperationRateMax = (
        self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
    )
    # processedOperationFix
    self.processedOperationRateFix = (
        self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
    )
    # processedOperationMin
    self.processedOperationRateMin = (
        self.aggregatedOperationRateMin if hasTSA else self.fullOperationRateMin
    )
    # processedCommodityConversions
    # timeInfo can either be ip or (commis,ip)
    for timeInfo in self.fullCommodityConversionFactors.keys():
        if self.fullCommodityConversionFactors[timeInfo] != {}:
            for commod in self.fullCommodityConversionFactors[timeInfo]:
                self.processedCommodityConversionFactors[timeInfo][commod] = (
                    self.aggregatedCommodityConversionFactors[timeInfo][commod]
                    if hasTSA
                    else self.fullCommodityConversionFactors[timeInfo][commod]
                )

EnergySystemModel

EnergySystemModel(
    locations,
    commodities,
    commodityUnitsDict,
    numberOfTimeSteps=8760,
    hoursPerTimeStep=1,
    startYear=0,
    numberOfInvestmentPeriods=1,
    investmentPeriodInterval=1,
    stochasticModel=False,
    costUnit="1e9 Euro",
    lengthUnit="km",
    verboseLogLevel=0,
    balanceLimit=None,
    pathwayBalanceLimit=None,
    annuityPerpetuity=False,
)

EnergySystemModel class.

The functionality provided by the EnergySystemModel class is fourfold:

  • With it, the basic structure (spatial and temporal resolution, considered commodities) of the investigated energy system is defined.
  • It serves as a container for all components investigated in the energy system model. These components, namely sources and sinks, conversion options, storage options, and transmission options (in the core module), can be added to an EnergySystemModel instance.
  • It provides the core functionality of modeling and optimizing the energy system based on the specified structure and components on the one hand and of specified simulation parameters on the other hand.
  • It stores optimization results which can then be post-processed with other modules.

The parameters which are stored in an instance of the class refer to:

  • the modeled spatial representation of the energy system (locations, lengthUnit)
  • the modeled temporal representation of the energy system (totalTimeSteps, hoursPerTimeStep, startYear, numberOfInvementPeriods, investmentPeriodInterval, periods, periodsOrder, periodsOccurrences, timeStepsPerPeriod, interPeriodTimeSteps, isTimeSeriesDataClustered, typicalPeriods, tsaInstance, timeUnit)
  • the considered commodities in the energy system (commodities, commodityUnitsDict)
  • the considered components in the energy system (componentNames, componentModelingDict, costUnit)
  • optimization related parameters (pyM, solverSpecs)

The parameters are first set when a class instance is initiated. The parameters which are related to the components (e.g. componentNames) are complemented by adding the components to the class instance.

Instances of this class provide functions for\n * adding components and their respective modeling classes (add) * clustering the time series data of all added components using the time series aggregation package tsam, cf. https://github.com/FZJ-IEK3-VSA/tsam (cluster) * optimizing the specified energy system (optimize), for which a pyomo concrete model instance is built and filled with

(0) basic time sets,
(1) sets, variables and constraints contributed by the component modeling classes,
(2) basic, component overreaching constraints, and
(3) an objective function.

The pyomo instance is then optimized by a specified solver. The optimization results are processed once available. * getting components and their attributes (getComponent, getCompAttr, getOptimizationSummary)

Create an EnergySystemModel class instance.

Required arguments:

:param locations: locations considered in the energy system :type locations: set of strings

:param commodities: commodities considered in the energy system :type commodities: set of strings

:param commodityUnitsDict: dictionary which assigns each commodity a quantitative unit per time (e.g. GW_el, GW_H2, Mio.t_CO2/h). The dictionary is used for results output.

.. note::
    Note for advanced users: the scale of these units can influence the numerical stability of the
    optimization solver, cf. http://files.gurobi.com/Numerics.pdf where a reasonable range of model
    coefficients is suggested.

:type commodityUnitsDict: dictionary of strings

Default arguments:

:param numberOfTimeSteps: number of time steps considered when modeling the energy system (for each time step, or each representative time step, variables and constraints are constituted). Together with the hoursPerTimeStep, the total number of hours considered can be derived. The total number of hours is again used for scaling the arising costs to the arising total annual costs (TAC) which are minimized during optimization. |br| * the default value is 8760. :type totalNumberOfHours: strictly positive integer

:param hoursPerTimeStep: hours per time step |br| * the default value is 1 :type hoursPerTimeStep: strictly positive float

:param numberOfInvestmentPeriods: number of investment periods of transformation path analysis, e.g. for a transformation pathway from 2020 to 2030 with the years 2020, 2025, 2030, the numberOfInvestmentPeriods is 3 |br| * the default value is 1 :type numberOfInvestmentPeriods: strictly positive integer

:param investmentPeriodInterval: interval between the investment of transformation path analysis, e.g. for a transformation pathway from 2020 to 2030 with the years 2020, 2025, 2030, the investmentPeriodInterval is 5 |br| * the default value is 1 :type investmentPeriodInterval: strictly positive integer

:param startYear: year name of first investment period, e.g. for a transformation pathway from 2020 to 2030 with the years 2020, 2025, 2030, the startYear is 2020 |br| * the default value is 0 :type startYear: integer

:param stochasticModel: defines whether to set up a stochastic optimization. The goal of the stochastic optimization is to find a more robust energy system by considering different requirements to find a single energy system design (e.g. various weather years or demand forecasts). These requirements are represented in different investment periods of the model. In contrast to the classical perfect foresight optimization the investment periods do not represent steps of a transformation pathway but possible boundary conditions for the energy system, which need to be considered for the system design and operation |br| * the default value is False :type mode: bool

:param costUnit: cost unit of all cost related values in the energy system. This argument sets the unit of all cost parameters which are given as an input to the EnergySystemModel instance (e.g. for the invest per capacity or the cost per operation).

.. note::
    Note for advanced users: the scale of this unit can influence the numerical stability of the
    optimization solver, cf. http://files.gurobi.com/Numerics.pdf where a reasonable range of model
    coefficients is suggested.

|br| * the default value is '10^9 Euro' (billion euros), which can be a suitable scale for national
energy systems.

:type costUnit: string

:param lengthUnit: length unit for all length-related values in the energy system.

.. note::
    Note for advanced users: the scale of this unit can influence the numerical stability of the
    optimization solver, cf. http://files.gurobi.com/Numerics.pdf where a reasonable range of model
    coefficients is suggested.

|br| * the default value is 'km' (kilometers).

:type lengthUnit: string

:param verboseLogLevel: defines how verbose the console logging is:

- 0: general model logging, warnings and optimization solver logging are displayed.
- 1: warnings are displayed.
- 2: no general model logging or warnings are displayed, the optimization solver logging is set to a
  minimum.

.. note::
    if required, the optimization solver logging can be separately enabled in the optimizationSpecs
    of the optimize function.

|br| * the default value is 0

:type verboseLogLevel: integer (0, 1 or 2)

:param balanceLimit: defines the balanceLimit constraint (various different balanceLimitIDs possible) for specific regions or the whole model and optional also per investment period. The balancelimitID can be assigned to various components of e.g. SourceSinkModel or TransmissionModel to limit the balance of production, consumption and im/export.

Regional dependency:
The balanceLimit is defined as a pd.DataFrame. Each row contains an individual balanceLimitID as
index, the corresponding regional scope as columns and the values as data. The regional scope can be set
for a region with the matching region name as column name or "Total" as column name for setting for the entire system.
If no balanceLimit is to be set for a given location, replace the corresponding entry with 'None' (not 'np.nan').

Temporal dependency:
If the balanceLimit is passed as a dict with the described pd.DataFrames as values it is considered per investment period.
Values are always given in the unit of the esM commodities unit.\n

Optional: A column named 'lowerBound' can be passed to specify if
the limit is an upper or lower bound.
By default an upperBound is considered ('lowerBound'=False).
However, multiple cases can be considered:\n
1) Sources:\n
    a) LowerBound=False: UpperBound for commodity from SourceComponent (Define positive value in
    balanceLimit). Example: Limit CO2-Emission\n
    b) LowerBound=True: LowerBound for commodity from SourceComponent (Define positive value in
    balanceLimit). Example: Require minimum production from renewables.\n
2) Sinks:\n
    a) LowerBound=False: UpperBound in a mathematical sense for commodity from SinkComponent
    (Logically minimum limit for negative values, define negative value in balanceLimit).
    Example: Minimum export/consumption of hydrogen.\n
    b) LowerBound=True: LowerBound in a mathematical sense for commodity from SourceComponent
    (Logically maximum limit for negative values, define negative value in balanceLimit).
    Example: Define upper limit for Carbon Capture & Storage.\n

Examples: - balanceLimit for commodity flow into system/ location e.g. due to a source [positive value; lowerBound=False] (e.g. natural gas field): balanceLimit=pd.DataFrame(columns=["Total"], index=["Gas production", "lowerBound"], data=[1000, False] ) - balanceLimit for commodity flow out of system/ location e.g. due to a sink [negative value; lowerBound=True] (e.g. CO2 sink): balanceLimit=pd.DataFrame(columns=["Total"], index=["CO2 Limit", "lowerBound"], data=[-1000, True] ) - balanceLimit per region and per system (flow into system/ location): balanceLimit=pd.DataFrame(columns=["Region1", "Total"], index=["CO2 Limit"], data=[1000, 2000] ) - multiple balanceLimits for different IDs: balanceLimit=pd.DataFrame(columns=["Total", "lowerBound"], index=["CO2 limit", "Gas production"], data=[[400, False], [1000, True]] ) - Different CO2 Limits for each investment period and minimum installed capacity for renewables: balanceLimit = { 2020: pd.DataFrame(index=["CO2 limit", "Renewables"], columns=["Total", "lowerBound"], data=[[-366 * (1 - CO2_reductionTarget * 0.33), True],[430000, True]] ), 2025: pd.DataFrame(index=["CO2 limit", "Renewables"], columns=["Region1", "Total", "lowerBound"], data=[[-366 * (1 - CO2_reductionTarget * 0.67), True],[430000, True]] ), 2030: pd.DataFrame(index=["CO2 limit", "Renewables"], columns=["Region1", "Total", "lowerBound"], data=[[-366 * (1 - CO2_reductionTarget), True],[430000, True]] ) } .. note:: If bounds for sinks shall be specified (e.g. min. export, max. sink volume), values must be defined as negative.

|br| * the default value is None

:type balanceLimit:

* pd.DataFrame
* dictionary with investment periods years as keys, and pd.DataFrame as values

|br| * the default value is None

:type lowerBound: bool

:param pathwayBalanceLimit: the pathway balance limit defines commodity balance (lower or upper bound) for the pathway. The structure is similar to the balanceLimit, however does without the temporal dependency per investment period. Examples: CO2 budget for the entire transformation pathway |br| * the default value is None :type pathwayBalanceLimit: None or pd.DataFrame

:param annuityPerpetuity: if set to True, it is assumed that the design and operation of the last investment period will be maintained forever. Therefore, the cost contribution of each component's last investment period is divided by the component's interest rate to account for perpetuity costs.

To enable annuity perpetuity the interest rate of every component must be greater than 0.

|br| * the default value is False

:type: annuityPerpetuity: bool

Methods:

  • add

    Add a component and, if required, its respective modeling class to the EnergySystemModel instance.

  • aggregateSpatially

    Spatially clusters the data of all components considered in the Energy System Model (esM) instance

  • aggregateTemporally

    Temporally cluster the time series data of all components considered in the EnergySystemModel instance and then

  • createTimeSeriesDataForAggregation

    Create and return the time series data, weights, and zero-data columns for aggregation.

  • declareBalanceLimitConstraint

    Declare balance limit constraint.

  • declareCommodityBalanceConstraints

    Declare commodity balance constraints (one balance constraint for each commodity, location and time step).

  • declareComponentLinkedQuantityConstraints

    Declare linked component quantity constraint, e.g. if an engine (E-Motor) is built also a storage (Battery)

  • declareObjective

    Declare the objective function by obtaining the contributions to the objective function from all modeling

  • declareOptimizationProblem

    Declare the optimization problem belonging to the specified energy system for which a pyomo concrete model

  • declareSharedPotentialConstraints

    Declare shared potential constraints, e.g. if a maximum potential of salt caverns has to be shared by

  • declareTimeSets

    Set and initialize basic time parameters and sets.

  • getComponent

    Return a component of the energy system.

  • getComponentAttribute

    Return an attribute of a component considered in the energy system.

  • getOptimizationSummary

    Return the optimization summary (design variables, aggregated operation variables, and objective contributions) of a modeling class.

  • optimize

    Optimize the specified energy system for which a pyomo ConcreteModel instance is built or called upon.

  • removeComponent

    Remove a component from the energy system.

  • updateComponent

    Overwrite selected attributes of an existing esM component with new values.

Source code in fine/energySystemModel.py
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
458
459
460
461
462
def __init__(
    self,
    locations,
    commodities,
    commodityUnitsDict,
    numberOfTimeSteps=8760,
    hoursPerTimeStep=1,
    startYear=0,
    numberOfInvestmentPeriods=1,
    investmentPeriodInterval=1,
    stochasticModel=False,
    costUnit="1e9 Euro",
    lengthUnit="km",
    verboseLogLevel=0,
    balanceLimit=None,
    pathwayBalanceLimit=None,
    annuityPerpetuity=False,
):
    r"""Create an EnergySystemModel class instance.

    **Required arguments:**

    :param locations: locations considered in the energy system
    :type locations: set of strings

    :param commodities: commodities considered in the energy system
    :type commodities: set of strings

    :param commodityUnitsDict: dictionary which assigns each commodity a quantitative unit per time
        (e.g. GW_el, GW_H2, Mio.t_CO2/h). The dictionary is used for results output.

        .. note::
            Note for advanced users: the scale of these units can influence the numerical stability of the
            optimization solver, cf. http://files.gurobi.com/Numerics.pdf where a reasonable range of model
            coefficients is suggested.

    :type commodityUnitsDict: dictionary of strings

    **Default arguments:**

    :param numberOfTimeSteps: number of time steps considered when modeling the energy system (for each
        time step, or each representative time step, variables and constraints are constituted). Together
        with the hoursPerTimeStep, the total number of hours considered can be derived. The total
        number of hours is again used for scaling the arising costs to the arising total annual costs (TAC)
        which are minimized during optimization.
        |br| * the default value is 8760.
    :type totalNumberOfHours: strictly positive integer

    :param hoursPerTimeStep: hours per time step
        |br| * the default value is 1
    :type hoursPerTimeStep: strictly positive float

    :param numberOfInvestmentPeriods: number of investment periods of transformation
        path analysis, e.g. for a transformation pathway from 2020 to 2030
        with the years 2020, 2025, 2030, the numberOfInvestmentPeriods is 3
        |br| * the default value is 1
    :type numberOfInvestmentPeriods: strictly positive integer

    :param investmentPeriodInterval: interval between the investment of transformation
        path analysis, e.g. for a transformation pathway from 2020 to 2030
        with the years 2020, 2025, 2030, the investmentPeriodInterval is 5
        |br| * the default value is 1
    :type investmentPeriodInterval: strictly positive integer

    :param startYear: year name of first investment period, e.g. for a transformation
        pathway from 2020 to 2030 with the years 2020, 2025, 2030, the startYear is 2020
        |br| * the default value is 0
    :type startYear: integer

    :param stochasticModel: defines whether to set up a stochastic optimization.
        The goal of the stochastic optimization is to find a more robust energy system by considering different
        requirements to find a single energy system design (e.g. various weather years or demand forecasts). These requirements
        are represented in different investment periods of the model. In contrast to the classical perfect foresight
        optimization the investment periods do not represent steps of a transformation pathway but possible boundary
        conditions for the energy system, which need to be considered for the system design and operation
        |br| * the default value is False
    :type mode: bool

    :param costUnit: cost unit of all cost related values in the energy system. This argument sets the unit of
        all cost parameters which are given as an input to the EnergySystemModel instance (e.g. for the
        invest per capacity or the cost per operation).

        .. note::
            Note for advanced users: the scale of this unit can influence the numerical stability of the
            optimization solver, cf. http://files.gurobi.com/Numerics.pdf where a reasonable range of model
            coefficients is suggested.

        |br| * the default value is '10^9 Euro' (billion euros), which can be a suitable scale for national
        energy systems.
    :type costUnit: string

    :param lengthUnit: length unit for all length-related values in the energy system.

        .. note::
            Note for advanced users: the scale of this unit can influence the numerical stability of the
            optimization solver, cf. http://files.gurobi.com/Numerics.pdf where a reasonable range of model
            coefficients is suggested.

        |br| * the default value is 'km' (kilometers).
    :type lengthUnit: string

    :param verboseLogLevel: defines how verbose the console logging is:

        - 0: general model logging, warnings and optimization solver logging are displayed.
        - 1: warnings are displayed.
        - 2: no general model logging or warnings are displayed, the optimization solver logging is set to a
          minimum.

        .. note::
            if required, the optimization solver logging can be separately enabled in the optimizationSpecs
            of the optimize function.

        |br| * the default value is 0
    :type verboseLogLevel: integer (0, 1 or 2)

    :param balanceLimit: defines the balanceLimit constraint (various different balanceLimitIDs possible)
        for specific regions or the whole model and optional also per investment period.
        The balancelimitID can be assigned to various components of e.g. SourceSinkModel or
        TransmissionModel to limit the balance of production, consumption and im/export.

        Regional dependency:
        The balanceLimit is defined as a pd.DataFrame. Each row contains an individual balanceLimitID as
        index, the corresponding regional scope as columns and the values as data. The regional scope can be set
        for a region with the matching region name as column name or "Total" as column name for setting for the entire system.
        If no balanceLimit is to be set for a given location, replace the corresponding entry with 'None' (not 'np.nan').

        Temporal dependency:
        If the balanceLimit is passed as a dict with the described pd.DataFrames as values it is considered per investment period.
        Values are always given in the unit of the esM commodities unit.\n

        Optional: A column named 'lowerBound' can be passed to specify if
        the limit is an upper or lower bound.
        By default an upperBound is considered ('lowerBound'=False).
        However, multiple cases can be considered:\n
        1) Sources:\n
            a) LowerBound=False: UpperBound for commodity from SourceComponent (Define positive value in
            balanceLimit). Example: Limit CO2-Emission\n
            b) LowerBound=True: LowerBound for commodity from SourceComponent (Define positive value in
            balanceLimit). Example: Require minimum production from renewables.\n
        2) Sinks:\n
            a) LowerBound=False: UpperBound in a mathematical sense for commodity from SinkComponent
            (Logically minimum limit for negative values, define negative value in balanceLimit).
            Example: Minimum export/consumption of hydrogen.\n
            b) LowerBound=True: LowerBound in a mathematical sense for commodity from SourceComponent
            (Logically maximum limit for negative values, define negative value in balanceLimit).
            Example: Define upper limit for Carbon Capture & Storage.\n

    Examples:
        - balanceLimit for commodity flow into system/ location e.g. due to a source
        [positive value; lowerBound=False] (e.g. natural gas field):
            balanceLimit=pd.DataFrame(columns=["Total"],
                                      index=["Gas production", "lowerBound"],
                                      data=[1000, False]
                                      )
        - balanceLimit for commodity flow out of system/ location e.g. due to a sink
        [negative value; lowerBound=True] (e.g. CO2 sink):
            balanceLimit=pd.DataFrame(columns=["Total"],
                                      index=["CO2 Limit", "lowerBound"],
                                      data=[-1000, True]
                                      )
        - balanceLimit per region and per system (flow into system/ location):
            balanceLimit=pd.DataFrame(columns=["Region1", "Total"],
                                      index=["CO2 Limit"],
                                      data=[1000, 2000]
                                      )
        - multiple balanceLimits for different IDs:
            balanceLimit=pd.DataFrame(columns=["Total", "lowerBound"],
                                        index=["CO2 limit", "Gas production"],
                                        data=[[400, False], [1000, True]]
                                        )
        - Different CO2 Limits for each investment period and minimum installed capacity for renewables:
            balanceLimit = {
                2020: pd.DataFrame(index=["CO2 limit", "Renewables"],
                                   columns=["Total", "lowerBound"],
                                   data=[[-366 * (1 - CO2_reductionTarget * 0.33), True],[430000, True]]
                                   ),
                2025: pd.DataFrame(index=["CO2 limit", "Renewables"],
                                   columns=["Region1", "Total", "lowerBound"],
                                   data=[[-366 * (1 - CO2_reductionTarget * 0.67), True],[430000, True]]
                                   ),
                2030: pd.DataFrame(index=["CO2 limit", "Renewables"],
                                   columns=["Region1", "Total", "lowerBound"],
                                   data=[[-366 * (1 - CO2_reductionTarget), True],[430000, True]]
                                   )
                            }
        .. note::
            If bounds for sinks shall be specified (e.g. min. export, max. sink volume), values must be
            defined as negative.

        |br| * the default value is None
    :type balanceLimit:

        * pd.DataFrame
        * dictionary with investment periods years as keys, and pd.DataFrame as values

        |br| * the default value is None
    :type lowerBound: bool

    :param pathwayBalanceLimit: the pathway balance limit defines commodity balance (lower or upper bound) for the pathway.
        The structure is similar to the balanceLimit, however does without the temporal dependency per investment period.
        Examples: CO2 budget for the entire transformation pathway
        |br| * the default value is None
    :type pathwayBalanceLimit: None or pd.DataFrame

    :param annuityPerpetuity: if set to True, it is assumed that the design and operation of the last investment
        period will be maintained forever. Therefore, the cost contribution of each component's last investment
        period is divided by the component's interest rate to account for perpetuity costs.

        To enable annuity perpetuity the interest rate of every component must be greater than 0.

        |br| * the default value is False
    :type: annuityPerpetuity: bool

    """
    # Check correctness of inputs
    utils.checkEnergySystemModelInput(
        locations,
        commodities,
        commodityUnitsDict,
        numberOfTimeSteps,
        hoursPerTimeStep,
        numberOfInvestmentPeriods,
        investmentPeriodInterval,
        startYear,
        stochasticModel,
        costUnit,
        lengthUnit,
    )

    ################################################################################################################
    #                                        Spatial resolution parameters                                         #
    ################################################################################################################

    # The locations (set of string) name the considered locations in an energy system model instance. The parameter
    # is used throughout the build of the energy system model to validate inputs and declare relevant sets,
    # variables and constraints.
    # The length unit refers to the measure of length referred throughout the model.

    self.locations, self.lengthUnit = locations, lengthUnit
    self._locationsOrdered = sorted(locations)

    self.numberOfTimeSteps = numberOfTimeSteps

    ################################################################################################################
    #                                            Time series parameters                                            #
    ################################################################################################################

    # The totalTimeSteps parameter (list, ranging from 0 to the total numberOfTimeSteps-1) refers to the total
    # number of time steps considered when modeling the specified energy system. The parameter is used for
    # validating time series data input and for setting other time series parameters when modeling a full temporal
    # resolution. The hoursPerTimeStep parameter (float > 0) refers to the temporal length of a time step in the
    # totalTimeSteps. From the numberOfTimeSteps and the hoursPerTimeStep the numberOfYears parameter is computed.
    self.totalTimeSteps, self.hoursPerTimeStep = (
        list(range(numberOfTimeSteps)),
        hoursPerTimeStep,
    )
    self.numberOfTimeSteps = numberOfTimeSteps
    self.numberOfYears = numberOfTimeSteps * hoursPerTimeStep / 8760.0

    # The periods parameter (list, [0] when considering a full temporal resolution, range of [0, ...,
    # totalNumberOfTimeSteps/numberOfTimeStepsPerPeriod] when applying time series aggregation) represents
    # the periods considered when modeling the energy system. Only one period exists when considering the full
    # temporal resolution. When applying time series aggregation, the full time series are broken down into
    # periods to which a typical period is assigned to.
    # These periods have an order which is stored in the periodsOrder parameter (list, [0] when considering a full
    # temporal resolution, [typicalPeriod(0), ... ,
    # typicalPeriod(totalNumberOfTimeSteps/numberOfTimeStepsPerPeriod-1)] when applying time series aggregation).
    # The occurrences of these periods are stored in the periodsOccurrences parameter (list, [1] when considering a
    # full temporal resolution, [occurrences(0), ..., occurrences(numberOfTypicalPeriods-1)] when applying time
    # series aggregation).
    self.periods, self.periodsOrder, self.periodOccurrences = [0], [0], [1]
    self.timeStepsPerPeriod = list(range(numberOfTimeSteps))
    self.interPeriodTimeSteps = list(
        range(int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)) + 1)
    )

    # The isTimeSeriesDataClustered parameter is used to check data consistency.
    # It is set to True if the class' cluster function is called. It is set to False if a new component is added.
    # If the cluster function is called, the typicalPeriods parameter is set from None to
    # [0, ..., numberOfTypicalPeriods-1] and, if specified, the resulting TimeSeriesAggregation instance is stored
    # in the tsaInstance parameter (default None).
    # The time unit refers to time measure referred throughout the model. Currently, it has to be an hour 'h'.
    self.isTimeSeriesDataClustered, self.typicalPeriods, self.tsaInstance = (
        False,
        None,
        None,
    )
    self.timeUnit = "h"

    ################################################################################################################
    #                                Stochastic/Pathway parameters                                                 #
    ################################################################################################################
    self.stochasticModel = stochasticModel
    ######################################################################
    self.startYear = startYear
    self.investmentPeriodInterval = investmentPeriodInterval
    self.numberOfInvestmentPeriods = numberOfInvestmentPeriods
    self.annuityPerpetuity = utils.checkAndSetAnnuityPerpetuity(
        annuityPerpetuity, numberOfInvestmentPeriods
    )
    # set up the modelling years by the start year, interval and number of investment periods
    finalyear = startYear + numberOfInvestmentPeriods * investmentPeriodInterval
    # clear names, e.g.  [2020, 2025,...]
    self.investmentPeriodNames = list(
        range(startYear, finalyear, investmentPeriodInterval)
    )
    # internal names, e.g.  [0,1,...]
    self.investmentPeriods = list(range(numberOfInvestmentPeriods))

    ################################################################################################################
    #                                        Commodity specific parameters                                         #
    ################################################################################################################

    # The commodities parameter is a set of strings which describes what commodities are considered in the energy
    # system, and hence, which commodity balances need to be considered in the energy system model and its
    # optimization.
    # The commodityUnitsDict parameter is a dictionary which assigns each considered commodity (string) a
    # unit (string) which can be used by results output functions.
    self.commodities = commodities
    self.commodityUnitsDict = commodityUnitsDict

    # The balanceLimit can be used to limit certain balanceLimitIDs defined in the components.
    self.balanceLimit = balanceLimit
    self.pathwayBalanceLimit = pathwayBalanceLimit
    self.processedBalanceLimit = utils.checkAndSetBalanceLimit(
        self, balanceLimit, locations
    )
    self.processedPathwayBalanceLimit = utils.checkAndSetPathwayBalanceLimit(
        self, pathwayBalanceLimit, locations
    )

    ################################################################################################################
    #                                        Component specific parameters                                         #
    ################################################################################################################

    # The componentNames parameter is a set of strings in which all in the EnergySystemModel instance considered
    # components are stored. It is used to check that all components have unique indices.
    # The componentModelingDict is a dictionary (modelingClass name: modelingClass instance) in which the in the
    # energy system considered modeling classes are stored (in which again the components modeled with the
    # modelingClass as well as the equations to model them with are stored).
    # The costUnit parameter (string) is the parameter in which all cost input parameter have to be specified.
    self.componentNames = {}
    self.componentModelingDict = {}
    self.sharedPotentialDict = {}
    self.costUnit = costUnit

    ################################################################################################################
    #                                           Optimization parameters                                            #
    ################################################################################################################

    # The pyM parameter is None when the EnergySystemModel is initialized. After calling the optimize function,
    # the pyM parameter stores a Concrete Pyomo Model instance which contains parameters, sets, variables,
    # constraints and objective required for the optimization set up and solving.
    # The solverSpecs parameter is a dictionary (string: param) which stores different parameters that are used
    # for solving the optimization problem. The parameters are: solver (string, solver which is used to solve
    # the optimization problem), optimizationSpecs (string, representing **kwargs for the solver), hasTSA (boolean,
    # indicating if time series aggregation is used for the optimization), buildtime (positive float, time needed
    # to declare the optimization problem in seconds), solvetime (positive float, time needed to solve the
    # optimization problem in seconds), runtime (positive float, runtime of the optimization run in seconds),
    # timeLimit (positive float or None, if specified, indicates the maximum allowed runtime of the solver),
    # threads (positive int, number of threads used for optimization, can depend on solver), logFileName
    # (string, name of logfile).
    # The objectiveValue parameter is None when the EnergySystemModel is initialized. After calling the
    # optimize function, the objective value (i.e. TAC of the analyzed energy system) is stored in the
    # objectiveValue parameter for easier access.

    self.pyM = None
    self.solverSpecs = {
        "solver": "",
        "optimizationSpecs": "",
        "hasTSA": False,
        "buildtime": 0,
        "solvetime": 0,
        "runtime": 0,
        "timeLimit": None,
        "threads": 0,
        "logFileName": "",
    }
    self.objectiveValue = None

    ################################################################################################################
    #                                           General model parameters                                           #
    ################################################################################################################

    # The verbose parameter defines how verbose the console logging is: 0: general model logging, warnings
    # and optimization solver logging are displayed, 1: warnings are displayed, 2: no general model logging or
    # warnings are displayed, the optimization solver logging is set to a minimum.
    # The optimization solver logging can be separately enabled in the optimizationSpecs of the optimize function.
    self.verboseLogLevel = verboseLogLevel

add

add(component)

Add a component and, if required, its respective modeling class to the EnergySystemModel instance. The added component has to inherit from the FINE class Component.

:param component: the component to be added :type component: An object which inherits from the FINE class Component

Source code in fine/energySystemModel.py
def add(self, component):
    """Add a component and, if required, its respective modeling class to the EnergySystemModel instance.
    The added component has to inherit from the FINE class Component.

    :param component: the component to be added
    :type component: An object which inherits from the FINE class Component
    """
    if not issubclass(type(component), Component):
        raise TypeError(
            "The added component has to inherit from the FINE class Component."
        )
    if not issubclass(component.modelingClass, ComponentModel):
        raise TypeError(
            "The added component has to inherit from the FINE class ComponentModel."
        )
    component.addToEnergySystemModel(self)

aggregateSpatially

aggregateSpatially(
    shapefile,
    grouping_mode="parameter_based",
    n_groups=3,
    distance_threshold=None,
    aggregatedResultsPath=None,
    **kwargs,
)

Spatially clusters the data of all components considered in the Energy System Model (esM) instance and returns a new esM instance with the aggregated data.

:param shapefile: Either the path to the shapefile or the read-in shapefile :type shapefile: string, GeoDataFrame

Default arguments:

:param grouping_mode: Defines how to spatially group the regions. Refer to grouping.py for more information. |br| * the default value is 'parameter_based' :type grouping_mode: string, Options: 'string_based', 'distance_based', 'parameter_based'

:param n_groups: The number of region groups to be formed from the original region set. This parameter is irrelevant if grouping_mode is 'string_based'. |br| * the default value is 3 :type n_groups: strictly positive integer, None

:param distance_threshold: The distance threshold at or above which regions will not be aggregated into one. |br| * the default value is None. If not None, n_groups must be None :type distance_threshold: float

:param aggregatedResultsPath: Indicates path to which the aggregated results should be saved. If None, results are not saved. |br| * the default value is None :type aggregatedResultsPath: string, None

Additional keyword arguments that can be passed via kwargs:

:param geom_col_name: The geometry column name in shapefile |br| * the default value is 'geometry' :type geom_col_name: string

:param geom_id_col_name: The column in shapefile consisting geom IDs |br| * the default value is 'index' :type geom_id_col_name: string

:param separator: Relevant only if grouping_mode is 'string_based'. The character or string in the region IDs that defines where the ID should be split. E.g.: region IDs -> ['01_es', '02_es'] and separator='_', then IDs are split at _ and the last part ('es') is taken as the group ID |br| * the default value is None :type separator: string

:param position: Relevant only if grouping_mode is 'string_based'. Used to define the position(s) of the region IDs where the split should happen. An int i would mean the part from 0 to i is taken as the group ID. A tuple (i,j) would mean the part i to j is taken at the group ID.

.. note:: either `separator` or `position` must be passed in order to perform string_based_grouping

|br| * the default value is None

:type position: integer/tuple

:param weights: Relevant only if grouping_mode is 'parameter_based'. Through the weights dictionary, one can assign weights to variable-component pairs. When calculating distance corresponding to each variable-component pair, these specified weights are considered, otherwise taken as 1.

It must be in one of the formats:

- If you want to specify weights for particular variables and particular corresponding components:

    { 'components' : Dict[<component_name>, <weight>}], 'variables' : List[<variable_name>] }

- If you want to specify weights for particular variables, but all corresponding components:

    { 'components' : {'all' : <weight>}, 'variables' : List[<variable_name>] }

- If you want to specify weights for all variables, but particular corresponding components:

    { 'components' : Dict[<component_name>, <weight>}], 'variables' : 'all' }

<weight> can be of type integer/float

|br| * the default value is None

:type weights: dictionary

:param aggregation_method: Relevant only if grouping_mode is 'parameter_based'. The clustering method that should be used to group the regions. Options:

    - 'kmedoids_contiguity':
        kmedoids clustering with added contiguity constraint.
        Refer to TSAM docs for more info: https://github.com/FZJ-IEK3-VSA/tsam/blob/master/tsam/utils/k_medoids_contiguity.py
    - 'hierarchical':
        sklearn's agglomerative clustering with complete linkage, with a connectivity matrix to ensure contiguity.
        Refer to Sklearn docs for more info: https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html

|br| * the default value is 'kmedoids_contiguity'

:type aggregation_method: string, Options: 'kmedoids_contiguity', 'hierarchical'

:param solver: Relevant only if grouping_mode is 'parameter_based' and aggregation_method is 'kmedoids_contiguity' The optimization solver to be chosen. |br| * the default value is 'gurobi' :type solver: string, Options: 'gurobi', 'highs', 'glpk'

:param aggregation_function_dict: Contains information regarding the mode of aggregation for each individual variable.

- Possibilities: mean, weighted mean, sum, bool (boolean OR).
- Format of the dictionary

    - {<variable_name>: (<mode_of_aggregation>, <weights>),
      <variable_name>: (<mode_of_aggregation>, None)}.

    <weights> is required only if <mode_of_aggregation> is
    'weighted mean'. The name of the variable that should act as weights should be provided. Can be None otherwise.

A default dictionary is considered with the following corresponding modes.
If `aggregation_function_dict` is passed, this default dictionary is updated.

| {"operationRateMax": ("weighted mean", "capacityMax"),
| "operationRateFix": ("sum", None),
| "processedLocationalEligibility": ("bool", None),
| "capacityMax": ("sum", None),
| "investPerCapacity": ("mean", None),
| "investIfBuilt": ("bool", None),
| "opexPerOperation": ("mean", None),
| "opexPerCapacity": ("mean", None),
| "opexIfBuilt": ("bool", None),
| "interestRate": ("mean", None),
| "economicLifetime": ("mean", None),
| "capacityFix": ("sum", None),
| "losses": ("mean", None),
| "distances": ("mean", None),
| "commodityCost": ("mean", None),
| "commodityRevenue": ("mean", None),
| "opexPerChargeOperation": ("mean", None),
| "opexPerDischargeOperation": ("mean", None),
| "QPcostScale": ("sum", None),
| "technicalLifetime": ("mean", None)}

:type aggregation_function_dict: dictionary

:param aggregated_shp_name: Name to be given to the saved shapefiles after aggregation |br| * the default value is 'aggregated_regions' :type aggregated_shp_name: string

:param crs: Coordinate reference system (crs) in which to save the shapefiles |br| * the default value is 3035 :type crs: integer

:param crs: Coordinate reference system (crs) in which to save the shapefiles |br| * the default value is 3035 :type crs: integer

:param aggregated_xr_filename: Name to be given to the saved netCDF file containing aggregated esM data |br| * the default value is 'aggregated_xr_dataset.nc' :type aggregated_xr_filename: string

:returns: Aggregated esM instance

Source code in fine/energySystemModel.py
def aggregateSpatially(
    self,
    shapefile,
    grouping_mode="parameter_based",
    n_groups=3,
    distance_threshold=None,
    aggregatedResultsPath=None,
    **kwargs,
):
    """Spatially clusters the data of all components considered in the Energy System Model (esM) instance
    and returns a new esM instance with the aggregated data.

    :param shapefile: Either the path to the shapefile or the read-in shapefile
    :type shapefile: string, GeoDataFrame

    **Default arguments:**

    :param grouping_mode: Defines how to spatially group the regions.
        Refer to grouping.py for more information.
        |br| * the default value is 'parameter_based'
    :type grouping_mode: string, Options: 'string_based', 'distance_based', 'parameter_based'

    :param n_groups: The number of region groups to be formed from the original region set.
        This parameter is irrelevant if `grouping_mode` is 'string_based'.
        |br| * the default value is 3
    :type n_groups: strictly positive integer, None

    :param distance_threshold: The distance threshold at or above which regions will not be aggregated into one.
        |br| * the default value is None. If not None, n_groups must be None
    :type distance_threshold: float

    :param aggregatedResultsPath: Indicates path to which the aggregated results should be saved.
        If None, results are not saved.
        |br| * the default value is None
    :type aggregatedResultsPath: string, None

    **Additional keyword arguments that can be passed via kwargs:**

    :param geom_col_name: The geometry column name in `shapefile`
        |br| * the default value is 'geometry'
    :type geom_col_name: string

    :param geom_id_col_name: The column in `shapefile` consisting geom IDs
        |br| * the default value is 'index'
    :type geom_id_col_name: string

    :param separator: Relevant only if `grouping_mode` is 'string_based'.
        The character or string in the region IDs that defines where the ID should be split.
        E.g.: region IDs -> ['01_es', '02_es'] and separator='_', then IDs are split at _
        and the last part ('es') is taken as the group ID
        |br| * the default value is None
    :type separator: string

    :param position: Relevant only if `grouping_mode` is 'string_based'.
        Used to define the position(s) of the region IDs where the split should happen.
        An int i would mean the part from 0 to i is taken as the group ID. A tuple (i,j) would mean
        the part i to j is taken at the group ID.

        .. note:: either `separator` or `position` must be passed in order to perform string_based_grouping

        |br| * the default value is None
    :type position: integer/tuple

    :param weights: Relevant only if `grouping_mode` is 'parameter_based'.
        Through the `weights` dictionary, one can assign weights to variable-component pairs. When calculating
        distance corresponding to each variable-component pair, these specified weights are
        considered, otherwise taken as 1.

        It must be in one of the formats:

        - If you want to specify weights for particular variables and particular corresponding components:

            { 'components' : Dict[<component_name>, <weight>}], 'variables' : List[<variable_name>] }

        - If you want to specify weights for particular variables, but all corresponding components:

            { 'components' : {'all' : <weight>}, 'variables' : List[<variable_name>] }

        - If you want to specify weights for all variables, but particular corresponding components:

            { 'components' : Dict[<component_name>, <weight>}], 'variables' : 'all' }

        <weight> can be of type integer/float

        |br| * the default value is None
    :type weights: dictionary

    :param aggregation_method: Relevant only if `grouping_mode` is 'parameter_based'.
        The clustering method that should be used to group the regions. Options:

            - 'kmedoids_contiguity':
                kmedoids clustering with added contiguity constraint.
                Refer to TSAM docs for more info: https://github.com/FZJ-IEK3-VSA/tsam/blob/master/tsam/utils/k_medoids_contiguity.py
            - 'hierarchical':
                sklearn's agglomerative clustering with complete linkage, with a connectivity matrix to ensure contiguity.
                Refer to Sklearn docs for more info: https://scikit-learn.org/stable/modules/generated/sklearn.cluster.AgglomerativeClustering.html

        |br| * the default value is 'kmedoids_contiguity'
    :type aggregation_method: string, Options: 'kmedoids_contiguity', 'hierarchical'

    :param solver: Relevant only if `grouping_mode` is 'parameter_based' and `aggregation_method` is 'kmedoids_contiguity'
        The optimization solver to be chosen.
        |br| * the default value is 'gurobi'
    :type solver: string, Options: 'gurobi', 'highs', 'glpk'

    :param aggregation_function_dict: Contains information regarding the mode of aggregation for each individual variable.

        - Possibilities: mean, weighted mean, sum, bool (boolean OR).
        - Format of the dictionary

            - {<variable_name>: (<mode_of_aggregation>, <weights>),
              <variable_name>: (<mode_of_aggregation>, None)}.

            <weights> is required only if <mode_of_aggregation> is
            'weighted mean'. The name of the variable that should act as weights should be provided. Can be None otherwise.

        A default dictionary is considered with the following corresponding modes.
        If `aggregation_function_dict` is passed, this default dictionary is updated.

        | {"operationRateMax": ("weighted mean", "capacityMax"),
        | "operationRateFix": ("sum", None),
        | "processedLocationalEligibility": ("bool", None),
        | "capacityMax": ("sum", None),
        | "investPerCapacity": ("mean", None),
        | "investIfBuilt": ("bool", None),
        | "opexPerOperation": ("mean", None),
        | "opexPerCapacity": ("mean", None),
        | "opexIfBuilt": ("bool", None),
        | "interestRate": ("mean", None),
        | "economicLifetime": ("mean", None),
        | "capacityFix": ("sum", None),
        | "losses": ("mean", None),
        | "distances": ("mean", None),
        | "commodityCost": ("mean", None),
        | "commodityRevenue": ("mean", None),
        | "opexPerChargeOperation": ("mean", None),
        | "opexPerDischargeOperation": ("mean", None),
        | "QPcostScale": ("sum", None),
        | "technicalLifetime": ("mean", None)}

    :type aggregation_function_dict: dictionary

    :param aggregated_shp_name: Name to be given to the saved shapefiles after aggregation
        |br| * the default value is 'aggregated_regions'
    :type aggregated_shp_name: string

    :param crs: Coordinate reference system (crs) in which to save the shapefiles
        |br| * the default value is 3035
    :type crs: integer

    :param crs: Coordinate reference system (crs) in which to save the shapefiles
        |br| * the default value is 3035
    :type crs: integer

    :param aggregated_xr_filename: Name to be given to the saved netCDF file containing aggregated esM data
        |br| * the default value is 'aggregated_xr_dataset.nc'
    :type aggregated_xr_filename: string

    :returns: Aggregated esM instance
    """
    # STEP 1. Obtain xr dataset from esM
    xr_dataset = xrIO.convertOptimizationInputToDatasets(
        self, useProcessedValues=True
    )

    # STEP 2. Perform spatial aggregation
    aggregated_xr_dataset = spagat.perform_spatial_aggregation(
        xr_dataset,
        shapefile,
        grouping_mode,
        n_groups,
        distance_threshold,
        aggregatedResultsPath,
        **kwargs,
    )

    # STEP 3. Obtain aggregated esM
    return xrIO.convertDatasetsToEnergySystemModel(aggregated_xr_dataset)

aggregateTemporally

aggregateTemporally(
    numberOfTypicalPeriods=40,
    numberOfTimeStepsPerPeriod=24,
    segmentation=True,
    numberOfSegmentsPerPeriod=12,
    clusterMethod="hierarchical",
    representationMethod="durationRepresentation",
    sortValues=False,
    storeTSAinstance=False,
    rescaleClusterPeriods=False,
    **kwargs,
)

Temporally cluster the time series data of all components considered in the EnergySystemModel instance and then stores the clustered data in the respective components. For this, the time series data is broken down into an ordered sequence of periods (e.g. 365 days) and to each period a typical period (e.g. 7 typical days with 24 hours) is assigned. Moreover, the time steps within the periods can further be clustered to bigger time steps with an irregular duration using the segmentation option. For the clustering itself, the tsam package is used (cf. https://github.com/FZJ-IEK3-VSA/tsam). Additional keyword arguments for the TimeSeriesAggregation instance can be added (facilitated by kwargs). As an example: it might be useful to add extreme periods to the clustered typical periods.

.. note:: The segmentation option can be freely combined with all subclasses. However, an irregular time step length is not meaningful for the minimumDownTime and minimumUpTime in the conversionDynamic module, because the time would be different for each segment.

Default arguments:

:param numberOfTypicalPeriods: states the number of typical periods into which the time series data should be clustered. The number of time steps per period must be an integer multiple of the total number of considered time steps in the energy system.

.. note::
    Please refer to the tsam package documentation of the parameter noTypicalPeriods for more
    information.

|br| * the default value is 7

:type numberOfTypicalPeriods: strictly positive integer

:param numberOfTimeStepsPerPeriod: states the number of time steps per period |br| * the default value is 24 :type numberOfTimeStepsPerPeriod: strictly positive integer

:param segmentation: states whether the typical periods should be further segmented to fewer time steps |br| * the default value is False :type segmentation: boolean

:param numberOfSegmentsPerPeriod: states the number of segments per period |br| * the default value is 24 :type numberOfSegmentsPerPeriod: strictly positive integer

:param clusterMethod: states the method which is used in the tsam package for clustering the time series data. Options are for example 'averaging', 'k_means', 'exact k_medoid' or 'hierarchical'.

.. note::
    Please refer to the tsam package documentation of the parameter clusterMethod for more information.

|br| * the default value is 'hierarchical'

:type clusterMethod: string

:param representationMethod: Chosen representation. If specified, the clusters are represented in the chosen way. Otherwise, each clusterMethod has its own commonly used default representation method.

.. note::
    Please refer to the tsam package documentation of the parameter representationMethod for more information.

|br| * the default Value is "durationRepresentation"

:type representationMethod: string

:param rescaleClusterPeriods: states if the cluster periods shall get rescaled such that their weighted mean value fits the mean value of the original time series

.. note::
    Please refer to the tsam package documentation of the parameter rescaleClusterPeriods for more information.

|br| * the default value is False

:type rescaleClusterPeriods: boolean

:param sortValues: states if the algorithm in the tsam package should use

(a) the sorted duration curves (-> True) or
(b) the original profiles (-> False)

of the time series data within a period for clustering.

.. note::
    Please refer to the tsam package documentation of the parameter sortValues for more information.

|br| * the default value is True

:type sortValues: boolean

:param storeTSAinstance: states if the TimeSeriesAggregation instance created during clustering should be stored in the EnergySystemModel instance. |br| * the default value is False :type storeTSAinstance: boolean

Source code in fine/energySystemModel.py
def aggregateTemporally(
    self,
    numberOfTypicalPeriods=40,
    numberOfTimeStepsPerPeriod=24,
    segmentation=True,
    numberOfSegmentsPerPeriod=12,
    clusterMethod="hierarchical",
    representationMethod="durationRepresentation",
    sortValues=False,
    storeTSAinstance=False,
    rescaleClusterPeriods=False,
    **kwargs,
):
    """Temporally cluster the time series data of all components considered in the EnergySystemModel instance and then
    stores the clustered data in the respective components. For this, the time series data is broken down
    into an ordered sequence of periods (e.g. 365 days) and to each period a typical period (e.g. 7 typical
    days with 24 hours) is assigned. Moreover, the time steps within the periods can further be clustered to bigger
    time steps with an irregular duration using the segmentation option.
    For the clustering itself, the tsam package is used (cf. https://github.com/FZJ-IEK3-VSA/tsam). Additional
    keyword arguments for the TimeSeriesAggregation instance can be added (facilitated by kwargs). As an example: it
    might be useful to add extreme periods to the clustered typical periods.

    .. note::
        The segmentation option can be freely combined with all subclasses. However, an irregular time step length
        is not meaningful for the minimumDownTime and minimumUpTime in the conversionDynamic module, because the time
        would be different for each segment.

    **Default arguments:**

    :param numberOfTypicalPeriods: states the number of typical periods into which the time series data
        should be clustered. The number of time steps per period must be an integer multiple of the total
        number of considered time steps in the energy system.

        .. note::
            Please refer to the tsam package documentation of the parameter noTypicalPeriods for more
            information.

        |br| * the default value is 7
    :type numberOfTypicalPeriods: strictly positive integer

    :param numberOfTimeStepsPerPeriod: states the number of time steps per period
        |br| * the default value is 24
    :type numberOfTimeStepsPerPeriod: strictly positive integer

    :param segmentation: states whether the typical periods should be further segmented to fewer time steps
        |br| * the default value is False
    :type segmentation: boolean

    :param numberOfSegmentsPerPeriod: states the number of segments per period
        |br| * the default value is 24
    :type numberOfSegmentsPerPeriod:  strictly positive integer

    :param clusterMethod: states the method which is used in the tsam package for clustering the time series
        data. Options are for example 'averaging', 'k_means', 'exact k_medoid' or 'hierarchical'.

        .. note::
            Please refer to the tsam package documentation of the parameter clusterMethod for more information.

        |br| * the default value is 'hierarchical'
    :type clusterMethod: string

    :param representationMethod: Chosen representation. If specified, the clusters are represented in the chosen
        way. Otherwise, each clusterMethod has its own commonly used default representation method.

        .. note::
            Please refer to the tsam package documentation of the parameter representationMethod for more information.

        |br| * the default Value is "durationRepresentation"
    :type representationMethod: string

    :param rescaleClusterPeriods: states if the cluster periods shall get rescaled such that their
        weighted mean value fits the mean value of the original time series

        .. note::
            Please refer to the tsam package documentation of the parameter rescaleClusterPeriods for more information.

        |br| * the default value is False
    :type rescaleClusterPeriods: boolean

    :param sortValues: states if the algorithm in the tsam package should use

        (a) the sorted duration curves (-> True) or
        (b) the original profiles (-> False)

        of the time series data within a period for clustering.

        .. note::
            Please refer to the tsam package documentation of the parameter sortValues for more information.

        |br| * the default value is True
    :type sortValues: boolean

    :param storeTSAinstance: states if the TimeSeriesAggregation instance created during clustering should be
        stored in the EnergySystemModel instance.
        |br| * the default value is False
    :type storeTSAinstance: boolean
    """
    # Check input arguments which have to fit the temporal representation of the energy system
    utils.checkClusteringInput(
        numberOfTypicalPeriods, numberOfTimeStepsPerPeriod, len(self.totalTimeSteps)
    )
    if segmentation:
        if numberOfSegmentsPerPeriod > numberOfTimeStepsPerPeriod:
            if self.verboseLogLevel < 2:
                warnings.warn(
                    "The chosen number of segments per period exceeds the number of time steps per"
                    "period. The number of segments per period is set to the number of time steps per "
                    "period."
                )
            numberOfSegmentsPerPeriod = numberOfTimeStepsPerPeriod
    hoursPerPeriod = int(numberOfTimeStepsPerPeriod * self.hoursPerTimeStep)

    timeStart = time.time()
    if segmentation:
        utils.output(
            "\nClustering time series data with "
            + str(numberOfTypicalPeriods)
            + " typical periods and "
            + str(numberOfTimeStepsPerPeriod)
            + " time steps per period \nfurther clustered to "
            + str(numberOfSegmentsPerPeriod)
            + " segments per period...",
            self.verboseLogLevel,
            0,
        )
    else:
        utils.output(
            "\nClustering time series data with "
            + str(numberOfTypicalPeriods)
            + " typical periods and "
            + str(numberOfTimeStepsPerPeriod)
            + " time steps per period...",
            self.verboseLogLevel,
            0,
        )

    # Format data to fit the input requirements of the tsam package:
    # (a) append the time series data from all components stored in all initialized modeling classes to a pandas
    #     DataFrame with unique column names
    # (b) thereby collect the weights which should be considered for each time series as well in a dictionary

    #############################################################################################################
    # adjusted for perfect foresight approach
    # periodsOrder and Occurrences now dictionaries
    self.periodsOrder = {}
    self.periodOccurrences = {}
    self.timeStepsPerSegment = {}
    self.hoursPerSegment = {}
    self.segmentStartTime = {}

    # clustering of the time series data per investment period individually
    for ip in self.investmentPeriods:
        timeSeriesData, weightDict, zero_data_cols = (
            self.createTimeSeriesDataForAggregation(ip)
        )

        if segmentation:
            clusterClass = TimeSeriesAggregation(
                timeSeries=timeSeriesData,
                noTypicalPeriods=numberOfTypicalPeriods,
                segmentation=segmentation,
                noSegments=numberOfSegmentsPerPeriod,
                hoursPerPeriod=hoursPerPeriod,
                clusterMethod=clusterMethod,
                sortValues=sortValues,
                weightDict=weightDict,
                rescaleClusterPeriods=rescaleClusterPeriods,
                representationMethod=representationMethod,
                **kwargs,
            )
            # Convert the clustered data to a pandas DataFrame with the first index as typical period number and the
            # second index as segment number per typical period.
            data = pd.DataFrame.from_dict(
                clusterClass.clusterPeriodDict
            ).reset_index(level=2, drop=True)
            # Get the length of each segment in each typical period with the first index as typical period number and
            # the second index as segment number per typical period.
            timeStepsPerSegment = pd.DataFrame.from_dict(
                clusterClass.segmentDurationDict
            )["Segment Duration"]
        else:
            clusterClass = TimeSeriesAggregation(
                timeSeries=timeSeriesData,
                noTypicalPeriods=numberOfTypicalPeriods,
                hoursPerPeriod=hoursPerPeriod,
                clusterMethod=clusterMethod,
                sortValues=sortValues,
                weightDict=weightDict,
                rescaleClusterPeriods=rescaleClusterPeriods,
                representationMethod=representationMethod,
                **kwargs,
            )
            # Convert the clustered data to a pandas DataFrame with the first index as typical period number and the
            # second index as time step number per typical period.
            data = pd.DataFrame.from_dict(clusterClass.clusterPeriodDict)

        # add zeros data back to data
        data[zero_data_cols] = 0.0

        # Store the respective clustered time series data in the associated components
        for mdlName, mdl in self.componentModelingDict.items():
            for compName, comp in mdl.componentsDict.items():
                comp.setAggregatedTimeSeriesData(data, ip)

        # Store time series aggregation parameters in class instance
        if storeTSAinstance:
            self.tsaInstance = clusterClass
        self.typicalPeriods = clusterClass.clusterPeriodIdx
        self.timeStepsPerPeriod = list(range(numberOfTimeStepsPerPeriod))
        self.segmentation = segmentation
        if segmentation:
            self.segmentsPerPeriod = list(range(numberOfSegmentsPerPeriod))
            # ip-dependent
            self.timeStepsPerSegment[ip] = timeStepsPerSegment
            self.hoursPerSegment[ip] = (
                self.hoursPerTimeStep * self.timeStepsPerSegment[ip]
            )  # ip-dependent
            # Define start time hour of each segment in each typical period
            segmentStartTime = self.hoursPerSegment[ip].groupby(level=0).cumsum()
            segmentStartTime.index = segmentStartTime.index.set_levels(
                segmentStartTime.index.levels[1] + 1, level=1
            )
            lvl0, lvl1 = segmentStartTime.index.levels
            segmentStartTime = segmentStartTime.reindex(
                pd.MultiIndex.from_product([lvl0, [0, *lvl1]])
            )
            segmentStartTime[segmentStartTime.index.get_level_values(1) == 0] = 0
            self.segmentStartTime[ip] = segmentStartTime  # ip-dependent

        self.periodsOrder[ip] = clusterClass.clusterOrder
        self.periodOccurrences[ip] = [
            (self.periodsOrder[ip] == tp).sum() for tp in self.typicalPeriods
        ]

    self.periods = list(
        range(int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)))
    )

    self.interPeriodTimeSteps = list(
        range(int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)) + 1)
    )

    self.numberOfInterPeriodTimeSteps = int(
        len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)
    )

    # Set cluster flag to true (used to ensure consistently clustered time series data)
    self.isTimeSeriesDataClustered = True
    timeEnd = time.time()
    if storeTSAinstance:
        clusterClass.tsaBuildTime = timeEnd - timeStart
        self.tsaInstance = clusterClass
    utils.output(
        "\t\t(%.4f" % (timeEnd - timeStart) + " sec)\n", self.verboseLogLevel, 0
    )

createTimeSeriesDataForAggregation

createTimeSeriesDataForAggregation(
    ip,
) -> tuple[DataFrame, dict, Index]

Create and return the time series data, weights, and zero-data columns for aggregation.

Returns: tuple[pd.DataFrame, dict, pd.Index]: Time series data, weight dictionary, and columns containing only zero values.

Source code in fine/energySystemModel.py
def createTimeSeriesDataForAggregation(
    self,
    ip,
) -> tuple[pd.DataFrame, dict, pd.Index]:
    """Create and return the time series data, weights, and zero-data columns for aggregation.

    Returns:
        tuple[pd.DataFrame, dict, pd.Index]: Time series data, weight dictionary,
        and columns containing only zero values.

    """
    timeSeriesData, weightDict = [], {}
    for mdlName, mdl in self.componentModelingDict.items():
        for compName, comp in mdl.componentsDict.items():
            (
                compTimeSeriesData,
                compWeightDict,
            ) = comp.getDataForTimeSeriesAggregation(ip)
            if compTimeSeriesData is not None:
                (
                    timeSeriesData.append(compTimeSeriesData),
                    weightDict.update(compWeightDict),
                )
    timeSeriesData = pd.concat(timeSeriesData, axis=1)
    # Note: Sets index for the time series data. The index is of no further relevance in the energy system model.
    timeSeriesData.index = pd.date_range(
        "2050-01-01 00:30:00",
        periods=len(self.totalTimeSteps),
        freq=(str(self.hoursPerTimeStep) + "h"),
        tz="Europe/Berlin",
    )

    # Cluster data with tsam package (the reindex call is here for reproducibility of TimeSeriesAggregation
    # call) depending on whether segmentation is activated or not
    timeSeriesData = timeSeriesData.reindex(sorted(timeSeriesData.columns), axis=1)
    # find data with only zeros
    zero_data_cols = timeSeriesData.columns[(timeSeriesData == 0).all()]
    # drop columns with only zeros
    timeSeriesData = timeSeriesData.drop(columns=zero_data_cols)
    weightDict = {k: v for k, v in weightDict.items() if k not in zero_data_cols}

    return timeSeriesData, weightDict, zero_data_cols

declareBalanceLimitConstraint

declareBalanceLimitConstraint(pyM, timeSeriesAggregation)

Declare balance limit constraint.

Balance limit constraint can limit the exchange of commodities within the model or over the model region boundaries. See the documentation of the parameters for further explanation. In general the following equation applies:

E_source - E_sink + E_exchange,in - E_exchange,out <= E_lim (LowerBound=False)
E_source - E_sink + E_exchange,in - E_exchange,out >= E_lim (LowerBound=True)

:param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables, constraints and objective required for the optimization set up and solving. :type pyM: pyomo ConcreteModel

:param timeSeriesAggregation: states if the optimization of the energy system model should be done with

(a) the full time series (False) or
(b) clustered time series data (True).

|br| * the default value is False

:type timeSeriesAggregation: boolean

Source code in fine/energySystemModel.py
def declareBalanceLimitConstraint(self, pyM, timeSeriesAggregation):
    """Declare balance limit constraint.

    Balance limit constraint can limit the exchange of commodities within the model or over the model region
    boundaries. See the documentation of the parameters for further explanation. In general the following equation
    applies:

        E_source - E_sink + E_exchange,in - E_exchange,out <= E_lim (LowerBound=False)
        E_source - E_sink + E_exchange,in - E_exchange,out >= E_lim (LowerBound=True)

    :param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables,
        constraints and objective required for the optimization set up and solving.
    :type pyM: pyomo ConcreteModel

    :param timeSeriesAggregation: states if the optimization of the energy system model should be done with

        (a) the full time series (False) or
        (b) clustered time series data (True).

        |br| * the default value is False
    :type timeSeriesAggregation: boolean
    """
    # 1) Yearly Balance Limit
    if self.processedBalanceLimit is not None:
        # Get Components per balance limit
        componentsOfBalanceLimit = {}
        for mdl_type, mdl in self.componentModelingDict.items():
            if mdl_type in ("SourceSinkModel", "TransmissionModel"):
                for compName, comp in mdl.componentsDict.items():
                    if comp.balanceLimitID is not None:
                        componentsOfBalanceLimit.setdefault(
                            comp.balanceLimitID, []
                        ).append(compName)

        yearlyBalanceLimitDict = {}

        # iterate over balance limit to define either minimal, maximal or both balance limits per balanceLimitID
        for ip in self.investmentPeriods:
            if self.processedBalanceLimit[ip] is not None:
                for balanceLimitID, data in self.processedBalanceLimit[
                    ip
                ].iterrows():
                    # check for regional constraints
                    for loc in self.locations:
                        if data[loc] is not None:
                            yearlyBalanceLimitDict.setdefault(
                                (
                                    balanceLimitID,
                                    loc,
                                    ip,
                                    data["lowerBound"],
                                    data[loc],
                                ),
                                componentsOfBalanceLimit[balanceLimitID],
                            )
                    # check for total constraints over all regions
                    if data["Total"] is not None:
                        yearlyBalanceLimitDict.setdefault(
                            (
                                balanceLimitID,
                                "Total",
                                ip,
                                data["lowerBound"],
                                data["Total"],
                            ),
                            componentsOfBalanceLimit[balanceLimitID],
                        )

        setattr(pyM, "yearlyBalanceLimitDict", yearlyBalanceLimitDict)

        def yearlyBalanceLimitConstraint(pyM, ID, loc, ip, lowerBound, value):
            # yearly restriction
            balanceSum = sum(
                mdl.getBalanceLimitContribution(
                    esM=self,
                    pyM=pyM,
                    ID=ID,
                    ip=ip,
                    timeSeriesAggregation=timeSeriesAggregation,
                    loc=loc,
                    componentNames=yearlyBalanceLimitDict[
                        (ID, loc, ip, lowerBound, value)
                    ],
                )
                for mdl_type, mdl in self.componentModelingDict.items()
                if (mdl_type in ("SourceSinkModel", "TransmissionModel"))
            )
            # Check whether we want to consider an upper or lower bound.
            if lowerBound == 0:
                return balanceSum <= value
            return balanceSum >= value

        pyM.yearlyBalanceLimitConstraint = pyomo.Constraint(
            pyM.yearlyBalanceLimitDict.keys(),
            rule=yearlyBalanceLimitConstraint,
        )

    # 2) Pathway Balance Limit
    if self.processedPathwayBalanceLimit is not None:
        # Get Components per balance limit
        componentsOfBalanceLimit = {}
        for mdl_type, mdl in self.componentModelingDict.items():
            if mdl_type in ("SourceSinkModel", "TransmissionModel"):
                for compName, comp in mdl.componentsDict.items():
                    if comp.pathwayBalanceLimitID is not None:
                        componentsOfBalanceLimit.setdefault(
                            comp.pathwayBalanceLimitID, []
                        ).append(compName)

        pathwayBalanceLimitDict = {}
        # Check for node specific and total limits:
        # iterate over balance limit to define either minimal, maximal or both balance limits per balanceLimitID
        for balanceLimitID, data in self.processedPathwayBalanceLimit.iterrows():
            # check for regional constraints
            for loc in self.locations:
                if data[loc] is not None:
                    pathwayBalanceLimitDict.setdefault(
                        (
                            balanceLimitID,
                            loc,
                            data["lowerBound"],
                            data[loc],
                        ),
                        componentsOfBalanceLimit[balanceLimitID],
                    )
            # check for total constraints over all regions
            if data["Total"] is not None:
                pathwayBalanceLimitDict.setdefault(
                    (
                        balanceLimitID,
                        "Total",
                        data["lowerBound"],
                        data["Total"],
                    ),
                    componentsOfBalanceLimit[balanceLimitID],
                )

        setattr(pyM, "pathwayBalanceLimitDict", pathwayBalanceLimitDict)

        def pathwayBalanceLimitConstraint(pyM, ID, loc, lowerBound, value):
            # pathway restriction
            balanceSum = sum(
                mdl.getBalanceLimitContribution(
                    esM=self,
                    pyM=pyM,
                    ID=ID,
                    ip=_ip,
                    timeSeriesAggregation=timeSeriesAggregation,
                    loc=loc,
                    componentNames=pathwayBalanceLimitDict[
                        (ID, loc, lowerBound, value)
                    ],
                )
                for mdl_type, mdl in self.componentModelingDict.items()
                for _ip in self.investmentPeriods
                if (mdl_type in ("SourceSinkModel", "TransmissionModel"))
            )
            value = self.processedPathwayBalanceLimit.loc[ID, loc]
            lowerBound = self.processedPathwayBalanceLimit.loc[ID, "lowerBound"]
            temporalScope = self.investmentPeriodInterval
            # Check whether we want to consider an upper or lower bound.
            if lowerBound == 0:
                return balanceSum * temporalScope <= value
            return balanceSum * temporalScope >= value

        pyM.pathwayBalanceLimitConstraint = pyomo.Constraint(
            pyM.pathwayBalanceLimitDict.keys(),
            rule=pathwayBalanceLimitConstraint,
        )

declareCommodityBalanceConstraints

declareCommodityBalanceConstraints(pyM)

Declare commodity balance constraints (one balance constraint for each commodity, location and time step).

.. math::

\\underset{\\text{comp} \\in \\mathcal{C}^{comm}_{loc}}{\\sum} \\text{C}^{comp,comm}_{loc,ip,p,t} = 0

:param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables, constraints and objective required for the optimization set up and solving. :type pyM: pyomo ConcreteModel

Source code in fine/energySystemModel.py
def declareCommodityBalanceConstraints(self, pyM):
    r"""Declare commodity balance constraints (one balance constraint for each commodity, location and time step).

    .. math::

        \\underset{\\text{comp} \\in \\mathcal{C}^{comm}_{loc}}{\\sum} \\text{C}^{comp,comm}_{loc,ip,p,t} = 0

    :param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables,
        constraints and objective required for the optimization set up and solving.
    :type pyM: pyomo ConcreteModel
    """
    utils.output("Declaring commodity balances...", self.verboseLogLevel, 0)

    # Declare and initialize a set that states for which location and commodity the commodity balance constraints
    # are non-trivial (i.e. not 0 == 0; trivial constraints raise errors in pyomo).
    def initLocationCommoditySet(pyM):
        return (
            (loc, commod)
            for loc in self.locations
            for commod in self.commodities
            if any(
                [
                    mdl.hasOpVariablesForLocationCommodity(self, loc, commod)
                    for mdl in self.componentModelingDict.values()
                ]
            )
        )

    pyM.locationCommoditySet = pyomo.Set(
        dimen=2, initialize=initLocationCommoditySet
    )

    # Declare and initialize commodity balance constraints by checking for each location and commodity in the
    # locationCommoditySet and for each period and time step within the period if the commodity source and sink
    # terms add up to zero. For this, get the contribution to commodity balance from each modeling class.
    def commodityBalanceConstraint(pyM, loc, commod, ip, p, t):
        return (
            sum(
                mdl.getCommodityBalanceContribution(pyM, commod, loc, ip, p, t)
                for mdl in self.componentModelingDict.values()
            )
            == 0
        )

    pyM.commodityBalanceConstraint = pyomo.Constraint(
        pyM.locationCommoditySet, pyM.timeSet, rule=commodityBalanceConstraint
    )

declareComponentLinkedQuantityConstraints

declareComponentLinkedQuantityConstraints(pyM)

Declare linked component quantity constraint, e.g. if an engine (E-Motor) is built also a storage (Battery) and a vehicle body (e.g. BEV Car) needs to be built. Not the capacity of the components, but the number of the components is linked.

:param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables, constraints and objective required for the optimization set up and solving. :type pyM: pyomo ConcreteModel

Source code in fine/energySystemModel.py
def declareComponentLinkedQuantityConstraints(self, pyM):
    """Declare linked component quantity constraint, e.g. if an engine (E-Motor) is built also a storage (Battery)
    and a vehicle body (e.g. BEV Car) needs to be built. Not the capacity of the components, but the number of
    the components is linked.

    :param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables,
        constraints and objective required for the optimization set up and solving.
    :type pyM: pyomo ConcreteModel
    """
    utils.output(
        "Declaring linked component quantity constraint...", self.verboseLogLevel, 0
    )

    compDict = {}
    for mdl in self.componentModelingDict.values():
        for compName, comp in mdl.componentsDict.items():
            if comp.linkedQuantityID is not None:
                [
                    compDict.setdefault((comp.linkedQuantityID, loc), []).append(
                        compName
                    )
                    for loc in comp.processedLocationalEligibility.index
                ]
    pyM.linkedQuantityDict = compDict

    def linkedQuantityConstraint(pyM, ID, loc, compName1, compName2, ip):
        abbrvName1 = self.componentModelingDict[
            self.componentNames[compName1]
        ].abbrvName
        abbrvName2 = self.componentModelingDict[
            self.componentNames[compName2]
        ].abbrvName
        commisVar1 = getattr(pyM, "commis_" + abbrvName1)
        commisVar2 = getattr(pyM, "commis_" + abbrvName2)
        capPPU1 = (
            self.componentModelingDict[self.componentNames[compName1]]
            .componentsDict[compName1]
            .processedCapacityPerPlantUnit[ip]
        )
        capPPU2 = (
            self.componentModelingDict[self.componentNames[compName2]]
            .componentsDict[compName2]
            .processedCapacityPerPlantUnit[ip]
        )
        return (
            commisVar1[loc, compName1, ip] / capPPU1
            == commisVar2[loc, compName2, ip] / capPPU2
        )

    for i, j in pyM.linkedQuantityDict.keys():
        linkedQuantityList = []
        linkedQuantityList.append((i, j))
        setattr(
            pyM,
            "ConstraintLinkedQuantity_" + str(i) + "_" + str(j),
            pyomo.Constraint(
                linkedQuantityList,
                pyM.linkedQuantityDict[i, j],
                pyM.linkedQuantityDict[i, j],
                pyM.investSet,
                rule=linkedQuantityConstraint,
            ),
        )

declareObjective

declareObjective(pyM)

Declare the objective function by obtaining the contributions to the objective function from all modeling classes. Currently, the only objective function which can be selected is the sum of the net present value of all components.

.. math:: z^* = \min \underset{comp \in \mathcal{C}}{\sum} \ \underset{loc \in \mathcal{L}^{comp}}{\sum} \left( NPV_{loc}^{comp,cap} + NPV_{loc}^{comp,bin} + NPV_{loc}^{comp,op} \right)

Objective Function detailed:

.. math:: z^* = \min \underset{comp \in \mathcal{C}}{\sum} \ \underset{loc \in \mathcal{L}^{comp}}{\sum} \ \underset{ip \in \mathcal{IP}}{\sum} \text{design}^{comp}{loc,ip} + \text{design}^{comp}} + \text{op}^{comp}_{loc,ip

Contribution of design variable to the objective function

.. math:: design^{comp}{loc,ip} = \sum\limits \text{F}^{comp,bin}}}^{ip{loc,year} \cdot \left( \frac{\text{investPerCap}^{comp}}}{\text{CCF}^{comp{loc,year}} + \text{opexPerCap}^{comp}} \right) \cdot commis^{comp{loc,year} \cdot \text{APVF}^{comp}} \cdot \text{discFactor}^{comp}_{loc,ip

Contribution of binary design variables to the objective function

.. math:: design^{comp}{bin\ loc,ip} = \sum\limits \text{F}^{comp,bin}}}^{ip{loc,year} \cdot \left( \frac{\text{investIfBuilt}^{comp}}} {\text{CCF}^{comp{loc,year}} + \text{opexIfBuilt}^{comp}} \right) \cdot bin^{comp{loc,year} \cdot \text{APVF}^{comp}} \cdot \text{discFactor}^{comp}_{loc,ip

Contribution of operation variables to the objective function

.. math:: op^{comp}{loc,ip} = \underset{(p,t) \in \mathcal{P} \times \mathcal{T}}{\sum} \ \underset{\text{opType} \in \mathcal{O}^{comp}}{\sum} \text{factorPerOp}^{comp,opType}} \cdot op^{comp,opType{loc,ip,p,t} \cdot \frac{\text{freq(p)}}{\tau^{years}} \cdot \text{APVF}^{comp}} \cdot \text{discFactor}^{comp}_{loc,ip

With the annuity present value factor (Rentenbarwertfaktor):

.. math:: APVF^{comp}{loc} = \frac{(1 + \text{interestRate}^{comp}})^{interval} - 1}{\text{interestRate}^{comp{loc} \cdot (1 + \text{interestRate}^{comp} != 0 \ else \ 1})^{interval}} \ if \text{interestRate}^{comp}_{loc

and the discount factor.

.. math:: \text{discFactor}^{comp}{loc,ip} = \frac{1+\text{interestRate}^{comp})^{ip \cdot \text{interval}}}}}{(1+\text{interestRate}^{comp}_{loc

:param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables, constraints and objective required for the optimization set up and solving. :type pyM: pyomo ConcreteModel

Source code in fine/energySystemModel.py
def declareObjective(self, pyM):
    r"""Declare the objective function by obtaining the contributions to the objective function from all modeling
    classes. Currently, the only objective function which can be selected is the sum of the net present value of all
    components.

    .. math::
        z^* = \\min \\underset{comp \\in \\mathcal{C}}{\\sum} \\ \\underset{loc \\in \\mathcal{L}^{comp}}{\\sum}
        \\left( NPV_{loc}^{comp,cap}  +  NPV_{loc}^{comp,bin} + NPV_{loc}^{comp,op} \\right)

    Objective Function detailed:

    .. math::
        z^* = \\min \\underset{comp \\in \\mathcal{C}}{\\sum}  \\ \\underset{loc \\in \\mathcal{L}^{comp}}{\\sum}  \\ \\underset{ip \\in \\mathcal{IP}}{\\sum}  \\text{design}^{comp}_{loc,ip} + \\text{design}^{comp}_{bin, \\ loc,ip} + \\text{op}^{comp}_{loc,ip}

    Contribution of design variable to the objective function

    .. math::
            design^{comp}_{loc,ip} =
            \\sum\\limits_{year=ip-\\text{ipEconomicLifetime}}^{ip}
            \\text{F}^{comp,bin}_{loc,year}
            \\cdot \\left(  \\frac{\\text{investPerCap}^{comp}_{loc,year}}{\\text{CCF}^{comp}_{loc,year}}
            + \\text{opexPerCap}^{comp}_{loc,year} \\right) \\cdot commis^{comp}_{loc,year}
            \\cdot  \\text{APVF}^{comp}_{loc} \\cdot \\text{discFactor}^{comp}_{loc,ip}

    Contribution of binary design variables to the objective function

    .. math::
            design^{comp}_{bin\\ loc,ip} =
            \\sum\\limits_{year=ip-\\text{ipEconomicLifetime}}^{ip}
            \\text{F}^{comp,bin}_{loc,year} \\cdot \\left( \\frac{\\text{investIfBuilt}^{comp}_{loc,year}}	{\\text{CCF}^{comp}_{loc,year}}
            + \\text{opexIfBuilt}^{comp}_{loc,year} \\right)  \\cdot  bin^{comp}_{loc,year}
            \\cdot  \\text{APVF}^{comp}_{loc} \\cdot \\text{discFactor}^{comp}_{loc,ip}

    Contribution of operation variables to the objective function

    .. math::
            op^{comp}_{loc,ip} =
            \\underset{(p,t) \\in \\mathcal{P} \\times \\mathcal{T}}{\\sum} \\ \\underset{\\text{opType} \\in \\mathcal{O}^{comp}}{\\sum}
            \\text{factorPerOp}^{comp,opType}_{loc,ip} \\cdot op^{comp,opType}_{loc,ip,p,t} \\cdot  \\frac{\\text{freq(p)}}{\\tau^{years}}
            \\cdot  \\text{APVF}^{comp}_{loc} \\cdot \\text{discFactor}^{comp}_{loc,ip}

    With the annuity present value factor (Rentenbarwertfaktor):

    .. math::
        APVF^{comp}_{loc} = \\frac{(1 + \\text{interestRate}^{comp}_{loc})^{interval} - 1}{\\text{interestRate}^{comp}_{loc} \\cdot
        (1 + \\text{interestRate}^{comp}_{loc})^{interval}} \\ if \\text{interestRate}^{comp}_{loc} != 0 \\  else \\  1

    and the discount factor.

    .. math::
        \\text{discFactor}^{comp}_{loc,ip} = \\frac{1+\\text{interestRate}^{comp}_{loc}}{(1+\\text{interestRate}^{comp}_{loc})^{ip \\cdot
        \\text{interval}}}

    :param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables,
        constraints and objective required for the optimization set up and solving.
    :type pyM: pyomo ConcreteModel
    """
    utils.output("Declaring objective function...", self.verboseLogLevel, 0)

    def objective(pyM):
        NPV = sum(
            mdl.getObjectiveFunctionContribution(self, pyM)
            for mdl in self.componentModelingDict.values()
        )
        if hasattr(self, "pwlcfModel"):
            NPV += self.pwlcfModel.getObjectiveFunctionContribution(self, pyM)

        return NPV

    pyM.Obj = pyomo.Objective(rule=objective)

declareOptimizationProblem

declareOptimizationProblem(
    timeSeriesAggregation=False,
    relaxIsBuiltBinary=False,
    relevanceThreshold=None,
)

Declare the optimization problem belonging to the specified energy system for which a pyomo concrete model instance is built and filled with.

  • basic time sets,
  • sets, variables and constraints contributed by the component modeling classes,
  • basic, component overreaching constraints, and
  • an objective function.

Default arguments:

:param timeSeriesAggregation: states if the optimization of the energy system model should be done with

(a) the full time series (False) or
(b) clustered time series data (True).

|br| * the default value is False

:type timeSeriesAggregation: boolean

:param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower bound of the problem. |br| * the default value is False :type declaresOptimizationProblem: boolean

:param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold. |br| * the default value is None :type relevanceThreshold: float (>=0) or None

Source code in fine/energySystemModel.py
def declareOptimizationProblem(
    self,
    timeSeriesAggregation=False,
    relaxIsBuiltBinary=False,
    relevanceThreshold=None,
):
    """Declare the optimization problem belonging to the specified energy system for which a pyomo concrete model
    instance is built and filled with.

    * basic time sets,
    * sets, variables and constraints contributed by the component modeling classes,
    * basic, component overreaching constraints, and
    * an objective function.

    **Default arguments:**

    :param timeSeriesAggregation: states if the optimization of the energy system model should be done with

        (a) the full time series (False) or
        (b) clustered time series data (True).

        |br| * the default value is False
    :type timeSeriesAggregation: boolean

    :param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower
        bound of the problem.
        |br| * the default value is False
    :type declaresOptimizationProblem: boolean

    :param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold.
        |br| * the default value is None
    :type relevanceThreshold: float (>=0) or None
    """
    # Get starting time of the optimization to, later on, obtain the total run time of the optimize function call
    timeStart = time.time()

    # Check correctness of inputs
    utils.checkDeclareOptimizationProblemInput(
        timeSeriesAggregation, self.isTimeSeriesDataClustered
    )

    # Set segmentation value if time series aggregation is True
    if timeSeriesAggregation:
        segmentation = self.segmentation
    else:
        segmentation = False

    ################################################################################################################
    #                           Initialize mathematical model (ConcreteModel) instance                             #
    ################################################################################################################

    # Initialize a pyomo ConcreteModel which will be used to store the mathematical formulation of the model.
    # The ConcreteModel instance is stored in the EnergySystemModel instance, which makes it available for
    # post-processing or debugging. A pyomo Suffix with the name dual is declared to make dual values associated
    # to the model's constraints available after optimization.
    self.pyM = pyomo.ConcreteModel()
    pyM = self.pyM
    pyM.dual = pyomo.Suffix(direction=pyomo.Suffix.IMPORT)

    # Set time sets for the model instance
    self.declareTimeSets(pyM, timeSeriesAggregation, segmentation)

    ################################################################################################################
    #                         Declare component specific sets, variables and constraints                           #
    ################################################################################################################

    for key, mdl in self.componentModelingDict.items():
        _t = time.time()
        utils.output(
            "Declaring sets, variables and constraints for " + key,
            self.verboseLogLevel,
            0,
        )
        (
            utils.output("\tdeclaring sets... ", self.verboseLogLevel, 0),
            mdl.declareSets(self, pyM),
        )
        (
            utils.output("\tdeclaring variables... ", self.verboseLogLevel, 0),
            mdl.declareVariables(self, pyM, relaxIsBuiltBinary, relevanceThreshold),
        )
        (
            utils.output("\tdeclaring constraints... ", self.verboseLogLevel, 0),
            mdl.declareComponentConstraints(self, pyM),
        )
        utils.output(
            "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
        )

    if hasattr(self, "pwlcfModel"):
        utils.output(
            "Declaring sets, variables and constraints for PWLCF components",
            self.verboseLogLevel,
            0,
        )
        self.pwlcfModel.declareSets(self, pyM)
        self.pwlcfModel.declareVariables(self, pyM)
        self.pwlcfModel.declareComponentConstraints(self, pyM)
        utils.output(
            "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
        )

    ################################################################################################################
    #                              Declare cross-componential sets and constraints                                 #
    ################################################################################################################

    # Declare constraints for enforcing shared capacities
    _t = time.time()
    self.declareSharedPotentialConstraints(pyM)
    utils.output(
        "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
    )

    # Declare constraints for linked quantities
    _t = time.time()
    self.declareComponentLinkedQuantityConstraints(pyM)
    utils.output(
        "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
    )

    # Declare commodity balance constraints (one balance constraint for each commodity, location and time step)
    _t = time.time()
    self.declareCommodityBalanceConstraints(pyM)
    utils.output(
        "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
    )

    # Declare constraint for balanceLimit
    _t = time.time()
    self.declareBalanceLimitConstraint(pyM, timeSeriesAggregation)
    utils.output(
        "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
    )

    ################################################################################################################
    #                                         Declare objective function                                           #
    ################################################################################################################

    # Declare objective function by obtaining the contributions to the objective function from all modeling classes
    _t = time.time()
    self.declareObjective(pyM)
    utils.output(
        "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
    )

    # Store the build time of the optimize function call in the EnergySystemModel instance
    self.solverSpecs["buildtime"] = time.time() - timeStart

declareSharedPotentialConstraints

declareSharedPotentialConstraints(pyM)

Declare shared potential constraints, e.g. if a maximum potential of salt caverns has to be shared by salt cavern storing methane and salt caverns storing hydrogen.

.. math::

\\underset{\\text{comp} \\in \\mathcal{C}^{ID}}{\\sum} \\text{cap}^{comp}_{loc} / \\text{capMax}^{comp}_{loc} \\leq 1

:param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables, constraints and objective required for the optimization set up and solving. :type pyM: pyomo ConcreteModel

Source code in fine/energySystemModel.py
def declareSharedPotentialConstraints(self, pyM):
    r"""Declare shared potential constraints, e.g. if a maximum potential of salt caverns has to be shared by
    salt cavern storing methane and salt caverns storing hydrogen.

    .. math::

        \\underset{\\text{comp} \\in \\mathcal{C}^{ID}}{\\sum} \\text{cap}^{comp}_{loc} / \\text{capMax}^{comp}_{loc} \\leq 1


    :param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables,
        constraints and objective required for the optimization set up and solving.
    :type pyM: pyomo ConcreteModel

    """
    utils.output(
        "Declaring shared potential constraint...", self.verboseLogLevel, 0
    )

    # Define and initialize constraints for each instance and location where components have to share an available
    # potential. Sum up the relative contributions to the shared potential and ensure that the total share is
    # <= 100%. For this, get the contributions to the shared potential for the corresponding ID and
    # location from each modeling class.
    def sharedPotentialConstraint(pyM, ID, loc, ip):
        return (
            sum(
                mdl.getSharedPotentialContribution(pyM, ID, loc, ip)
                for mdl in self.componentModelingDict.values()
            )
            <= 1
        )

    pyM.ConstraintSharedPotentials = pyomo.Constraint(
        self.sharedPotentialDict.keys(), rule=sharedPotentialConstraint
    )

declareTimeSets

declareTimeSets(pyM, timeSeriesAggregation, segmentation)

Set and initialize basic time parameters and sets.

:param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables, constraints and objective required for the optimization set up and solving. :type pyM: pyomo ConcreteModel

:param timeSeriesAggregation: states if the optimization of the energy system model should be done with

(a) the full time series (False) or
(b) clustered time series data (True).

|br| * the default value is False

:type timeSeriesAggregation: boolean

:param segmentation: states if the optimization of the energy system model based on clustered time series data should be done with

(a) aggregated typical periods with the original time step length (False) or
(b) aggregated typical periods with further segmented time steps (True).

|br| * the default value is False

:type segmentation: boolean

Source code in fine/energySystemModel.py
def declareTimeSets(self, pyM, timeSeriesAggregation, segmentation):
    """Set and initialize basic time parameters and sets.

    :param pyM: a pyomo ConcreteModel instance which contains parameters, sets, variables,
        constraints and objective required for the optimization set up and solving.
    :type pyM: pyomo ConcreteModel

    :param timeSeriesAggregation: states if the optimization of the energy system model should be done with

        (a) the full time series (False) or
        (b) clustered time series data (True).

        |br| * the default value is False
    :type timeSeriesAggregation: boolean

    :param segmentation: states if the optimization of the energy system model based on clustered time series data
        should be done with

        (a) aggregated typical periods with the original time step length (False) or
        (b) aggregated typical periods with further segmented time steps (True).

        |br| * the default value is False
    :type segmentation: boolean
    """
    # Store the information if aggregated time series data is considered for modeling the energy system in the pyomo
    # model instance and set the time series which is again considered for modeling in all components accordingly
    pyM.hasTSA = timeSeriesAggregation
    pyM.hasSegmentation = segmentation
    for mdl in self.componentModelingDict.values():
        if mdl.abbrvName != "pwlcf":
            for comp in mdl.componentsDict.values():
                comp.setTimeSeriesData(pyM.hasTSA)

    # Set the time set and the inter time steps set. The time set is a set of tuples. A tuple consists of two
    # entries, the first one indicates an index of a period and the second one indicates a time step inside that
    # period. If time series aggregation is not considered, only one period (period 0) exists and the time steps
    # range from 0 up until the specified number of total time steps - 1. Otherwise, the time set is initialized for
    # each typical period (0 ... numberOfTypicalPeriods-1) and the number of time steps per period (0 ...
    # numberOfTimeStepsPerPeriod-1).
    # The inter time steps set is a set of tuples as well, which again consist of two values. The first value again
    # indicates the period, however, the second one now refers to a point in time right before or after a time step
    # (or between two time steps). Hence, the second value reaches values from (0 ... numberOfTimeStepsPerPeriod).
    if not pyM.hasTSA:
        # Reset timeStepsPerPeriod in case it was overwritten by the clustering function
        self.timeStepsPerPeriod = self.totalTimeSteps
        self.interPeriodTimeSteps = list(
            range(int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)) + 1)
        )
        self.periods = [0]
        self.periodsOrder = {}
        self.periodOccurrences = {}
        # fill dictionaries with zeros or ones, if no TSA
        for ip in self.investmentPeriods:
            self.periodsOrder[ip] = [0]
            self.periodOccurrences[ip] = [1]

        # Define sets
        def initTimeSet(pyM):
            return (
                (ip, p, t)
                for ip in self.investmentPeriods
                for p in self.periods
                for t in self.timeStepsPerPeriod
            )

        def initInterTimeStepsSet(pyM):
            return (
                (p, t)
                for p in self.periods
                for t in range(len(self.timeStepsPerPeriod) + 1)
            )

        def initIntraYearTimeSet(pyM):
            return ((p, t) for p in self.periods for t in self.timeStepsPerPeriod)

        def initInvestPeriodInterPeriodSet(pyM):
            return (
                (t_inter)
                for t_inter in range(
                    int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)) + 1
                )
            )

    elif not pyM.hasSegmentation:
        utils.output(
            "Time series aggregation specifications:\n"
            "Number of typical periods:"
            + str(len(self.typicalPeriods))
            + ", number of time steps per period:"
            + str(len(self.timeStepsPerPeriod))
            + "\n",
            self.verboseLogLevel,
            0,
        )

        # Define sets
        # To-Do: Add explanation perfect foresight
        def initTimeSet(pyM):
            return (
                (ip, p, t)
                for ip in self.investmentPeriods
                for p in self.typicalPeriods
                for t in self.timeStepsPerPeriod
            )

        def initInterTimeStepsSet(pyM):
            return (
                (p, t)
                for p in self.typicalPeriods
                for t in range(len(self.timeStepsPerPeriod) + 1)
            )

        def initIntraYearTimeSet(pyM):
            return (
                (p, t) for p in self.typicalPeriods for t in self.timeStepsPerPeriod
            )

        def initInvestPeriodInterPeriodSet(pyM):
            return (
                (t_inter)
                for t_inter in range(
                    int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)) + 1
                )
            )

    else:
        utils.output(
            "Time series aggregation specifications:\n"
            "Number of typical periods:"
            + str(len(self.typicalPeriods))
            + ", number of time steps per period:"
            + str(len(self.timeStepsPerPeriod))
            + ", number of segments per period:"
            + str(len(self.segmentsPerPeriod))
            + "\n",
            self.verboseLogLevel,
            0,
        )

        # Define sets
        def initTimeSet(pyM):
            return (
                (ip, p, t)
                for ip in self.investmentPeriods
                for p in self.typicalPeriods
                for t in self.segmentsPerPeriod
            )

        def initInterTimeStepsSet(pyM):
            return (
                (p, t)
                for p in self.typicalPeriods
                for t in range(len(self.segmentsPerPeriod) + 1)
            )

        def initIntraYearTimeSet(pyM):
            return (
                (p, t) for p in self.typicalPeriods for t in self.segmentsPerPeriod
            )

        def initInvestPeriodInterPeriodSet(pyM):
            return (
                (t_inter)
                for t_inter in range(
                    int(len(self.totalTimeSteps) / len(self.timeStepsPerPeriod)) + 1
                )
            )

    def initInvestSet(pyM):
        return (ip for ip in self.investmentPeriods)

    # Initialize sets
    pyM.timeSet = pyomo.Set(dimen=3, initialize=initTimeSet)
    pyM.interTimeStepsSet = pyomo.Set(dimen=2, initialize=initInterTimeStepsSet)
    pyM.intraYearTimeSet = pyomo.Set(dimen=2, initialize=initIntraYearTimeSet)
    pyM.investSet = pyomo.Set(dimen=1, initialize=initInvestSet)
    pyM.investPeriodInterPeriodSet = pyomo.Set(
        dimen=1, initialize=initInvestPeriodInterPeriodSet
    )

getComponent

getComponent(componentName)

Return a component of the energy system.

:param componentName: name of the component that should be returned :type componentName: string

:returns: the component which has the name componentName :rtype: Component

Source code in fine/energySystemModel.py
def getComponent(self, componentName):
    """Return a component of the energy system.

    :param componentName: name of the component that should be returned
    :type componentName: string

    :returns: the component which has the name componentName
    :rtype: Component
    """
    if componentName not in self.componentNames.keys():
        raise ValueError(
            "The component "
            + componentName
            + " cannot be found in the energy system model.\n"
            + "The components considered in the model are: "
            + str(self.componentNames.keys())
        )
    modelingClass = self.componentNames[componentName]
    return self.componentModelingDict[modelingClass].componentsDict[componentName]

getComponentAttribute

getComponentAttribute(componentName, attributeName)

Return an attribute of a component considered in the energy system.

:param componentName: name of the component from which the attribute should be obtained :type componentName: string

:param attributeName: name of the attribute that should be returned :type attributeName: string

:returns: the attribute specified by the attributeName of the component with the name componentName :rtype: depends on the specified attribute

Source code in fine/energySystemModel.py
def getComponentAttribute(self, componentName, attributeName):
    """Return an attribute of a component considered in the energy system.

    :param componentName: name of the component from which the attribute should be obtained
    :type componentName: string

    :param attributeName: name of the attribute that should be returned
    :type attributeName: string

    :returns: the attribute specified by the attributeName of the component with the name componentName
    :rtype: depends on the specified attribute
    """
    # if there is only data for one investment period, the function
    # directly returns the value instead of {0:value}. This allows old
    # models to run without modification
    attr = getattr(self.getComponent(componentName), attributeName)
    if isinstance(attr, dict) and list(attr.keys()) == [0]:
        return attr[0]
    return attr

getOptimizationSummary

getOptimizationSummary(modelingClass, ip=0, outputLevel=0)

Return the optimization summary (design variables, aggregated operation variables, and objective contributions) of a modeling class.

:param modelingClass: name of the modeling class from which the optimization summary should be obtained :type modelingClass: string

:param outputLevel: states the level of detail of the output summary:

- 0: full optimization summary is returned
- 1: full optimization summary is returned but rows in which all values are NaN (not a number) are dropped
- 2: full optimization summary is returned but rows in which all values are NaN or 0 are dropped

|br| * the default value is 0

:type outputLevel: integer (0, 1 or 2)

:returns: the optimization summary of the requested modeling class :rtype: pandas DataFrame

Source code in fine/energySystemModel.py
def getOptimizationSummary(self, modelingClass, ip=0, outputLevel=0):
    """Return the optimization summary (design variables, aggregated operation variables, and objective contributions) of a modeling class.

    :param modelingClass: name of the modeling class from which the optimization summary should be obtained
    :type modelingClass: string

    :param outputLevel: states the level of detail of the output summary:

        - 0: full optimization summary is returned
        - 1: full optimization summary is returned but rows in which all values are NaN (not a number) are dropped
        - 2: full optimization summary is returned but rows in which all values are NaN or 0 are dropped

        |br| * the default value is 0
    :type outputLevel: integer (0, 1 or 2)

    :returns: the optimization summary of the requested modeling class
    :rtype: pandas DataFrame
    """
    if ip not in self.investmentPeriodNames:
        raise ValueError(
            f"No optimization summary exists for passed ip {ip}. "
            + "Please define a valid investment period  "
            + f"(from '{self.investmentPeriodNames}')"
        )

    # adjust columns name and remove "space" or "space_2" in case it exists
    self.componentModelingDict[modelingClass]._optSummary[ip].columns.name = None

    if outputLevel == 0:
        return self.componentModelingDict[modelingClass]._optSummary[ip]
    if outputLevel == 1:
        return (
            self.componentModelingDict[modelingClass]
            ._optSummary[ip]
            .dropna(how="all")
        )
    if outputLevel != 2 and self.verboseLogLevel < 2:
        warnings.warn("Invalid input. An outputLevel parameter of 2 is assumed.")
    df = self.componentModelingDict[modelingClass]._optSummary[ip].dropna(how="all")
    return df.loc[((df != 0) & (~df.isnull())).any(axis=1)]

optimize

optimize(
    declaresOptimizationProblem=True,
    relaxIsBuiltBinary=False,
    timeSeriesAggregation=False,
    logFileName="",
    threads=3,
    solver="None",
    timeLimit=None,
    optimizationSpecs="",
    warmstart=False,
    relevanceThreshold=None,
    includePerformanceSummary=False,
)

Optimize the specified energy system for which a pyomo ConcreteModel instance is built or called upon. A pyomo instance is optimized with the specified inputs, and the optimization results are further processed.

Default arguments:

:param declaresOptimizationProblem: states if the optimization problem should be declared (True) or not (False).

(a) If true, the declareOptimizationProblem function is called and a pyomo ConcreteModel instance is built.
(b) If false a previously declared pyomo ConcreteModel instance is used.

|br| * the default value is True

:type declaresOptimizationProblem: boolean

:param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower bound of the problem. |br| * the default value is False :type declaresOptimizationProblem: boolean

:param timeSeriesAggregation: states if the optimization of the energy system model should be done with

(a) the full time series (False) or
(b) clustered time series data (True).

|br| * the default value is False

:type timeSeriesAggregation: boolean

:param segmentation: states if the optimization of the energy system model based on clustered time series data should be done with

(a) aggregated typical periods with the original time step length (False) or
(b) aggregated typical periods with further segmented time steps (True).

|br| * the default value is False

:type segmentation: boolean

:param logFileName: logFileName is used for naming the log file of the optimization solver output if gurobi is used as the optimization solver. If the logFileName is given as an absolute path (e.g. logFileName = os.path.join(os.getcwd(), 'Results', 'logFileName.txt')) the log file will be stored in the specified directory. Otherwise, it will be stored by default in the directory where the executing python script is called. |br| * the default value is 'job' :type logFileName: string

:param threads: number of computational threads used for solving the optimization (solver dependent input) if gurobi is used as the solver. A value of 0 results in using all available threads. If a value larger than the available number of threads are chosen, the value will reset to the maximum number of threads. |br| * the default value is 3 :type threads: positive integer

:param solver: specifies which solver should solve the optimization problem (which of course has to be installed on the machine on which the model is run). |br| * the default value is 'gurobi' :type solver: string

:param timeLimit: if not specified as None, indicates the maximum solve time of the optimization problem in seconds (solver dependent input). The use of this parameter is suggested when running models in runtime restricted environments (such as clusters with job submission systems). If the runtime limitation is triggered before an optimal solution is available, the best solution obtained up until then (if available) is processed. |br| * the default value is None :type timeLimit: strictly positive integer or None

:param optimizationSpecs: specifies parameters for the optimization solver (see the respective solver documentation for more information). Example: 'LogToConsole=1 OptimalityTol=1e-6' |br| * the default value is an empty string ('') :type optimizationSpecs: string

:param warmstart: specifies if a warm start of the optimization should be considered (not always supported by the solvers). |br| * the default value is False :type warmstart: boolean

:param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold. |br| * the default value is None :type relevanceThreshold: float (>=0) or None

:param includePerformanceSummary: If True this will store a performance summary (in Dataframe format) as attribute ('self.performanceSummary') in the esM instance. The performance summary includes Data about RAM usage (assesed by the psutil package), Gurobi values (extracted from gurobi log with the gurobi-logtools package) and other various paramerts such as model buildtime, runtime and time series aggregation paramerters. |br| * the default value is False :type includePerformanceSummary: boolean

Last edited: November 16, 2023 |br| @author: FINE Developer Team (FZJ IEK-3)

Source code in fine/energySystemModel.py
1890
1891
1892
1893
1894
1895
1896
1897
1898
1899
1900
1901
1902
1903
1904
1905
1906
1907
1908
1909
1910
1911
1912
1913
1914
1915
1916
1917
1918
1919
1920
1921
1922
1923
1924
1925
1926
1927
1928
1929
1930
1931
1932
1933
1934
1935
1936
1937
1938
1939
1940
1941
1942
1943
1944
1945
1946
1947
1948
1949
1950
1951
1952
1953
1954
1955
1956
1957
1958
1959
1960
1961
1962
1963
1964
1965
1966
1967
1968
1969
1970
1971
1972
1973
1974
1975
1976
1977
1978
1979
1980
1981
1982
1983
1984
1985
1986
1987
1988
1989
1990
1991
1992
1993
1994
1995
1996
1997
1998
1999
2000
2001
2002
2003
2004
2005
2006
2007
2008
2009
2010
2011
2012
2013
2014
2015
2016
2017
2018
2019
2020
2021
2022
2023
2024
2025
2026
2027
2028
2029
2030
2031
2032
2033
2034
2035
2036
2037
2038
2039
2040
2041
2042
2043
2044
2045
2046
2047
2048
2049
2050
2051
2052
2053
2054
2055
2056
2057
2058
2059
2060
2061
2062
2063
2064
2065
2066
2067
2068
2069
2070
2071
2072
2073
2074
2075
2076
2077
2078
2079
2080
2081
2082
2083
2084
2085
2086
2087
2088
2089
2090
2091
2092
2093
2094
2095
2096
2097
2098
2099
2100
2101
2102
2103
2104
2105
2106
2107
2108
2109
2110
2111
2112
2113
2114
2115
2116
2117
2118
2119
2120
2121
2122
2123
2124
2125
2126
2127
2128
2129
2130
2131
2132
2133
2134
2135
2136
2137
2138
2139
2140
2141
2142
2143
2144
2145
2146
2147
2148
2149
2150
2151
2152
2153
2154
2155
2156
2157
2158
2159
2160
2161
2162
2163
2164
2165
2166
2167
2168
2169
2170
2171
2172
2173
2174
2175
2176
2177
2178
2179
2180
2181
2182
2183
2184
2185
2186
2187
2188
2189
2190
2191
2192
2193
2194
2195
2196
2197
2198
2199
2200
2201
2202
2203
2204
2205
2206
2207
2208
2209
2210
2211
2212
2213
2214
2215
2216
2217
2218
2219
2220
2221
2222
2223
2224
2225
2226
2227
2228
2229
2230
2231
2232
2233
2234
2235
2236
2237
2238
2239
2240
2241
2242
2243
2244
2245
2246
2247
2248
2249
2250
2251
2252
2253
2254
2255
2256
2257
2258
2259
2260
2261
2262
2263
2264
2265
2266
2267
2268
2269
2270
2271
2272
2273
2274
2275
2276
2277
2278
2279
2280
2281
2282
2283
2284
2285
2286
2287
2288
2289
2290
2291
2292
2293
2294
2295
2296
2297
2298
2299
2300
2301
2302
2303
2304
2305
2306
2307
2308
2309
2310
2311
2312
2313
2314
2315
2316
2317
2318
2319
2320
2321
2322
2323
2324
2325
2326
2327
2328
2329
2330
2331
2332
2333
2334
2335
2336
2337
2338
2339
2340
2341
2342
2343
2344
2345
2346
2347
2348
2349
2350
2351
2352
2353
2354
2355
2356
2357
2358
2359
2360
2361
2362
2363
2364
2365
2366
2367
2368
2369
2370
2371
2372
2373
2374
2375
2376
2377
2378
2379
2380
2381
2382
2383
2384
2385
2386
2387
2388
2389
2390
def optimize(
    self,
    declaresOptimizationProblem=True,
    relaxIsBuiltBinary=False,
    timeSeriesAggregation=False,
    logFileName="",
    threads=3,
    solver="None",
    timeLimit=None,
    optimizationSpecs="",
    warmstart=False,
    relevanceThreshold=None,
    includePerformanceSummary=False,
):
    """Optimize the specified energy system for which a pyomo ConcreteModel instance is built or called upon.
    A pyomo instance is optimized with the specified inputs, and the optimization results are further
    processed.

    **Default arguments:**

    :param declaresOptimizationProblem: states if the optimization problem should be declared (True) or not (False).

        (a) If true, the declareOptimizationProblem function is called and a pyomo ConcreteModel instance is built.
        (b) If false a previously declared pyomo ConcreteModel instance is used.

        |br| * the default value is True
    :type declaresOptimizationProblem: boolean

    :param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower
        bound of the problem.
        |br| * the default value is False
    :type declaresOptimizationProblem: boolean

    :param timeSeriesAggregation: states if the optimization of the energy system model should be done with

        (a) the full time series (False) or
        (b) clustered time series data (True).

        |br| * the default value is False
    :type timeSeriesAggregation: boolean

    :param segmentation: states if the optimization of the energy system model based on clustered time series data
        should be done with

        (a) aggregated typical periods with the original time step length (False) or
        (b) aggregated typical periods with further segmented time steps (True).

        |br| * the default value is False
    :type segmentation: boolean

    :param logFileName: logFileName is used for naming the log file of the optimization solver output
        if gurobi is used as the optimization solver.
        If the logFileName is given as an absolute path (e.g. logFileName = os.path.join(os.getcwd(),
        'Results', 'logFileName.txt')) the log file will be stored in the specified directory. Otherwise,
        it will be stored by default in the directory where the executing python script is called.
        |br| * the default value is 'job'
    :type logFileName: string

    :param threads: number of computational threads used for solving the optimization (solver dependent
        input) if gurobi is used as the solver. A value of 0 results in using all available threads. If
        a value larger than the available number of threads are chosen, the value will reset to the maximum
        number of threads.
        |br| * the default value is 3
    :type threads: positive integer

    :param solver: specifies which solver should solve the optimization problem (which of course has to be
        installed on the machine on which the model is run).
        |br| * the default value is 'gurobi'
    :type solver: string

    :param timeLimit: if not specified as None, indicates the maximum solve time of the optimization problem
        in seconds (solver dependent input). The use of this parameter is suggested when running models in
        runtime restricted environments (such as clusters with job submission systems). If the runtime
        limitation is triggered before an optimal solution is available, the best solution obtained up
        until then (if available) is processed.
        |br| * the default value is None
    :type timeLimit: strictly positive integer or None

    :param optimizationSpecs: specifies parameters for the optimization solver (see the respective solver
        documentation for more information). Example: 'LogToConsole=1 OptimalityTol=1e-6'
        |br| * the default value is an empty string ('')
    :type optimizationSpecs: string

    :param warmstart: specifies if a warm start of the optimization should be considered
        (not always supported by the solvers).
        |br| * the default value is False
    :type warmstart: boolean

    :param relevanceThreshold: Force operation parameters to be 0 if values are below the relevance threshold.
        |br| * the default value is None
    :type relevanceThreshold: float (>=0) or None

    :param includePerformanceSummary: If True this will store a performance summary (in Dataframe format) as attribute ('self.performanceSummary') in the esM instance.
        The performance summary includes Data about RAM usage (assesed by the psutil package),
        Gurobi values (extracted from gurobi log with the gurobi-logtools package) and other various paramerts
        such as model buildtime, runtime and time series aggregation paramerters.
        |br| * the default value is False
    :type includePerformanceSummary: boolean

    Last edited: November 16, 2023
    |br| @author: FINE Developer Team (FZJ IEK-3)
    """
    if not timeSeriesAggregation:
        self.segmentation = False

    if declaresOptimizationProblem:
        self.declareOptimizationProblem(
            timeSeriesAggregation=timeSeriesAggregation,
            relaxIsBuiltBinary=relaxIsBuiltBinary,
            relevanceThreshold=relevanceThreshold,
        )
    elif self.pyM is None:
        raise TypeError(
            "The optimization problem is not declared yet. Set the argument declaresOptimization"
            " problem to True or call the declareOptimizationProblem function first."
        )

    if includePerformanceSummary:
        """
        this will store a performance summary (in Dataframe format) as attribute ('self.performanceSummary') in the esM instance.
        """
        ## make sure logging is enabled for gurobi, otherwise gurobi values cannot be included in the performance summary
        if logFileName == "":
            warnings.warn(
                "A logFile Name has to be specified in order to extract Gurobi values! Gurobi values will not be listed in performance summary!"
            )
        # If time series aggregation is enabled, the TSA instance needs to be saved in order to be included in the performance summary
        if self.isTimeSeriesDataClustered and (self.tsaInstance is None):
            warnings.warn(
                "storeTSAinstance has to be set to true to extract TSA Parameters! TSA parameters will not be listed in performance summary!"
            )

        # get RAM usage of process before and after optimization
        process = psutil.Process(os.getpid())
        rss_by_psutil_start = process.memory_info().rss / (
            1024 * 1024 * 1024
        )  # from Bytes to GB
        # start optimization

    # Get starting time of the optimization to, later on, obtain the total run time of the optimize function call
    timeStart = time.time()

    # Check correctness of inputs
    utils.checkOptimizeInput(
        timeSeriesAggregation,
        self.isTimeSeriesDataClustered,
        logFileName,
        threads,
        solver,
        timeLimit,
        optimizationSpecs,
        warmstart,
    )

    # Store keyword arguments in the EnergySystemModel instance
    self.solverSpecs["logFileName"], self.solverSpecs["threads"] = (
        logFileName,
        threads,
    )
    self.solverSpecs["solver"], self.solverSpecs["timeLimit"] = solver, timeLimit
    self.solverSpecs["optimizationSpecs"], self.solverSpecs["hasTSA"] = (
        optimizationSpecs,
        timeSeriesAggregation,
    )

    # Check which solvers are available and choose default solver if no solver is specified explicitely
    # Order of possible solvers in solverList defines the priority of chosen default solver.
    solverList = [
        ImplementedSolvers.STANDARD_SOLVER.value,
        ImplementedSolvers.HIGHS.value,
    ]

    if solver != "None":
        try:
            opt.SolverFactory(solver).available()
        except (ApplicationError, ValueError, OSError):
            solver = "None"

    if solver == "None":
        for nSolver in solverList:
            if solver == "None":
                try:
                    if opt.SolverFactory(nSolver).available():
                        solver = nSolver
                        utils.output(
                            "Either solver not selected or specified solver not available."
                            + str(nSolver)
                            + " is set as solver.",
                            self.verboseLogLevel,
                            0,
                        )
                except (ApplicationError, ValueError, OSError):
                    pass

    if solver == "None":
        raise TypeError(
            "At least one solver must be installed."
            " Have a look at the FINE documentation to see how to install possible solvers."
            " https://vsa-fine.readthedocs.io/en/latest/"
        )

    ################################################################################################################
    #                                  Solve the specified optimization problem                                    #
    ################################################################################################################

    if solver == "gurobi" and importlib.util.find_spec("gurobipy"):
        from gurobipy import Env  # noqa: PLC0415

        # Use the direct gurobi solver that uses the Python API.
        wlsaccessid = os.environ.get("WLSACCESSID", "")
        wlssecret = os.environ.get("WLSSECRET", "")
        licenseid = os.environ.get("LICENSEID", "")
        if wlsaccessid and wlssecret and licenseid:
            params = {
                "WLSACCESSID": wlsaccessid,
                "WLSSECRET": wlssecret,
                "LICENSEID": int(licenseid),
            }
            with Env(params=params) as env:
                optimizer = opt.SolverFactory(solver, solver_io="python", env=env)

        else:
            optimizer = opt.SolverFactory(solver, solver_io="python")
    elif solver == ImplementedSolvers.HIGHS.value:
        # Handle cases with no duals without exceptions in highs
        class LegacyHighs(LegacySolverInterface, Highs):
            def get_duals(self, cons_to_load=None):
                if self._sol is None or not self._sol.dual_valid:
                    return {}
                return super().get_duals(cons_to_load)

        optimizer = LegacyHighs()
        if optimizationSpecs is not None:
            solver_options = opt.OptSolver._options_string_to_dict(
                optimizationSpecs
            )

            for k, v in solver_options.items():
                optimizer.highs_options[k] = v
        if timeLimit is not None:
            optimizer.highs_options["time_limit"] = timeLimit
    else:
        optimizer = opt.SolverFactory(solver)

    # Set, if specified, the time limit
    if (
        self.solverSpecs["timeLimit"] is not None
        and solver == ImplementedSolvers.GUROBI.value
    ):
        optimizer.options["timelimit"] = timeLimit

    # Set the specified solver options
    if (
        "LogToConsole=" not in optimizationSpecs
        and solver == ImplementedSolvers.GUROBI.value
    ):
        if self.verboseLogLevel == 2:
            optimizationSpecs += " LogToConsole=0"

    # Solve optimization problem. The optimization solve time is stored and the solver information is printed.
    if solver == ImplementedSolvers.GUROBI.value:
        optimizer.set_options(
            "Threads="
            + str(threads)
            + " logfile="
            + logFileName
            + " "
            + optimizationSpecs
        )

        solver_info = optimizer.solve(
            self.pyM,
            warmstart=warmstart,
            tee=True,
        )
    elif solver == ImplementedSolvers.GLPK.value:
        optimizer.set_options(optimizationSpecs)
        solver_info = optimizer.solve(self.pyM, tee=True)
    else:
        solver_info = optimizer.solve(self.pyM, tee=True)
    self.solverSpecs["solvetime"] = time.time() - timeStart
    (
        utils.output(solver_info.solver(), self.verboseLogLevel, 0),
        utils.output(solver_info.problem(), self.verboseLogLevel, 0),
    )
    utils.output(
        "Solve time: " + str(self.solverSpecs["solvetime"]) + " sec.",
        self.verboseLogLevel,
        0,
    )

    ################################################################################################################
    #                                      Post-process optimization output                                        #
    ################################################################################################################

    _t = time.time()

    # Post-process the optimization output by differentiating between different solver statuses and termination
    # conditions. First, check if the status and termination_condition of the optimization are acceptable.
    # If not, no output is generated.
    status, termCondition = (
        solver_info.solver.status,
        solver_info.solver.termination_condition,
    )
    self.solverSpecs["status"] = str(status)
    self.solverSpecs["terminationCondition"] = str(termCondition)
    if status in (
        opt.SolverStatus.error,
        opt.SolverStatus.aborted,
        opt.SolverStatus.unknown,
    ):
        utils.output(
            "Solver status:  "
            + str(status)
            + ", termination condition:  "
            + str(termCondition)
            + ". No output is generated.",
            self.verboseLogLevel,
            0,
        )
    elif termCondition in (
        opt.TerminationCondition.infeasibleOrUnbounded,
        opt.TerminationCondition.infeasible,
        opt.TerminationCondition.unbounded,
    ):
        utils.output(
            "Optimization problem is "
            + str(termCondition)
            + ". No output is generated.",
            self.verboseLogLevel,
            0,
        )
    elif termCondition == opt.TerminationCondition.other:
        utils.output(
            "No solution was found (TerminationCondition: other). No output is generated.",
            self.verboseLogLevel,
            0,
        )
    else:
        # If the solver status is not okay (hence either has a warning, an error, was aborted or has an unknown
        # status), show a warning message.
        if (
            not termCondition == opt.TerminationCondition.optimal
            and self.verboseLogLevel < 2
        ):
            warnings.warn("Output is generated for a non-optimal solution.")
        utils.output("\nProcessing optimization output...", self.verboseLogLevel, 0)
        # Declare component specific sets, variables and constraints
        w = str(len(max(self.componentModelingDict.keys())) + 6)

        # iterate over investment periods, to get yearly results
        for key, mdl in self.componentModelingDict.items():
            if not isinstance(mdl._capacityVariablesOptimum, dict):
                mdl._capacityVariablesOptimum = {}
            __t = time.time()
            # if _capacityVariablesOptimum is not a dict, convert to dict
            # (if single year system is optimized several times)

            mdl.setOptimalValues(self, self.pyM)
            outputString = (
                ("for {:" + w + "}").format(key + " ...")
                + "(%.4f" % (time.time() - __t)
                + "sec)"
            )
            utils.output(outputString, self.verboseLogLevel, 0)

            # convert optimal values from internal name to external name
            # e.g. from _capacityVariablesOptimum to capacityVariablesOptimum
            # For perfectForesight the data stays the same, for a single year optimization
            # the data is converted from a dict with a single entry to a dataframe
            # By this, old models will not fail.
            def convertOptimalValues(esM, mdl, key):
                if key in mdl.__dict__.keys():
                    if esM.numberOfInvestmentPeriods == 1:
                        setattr(
                            mdl,
                            key.replace("_", ""),
                            getattr(mdl, key)[esM.investmentPeriodNames[0]],
                        )
                    else:
                        setattr(mdl, key.replace("_", ""), getattr(mdl, key))
                else:
                    pass

            optimalValueParameters = [
                "_optSummary",
                "_stateOfChargeOperationVSariablesOptimum",
                "_chargeOperationVariablesOptimum",
                "_dischargeOperationVariablesOptimum",
                "_phaseAngleVariablesOptimum",
                "_operationVariablesOptimum",
                "_discretizationPointVariablesOptimum",
                "_discretizationSegmentConVariablesOptimum",
                "_discretizationSegmentBinVariablesOptimum",
                "_capacityVariablesOptimum",
                "_isBuiltVariablesOptimum",
                "_commissioningVariablesOptimum",
                "_decommissioningVariablesOptimum",
            ]

            for optParam in optimalValueParameters:
                convertOptimalValues(self, mdl, optParam)

        if hasattr(self, "pwlcfModel"):
            self.pwlcfModel.setOptimalValues(self, self.pyM)

        # Store the objective value in the EnergySystemModel instance.
        self.objectiveValue = self.pyM.Obj()

    utils.output(
        "\t\t(%.4f" % (time.time() - _t) + " sec)\n", self.verboseLogLevel, 0
    )

    # Store the runtime of the optimize function call in the EnergySystemModel instance
    self.solverSpecs["runtime"] = (
        self.solverSpecs["buildtime"] + time.time() - timeStart
    )

    if includePerformanceSummary:
        rss_by_psutil_end = process.memory_info().rss / (
            1024 * 1024 * 1024
        )  # from Bytes to GB

        # CREATE PERFORMANCE SUMMARY

        # FINE Model
        fine_parameters_dict = {
            "noOfRegions": len(self.locations),
            "numberOfTimeSteps": self.numberOfTimeSteps,
            "hoursPerTimestep": self.hoursPerTimeStep,
            "numberOfYears": self.numberOfYears,
            "optimizationSpecs": self.solverSpecs["optimizationSpecs"],
        }

        # RAM Usage
        ram_usage_dict = {
            "ramUsageStartGB": rss_by_psutil_start,
            "ramUsageEndGB": rss_by_psutil_end,
        }

        # TSA Values
        if self.isTimeSeriesDataClustered and (self.tsaInstance is not None):
            tsaBuildTime = self.tsaInstance.tsaBuildTime

            tsa_parameters_dict = {
                "clusterMethod": self.tsaInstance.clusterMethod,
                "noTypicalPeriods": self.tsaInstance.noTypicalPeriods,
                "hoursPerPeriod": self.tsaInstance.hoursPerPeriod,
                "segmentation": self.tsaInstance.segmentation,
                "noSegments": self.tsaInstance.noSegments,
                "tsaSolver": self.tsaInstance.solver,
                "timeStepsPerPeriod": self.tsaInstance.timeStepsPerPeriod,
                "tsaBuildTime": self.tsaInstance.tsaBuildTime,
            }
        else:
            tsa_parameters_dict = {}
            tsaBuildTime = None

        # Procesing Times
        processing_time_dict = {
            "buildtime": self.solverSpecs["buildtime"],
            "tsaBuildTime": tsaBuildTime,
            "solvetime": self.solverSpecs["solvetime"],
            "runtime": self.solverSpecs["runtime"],
        }

        if solver == "gurobi":
            # Create DataFrame from gurobi log file
            if logFileName == "":
                gurobi_summary_dict = {}
            else:
                absolute_logFilePath = Path(logFileName).resolve()
                gurobi_summary_dict = glt.get_dataframe(
                    [str(absolute_logFilePath)]  # passed path has to be a list
                ).T.to_dict()[0]
        else:
            gurobi_summary_dict = {}

        # Combine to Overall Summary
        summary_dict = {
            "FineParameters": fine_parameters_dict,
            "RAMUsage": ram_usage_dict,
            "ProcessingTimes": processing_time_dict,
            "TSAParameters": tsa_parameters_dict,
            "GurobiSummary": gurobi_summary_dict,
        }

        summary_dict = {
            (i, j): summary_dict[i][j]
            for i in summary_dict.keys()
            for j in summary_dict[i].keys()
        }

        mux = pd.MultiIndex.from_tuples(summary_dict.keys())
        mux = mux.set_names(["Category", "Parameter"])
        PerformanceSummary_df = pd.DataFrame(
            list(summary_dict.values()), index=mux, columns=["Value"]
        )

        # Save perfromance summary in the EnergySystemModel instance
        self.performanceSummary = PerformanceSummary_df

removeComponent

removeComponent(componentName, track=False)

Remove a component from the energy system.

:param componentName: name of the component that should be removed :type componentName: string

:param track: specifies if the removed components should be tracked or not |br| * the default value is False :type track: boolean

:returns: dictionary with the removed componentName and component instance if track is set to True else None. :rtype: dict or None

Source code in fine/energySystemModel.py
def removeComponent(self, componentName, track=False):
    """Remove a component from the energy system.

    :param componentName: name of the component that should be removed
    :type componentName: string

    :param track: specifies if the removed components should be tracked or not
        |br| * the default value is False
    :type track: boolean

    :returns: dictionary with the removed componentName and component instance if track is set to True else None.
    :rtype: dict or None
    """
    # Test if component exists
    if componentName not in self.componentNames.keys():
        raise ValueError(
            "The component "
            + componentName
            + " cannot be found in the energy system model.\n"
            + "The components considered in the model are: "
            + str(self.componentNames.keys())
        )
    modelingClass = self.componentNames[componentName]

    # Remove component from sharedPotentialDict if present
    keys_to_delete = [
        key
        for key, comps in self.sharedPotentialDict.items()
        if componentName in comps
    ]
    for key in keys_to_delete:
        self.sharedPotentialDict[key].remove(componentName)
        if not self.sharedPotentialDict[key]:
            del self.sharedPotentialDict[key]

    removedComp = dict()
    # If track: Return a dictionary including the name of the removed component and the component instance
    if track:
        removedComp = dict(
            {
                componentName: self.componentModelingDict[
                    modelingClass
                ].componentsDict.pop(componentName)
            }
        )
        # Remove component from the componentNames dict:
        del self.componentNames[componentName]
        # Test if all components of one modelingClass are removed. If so, remove modelingClass:
        if not self.componentModelingDict[
            modelingClass
        ].componentsDict:  # False if dict is empty
            del self.componentModelingDict[modelingClass]
        return removedComp
    # Remove component from the componentNames dict:
    del self.componentNames[componentName]
    # Remove component from the componentModelingDict:
    del self.componentModelingDict[modelingClass].componentsDict[componentName]
    # Test if all components of one modelingClass are removed. If so, remove modelingClass:
    if not self.componentModelingDict[
        modelingClass
    ].componentsDict:  # False if dict is empty
        del self.componentModelingDict[modelingClass]
    return None

updateComponent

updateComponent(componentName, updateAttrs)

Overwrite selected attributes of an existing esM component with new values.

.. note:: Be aware of the fact that some attributes are filled automatically while initializing a component. E.g., if you want to change attributes like economic lifetime, there might occur the error that the new value does not match with the technical lifetime of the component. Additionally: You cannot change the name of an existing component by using this function. If you do so, you will not update the component but create a new one with the new name. The old component will still exist.

:param componentName: Name of the component that shall be updated. :type componentName: str

:param updateAttrs: A dict of component attributes as keys and values that shall be set as dict values. :type updateAttrs: dict

Source code in fine/energySystemModel.py
def updateComponent(self, componentName, updateAttrs):
    """Overwrite selected attributes of an existing esM component with new values.

    .. note::
        Be aware of the fact that some attributes are filled automatically while initializing a component.
        E.g., if you want to change attributes like economic lifetime, there might occur the error that the new
        value does not match with the technical lifetime of the component.
        Additionally: You cannot change the name of an existing component by using this function.
        If you do so, you will not update the component but create a new one with the new name.
        The old component will still exist.

    :param componentName: Name of the component that shall be updated.
    :type componentName: str

    :param updateAttrs: A dict of component attributes as keys and values that shall be set as dict values.
    :type updateAttrs: dict
    """
    if componentName not in self.componentNames.keys():
        raise AttributeError(
            f"componentName '{componentName}' is not a component in this esM instance."
        )
    if not (isinstance(updateAttrs, dict) and len(updateAttrs) > 0):
        raise TypeError(
            "updateAttrs must be dict type with at least one key/value pair."
        )

    # get affected classes and extract relevant class attributes
    _class = self.getComponent(componentName).__class__

    # Get parameters from the class and its direct parent class (only for specific subclasses)
    class_attrs = set()
    # Get parameters from current class
    class_attrs.update(inspect.signature(_class).parameters.keys())

    # Get parameters from direct parent class only for specific subclasses
    subclass_names = [
        "ConversionDynamic",
        "ConversionPartLoadModel",
        "LinearOptimalPowerFlow",
    ]
    if (
        _class.__name__ in subclass_names
        and _class.__bases__
        and _class.__bases__[0] is not object
    ):
        class_attrs.update(inspect.signature(_class.__bases__[0]).parameters.keys())

    class_attrs = list(class_attrs)  # Convert back to list for compatibility

    # check if all arguments to be updated are class attributes
    for k in updateAttrs.keys():
        if k == "name":
            warnings.warn(
                "Updating the name will just create a new component."
                + "The old component will still exist with the old attributes."
            )

    # get attributes of original component
    old_attrs = self.getComponent(componentName).__dict__

    # extract all class parameter values from the existing object and write to dict
    new_args = dict([(x, old_attrs[x]) for x in class_attrs if x in old_attrs])

    # update the required arguments
    for _arg, _val in updateAttrs.items():
        new_args[_arg] = _val

    # overwrite the existing component with the new data
    self.add(_class(self, **new_args))

ImplementedSolvers

Implemented solvers.

Methods:

set_standard_solver classmethod

set_standard_solver()

Detect available solver and set STANDARD_SOLVER accordingly.

Source code in fine/utils.py
@classmethod
def set_standard_solver(cls):
    """Detect available solver and set STANDARD_SOLVER accordingly."""
    if cls._gurobi_available():
        cls.STANDARD_SOLVER.value = cls.GUROBI.value
    else:
        cls.STANDARD_SOLVER.value = cls.HIGHS.value

LinearOptimalPowerFlow

LinearOptimalPowerFlow(
    esM,
    name,
    commodity,
    reactances,
    losses=0,
    distances=None,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
)

Bases: Transmission

A LinearOptimalPowerFlow component shows the behavior of a Transmission component but additionally models a linearized power flow (i.e. for AC lines). The LinearOptimalPowerFlow class inherits from the Transmission class.

Create a LinearOptimalPowerFlow class instance. The LinearOptimalPowerFlow component specific input arguments are described below. The Transmission component specific input arguments are described in the Transmission class and the general component input arguments are described in the Component class.

Required arguments:

:param reactances: reactances for DC power flow modeling (of AC lines) given as a Pandas DataFrame. The row and column indices of the DataFrame have to equal the in the energy system model specified locations. :type reactances: Pandas DataFrame.

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

Source code in fine/subclasses/lopf.py
def __init__(
    self,
    esM,
    name,
    commodity,
    reactances,
    losses=0,
    distances=None,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
):
    """Create a LinearOptimalPowerFlow class instance.
    The LinearOptimalPowerFlow component specific input arguments are described below. The Transmission
    component specific input arguments are described in the Transmission class and the general component
    input arguments are described in the Component class.

    **Required arguments:**

    :param reactances: reactances for DC power flow modeling (of AC lines) given as a Pandas DataFrame. The row and column indices of the DataFrame have to equal
        the in the energy system model specified locations.
    :type reactances: Pandas DataFrame.
    """
    Transmission.__init__(
        self,
        esM,
        name,
        commodity,
        losses=losses,
        distances=distances,
        hasCapacityVariable=hasCapacityVariable,
        capacityVariableDomain=capacityVariableDomain,
        capacityPerPlantUnit=capacityPerPlantUnit,
        hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable,
        bigM=bigM,
        operationRateMax=operationRateMax,
        operationRateFix=operationRateFix,
        tsaWeight=tsaWeight,
        locationalEligibility=locationalEligibility,
        capacityMin=capacityMin,
        capacityMax=capacityMax,
        partLoadMin=partLoadMin,
        sharedPotentialID=sharedPotentialID,
        capacityFix=capacityFix,
        commissioningMin=commissioningMin,
        commissioningMax=commissioningMax,
        commissioningFix=commissioningFix,
        isBuiltFix=isBuiltFix,
        investPerCapacity=investPerCapacity,
        investIfBuilt=investIfBuilt,
        opexPerOperation=opexPerOperation,
        opexPerCapacity=opexPerCapacity,
        opexIfBuilt=opexIfBuilt,
        QPcostScale=QPcostScale,
        interestRate=interestRate,
        economicLifetime=economicLifetime,
        technicalLifetime=technicalLifetime,
        floorTechnicalLifetime=floorTechnicalLifetime,
        stockCommissioning=stockCommissioning,
    )

    self.modelingClass = LOPFModel

    self.reactances2dim = reactances

    try:
        self.reactances = pd.Series(self._mapC).apply(
            lambda loc: self.reactances2dim[loc[0]][loc[1]]
        )
    except (KeyError, TypeError, IndexError):
        self.reactances = utils.preprocess2dimData(self.reactances2dim)

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/transmission.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the conversion component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/transmission.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the conversion component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/transmission.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    self.processedOperationRateMax = (
        self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
    )
    self.processedOperationRateFix = (
        self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
    )

Sink

Sink(
    esM,
    name,
    commodity,
    hasCapacityVariable,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMin=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    commodityCost=0,
    commodityRevenue=0,
    commodityCostTimeSeries=None,
    commodityRevenueTimeSeries=None,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    balanceLimitID=None,
    pathwayBalanceLimitID=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
)

Bases: Source

A Sink component can transfer a commodity over the energy system boundary out of the system.

Create a Sink class instance.

The Sink class inherits from the Source class. They coincide with the input parameters (see Source class for the parameter description) and differ in the sign parameter, which is equal to -1 for Sink objects and +1 for Source objects.

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate, fixed operation rate, and cost or revenue time series depending on whether a time series analysis is requested or not.

Source code in fine/sourceSink.py
def __init__(
    self,
    esM,
    name,
    commodity,
    hasCapacityVariable,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMin=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    commodityCost=0,
    commodityRevenue=0,
    commodityCostTimeSeries=None,
    commodityRevenueTimeSeries=None,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    balanceLimitID=None,
    pathwayBalanceLimitID=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
):
    """Create a Sink class instance.

    The Sink class inherits from the Source class. They coincide with the input parameters
    (see Source class for the parameter description) and differ in the sign
    parameter, which is equal to -1 for Sink objects and +1 for Source objects.
    """
    Source.__init__(
        self,
        esM,
        name,
        commodity=commodity,
        hasCapacityVariable=hasCapacityVariable,
        capacityVariableDomain=capacityVariableDomain,
        capacityPerPlantUnit=capacityPerPlantUnit,
        hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable,
        bigM=bigM,
        operationRateMin=operationRateMin,
        operationRateMax=operationRateMax,
        operationRateFix=operationRateFix,
        tsaWeight=tsaWeight,
        locationalEligibility=locationalEligibility,
        capacityMin=capacityMin,
        capacityMax=capacityMax,
        partLoadMin=partLoadMin,
        sharedPotentialID=sharedPotentialID,
        linkedQuantityID=linkedQuantityID,
        capacityFix=capacityFix,
        commissioningMin=commissioningMin,
        commissioningMax=commissioningMax,
        commissioningFix=commissioningFix,
        isBuiltFix=isBuiltFix,
        investPerCapacity=investPerCapacity,
        investIfBuilt=investIfBuilt,
        opexPerOperation=opexPerOperation,
        commodityCost=commodityCost,
        commodityRevenue=commodityRevenue,
        commodityCostTimeSeries=commodityCostTimeSeries,
        commodityRevenueTimeSeries=commodityRevenueTimeSeries,
        opexPerCapacity=opexPerCapacity,
        opexIfBuilt=opexIfBuilt,
        QPcostScale=QPcostScale,
        interestRate=interestRate,
        economicLifetime=economicLifetime,
        technicalLifetime=technicalLifetime,
        balanceLimitID=balanceLimitID,
        pathwayBalanceLimitID=pathwayBalanceLimitID,
        stockCommissioning=stockCommissioning,
        floorTechnicalLifetime=floorTechnicalLifetime,
    )

    self.sign = -1

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/sourceSink.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMin:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMin,
            "_operationRateMin_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    weightDict, data = self.prepareTSAInput(
        self.fullCommodityCostTimeSeries,
        "_commodityCostTimeSeries_",
        self.tsaWeight,
        weightDict,
        data,
        ip,
    )
    weightDict, data = self.prepareTSAInput(
        self.fullCommodityRevenueTimeSeries,
        "_commodityRevenueTimeSeries_",
        self.tsaWeight,
        weightDict,
        data,
        ip,
    )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the source component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/sourceSink.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the source component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )
    self.aggregatedOperationRateMin[ip] = self.getTSAOutput(
        self.fullOperationRateMin, "_operationRateMin_", data, ip
    )
    self.aggregatedCommodityCostTimeSeries[ip] = self.getTSAOutput(
        self.fullCommodityCostTimeSeries, "_commodityCostTimeSeries_", data, ip
    )
    self.aggregatedCommodityRevenueTimeSeries[ip] = self.getTSAOutput(
        self.fullCommodityRevenueTimeSeries,
        "_commodityRevenueTimeSeries_",
        data,
        ip,
    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate, fixed operation rate, and cost or revenue time series depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/sourceSink.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate, fixed operation rate, and cost or revenue time series depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    self.processedOperationRateMin = (
        self.aggregatedOperationRateMin if hasTSA else self.fullOperationRateMin
    )

    self.processedOperationRateMax = (
        self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
    )

    self.processedOperationRateFix = (
        self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
    )

    self.processedCommodityCostTimeSeries = (
        self.aggregatedCommodityCostTimeSeries
        if hasTSA
        else self.fullCommodityCostTimeSeries
    )

    self.processedCommodityRevenueTimeSeries = (
        self.aggregatedCommodityRevenueTimeSeries
        if hasTSA
        else self.fullCommodityRevenueTimeSeries
    )

Source

Source(
    esM,
    name,
    commodity,
    hasCapacityVariable,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMin=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    commodityCost=0,
    commodityRevenue=0,
    commodityCostTimeSeries=None,
    commodityRevenueTimeSeries=None,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    yearlyFullLoadHoursMin=None,
    yearlyFullLoadHoursMax=None,
    balanceLimitID=None,
    pathwayBalanceLimitID=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
    pwlcfParameters=None,
)

Bases: Component

A Source component can transfer a commodity over the energy system boundary into the system.

Create a Source class instance. The Source component specific input arguments are described below. The general component input arguments are described in the Component class.

.. note:: The Sink class inherits from the Source class and is initialized with the same parameter set.

Required arguments:

:param commodity: to the component related commodity. :type commodity: string

:param hasCapacityVariable: specifies if the component should be modeled with a capacity or not.

Examples: * A wind turbine has a capacity given in GW_electric -> hasCapacityVariable is True. * Emitting CO2 into the environment is not per se limited by a capacity -> hasCapacityVariable is False.

:type hasCapacityVariable: boolean

Default arguments: :param operationRateMin: if specified, indicates a minimum operation rate for each location and each time, if required also for each investment period, if step by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit for each time step. |br| * the default value is None :type operationRateMin:

* None
* Pandas DataFrame with positive (>= 0) entries. The row indices have
  to match the in the energy system model specified time steps. The column indices have to equal the
  in the energy system model specified locations. The data in ineligible locations are set to zero.
* a dict

:param operationRateMax: if specified, indicates a maximum operation rate for each location and each time, if required also for each investment period, if step by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit for each time step. |br| * the default value is None :type operationRateMax:

* None
* Pandas DataFrame with positive (>= 0) entries. The row indices have
  to match the in the energy system model specified time steps. The column indices have to equal the
  in the energy system model specified locations. The data in ineligible locations are set to zero.
* a dictionary with investment periods as keys and one of the two options above as values

:param operationRateFix: if specified, indicates a fixed operation rate for each location and each time, if required also for each investment period, step by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit for each time step. |br| * the default value is None :type operationRateFix:

* None
* Pandas DataFrame with positive (>=0) per investment period. The row indices have
  to match the in the energy system model specified time steps. The column indices have to equal the
  in the energy system model specified locations. The data in ineligible locations are set to zero.
* a dictionary with investment periods as keys and one of the two options above as values

:param commodityCostTimeSeries: if specified, indicates commodity cost rates for each location and each time step, if required also for each investment period, by a positive float. The values are given as specific values relative to the commodityUnit for each time step. |br| * the default value is None :type commodityCostTimeSeries:

* None
* Pandas DataFrame with positive (>= 0) entries. The row indices have
  to match the in the energy system model specified time steps. The column indices have to equal the
  in the energy system model specified locations. The data in ineligible locations are set to zero.
* a dictionary with investment periods as keys and one of the two options above as values

:param commodityRevenueTimeSeries: if specified, indicates commodity revenue rate for each location and each time step, if required also for each investment period, by a positive float. The values are given as specific values relative to the commodityUnit for each time step. |br| * the default value is None :type commodityRevenueTimeSeries:

* None
* Pandas DataFrame with positive (>= 0) entries. The row indices
  have to match the in the energy system model specified time steps. The column indices have to equal
  the in the energy system model specified locations. The data in ineligible locations are set to zero.
* a dictionary with investment periods as keys and one of the two options above as values

:param tsaWeight: weight with which the time series of the component should be considered when applying time series aggregation. |br| * the default value is 1 :type tsaWeight: positive (>= 0) float

:param opexPerOperation: describes the cost for one unit of the operation. The cost which is directly proportional to the operation of the component is obtained by multiplying the opexPerOperation parameter with the annual sum of the operational time series of the components. The opexPerOperation can either be given as a float or a Pandas Series with location specific values or a dictionary per investment period with one of the previous options. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro). |br| * the default value is 0 :type opexPerOperation:

* positive (>=0) float
* Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations.
* a dictionary with investment periods as keys and one of the two options above as values.

:param commodityCost: describes the cost value of one operation´s unit of the component. The cost which is directly proportional to the operation of the component is obtained by multiplying the commodityCost parameter with the annual sum of the time series of the components. The commodityCost can either be given as a float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro).

Example: * In a national energy system, natural gas could be purchased from another country with a certain cost.

|br| * the default value is 0

:type commodityCost:

* positive (>=0) float
* Pandas Series with positive (>=0).The indices of the series have to equal the in the energy system model specified locations.
* a dictionary with investment periods as keys and one of the two options above as values.

:param commodityRevenue: describes the revenue of one operation´s unit of the component. The revenue which is directly proportional to the operation of the component is obtained by multiplying the commodityRevenue parameter with the annual sum of the time series of the components. The commodityRevenue can either be given as a float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro).

Example: * Modeling a PV electricity feed-in tariff for a household

|br| * the default value is 0

:type commodityRevenue:

* positive (>=0) float
* Pandas Series with positive (>=0). The indices of the series have to equal the in the energy system model specified locations.
* a dictionary with investment periods as keys and one of the two options above as values.

:param balanceLimitID: ID for the respective balance limit (out of the balance limits introduced in the esM). Should be specified if the respective component of the SourceSinkModel is supposed to be included in the balance analysis. If the commodity is transported out of the region, it is counted as a negative, if it is imported into the region it is considered positive. |br| * the default value is None :type balanceLimitID: string

:param pathwayBalanceLimitID: similar to balanceLimitID just as restriction over the entire pathway. |br| * the default value is None :type pathwayBalanceLimitID: string

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate, fixed operation rate, and cost or revenue time series depending on whether a time series analysis is requested or not.

Source code in fine/sourceSink.py
 11
 12
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
def __init__(
    self,
    esM,
    name,
    commodity,
    hasCapacityVariable,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMin=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    commodityCost=0,
    commodityRevenue=0,
    commodityCostTimeSeries=None,
    commodityRevenueTimeSeries=None,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    yearlyFullLoadHoursMin=None,
    yearlyFullLoadHoursMax=None,
    balanceLimitID=None,
    pathwayBalanceLimitID=None,
    stockCommissioning=None,
    floorTechnicalLifetime=True,
    pwlcfParameters=None,
):
    """Create a Source class instance.
    The Source component specific input arguments are described below. The general component
    input arguments are described in the Component class.

    .. note::
        The Sink class inherits from the Source class and is initialized with the same parameter set.

    **Required arguments:**

    :param commodity: to the component related commodity.
    :type commodity: string

    :param hasCapacityVariable: specifies if the component should be modeled with a capacity or not.

    Examples:
        * A wind turbine has a capacity given in GW_electric -> hasCapacityVariable is True.
        * Emitting CO2 into the environment is not per se limited by a capacity ->
          hasCapacityVariable is False.

    :type hasCapacityVariable: boolean

    **Default arguments:**
    :param operationRateMin: if specified, indicates a minimum operation rate for each location and each time, if required also for each investment period, if
        step by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit for each time step.
        |br| * the default value is None
    :type operationRateMin:

        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model specified time steps. The column indices have to equal the
          in the energy system model specified locations. The data in ineligible locations are set to zero.
        * a dict

    :param operationRateMax: if specified, indicates a maximum operation rate for each location and each time, if required also for each investment period, if
        step by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit for each time step.
        |br| * the default value is None
    :type operationRateMax:

        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model specified time steps. The column indices have to equal the
          in the energy system model specified locations. The data in ineligible locations are set to zero.
        * a dictionary with investment periods as keys and one of the two options above as values


    :param operationRateFix: if specified, indicates a fixed operation rate for each location and each time, if required also for each investment period,
        step by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit for each time step.
        |br| * the default value is None
    :type operationRateFix:

        * None
        * Pandas DataFrame with positive (>=0) per investment period. The row indices have
          to match the in the energy system model specified time steps. The column indices have to equal the
          in the energy system model specified locations. The data in ineligible locations are set to zero.
        * a dictionary with investment periods as keys and one of the two options above as values


    :param commodityCostTimeSeries: if specified, indicates commodity cost rates for each location and each
        time step, if required also for each investment period, by a positive float. The values are given as specific values relative to the commodityUnit
        for each time step.
        |br| * the default value is None
    :type commodityCostTimeSeries:

        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model specified time steps. The column indices have to equal the
          in the energy system model specified locations. The data in ineligible locations are set to zero.
        * a dictionary with investment periods as keys and one of the two options above as values


    :param commodityRevenueTimeSeries:  if specified, indicates commodity revenue rate for each location and
        each time step, if required also for each investment period, by a positive float. The values are given as specific values relative to the
        commodityUnit for each time step.
        |br| * the default value is None
    :type commodityRevenueTimeSeries:

        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices
          have to match the in the energy system model specified time steps. The column indices have to equal
          the in the energy system model specified locations. The data in ineligible locations are set to zero.
        * a dictionary with investment periods as keys and one of the two options above as values


    :param tsaWeight: weight with which the time series of the component should be considered when applying
        time series aggregation.
        |br| * the default value is 1
    :type tsaWeight: positive (>= 0) float


    :param opexPerOperation: describes the cost for one unit of the operation. The cost which is directly
        proportional to the operation of the component is obtained by multiplying the opexPerOperation parameter
        with the annual sum of the operational time series of the components.
        The opexPerOperation can either be given as a float or a Pandas Series with location specific values or a dictionary per investment period with one of the previous options.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro).
        |br| * the default value is 0
    :type opexPerOperation:

        * positive (>=0) float
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.


    :param commodityCost: describes the cost value of one operation´s unit of the component.
        The cost which is directly proportional to the operation of the component
        is obtained by multiplying the commodityCost parameter with the annual sum of the
        time series of the components. The commodityCost can either be given as a
        float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro).

    Example:
        * In a national energy system, natural gas could be purchased from another country with a
          certain cost.

        |br| * the default value is 0
    :type commodityCost:

        * positive (>=0) float
        * Pandas Series with positive (>=0).The indices of the series have to equal the in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.


    :param commodityRevenue: describes the revenue of one operation´s unit of the component.
        The revenue which is directly proportional to the operation of the component
        is obtained by multiplying the commodityRevenue parameter with the annual sum of the
        time series of the components. The commodityRevenue can either be given as a
        float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro).

    Example:
        * Modeling a PV electricity feed-in tariff for a household

        |br| * the default value is 0
    :type commodityRevenue:

        * positive (>=0) float
        * Pandas Series with positive (>=0). The indices of the series have to equal the in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.


    :param balanceLimitID: ID for the respective balance limit (out of the balance limits introduced in the esM).
        Should be specified if the respective component of the SourceSinkModel is supposed to be included in
        the balance analysis. If the commodity is transported out of the region, it is counted as a negative, if
        it is imported into the region it is considered positive.
        |br| * the default value is None
    :type balanceLimitID: string

    :param pathwayBalanceLimitID: similar to balanceLimitID just as restriction over the entire pathway.
        |br| * the default value is None
    :type pathwayBalanceLimitID: string

    """
    Component.__init__(
        self,
        esM,
        name,
        dimension=Dimension.ONE,
        hasCapacityVariable=hasCapacityVariable,
        capacityVariableDomain=capacityVariableDomain,
        capacityPerPlantUnit=capacityPerPlantUnit,
        hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable,
        bigM=bigM,
        locationalEligibility=locationalEligibility,
        capacityMin=capacityMin,
        capacityMax=capacityMax,
        partLoadMin=partLoadMin,
        sharedPotentialID=sharedPotentialID,
        linkedQuantityID=linkedQuantityID,
        capacityFix=capacityFix,
        commissioningMin=commissioningMin,
        commissioningMax=commissioningMax,
        commissioningFix=commissioningFix,
        isBuiltFix=isBuiltFix,
        investPerCapacity=investPerCapacity,
        investIfBuilt=investIfBuilt,
        opexPerCapacity=opexPerCapacity,
        opexIfBuilt=opexIfBuilt,
        QPcostScale=QPcostScale,
        interestRate=interestRate,
        economicLifetime=economicLifetime,
        technicalLifetime=technicalLifetime,
        floorTechnicalLifetime=floorTechnicalLifetime,
        yearlyFullLoadHoursMin=yearlyFullLoadHoursMin,
        yearlyFullLoadHoursMax=yearlyFullLoadHoursMax,
        stockCommissioning=stockCommissioning,
        pwlcfParameters=pwlcfParameters,
    )

    # Set general source/sink data: ID and yearly limit
    utils.isEnergySystemModelInstance(esM), utils.checkCommodities(esM, {commodity})
    self.commodity, self.commodityUnit = (
        commodity,
        esM.commodityUnitsDict[commodity],
    )
    self.balanceLimitID = utils.checkAndSetBalanceLimitID(balanceLimitID)
    self.pathwayBalanceLimitID = utils.checkAndSetBalanceLimitID(
        pathwayBalanceLimitID
    )
    self.sign = 1
    self.modelingClass = SourceSinkModel

    # opexPerOperation
    self.opexPerOperation = opexPerOperation
    self.processedOpexPerOperation = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        opexPerOperation,
        Dimension.ONE,
        locationalEligibility,
        esM.investmentPeriods,
    )

    # commodityCost
    self.commodityCost = commodityCost
    self.processedCommodityCost = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        commodityCost,
        Dimension.ONE,
        locationalEligibility,
        esM.investmentPeriods,
    )

    # commodtyRevenue
    self.commodityRevenue = commodityRevenue
    self.processedCommodityRevenue = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        commodityRevenue,
        Dimension.ONE,
        locationalEligibility,
        esM.investmentPeriods,
    )

    # commodityCostTimeSeries
    self.commodityCostTimeSeries = commodityCostTimeSeries
    self.fullCommodityCostTimeSeries = (
        utils.checkAndSetInvestmentPeriodCostTimeSeries(
            esM, name, commodityCostTimeSeries, locationalEligibility
        )
    )
    self.aggregatedCommodityCostTimeSeries = dict.fromkeys(esM.investmentPeriods)
    self.processedCommodityCostTimeSeries = dict.fromkeys(esM.investmentPeriods)

    # commodityRevenueTimeSeries
    self.commodityRevenueTimeSeries = commodityRevenueTimeSeries
    self.fullCommodityRevenueTimeSeries = {}
    self.fullCommodityRevenueTimeSeries = (
        utils.checkAndSetInvestmentPeriodCostTimeSeries(
            esM, name, commodityRevenueTimeSeries, locationalEligibility
        )
    )
    self.aggregatedCommodityRevenueTimeSeries = dict.fromkeys(esM.investmentPeriods)
    self.processedCommodityRevenueTimeSeries = dict.fromkeys(esM.investmentPeriods)

    # operationRateMin
    self.operationRateMin = operationRateMin
    self.fullOperationRateMin = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateMin, locationalEligibility
    )
    self.aggregatedOperationRateMin = {}
    self.processedOperationRateMin = {}

    # operationRateMax
    self.operationRateMax = operationRateMax
    self.fullOperationRateMax = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateMax, locationalEligibility
    )
    self.aggregatedOperationRateMax = {}
    self.processedOperationRateMax = {}

    # operationRateFix
    self.operationRateFix = operationRateFix
    self.fullOperationRateFix = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateFix, locationalEligibility
    )
    self.aggregatedOperationRateFix = {}
    self.processedOperationRateFix = {}

    # check for operationRateMax and operationRateFix
    for ip in esM.investmentPeriods:
        if (
            self.fullOperationRateFix[ip] is not None
            and self.fullOperationRateMax[ip] is not None
        ):
            self.fullOperationRateMax[ip] = None
            if esM.verboseLogLevel < 2:
                warnings.warn(
                    "If operationRateFix is specified, the operationRateMax parameter is not required.\n"
                    + "The operationRateMax time series of investment period "
                    + f"'{esM.investmentPeriodNames[ip]}' was set to None."
                )
        if (
            self.fullOperationRateFix[ip] is not None
            and self.fullOperationRateMin[ip] is not None
        ):
            self.fullOperationRateMin[ip] = None
            if esM.verboseLogLevel < 2:
                warnings.warn(
                    "If operationRateFix is specified, the operationRateMin parameter is not required.\n"
                    + "The operationRateMin time series of investment period "
                    + f"'{esM.investmentPeriodNames[ip]}' was set to None."
                )

    utils.checkOperationRateForCapacityVariable(
        name,
        self.hasCapacityVariable,
        self.fullOperationRateMin,
        self.fullOperationRateMax,
        self.fullOperationRateFix,
    )

    # partLoadMin
    self.processedPartLoadMin = utils.checkAndSetPartLoadMin(
        esM,
        name,
        partLoadMin,
        self.fullOperationRateMax,
        self.fullOperationRateFix,
        self.bigM,
        self.hasCapacityVariable,
        self.fullOperationRateMin,
    )

    utils.isPositiveNumber(tsaWeight)
    self.tsaWeight = tsaWeight

    if any(x is not None for x in self.fullOperationRateFix.values()):
        operationTimeSeries = self.fullOperationRateFix
    elif any(x is not None for x in self.fullOperationRateMin.values()):
        operationTimeSeries = self.fullOperationRateMin
    elif any(x is not None for x in self.fullOperationRateMax.values()):
        operationTimeSeries = self.fullOperationRateMax
    else:
        operationTimeSeries = None

    self.processedLocationalEligibility = utils.setLocationalEligibility(
        esM,
        self.locationalEligibility,
        self.processedCapacityMax,
        self.processedCapacityFix,
        self.isBuiltFix,
        self.hasCapacityVariable,
        operationTimeSeries,
    )

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/sourceSink.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMin:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMin,
            "_operationRateMin_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    weightDict, data = self.prepareTSAInput(
        self.fullCommodityCostTimeSeries,
        "_commodityCostTimeSeries_",
        self.tsaWeight,
        weightDict,
        data,
        ip,
    )
    weightDict, data = self.prepareTSAInput(
        self.fullCommodityRevenueTimeSeries,
        "_commodityRevenueTimeSeries_",
        self.tsaWeight,
        weightDict,
        data,
        ip,
    )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the source component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/sourceSink.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the source component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )
    self.aggregatedOperationRateMin[ip] = self.getTSAOutput(
        self.fullOperationRateMin, "_operationRateMin_", data, ip
    )
    self.aggregatedCommodityCostTimeSeries[ip] = self.getTSAOutput(
        self.fullCommodityCostTimeSeries, "_commodityCostTimeSeries_", data, ip
    )
    self.aggregatedCommodityRevenueTimeSeries[ip] = self.getTSAOutput(
        self.fullCommodityRevenueTimeSeries,
        "_commodityRevenueTimeSeries_",
        data,
        ip,
    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate, fixed operation rate, and cost or revenue time series depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/sourceSink.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate, fixed operation rate, and cost or revenue time series depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    self.processedOperationRateMin = (
        self.aggregatedOperationRateMin if hasTSA else self.fullOperationRateMin
    )

    self.processedOperationRateMax = (
        self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
    )

    self.processedOperationRateFix = (
        self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
    )

    self.processedCommodityCostTimeSeries = (
        self.aggregatedCommodityCostTimeSeries
        if hasTSA
        else self.fullCommodityCostTimeSeries
    )

    self.processedCommodityRevenueTimeSeries = (
        self.aggregatedCommodityRevenueTimeSeries
        if hasTSA
        else self.fullCommodityRevenueTimeSeries
    )

Storage

Storage(
    esM,
    name,
    commodity,
    chargeRate=1,
    dischargeRate=1,
    chargeEfficiency=1,
    dischargeEfficiency=1,
    selfDischarge=0,
    cyclicLifetime=None,
    stateOfChargeMin=0,
    stateOfChargeMax=1,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    doPreciseTsaModeling=False,
    chargeOpRateMax=None,
    chargeOpRateFix=None,
    chargeTsaWeight=1,
    dischargeOpRateMax=None,
    dischargeOpRateFix=None,
    dischargeTsaWeight=1,
    isPeriodicalStorage=False,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerChargeOperation=0,
    opexPerDischargeOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    floorTechnicalLifetime=True,
    socOffsetDown=-1,
    socOffsetUp=-1,
    stockCommissioning=None,
    pwlcfParameters=None,
)

Bases: Component

A Storage component can store a commodity and thus transfers it between time steps.

Create a Storage class instance. The Storage component specific input arguments are described below. The general component input arguments are described in the Component class.

Required arguments:

:param commodity: to the component related commodity. :type commodity: string

Default arguments:

:param chargeRate: ratio of the maximum storage inflow (in commodityUnit/hour) to the storage capacity (in commodityUnit).

Example: * A hydrogen salt cavern which can store 133 GWh_H2_LHV can be charged 0.45 GWh_H2_LHV during one hour. The chargeRate thus equals 0.45/133 1/h.

|br| * the default value is 1

:type chargeRate: 0 < float

:param dischargeRate: ratio of the maximum storage outflow (in commodityUnit/hour) to the storage capacity (in commodityUnit).

Example: * A hydrogen salt cavern which can store 133 GWh_H2_LHV can be discharged 0.45 GWh_H2_LHV during one hour. The dischargeRate thus equals 0.45/133.

|br| * the default value is 1

:type dischargeRate: 0 < float

:param chargeEfficiency: defines the efficiency with which the storage can be charged (equals the percentage of the injected commodity that is transformed into stored commodity). Enter 0.98 for 98% etc. |br| * the default value is 1 :type chargeEfficiency: 0 <= float <=1

:param dischargeEfficiency: defines the efficiency with which the storage can be discharged (equals the percentage of the withdrawn commodity that is transformed into stored commodity). Enter 0.98 for 98% etc. |br| * the default value is 1 :type dischargeEfficiency: 0 <= float <=1

:param selfDischarge: percentage of self-discharge from the storage during one hour |br| * the default value is 0 :type selfDischarge: 0 <= float <=1

:param cyclicLifetime: if specified, the total number of full cycle equivalents that are supported by the technology.

Setting this parameter introduces a commissioning-dependent charge operation
variable with one entry per *(loc, compName, commis, ip, p, t)* tuple.
This can significantly increase the number of optimization variables and
constraints, especially for models with many investment periods or long
technical lifetimes, and may noticeably increase solver runtime.

The state of charge is tracked for the total installed capacity, not
per commissioning year. As a result, the optimizer may allocate charge to a
single commissioning year beyond what its commissioned capacity could
physically hold, as long as the aggregate SoC constraint is satisfied.

|br| * the default value is None

:type cyclicLifetime: None or positive float

:param stateOfChargeMin: threshold (percentage) that the state of charge can not drop under |br| * the default value is 0 :type stateOfChargeMax: * 0 <= float <=1 * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param stateOfChargeMax: threshold (percentage) that the state of charge can not exceed |br| * the default value is 1 :type stateOfChargeMax: * 0 <= float <=1 * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param doPreciseTsaModeling: determines whether the state of charge is limited precisely (True) or with a simplified method (False). The error is small if the selfDischarge is small. |br| * the default value is False :type doPreciseTsaModeling: boolean

:param chargeOpRateMax: if specified, indicates a maximum charging rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit, referring to the charged commodity (before multiplying the charging efficiency) during one time step. |br| * the default value is None :type chargeOpRateMax: * None * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param chargeOpRateFix: if specified, indicates a fixed charging rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit, referring to the charged commodity (before multiplying the charging efficiency) during one time step. |br| * the default value is None :type chargeOpRateFix: * None * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param chargeTsaWeight: weight with which the chargeOpRate (max/fix) time series of the component should be considered when applying time series aggregation. |br| * the default value is 1 :type chargeTsaWeight: positive (>= 0) float

:param dischargeOpRateMax: if specified, indicates a maximum discharging rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit, referring to the discharged commodity (after multiplying the discharging efficiency) during one time step. |br| * the default value is None :type dischargeOpRateMax: * None * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param dischargeOpRateFix: if specified, indicates a fixed discharging rate for each location and each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit, referring to the charged commodity (after multiplying the discharging efficiency) during one time step. |br| * the default value is None :type dischargeOpRateFix: * None * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices have to match the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param dischargeTsaWeight: weight with which the dischargeOpRate (max/fix) time series of the component should be considered when applying time series aggregation. |br| * the default value is 1 :type dischargeTsaWeight: positive (>= 0) float

:param isPeriodicalStorage: indicates if the state of charge of the storage has to be at the same value after the end of each period. This is especially relevant when using daily periods where short term storage can be restrained to daily cycles. Benefits the run time of the model. |br| * the default value is False :type isPeriodicalStorage: boolean

:param opexPerChargeOperation: describes the cost for one unit of the charge operation. The cost which is directly proportional to the charge operation of the component is obtained by multiplying the opexPerChargeOperation parameter with the annual sum of the operational time series of the components. The opexPerChargeOperation can either be given as a float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro). |br| * the default value is 0 :type opexPerChargeOperation: positive (>=0) float or Pandas Series with positive (>=0) values or dict of positive (>=0) float or Pandas Series with positive (>=0) values per investment period. The indices of the series have to equal the in the energy system model specified locations.

:param opexPerDischargeOperation: describes the cost for one unit of the discharge operation. The cost which is directly proportional to the discharge operation of the component is obtained by multiplying the opexPerDischargeOperation parameter with the annual sum of the operational time series of the components. The opexPerDischargeOperation can either be given as a float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro). |br| * the default value is 0 :type opexPerDischargeOperation: * positive (>=0) float * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param socOffsetDown: determines whether the state of charge at the end of a period p has to be equal to the one at the beginning of a period p+1 (socOffsetDown=-1) or if it can be smaller at the beginning of p+1 (socOffsetDown>=0). In the latter case, the product of the parameter socOffsetDown and the actual soc offset is used as a penalty factor in the objective function. |br| * the default value is -1 :type socOffsetDown: float

:param socOffsetUp: determines whether the state of charge at the end of a period p has to be equal to the one at the beginning of a period p+1 (socOffsetUp=-1) or if it can be larger at the beginning of p+1 (socOffsetUp>=0). In the latter case, the product of the parameter socOffsetUp and the actual soc offset is used as a penalty factor in the objective function. |br| * the default value is -1 :type socOffsetUp: float

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate for charging and discharging.

  • setTimeSeriesData

    Set the maximum operation rate and fixed operation rate for charging and discharging depending on whether a time series analysis is requested or not.

Source code in fine/storage.py
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
456
457
def __init__(
    self,
    esM,
    name,
    commodity,
    chargeRate=1,
    dischargeRate=1,
    chargeEfficiency=1,
    dischargeEfficiency=1,
    selfDischarge=0,
    cyclicLifetime=None,
    stateOfChargeMin=0,
    stateOfChargeMax=1,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    doPreciseTsaModeling=False,
    chargeOpRateMax=None,
    chargeOpRateFix=None,
    chargeTsaWeight=1,
    dischargeOpRateMax=None,
    dischargeOpRateFix=None,
    dischargeTsaWeight=1,
    isPeriodicalStorage=False,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerChargeOperation=0,
    opexPerDischargeOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    floorTechnicalLifetime=True,
    socOffsetDown=-1,
    socOffsetUp=-1,
    stockCommissioning=None,
    pwlcfParameters=None,
):
    """Create a Storage class instance.
    The Storage component specific input arguments are described below. The general component
    input arguments are described in the Component class.

    **Required arguments:**

    :param commodity: to the component related commodity.
    :type commodity: string

    **Default arguments:**

    :param chargeRate: ratio of the maximum storage inflow (in commodityUnit/hour) to the
        storage capacity (in commodityUnit).

    Example:
        * A hydrogen salt cavern which can store 133 GWh_H2_LHV can be charged 0.45 GWh_H2_LHV during
          one hour. The chargeRate thus equals 0.45/133 1/h.

        |br| * the default value is 1
    :type chargeRate: 0 < float

    :param dischargeRate: ratio of the maximum storage outflow (in commodityUnit/hour) to
        the storage capacity (in commodityUnit).

    Example:
        * A hydrogen salt cavern which can store 133 GWh_H2_LHV can be discharged 0.45 GWh_H2_LHV during
          one hour. The dischargeRate thus equals 0.45/133.

        |br| * the default value is 1
    :type dischargeRate: 0 < float

    :param chargeEfficiency: defines the efficiency with which the storage can be charged (equals
        the percentage of the injected commodity that is transformed into stored commodity).
        Enter 0.98 for 98% etc.
        |br| * the default value is 1
    :type chargeEfficiency: 0 <= float <=1

    :param dischargeEfficiency: defines the efficiency with which the storage can be discharged
        (equals the percentage of the withdrawn commodity that is transformed into stored commodity).
        Enter 0.98 for 98% etc.
        |br| * the default value is 1
    :type dischargeEfficiency: 0 <= float <=1

    :param selfDischarge: percentage of self-discharge from the storage during one hour
        |br| * the default value is 0
    :type selfDischarge: 0 <= float <=1

    :param cyclicLifetime: if specified, the total number of full cycle equivalents that are supported
        by the technology.

        Setting this parameter introduces a commissioning-dependent charge operation
        variable with one entry per *(loc, compName, commis, ip, p, t)* tuple.
        This can significantly increase the number of optimization variables and
        constraints, especially for models with many investment periods or long
        technical lifetimes, and may noticeably increase solver runtime.

        The state of charge is tracked for the total installed capacity, not
        per commissioning year. As a result, the optimizer may allocate charge to a
        single commissioning year beyond what its commissioned capacity could
        physically hold, as long as the aggregate SoC constraint is satisfied.

        |br| * the default value is None
    :type cyclicLifetime: None or positive float

    :param stateOfChargeMin: threshold (percentage) that the state of charge can not drop under
        |br| * the default value is 0
    :type stateOfChargeMax:
        * 0 <= float <=1
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model  specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param stateOfChargeMax: threshold (percentage) that the state of charge can not exceed
        |br| * the default value is 1
    :type stateOfChargeMax:
        * 0 <= float <=1
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model  specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param doPreciseTsaModeling: determines whether the state of charge is limited precisely (True) or
        with a simplified method (False). The error is small if the selfDischarge is small.
        |br| * the default value is False
    :type doPreciseTsaModeling: boolean

    :param chargeOpRateMax: if specified, indicates a maximum charging rate for each location and each time
        step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit, referring to the charged commodity (before multiplying the charging efficiency)
        during one time step.
        |br| * the default value is None
    :type chargeOpRateMax:
        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model  specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param chargeOpRateFix: if specified, indicates a fixed charging rate for each location and each time
        step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit, referring to the charged commodity (before multiplying the charging efficiency)
        during one time step.
        |br| * the default value is None
    :type chargeOpRateFix:
        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model  specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param chargeTsaWeight: weight with which the chargeOpRate (max/fix) time series of the
        component should be considered when applying time series aggregation.
        |br| * the default value is 1
    :type chargeTsaWeight: positive (>= 0) float

    :param dischargeOpRateMax: if specified, indicates a maximum discharging rate for each location and each
        time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit, referring to the discharged commodity (after multiplying the discharging
        efficiency) during one time step.
        |br| * the default value is None
    :type dischargeOpRateMax:
        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model  specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param dischargeOpRateFix: if specified, indicates a fixed discharging rate for each location and each
        time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative
        to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the
        capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form
        of the commodityUnit, referring to the charged commodity (after multiplying the discharging
        efficiency) during one time step.
        |br| * the default value is None
    :type dischargeOpRateFix:
        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model  specified time steps. The column indices have to match the
          in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param dischargeTsaWeight: weight with which the dischargeOpRate (max/fix) time series of the
        component should be considered when applying time series aggregation.
        |br| * the default value is 1
    :type dischargeTsaWeight: positive (>= 0) float

    :param isPeriodicalStorage: indicates if the state of charge of the storage has to be at the same value
        after the end of each period. This is especially relevant when using daily periods where short term
        storage can be restrained to daily cycles. Benefits the run time of the model.
        |br| * the default value is False
    :type isPeriodicalStorage: boolean

    :param opexPerChargeOperation: describes the cost for one unit of the charge operation.
        The cost which is directly proportional to the charge operation of the
        component is obtained by multiplying the opexPerChargeOperation parameter with the annual sum of the
        operational time series of the components. The opexPerChargeOperation can either be given as a float
        or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro).
        |br| * the default value is 0
    :type opexPerChargeOperation: positive (>=0) float or Pandas Series with positive (>=0) values or dict of
        positive (>=0) float or Pandas Series with positive (>=0) values per investment period.
        The indices of the series have to equal the in the energy system model specified locations.

    :param opexPerDischargeOperation: describes the cost for one unit of the discharge operation.
        The cost which is directly proportional to the discharge operation
        of the component is obtained by multiplying the opexPerDischargeOperation parameter with the annual sum
        of the operational time series of the components. The opexPerDischargeOperation can either be given as
        a float or a Pandas Series with location specific values or a dictionary per investment period with one of the two previous options.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro).
        |br| * the default value is 0
    :type opexPerDischargeOperation:
        * positive (>=0) float
        * Pandas Series with positive (>=0) values. The indices of the series have to equal the in the energy system model specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param socOffsetDown: determines whether the state of charge at the end of a period p has
        to be equal to the one at the beginning of a period p+1 (socOffsetDown=-1) or if
        it can be smaller at the beginning of p+1 (socOffsetDown>=0). In the latter case,
        the product of the parameter socOffsetDown and the actual soc offset is used as a penalty
        factor in the objective function.
        |br| * the default value is -1
    :type socOffsetDown: float

    :param socOffsetUp: determines whether the state of charge at the end of a period p has
        to be equal to the one at the beginning of a period p+1 (socOffsetUp=-1) or if
        it can be larger at the beginning of p+1 (socOffsetUp>=0). In the latter case,
        the product of the parameter socOffsetUp and the actual soc offset is used as a penalty
        factor in the objective function.
        |br| * the default value is -1
    :type socOffsetUp: float

    """
    Component.__init__(
        self,
        esM,
        name,
        dimension=Dimension.ONE,
        hasCapacityVariable=hasCapacityVariable,
        capacityVariableDomain=capacityVariableDomain,
        capacityPerPlantUnit=capacityPerPlantUnit,
        hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable,
        bigM=bigM,
        locationalEligibility=locationalEligibility,
        capacityMin=capacityMin,
        capacityMax=capacityMax,
        partLoadMin=partLoadMin,
        sharedPotentialID=sharedPotentialID,
        linkedQuantityID=linkedQuantityID,
        capacityFix=capacityFix,
        commissioningMin=commissioningMin,
        commissioningMax=commissioningMax,
        commissioningFix=commissioningFix,
        isBuiltFix=isBuiltFix,
        investPerCapacity=investPerCapacity,
        investIfBuilt=investIfBuilt,
        opexPerCapacity=opexPerCapacity,
        opexIfBuilt=opexIfBuilt,
        interestRate=interestRate,
        economicLifetime=economicLifetime,
        technicalLifetime=technicalLifetime,
        stockCommissioning=stockCommissioning,
        floorTechnicalLifetime=floorTechnicalLifetime,
        pwlcfParameters=pwlcfParameters,
    )

    # Set general storage component data: chargeRate, dischargeRate, chargeEfficiency, dischargeEfficiency,
    # selfDischarge, cyclicLifetime, stateOfChargeMin, stateOfChargeMax, isPeriodicalStorage, doPreciseTsaModeling,
    # relaxedPeriodConnection
    utils.checkCommodities(esM, {commodity})
    self.commodity, self.commodityUnit = (
        commodity,
        esM.commodityUnitsDict[commodity],
    )

    utils.isStrictlyPositiveNumber(chargeRate)
    self.chargeRate = chargeRate
    utils.isStrictlyPositiveNumber(dischargeRate)
    self.dischargeRate = dischargeRate
    self.chargeEfficiency = utils.isInRange(chargeEfficiency, 0, 1)
    self.dischargeEfficiency = utils.isInRange(dischargeEfficiency, 0, 1)
    self.selfDischarge = utils.isInRange(selfDischarge, 0, 1)
    self.cyclicLifetime = cyclicLifetime
    self.stateOfChargeMin = stateOfChargeMin
    self.stateOfChargeMax = stateOfChargeMax
    self.isPeriodicalStorage = isPeriodicalStorage
    self.doPreciseTsaModeling = doPreciseTsaModeling
    self.socOffsetUp = socOffsetUp
    self.socOffsetDown = socOffsetDown
    self.modelingClass = StorageModel

    self.fullStateOfChargeMin = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, stateOfChargeMin, locationalEligibility
    )
    self.aggregatedStateOfChargeMin = dict.fromkeys(esM.investmentPeriods)

    self.fullStateOfChargeMax = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, stateOfChargeMax, locationalEligibility
    )
    self.aggregatedStateOfChargeMax = dict.fromkeys(esM.investmentPeriods)

    # opexPerChargeOperation
    self.opexPerChargeOperation = opexPerChargeOperation
    self.processedOpexPerChargeOperation = (
        utils.checkAndSetInvestmentPeriodCostParameter(
            esM,
            name,
            opexPerChargeOperation,
            Dimension.ONE,
            locationalEligibility,
            esM.investmentPeriods,
        )
    )

    # opexPerDischargeOperation
    self.opexPerDischargeOperation = opexPerDischargeOperation
    self.processedOpexPerDischargeOperation = (
        utils.checkAndSetInvestmentPeriodCostParameter(
            esM,
            name,
            opexPerDischargeOperation,
            Dimension.ONE,
            locationalEligibility,
            esM.investmentPeriods,
        )
    )

    # chargeOpRateFix and chargeOpRateMax
    self.chargeOpRateMax = chargeOpRateMax
    self.chargeOpRateFix = chargeOpRateFix

    # chargeOpRateMax
    self.fullChargeOpRateMax = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, chargeOpRateMax, locationalEligibility
    )
    self.aggregatedChargeOpRateMax = dict.fromkeys(esM.investmentPeriods)

    # chargeOpRateFix
    self.fullChargeOpRateFix = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, chargeOpRateFix, locationalEligibility
    )
    self.aggregatedChargeOpRateFix = dict.fromkeys(esM.investmentPeriods)

    # dischargeOpRateMax
    self.dischargeOpRateMax = dischargeOpRateMax
    self.fullDischargeOpRateMax = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, dischargeOpRateMax, locationalEligibility
    )
    self.aggregatedDischargeOpRateMax = {}

    # dischargeOpRateFix
    self.dischargeOpRateFix = dischargeOpRateFix
    self.fullDischargeOpRateFix = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, dischargeOpRateFix, locationalEligibility
    )
    self.aggregatedDischargeOpRateFix = dict.fromkeys(esM.investmentPeriods)

    # check for chargeOpRateMax and chargeOpRateFix
    for ip in esM.investmentPeriods:
        if (
            self.fullChargeOpRateMax[ip] is not None
            and self.fullChargeOpRateFix[ip] is not None
        ):
            self.fullChargeOpRateMax[ip] = None
            if esM.verboseLogLevel < 2:
                warnings.warn(
                    "If chargeOpRateFix is specified, the chargeOpRateMax parameter is not required.\n"
                    + "The chargeOpRateMax time series was set to None."
                )

    # partLoadMin
    self.processedPartLoadMin = utils.checkAndSetPartLoadMin(
        esM,
        name,
        partLoadMin,
        self.fullChargeOpRateMax,
        self.fullChargeOpRateFix,
        self.bigM,
        self.hasCapacityVariable,
    )

    utils.isPositiveNumber(dischargeTsaWeight)
    self.dischargeTsaWeight = dischargeTsaWeight
    utils.isPositiveNumber(chargeTsaWeight)
    self.chargeTsaWeight = chargeTsaWeight

    timeSeriesData = {}
    for ip in esM.investmentPeriods:
        tsNb = sum(
            [
                0 if data is None else 1
                for data in [
                    self.fullChargeOpRateMax[ip],
                    self.fullChargeOpRateFix[ip],
                    self.fullDischargeOpRateMax[ip],
                    self.fullDischargeOpRateFix[ip],
                ]
            ]
        )
        if tsNb > 0:
            timeSeriesData[ip] = sum(
                [
                    data
                    for data in [
                        self.fullChargeOpRateMax[ip],
                        self.fullChargeOpRateFix[ip],
                        self.fullDischargeOpRateMax[ip],
                        self.fullDischargeOpRateFix[ip],
                    ]
                    if data is not None
                ]
            )
        else:
            timeSeriesData[ip] = None

    self.processedLocationalEligibility = utils.setLocationalEligibility(
        esM,
        self.locationalEligibility,
        self.processedCapacityMax,
        self.processedCapacityFix,
        self.isBuiltFix,
        self.hasCapacityVariable,
        timeSeriesData,
    )

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/storage.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int

    """
    weightDict, data = {}, []
    tsa_input = [
        (
            self.fullChargeOpRateFix,
            self.fullChargeOpRateMax,
            "chargeRate_",
            self.chargeTsaWeight,
        ),
        (
            self.fullDischargeOpRateFix,
            self.fullDischargeOpRateMax,
            "dischargeRate_",
            self.dischargeTsaWeight,
        ),
    ]

    for rateFix, rateMax, rateName, rateWeight in tsa_input:
        if rateFix:
            weightDict, data = self.prepareTSAInput(
                rateFix, rateName, rateWeight, weightDict, data, ip
            )
        if rateMax:
            weightDict, data = self.prepareTSAInput(
                rateMax, rateName, rateWeight, weightDict, data, ip
            )

    combinedTsaWeight = (self.chargeTsaWeight + self.dischargeTsaWeight) / 2
    if not isinstance(self.stateOfChargeMin, int | float):
        # only consider stateOfChargeMin in tsa if it is not a constant value
        weightDict, data = self.prepareTSAInput(
            self.fullStateOfChargeMin,
            "stateOfChargeMin_",
            combinedTsaWeight,
            weightDict,
            data,
            ip,
        )
    if not isinstance(self.stateOfChargeMax, int | float):
        # only consider stateOfChargeMax in tsa if it is not a constant value
        weightDict, data = self.prepareTSAInput(
            self.fullStateOfChargeMax,
            "stateOfChargeMax_",
            combinedTsaWeight,
            weightDict,
            data,
            ip,
        )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate for charging and discharging.

:param data: Pandas DataFrame with the clustered time series data of the source component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/storage.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate for charging and discharging.

    :param data: Pandas DataFrame with the clustered time series data of the source component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    self.aggregatedChargeOpRateFix[ip] = self.getTSAOutput(
        self.fullChargeOpRateFix, "chargeRate_", data, ip
    )
    self.aggregatedChargeOpRateMax[ip] = self.getTSAOutput(
        self.fullChargeOpRateMax, "chargeRate_", data, ip
    )

    self.aggregatedDischargeOpRateFix[ip] = self.getTSAOutput(
        self.fullDischargeOpRateFix, "dischargeRate_", data, ip
    )
    self.aggregatedDischargeOpRateMax[ip] = self.getTSAOutput(
        self.fullDischargeOpRateMax, "dischargeRate_", data, ip
    )

    if isinstance(self.stateOfChargeMin, int | float):
        # if stateOfChargeMin is a constant value, the aggregatedStateOfChargeMax is
        # created from a constant value as it was not considered within the tsa
        self.aggregatedStateOfChargeMin[ip] = pd.DataFrame(
            index=data.index,
            columns=self.fullStateOfChargeMin[ip].columns,
            data=self.stateOfChargeMin,
        )
    else:
        self.aggregatedStateOfChargeMin[ip] = self.getTSAOutput(
            self.fullStateOfChargeMin, "stateOfChargeMin_", data, ip
        )

    if isinstance(self.stateOfChargeMax, int | float):
        # if stateOfChargeMax is a constant value, the aggregatedStateOfChargeMax is
        # created from a constant value as it was not considered within the tsa
        self.aggregatedStateOfChargeMax[ip] = pd.DataFrame(
            index=data.index,
            columns=self.fullStateOfChargeMax[ip].columns,
            data=self.stateOfChargeMax,
        )
    else:
        self.aggregatedStateOfChargeMax[ip] = self.getTSAOutput(
            self.fullStateOfChargeMax, "stateOfChargeMax_", data, ip
        )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate and fixed operation rate for charging and discharging depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/storage.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate and fixed operation rate for charging and discharging depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    self.processedChargeOpRateMax = (
        self.aggregatedChargeOpRateMax if hasTSA else self.fullChargeOpRateMax
    )
    self.processedChargeOpRateFix = (
        self.aggregatedChargeOpRateFix if hasTSA else self.fullChargeOpRateFix
    )
    self.processedDischargeOpRateMax = (
        self.aggregatedDischargeOpRateMax if hasTSA else self.fullDischargeOpRateMax
    )
    self.processedDischargeOpRateFix = (
        self.aggregatedDischargeOpRateFix if hasTSA else self.fullDischargeOpRateFix
    )
    self.processedStateOfChargeMax = (
        self.aggregatedStateOfChargeMax if hasTSA else self.fullStateOfChargeMax
    )
    self.processedStateOfChargeMin = (
        self.aggregatedStateOfChargeMin if hasTSA else self.fullStateOfChargeMin
    )

Transmission

Transmission(
    esM,
    name,
    commodity,
    losses=0,
    distances=None,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    floorTechnicalLifetime=True,
    balanceLimitID=None,
    pathwayBalanceLimitID=None,
    stockCommissioning=None,
    pwlcfParameters=None,
)

Bases: Component

A Transmission component can transmit a commodity between locations of the energy system.

Create a Transmission class instance. The Transmission component specific input arguments are described below. The general component input arguments are described in the Component class.

Required arguments:

:param commodity: to the component related commodity. :type commodity: string

Default arguments:

:param losses: relative losses per lengthUnit (lengthUnit as specified in the energy system model) in percentage of the commodity flow. This loss factor can capture simple linear losses

.. math::
    trans_{in, ij} = (1 - \\text{losses} \\cdot \\text{distances}) \\cdot trans_{out, ij}

(with trans being the commodity flow at a certain point in
time and i and j being locations in the energy system). The losses can either be given as a float or a
Pandas DataFrame with location specific values.
|br| * the default value is 0

:type losses: positive float (0 <= float <= 1) or Pandas DataFrame with positive values (0 <= float <= 1). The row and column indices of the DataFrame have to equal the in the energy system model specified locations.

:param distances: distances between locations given in the lengthUnit (lengthUnit as specified in the energy system model). |br| * the default value is None :type distances: positive float (>= 0) or Pandas DataFrame with positive values (>= 0). The row and column indices of the DataFrame have to equal the in the energy system model specified locations.

:param operationRateMax: if specified, indicates a maximum operation rate for all possible connections (both directions) of the transmission component at each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit, referring to the transmitted commodity (before considering losses) during one time step. |br| * the default value is None :type operationRateMax: * None * Pandas DataFrame with positive (>= 0) entries. The row indices have to match the in the energy system model specified time steps. The column indices are combinations of locations (as defined in the energy system model), separated by a underscore (e.g. "location1_location2"). The first location indicates where the commodity is coming from. The second location indicates where the commodity is going too. If a flow is specified from location i to location j, it also has to be specified from j to i. * a dictionary with investment periods as keys and one of the two options above as values.

:param operationRateFix: if specified, indicates a fixed operation rate for all possible connections (both directions) of the transmission component at each time step, if required also for each investment period, by a positive float. If hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e. a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable is set to False, the values are given as absolute values in form of the commodityUnit, referring to the transmitted commodity (before considering losses) during one time step. |br| * the default value is None :type operationRateFix: * None * Pandas DataFrame with positive (>= 0). The row indices have to match the in the energy system model specified time steps. The column indices are combinations of locations (as defined in the energy system model), separated by a underscore (e.g. "location1_location2"). The first location indicates where the commodity is coming from. The second one location indicates where the commodity is going too. If a flow is specified from location i to location j, it also has to be specified from j to i. * a dictionary with investment periods as keys and one of the two options above as values.

:param tsaWeight: weight with which the time series of the component should be considered when applying time series aggregation. |br| * the default value is 1 :type tsaWeight: positive (>= 0) float

:param opexPerOperation: describes the cost for one unit of the operation. The cost which is directly proportional to the operation of the component is obtained by multiplying the opexPerOperation parameter with the annual sum of the operational time series of the components. The opexPerOperation can either be given as a float or a Pandas DataFrame with location specific values or a dictionary per investment period with one of the previous options. The cost unit in which the parameter is given has to match the one specified in the energy system model (e.g. Euro, Dollar, 1e6 Euro). The value has to match the unit costUnit/operationUnit (e.g. Euro/kWh, Dollar/kWh). |br| * the default value is 0 :type opexPerOperation: * positive (>=0) float * Pandas DataFrame with positive (>=0).The row and column indices of the DataFrame have to equal the in the energy system model specified locations. * a dictionary with investment periods as keys and one of the two options above as values.

:param balanceLimitID: ID for the respective balance limit (out of the balance limits introduced in the esM). Should be specified if the respective component of the TransmissionModel is supposed to be included in the balance analysis. If the commodity is transported out of the region, it is counted as a negative, if it is imported into the region it is considered positive. |br| * the default value is None :type balanceLimitID: string

:param pathwayBalanceLimitID: similar to balanceLimitID just as restriction over the entire pathway. |br| * the default value is None :type pathwayBalanceLimitID: string

Methods:

  • addToEnergySystemModel

    Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in

  • getDataForTimeSeriesAggregation

    Get the required data if a time series aggregation is requested.

  • getTSAOutput

    Return a reformatted time series data after applying time series aggregation, if the original time series

  • prepareTSAInput

    Format the time series data of a component to fit the requirements of the time series aggregation package and

  • setAggregatedTimeSeriesData

    Determine the aggregated maximum rate and the aggregated fixed operation rate.

  • setTimeSeriesData

    Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

Source code in fine/transmission.py
 13
 14
 15
 16
 17
 18
 19
 20
 21
 22
 23
 24
 25
 26
 27
 28
 29
 30
 31
 32
 33
 34
 35
 36
 37
 38
 39
 40
 41
 42
 43
 44
 45
 46
 47
 48
 49
 50
 51
 52
 53
 54
 55
 56
 57
 58
 59
 60
 61
 62
 63
 64
 65
 66
 67
 68
 69
 70
 71
 72
 73
 74
 75
 76
 77
 78
 79
 80
 81
 82
 83
 84
 85
 86
 87
 88
 89
 90
 91
 92
 93
 94
 95
 96
 97
 98
 99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
441
442
443
444
445
446
447
448
449
450
451
452
453
454
455
def __init__(
    self,
    esM,
    name,
    commodity,
    losses=0,
    distances=None,
    hasCapacityVariable=True,
    capacityVariableDomain="continuous",
    capacityPerPlantUnit=1,
    hasIsBuiltBinaryVariable=False,
    bigM=None,
    operationRateMax=None,
    operationRateFix=None,
    tsaWeight=1,
    locationalEligibility=None,
    capacityMin=None,
    capacityMax=None,
    partLoadMin=None,
    sharedPotentialID=None,
    linkedQuantityID=None,
    capacityFix=None,
    commissioningMin=None,
    commissioningMax=None,
    commissioningFix=None,
    isBuiltFix=None,
    investPerCapacity=0,
    investIfBuilt=0,
    opexPerOperation=0,
    opexPerCapacity=0,
    opexIfBuilt=0,
    QPcostScale=0,
    interestRate=0.08,
    economicLifetime=10,
    technicalLifetime=None,
    floorTechnicalLifetime=True,
    balanceLimitID=None,
    pathwayBalanceLimitID=None,
    stockCommissioning=None,
    pwlcfParameters=None,
):
    r"""Create a Transmission class instance.
    The Transmission component specific input arguments are described below. The general component
    input arguments are described in the Component class.

    **Required arguments:**

    :param commodity: to the component related commodity.
    :type commodity: string

    **Default arguments:**

    :param losses: relative losses per lengthUnit (lengthUnit as specified in the energy system model) in
        percentage of the commodity flow. This loss factor can capture simple linear losses

        .. math::
            trans_{in, ij} = (1 - \\text{losses} \\cdot \\text{distances}) \\cdot trans_{out, ij}

        (with trans being the commodity flow at a certain point in
        time and i and j being locations in the energy system). The losses can either be given as a float or a
        Pandas DataFrame with location specific values.
        |br| * the default value is 0
    :type losses: positive float (0 <= float <= 1) or Pandas DataFrame with positive values
        (0 <= float <= 1). The row and column indices of the DataFrame have to equal the in the energy
        system model specified locations.

    :param distances: distances between locations given in the lengthUnit (lengthUnit as specified in
        the energy system model).
        |br| * the default value is None
    :type distances: positive float (>= 0) or Pandas DataFrame with positive values (>= 0). The row and
        column indices of the DataFrame have to equal the in the energy system model specified locations.

    :param operationRateMax: if specified, indicates a maximum operation rate for all possible connections
        (both directions) of the transmission component at each time step, if required also for each investment period, by a positive float. If
        hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e.
        a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable
        is set to False, the values are given as absolute values in form of the commodityUnit,
        referring to the transmitted commodity (before considering losses) during one time step.
        |br| * the default value is None
    :type operationRateMax:
        * None
        * Pandas DataFrame with positive (>= 0) entries. The row indices have
          to match the in the energy system model specified time steps. The column indices are combinations
          of locations (as defined in the energy system model), separated by a underscore (e.g.
          "location1_location2"). The first location indicates where the commodity is coming from. The second
          location indicates where the commodity is going too. If a flow is specified from location i to
          location j, it also has to be specified from j to i.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param operationRateFix: if specified, indicates a fixed operation rate for all possible connections
        (both directions) of the transmission component at each time step, if required also for each investment period, by a positive float. If
        hasCapacityVariable is set to True, the values are given relative to the installed capacities (i.e.
        a value of 1 indicates a utilization of 100% of the capacity). If hasCapacityVariable
        is set to False, the values are given as absolute values in form of the commodityUnit,
        referring to the transmitted commodity (before considering losses) during one time step.
        |br| * the default value is None
    :type operationRateFix:
        * None
        * Pandas DataFrame with positive (>= 0). The row indices have
          to match the in the energy system model specified time steps. The column indices are combinations
          of locations (as defined in the energy system model), separated by a underscore (e.g.
          "location1_location2"). The first location indicates where the commodity is coming from. The second
          one location indicates where the commodity is going too. If a flow is specified from location i to
          location j, it also has to be specified from j to i.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param tsaWeight: weight with which the time series of the component should be considered when applying
        time series aggregation.
        |br| * the default value is 1
    :type tsaWeight: positive (>= 0) float

    :param opexPerOperation: describes the cost for one unit of the operation.
        The cost which is directly proportional to the operation of the component is obtained by multiplying
        the opexPerOperation parameter with the annual sum of the operational time series of the components.
        The opexPerOperation can either be given as a float or a Pandas DataFrame with location specific values or a dictionary per investment period with one of the previous options.
        The cost unit in which the parameter is given has to match the one specified in the energy
        system model (e.g. Euro, Dollar, 1e6 Euro). The value has to match the unit costUnit/operationUnit
        (e.g. Euro/kWh, Dollar/kWh).
        |br| * the default value is 0
    :type opexPerOperation:
        * positive (>=0) float
        * Pandas DataFrame with positive (>=0).The row and column indices of the DataFrame have to equal the in the energy system model
          specified locations.
        * a dictionary with investment periods as keys and one of the two options above as values.

    :param balanceLimitID: ID for the respective balance limit (out of the balance limits introduced in the esM).
        Should be specified if the respective component of the TransmissionModel is supposed to be included in
        the balance analysis. If the commodity is transported out of the region, it is counted as a negative, if
        it is imported into the region it is considered positive.
        |br| * the default value is None
    :type balanceLimitID: string

    :param pathwayBalanceLimitID: similar to balanceLimitID just as restriction over the entire pathway.
        |br| * the default value is None
    :type pathwayBalanceLimitID: string
    """
    self.capacityMax = utils.checkCapacityOrCommissioningTransmission(capacityMax)
    self.capacityMin = utils.checkCapacityOrCommissioningTransmission(capacityMin)
    self.capacityFix = utils.checkCapacityOrCommissioningTransmission(capacityFix)
    self.commissioningMax = utils.checkCapacityOrCommissioningTransmission(
        commissioningMax
    )
    self.commissioningMin = utils.checkCapacityOrCommissioningTransmission(
        commissioningMin
    )
    self.commissioningFix = utils.checkCapacityOrCommissioningTransmission(
        commissioningFix
    )

    # Preprocess two-dimensional data
    self.locationalEligibility = utils.preprocess2dimData(locationalEligibility)
    preprocessedCapacityMax = utils.process2dimCapacityData(
        esM,
        "capacityMax",
        capacityMax,
        esM.investmentPeriods,
    )
    preprocessedCapacityFix = utils.process2dimCapacityData(
        esM,
        "capacityFix",
        capacityFix,
        esM.investmentPeriods,
    )
    self.isBuiltFix = utils.preprocess2dimData(
        isBuiltFix, locationalEligibility=locationalEligibility
    )

    # Set locational eligibility
    if operationRateFix is None:
        operationTimeSeries = operationRateMax
    elif not isinstance(operationRateFix, dict):
        operationTimeSeries = operationRateFix
    elif isinstance(operationRateFix, dict) and any(
        x is not None for x in operationRateFix.values()
    ):
        if not all(x is not None for x in operationRateFix.values()):
            raise ValueError()
        operationTimeSeries = operationRateFix
    else:
        operationTimeSeries = operationRateMax

    if not isinstance(operationTimeSeries, dict):
        operationTimeSeries = dict.fromkeys(
            esM.investmentPeriods, operationTimeSeries
        )
    if all(x is None for x in operationTimeSeries.values()):
        operationTimeSeries = None

    self.locationalEligibility = utils.setLocationalEligibility(
        esM,
        self.locationalEligibility,
        preprocessedCapacityMax,
        preprocessedCapacityFix,
        self.isBuiltFix,
        hasCapacityVariable,
        operationTimeSeries,
        Dimension.TWO,
    )

    self._mapC, self._mapL, self._mapI = {}, {}, {}
    for loc1 in esM.locations:
        for loc2 in esM.locations:
            if loc1 + "_" + loc2 in self.locationalEligibility.index:
                if self.locationalEligibility[loc1 + "_" + loc2] == 0:
                    self.locationalEligibility.drop(
                        labels=loc1 + "_" + loc2, inplace=True
                    )
                self._mapC.update({loc1 + "_" + loc2: (loc1, loc2)})
                self._mapL.setdefault(loc1, {}).update({loc2: loc1 + "_" + loc2})
                self._mapI.update({loc1 + "_" + loc2: loc2 + "_" + loc1})

    # capacity parameter
    preprocessedCapacityMax = utils.preprocess2dimData(
        capacityMax, self._mapC, locationalEligibility=self.locationalEligibility
    )
    preprocessedCapacityFix = utils.preprocess2dimData(
        capacityFix, self._mapC, locationalEligibility=self.locationalEligibility
    )
    preprocessedCapacityMin = utils.preprocess2dimData(
        capacityMin, self._mapC, locationalEligibility=self.locationalEligibility
    )
    preprocessedCommissioningMin = utils.preprocess2dimData(
        commissioningMin,
        self._mapC,
        locationalEligibility=self.locationalEligibility,
    )
    preprocessedCommissioningMax = utils.preprocess2dimData(
        commissioningMax,
        self._mapC,
        locationalEligibility=self.locationalEligibility,
    )
    preprocessedCommissioningFix = utils.preprocess2dimData(
        commissioningFix,
        self._mapC,
        locationalEligibility=self.locationalEligibility,
    )
    # stockCommissioning
    if stockCommissioning is None:
        self.stockCommissioning = stockCommissioning
    elif isinstance(stockCommissioning, dict):
        self.stockCommissioning = {}
        for potential_ip in stockCommissioning.keys():
            self.stockCommissioning[potential_ip] = utils.preprocess2dimData(
                stockCommissioning[potential_ip],
                locationalEligibility=locationalEligibility,
            )
    else:
        raise ValueError("stockCommissioning must be None or a dict.")

    self.isBuiltFix = utils.preprocess2dimData(
        isBuiltFix, self._mapC, locationalEligibility=self.locationalEligibility
    )

    self.interestRate = utils.preprocess2dimData(interestRate, self._mapC)
    self.economicLifetime = utils.preprocess2dimData(economicLifetime, self._mapC)
    self.technicalLifetime = utils.preprocess2dimData(technicalLifetime, self._mapC)
    self.balanceLimitID = balanceLimitID
    self.pathwayBalanceLimitID = pathwayBalanceLimitID

    Component.__init__(
        self,
        esM,
        name,
        dimension=Dimension.TWO,
        hasCapacityVariable=hasCapacityVariable,
        capacityVariableDomain=capacityVariableDomain,
        capacityPerPlantUnit=capacityPerPlantUnit,
        hasIsBuiltBinaryVariable=hasIsBuiltBinaryVariable,
        bigM=bigM,
        locationalEligibility=self.locationalEligibility,
        capacityMin=preprocessedCapacityMin,
        capacityMax=preprocessedCapacityMax,
        partLoadMin=partLoadMin,
        sharedPotentialID=sharedPotentialID,
        linkedQuantityID=linkedQuantityID,
        capacityFix=preprocessedCapacityFix,
        commissioningMin=preprocessedCommissioningMin,
        commissioningMax=preprocessedCommissioningMax,
        commissioningFix=preprocessedCommissioningFix,
        isBuiltFix=self.isBuiltFix,
        investPerCapacity=0,
        investIfBuilt=0,
        opexPerCapacity=0,
        opexIfBuilt=0,
        interestRate=self.interestRate,
        QPcostScale=QPcostScale,
        economicLifetime=self.economicLifetime,
        technicalLifetime=self.technicalLifetime,
        floorTechnicalLifetime=floorTechnicalLifetime,
        stockCommissioning=self.stockCommissioning,
        pwlcfParameters=pwlcfParameters,
    )
    # Set general component data
    utils.checkCommodities(esM, {commodity})
    self.commodity, self.commodityUnit = (
        commodity,
        esM.commodityUnitsDict[commodity],
    )
    self.distances = utils.preprocess2dimData(
        distances, self._mapC, locationalEligibility=self.locationalEligibility
    )
    self.losses = utils.preprocess2dimData(losses, self._mapC)
    self.distances = utils.checkAndSetDistances(
        self.distances, self.locationalEligibility, esM
    )
    self.losses = utils.checkAndSetTransmissionLosses(
        self.losses, self.distances, self.locationalEligibility
    )
    self.modelingClass = TransmissionModel

    # these are initialized with 0 in the component.__init__ and overwritten here,
    # due to its different structure otherwise the tests fail in the component
    self.investPerCapacity = investPerCapacity
    self.preprocessedInvestPerCapacity = utils.preprocess2dimInvestmentPeriodData(
        esM,
        "investPerCapacity",
        investPerCapacity,
        self.processedStockYears + esM.investmentPeriods,
        mapC=self._mapC,
    )

    self.investIfBuilt = investIfBuilt
    self.preprocessedInvestIfBuilt = utils.preprocess2dimInvestmentPeriodData(
        esM,
        "investIfBuilt",
        investIfBuilt,
        self.processedStockYears + esM.investmentPeriods,
        mapC=self._mapC,
    )

    self.opexPerCapacity = opexPerCapacity
    self.preprocessedOpexPerCapacity = utils.preprocess2dimInvestmentPeriodData(
        esM,
        "opexPerCapacity",
        opexPerCapacity,
        self.processedStockYears + esM.investmentPeriods,
        mapC=self._mapC,
    )

    self.opexIfBuilt = opexIfBuilt
    self.preprocessedOpexIfBuilt = utils.preprocess2dimInvestmentPeriodData(
        esM,
        "opexIfBuilt",
        opexIfBuilt,
        self.processedStockYears + esM.investmentPeriods,
        mapC=self._mapC,
    )

    # Set distance related costs data
    self.processedInvestPerCapacity = {}
    self.processedInvestIfBuilt = {}
    self.processedOpexPerCapacity = {}
    self.processedOpexIfBuilt = {}
    for year in self.processedStockYears + esM.investmentPeriods:
        self.processedInvestPerCapacity[year] = (
            utils.preprocess2dimData(
                self.preprocessedInvestPerCapacity[year],
                self._mapC,
                self.locationalEligibility,
            )
            * self.distances
            * 0.5
        )
        self.processedInvestIfBuilt[year] = (
            utils.preprocess2dimData(
                self.preprocessedInvestIfBuilt[year],
                self._mapC,
                self.locationalEligibility,
            )
            * self.distances
            * 0.5
        )
        self.processedOpexPerCapacity[year] = (
            utils.preprocess2dimData(
                self.preprocessedOpexPerCapacity[year],
                self._mapC,
                self.locationalEligibility,
            )
            * self.distances
            * 0.5
        )
        self.processedOpexIfBuilt[year] = (
            utils.preprocess2dimData(
                self.preprocessedOpexIfBuilt[year],
                self._mapC,
                self.locationalEligibility,
            )
            * self.distances
            * 0.5
        )

    # Set additional economic data
    # opexPerOperation
    self.opexPerOperation = utils.preprocess2dimData(opexPerOperation, self._mapC)
    self.processedOpexPerOperation = utils.checkAndSetInvestmentPeriodCostParameter(
        esM,
        name,
        self.opexPerOperation,
        Dimension.TWO,
        self.locationalEligibility,
        esM.investmentPeriods,
    )

    # operationRateMax
    self.operationRateMax = operationRateMax
    self.fullOperationRateMax = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateMax, self.locationalEligibility, Dimension.TWO
    )
    self.aggregatedOperationRateMax = dict.fromkeys(esM.investmentPeriods)
    self.processedOperationRateMax = dict.fromkeys(esM.investmentPeriods)

    # operationRateFix
    self.operationRateFix = operationRateFix
    self.fullOperationRateFix = utils.checkAndSetInvestmentPeriodTimeSeries(
        esM, name, operationRateFix, self.locationalEligibility, Dimension.TWO
    )
    self.aggregatedOperationRateFix = dict.fromkeys(esM.investmentPeriods)
    self.processedOperationRateFix = dict.fromkeys(esM.investmentPeriods)

    utils.checkOperationRateForCapacityVariable(
        name,
        self.hasCapacityVariable,
        self.fullOperationRateMax,
        self.fullOperationRateFix,
    )

    # partLoadMin
    self.processedPartLoadMin = utils.checkAndSetPartLoadMin(
        esM,
        name,
        partLoadMin,
        self.fullOperationRateMax,
        self.fullOperationRateFix,
        self.bigM,
        self.hasCapacityVariable,
    )

    utils.isPositiveNumber(tsaWeight)
    self.tsaWeight = tsaWeight

    self.processedLocationalEligibility = (
        self.locationalEligibility
    )  # checks already during setting of locationalEligibility

addToEnergySystemModel

addToEnergySystemModel(esM)

Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in the esM, it is added as well.

:param esM: EnergySystemModel instance representing the energy system in which the component should be modeled. :type esM: EnergySystemModel instance

Source code in fine/component.py
def addToEnergySystemModel(self, esM):
    """Add the component to an EnergySystemModel instance (esM). If the respective component class is not already in
    the esM, it is added as well.

    :param esM: EnergySystemModel instance representing the energy system in which the component should be modeled.
    :type esM: EnergySystemModel instance
    """
    esM.isTimeSeriesDataClustered = False
    if self.name in esM.componentNames:
        if (
            esM.componentNames[self.name] == self.modelingClass.__name__
            and esM.verboseLogLevel < 2
        ):
            warnings.warn(
                "Component identifier "
                + self.name
                + " already exists. Data will be overwritten."
            )
        elif esM.componentNames[self.name] != self.modelingClass.__name__:
            raise ValueError("Component name " + self.name + " is not unique.")
    else:
        esM.componentNames.update({self.name: self.modelingClass.__name__})
    mdl = self.modelingClass.__name__
    if mdl not in esM.componentModelingDict:
        esM.componentModelingDict.update({mdl: self.modelingClass()})
    esM.componentModelingDict[mdl].componentsDict.update({self.name: self})

    if self.sharedPotentialID is not None:
        for ip in esM.investmentPeriods:
            for loc in self.processedLocationalEligibility.index:
                if self.processedCapacityMax[ip][loc] != 0:
                    esM.sharedPotentialDict.setdefault(
                        (self.sharedPotentialID, loc, ip), []
                    ).append(self.name)

    if self.pwlcf is not None:
        pwlcfModel = fine.expansionModules.piecewiseLinearCostFunction.PiecewiseLinearCostFunctionModel
        if not hasattr(esM, "pwlcfModel"):
            esM.pwlcfModel = pwlcfModel()
        esM.pwlcfModel.modulesDict.update({self.name: self.pwlcf})

getDataForTimeSeriesAggregation

getDataForTimeSeriesAggregation(ip)

Get the required data if a time series aggregation is requested.

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/transmission.py
def getDataForTimeSeriesAggregation(self, ip):
    """Get the required data if a time series aggregation is requested.

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    weightDict, data = {}, []
    if self.fullOperationRateFix:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateFix,
            "_operationRateFix_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    if self.fullOperationRateMax:
        weightDict, data = self.prepareTSAInput(
            self.fullOperationRateMax,
            "_operationRateMax_",
            self.tsaWeight,
            weightDict,
            data,
            ip,
        )
    return (pd.concat(data, axis=1), weightDict) if data else (None, {})

getTSAOutput

getTSAOutput(rate, rateName, data, ip)

Return a reformatted time series data after applying time series aggregation, if the original time series data is not None.

:param rate: Full (unclustered) time series data or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param data: Pandas DataFrame with the clustered time series data of all components in the energy system :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

:return: reformatted data or None :rtype: Pandas DataFrame

Source code in fine/component.py
def getTSAOutput(self, rate, rateName, data, ip):
    """Return a reformatted time series data after applying time series aggregation, if the original time series
    data is not None.

    :param rate: Full (unclustered) time series data or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param data: Pandas DataFrame with the clustered time series data of all components in the energy system
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: reformatted data or None
    :rtype: Pandas DataFrame
    """
    if rate is None:
        return None
    if isinstance(rate, dict):
        if rate[ip] is not None:
            uniqueIdentifiers = [
                self.name + rateName + loc for loc in rate[ip].columns
            ]
            data_ = data[uniqueIdentifiers].copy(deep=True)
            data_.rename(
                columns={
                    self.name + rateName + loc: loc for loc in rate[ip].columns
                },
                inplace=True,
            )
        else:
            return None
    elif isinstance(rate, pd.DataFrame):
        uniqueIdentifiers = [self.name + rateName + loc for loc in rate.columns]
        data_ = data[uniqueIdentifiers].copy(deep=True)
        data_.rename(
            columns={self.name + rateName + loc: loc for loc in rate.columns},
            inplace=True,
        )
    else:
        raise ValueError(f"Wrong type for rate of '{self.name}': {type(rate)}")
    return data_

prepareTSAInput

prepareTSAInput(
    rate, rateName, rateWeight, weightDict, data, ip
)

Format the time series data of a component to fit the requirements of the time series aggregation package and return a list of formatted data.

:param rate: a fixed/maximum/minimum operation time series or None :type rate: Pandas DataFrame or None

:param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series) :type rateName: string

:param rateWeight: weight of the time series in the clustering process :type rateWeight: positive float (>=0)

:param weightDict: dictionary to which the weight is added :type weightDict: dict

:param data: list to which the formatted data is added :type data: list of Pandas DataFrames

:param ip: investment period of transformation path analysis. :type ip: int

:return: data :rtype: Pandas DataFrame

Source code in fine/component.py
def prepareTSAInput(self, rate, rateName, rateWeight, weightDict, data, ip):
    """Format the time series data of a component to fit the requirements of the time series aggregation package and
    return a list of formatted data.

    :param rate: a fixed/maximum/minimum operation time series or None
    :type rate: Pandas DataFrame or None

    :param rateName: name of the time series (to ensure uniqueness if a component has multiple relevant time series)
    :type rateName: string

    :param rateWeight: weight of the time series in the clustering process
    :type rateWeight: positive float (>=0)

    :param weightDict: dictionary to which the weight is added
    :type weightDict: dict

    :param data: list to which the formatted data is added
    :type data: list of Pandas DataFrames

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :return: data
    :rtype: Pandas DataFrame
    """
    # rate can be passed as a dict with investment periods
    if isinstance(rate, dict):
        rate = rate[ip]
    else:
        pass

    data_ = rate
    if data_ is not None:
        data_ = data_.copy()
        uniqueIdentifiers = [self.name + rateName + loc for loc in data_.columns]
        data_.rename(
            columns={loc: self.name + rateName + loc for loc in data_.columns},
            inplace=True,
        )
        (
            weightDict.update({id: rateWeight for id in uniqueIdentifiers}),
            data.append(data_),
        )
    return weightDict, data

setAggregatedTimeSeriesData

setAggregatedTimeSeriesData(data, ip)

Determine the aggregated maximum rate and the aggregated fixed operation rate.

:param data: Pandas DataFrame with the clustered time series data of the conversion component :type data: Pandas DataFrame

:param ip: investment period of transformation path analysis. :type ip: int

Source code in fine/transmission.py
def setAggregatedTimeSeriesData(self, data, ip):
    """Determine the aggregated maximum rate and the aggregated fixed operation rate.

    :param data: Pandas DataFrame with the clustered time series data of the conversion component
    :type data: Pandas DataFrame

    :param ip: investment period of transformation path analysis.
    :type ip: int
    """
    self.aggregatedOperationRateFix[ip] = self.getTSAOutput(
        self.fullOperationRateFix, "_operationRateFix_", data, ip
    )
    self.aggregatedOperationRateMax[ip] = self.getTSAOutput(
        self.fullOperationRateMax, "_operationRateMax_", data, ip
    )

setTimeSeriesData

setTimeSeriesData(hasTSA)

Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

:param hasTSA: states whether a time series aggregation is requested (True) or not (False). :type hasTSA: boolean

Source code in fine/transmission.py
def setTimeSeriesData(self, hasTSA):
    """Set the maximum operation rate and fixed operation rate depending on whether a time series analysis is requested or not.

    :param hasTSA: states whether a time series aggregation is requested (True) or not (False).
    :type hasTSA: boolean
    """
    self.processedOperationRateMax = (
        self.aggregatedOperationRateMax if hasTSA else self.fullOperationRateMax
    )
    self.processedOperationRateFix = (
        self.aggregatedOperationRateFix if hasTSA else self.fullOperationRateFix
    )

fixBinaryVariables

fixBinaryVariables(esM)

Search for the optimized binary variables and set them as fixed.

:param esM: energy system model to which the component should be added. Used for unit checks. :type esM: EnergySystemModel instance from the FINE package

Source code in fine/expansionModules/optimizeTSAmultiStage.py
def fixBinaryVariables(esM):
    """Search for the optimized binary variables and set them as fixed.

    :param esM: energy system model to which the component should be added. Used for unit checks.
    :type esM: EnergySystemModel instance from the FINE package
    """
    for mdl in esM.componentModelingDict.keys():
        compValues = esM.componentModelingDict[mdl].getOptimalValues(
            name="isBuiltVariablesOptimum", ip=0
        )["values"]
        if compValues is not None:
            for comp in compValues.index.get_level_values(0).unique():
                values = utils.preprocess2dimData(
                    compValues.loc[comp]
                    .fillna(value=0)
                    .round(decimals=0)
                    .astype(np.int64),
                    discard=False,
                )
                esM.componentModelingDict[mdl].componentsDict[comp].isBuiltFix = values

getShadowPrices

getShadowPrices(
    esM,
    constraint,
    ip=0,
    dualValues=None,
    hasTimeSeries=False,
    periodOccurrences=None,
    periodsOrder=None,
)

Get dual values of constraint ("shadow prices").

:param esM: considered energy system model :type esM: EnergySystemModel class instance

:param constraint: constraint from which the dual values should be obtained (e.g. pyM.commodityBalanceConstraint) :type constraint: pyomo.core.base.constraint.SimpleConstraint

:param ip: investment period of transformation path analysis. :type ip: int

:param dualValues: dual values of the optimized model instance. If it is not specified, it is set by using the function getDualValues(). |br| * the default value is None :type dualValues: None or Series

:param hasTimeSeries: If the constaint is time dependent, this parameter concatenates the dual values to a full time series (particularly usefull if time series aggregation was considered). |br| * the default value is False :type hasTimeSeries: bool

:param periodOccurrences: Only required if hasTimeSeries is set to True. |br| * the default value is None :type periodOccurrences: list or None

:param periodsOrder: Only required if hasTimeSeries is set to True. |br| * the default value is None :type periodsOrder: list or None

:return: Pandas Series with the dual values of the specified constraint

Source code in fine/IOManagement/standardIO.py
def getShadowPrices(
    esM,
    constraint,
    ip=0,
    dualValues=None,
    hasTimeSeries=False,
    periodOccurrences=None,
    periodsOrder=None,
):
    """Get dual values of constraint ("shadow prices").

    :param esM: considered energy system model
    :type esM: EnergySystemModel class instance

    :param constraint: constraint from which the dual values should be obtained (e.g. pyM.commodityBalanceConstraint)
    :type constraint: pyomo.core.base.constraint.SimpleConstraint

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :param dualValues: dual values of the optimized model instance. If it is not specified, it is set by using the
        function getDualValues().
        |br| * the default value is None
    :type dualValues: None or Series

    :param hasTimeSeries: If the constaint is time dependent, this parameter concatenates the dual values
        to a full time series (particularly usefull if time series aggregation was considered).
        |br| * the default value is False
    :type hasTimeSeries: bool

    :param periodOccurrences: Only required if hasTimeSeries is set to True.
        |br| * the default value is None
    :type periodOccurrences: list or None

    :param periodsOrder: Only required if hasTimeSeries is set to True.
        |br| * the default value is None
    :type periodsOrder: list or None

    :return: Pandas Series with the dual values of the specified constraint
    """
    if esM.numberOfInvestmentPeriods > 1:
        warnings.warn(
            "Shadow prices obtained via getShadowPrices() are in present-value (NPV) units when "
            "multiple investment periods are used. The LP objective is the sum of discounted costs "
            "across all investment periods, so the dual value of the commodity balance for period "
            f"ip={ip} reflects [currency_present_value / commodity_unit], not the "
            "[currency_in_period_ip / commodity_unit] a user would typically expect. "
            "Converting to per-period units is not straightforward because the discount factor is "
            "component-specific (each component may have a different interest rate).",
            UserWarning,
            stacklevel=2,
        )

    if dualValues is None:
        dualValues = getDualValues(esM.pyM)

    SP = pd.Series(
        list(constraint.values()), index=pd.Index(list(constraint.keys()))
    ).map(dualValues)
    # Select rows where ip is equal to investigated ip
    SP = SP.iloc[SP.index.get_level_values(2) == ip]
    # Delete ip from multiindex
    SP = SP.droplevel(2, axis=0)

    if hasTimeSeries:
        SP = pd.DataFrame(SP).swaplevel(i=0, j=-2).sort_index()
        SP = SP.unstack(level=-1)
        SP.columns = SP.columns.droplevel()
        SP = SP.apply(lambda x: x / (periodOccurrences[ip][x.name[0]]), axis=1)
        SP = fn.utils.buildFullTimeSeries(
            SP, periodsOrder[ip], ip, esM=esM, divide=False
        )
        SP = SP.stack()

    return SP

optimizeSimpleMyopic

optimizeSimpleMyopic(
    esM,
    startYear,
    endYear=None,
    nbOfSteps=None,
    nbOfRepresentedYears=None,
    timeSeriesAggregation=True,
    numberOfTypicalPeriods=7,
    numberOfTimeStepsPerPeriod=24,
    clusterMethod="hierarchical",
    logFileName="",
    threads=3,
    solver="gurobi",
    timeLimit=None,
    optimizationSpecs="",
    warmstart=False,
    CO2Reference=366,
    CO2ReductionTargets=None,
    saveResults=True,
    trackESMs=True,
)

Optimization function for myopic approach. For each optimization run, the newly installed capacities will be given as a stock (with capacityFix) to the next optimization run.

:param esM: EnergySystemModel instance representing the energy system which should be optimized by considering the transformation pathway (myopic foresight). :type esM: esM - EnergySystemModel instance

:param startYear: year of the first optimization :type startYear: int

Default arguments:

:param endYear: year of the last optimization :type endYear: int

:param nbOfSteps: number of optimization runs excluding the start year (minimum number of optimization runs is 2: one optimization for the start year and one for the end year). |br| * the default value is None :type nbOfSteps: int or None

:param noOfRepresentedYears: number of years represented by one optimization run |br| * the default value is None :type nbOfRepresentedYears: int or None

:param timeSeriesAggregation: states if the optimization of the energy system model should be done with

(a) the full time series (False) or
(b) clustered time series data (True).

|br| * the default value is False

:type timeSeriesAggregation: boolean

:param numberOfTypicalPeriods: states the number of typical periods into which the time series data should be clustered. The number of time steps per period must be an integer multiple of the total number of considered time steps in the energy system. This argument is used if timeSeriesAggregation is set to True. Note: Please refer to the tsam package documentation of the parameter noTypicalPeriods for more information. |br| * the default value is 7 :type numberOfTypicalPeriods: strictly positive integer

:param numberOfTimeStepsPerPeriod: states the number of time steps per period |br| * the default value is 24 :type numberOfTimeStepsPerPeriod: strictly positive integer

:param clusterMethod: states the method which is used in the tsam package for clustering the time series data. Options are for example 'averaging','k_means','exact k_medoid' or 'hierarchical'.

.. note::
    Please refer to the tsam package documentation of the parameter clusterMethod for more information.

|br| * the default value is 'hierarchical'

:type clusterMethod: string

:param CO2Reference: gives the reference value of the CO2 emission to which the reduction should be applied to. The default value refers to the emissions of 1990 within the electricity sector (366kt CO2_eq) |br| * the default value is 366 :type CO2Reference: float

:param CO2ReductionTargets: specifies the CO2 reduction targets for all optimization periods. If specified, the length of the list must equal the number of optimization steps, and an object of the sink class which counts the CO2 emission is required. |br| * the default value is None :type CO2ReductionTargets: list of strictly positive integer or None

:param saveResults: specifies if the results are saves in excelfiles or not. |br| * the default value is True :type saveResults: boolean

:param trackESMs: specifies if the energy system model instances of each model run should be stored in a dictionary or not. It´s not recommended to track the ESMs if the model is quite big. |br| * the default value is True :type trackESMs: boolean

:returns: myopicResults: Store all optimization outputs in a dictionary for further analyses. If trackESMs is set to false, nothing is returned. :rtype: dict of all optimized instances of the EnergySystemModel class or None.

Source code in fine/expansionModules/transformationPath.py
def optimizeSimpleMyopic(
    esM,
    startYear,
    endYear=None,
    nbOfSteps=None,
    nbOfRepresentedYears=None,
    timeSeriesAggregation=True,
    numberOfTypicalPeriods=7,
    numberOfTimeStepsPerPeriod=24,
    clusterMethod="hierarchical",
    logFileName="",
    threads=3,
    solver="gurobi",
    timeLimit=None,
    optimizationSpecs="",
    warmstart=False,
    CO2Reference=366,
    CO2ReductionTargets=None,
    saveResults=True,
    trackESMs=True,
):
    """Optimization function for myopic approach. For each optimization run, the newly installed capacities
    will be given as a stock (with capacityFix) to the next optimization run.

    :param esM: EnergySystemModel instance representing the energy system which should be optimized by considering the
                transformation pathway (myopic foresight).
    :type esM: esM - EnergySystemModel instance

    :param startYear: year of the first optimization
    :type startYear: int

    **Default arguments:**

    :param endYear: year of the last optimization
    :type endYear: int

    :param nbOfSteps: number of optimization runs excluding the start year (minimum number
        of optimization runs is 2: one optimization for the start year and one for the end year).
        |br| * the default value is None
    :type nbOfSteps: int or None

    :param noOfRepresentedYears: number of years represented by one optimization run
        |br| * the default value is None
    :type nbOfRepresentedYears: int or None

    :param timeSeriesAggregation: states if the optimization of the energy system model should be done with

        (a) the full time series (False) or
        (b) clustered time series data (True).

        |br| * the default value is False
    :type timeSeriesAggregation: boolean

    :param numberOfTypicalPeriods: states the number of typical periods into which the time series data
        should be clustered. The number of time steps per period must be an integer multiple of the total
        number of considered time steps in the energy system. This argument is used if timeSeriesAggregation is set to True.
        Note: Please refer to the tsam package documentation of the parameter noTypicalPeriods for more
        information.
        |br| * the default value is 7
    :type numberOfTypicalPeriods: strictly positive integer

    :param numberOfTimeStepsPerPeriod: states the number of time steps per period
        |br| * the default value is 24
    :type numberOfTimeStepsPerPeriod: strictly positive integer

    :param clusterMethod: states the method which is used in the tsam package for clustering the time series
        data. Options are for example 'averaging','k_means','exact k_medoid' or 'hierarchical'.

        .. note::
            Please refer to the tsam package documentation of the parameter clusterMethod for more information.

        |br| * the default value is 'hierarchical'
    :type clusterMethod: string

    :param CO2Reference: gives the reference value of the CO2 emission to which the reduction should be applied to.
        The default value refers to the emissions of 1990 within the electricity sector (366kt CO2_eq)
        |br| * the default value is 366
    :type CO2Reference: float

    :param CO2ReductionTargets: specifies the CO2 reduction targets for all optimization periods.
        If specified, the length of the list must equal the number of optimization steps, and an object of the sink class
        which counts the CO2 emission is required.
        |br| * the default value is None
    :type CO2ReductionTargets: list of strictly positive integer or None

    :param saveResults: specifies if the results are saves in excelfiles or not.
        |br| * the default value is True
    :type saveResults: boolean

    :param trackESMs: specifies if the energy system model instances of each model run should be stored in a dictionary or not.
        It´s not recommended to track the ESMs if the model is quite big.
        |br| * the default value is True
    :type trackESMs: boolean

    :returns: myopicResults: Store all optimization outputs in a dictionary for further analyses. If trackESMs is set to false,
        nothing is returned.
    :rtype: dict of all optimized instances of the EnergySystemModel class or None.
    """
    if esM.numberOfInvestmentPeriods != 1:
        raise ValueError(
            "Myopic is based on single year optimizations. "
            + "numberOfInvestmentPeriods must be 1"
        )
    nbOfSteps, nbOfRepresentedYears = utils.checkAndSetTimeHorizon(
        startYear, endYear, nbOfSteps, nbOfRepresentedYears
    )
    utils.checkSinkCompCO2toEnvironment(esM, CO2ReductionTargets)
    utils.checkCO2ReductionTargets(CO2ReductionTargets, nbOfSteps)
    logger.info("Number of optimization runs: %s", nbOfSteps + 1)
    logger.info(
        "Number of years represented by one optimization: %s", nbOfRepresentedYears
    )
    mileStoneYear = startYear
    if trackESMs:
        myopicResults = dict()

    for step in range(0, nbOfSteps + 1):
        mileStoneYear = startYear + step * nbOfRepresentedYears
        logFileName = "log_" + str(mileStoneYear)
        utils.setNewCO2ReductionTarget(esM, CO2Reference, CO2ReductionTargets, step)

        # Optimization
        if timeSeriesAggregation:
            esM.aggregateTemporally(
                numberOfTypicalPeriods=numberOfTypicalPeriods,
                numberOfTimeStepsPerPeriod=numberOfTimeStepsPerPeriod,
                segmentation=False,
                clusterMethod=clusterMethod,
                solver=solver,
                sortValues=True,
                rescaleClusterPeriods=True,
                representationMethod=None,
            )

        esM.optimize(
            declaresOptimizationProblem=True,
            timeSeriesAggregation=timeSeriesAggregation,
            logFileName=logFileName,
            threads=threads,
            solver=solver,
            timeLimit=timeLimit,
            optimizationSpecs=optimizationSpecs,
            warmstart=False,
        )

        if saveResults:
            standardIO.writeOptimizationOutputToExcel(
                esM,
                outputFileName="ESM" + str(mileStoneYear),
                optSumOutputLevel=2,
                optValOutputLevel=1,
            )

        if trackESMs:
            tmp = esM
            del tmp.pyM  # Delete pyomo instance from esM before copying it (pyomo instances cannot be copied)
            myopicResults.update({"ESM_" + str(mileStoneYear): copy.deepcopy(tmp)})

        # Get stock if not all optimizations are done
        if step != nbOfSteps + 1:
            esM = getStock(esM, mileStoneYear, nbOfRepresentedYears)
    if trackESMs:
        return myopicResults
    return None

optimizeTSAmultiStage

optimizeTSAmultiStage(
    esM,
    declaresOptimizationProblem=True,
    relaxIsBuiltBinary=False,
    numberOfTypicalPeriods=30,
    numberOfTimeStepsPerPeriod=24,
    clusterMethod="hierarchical",
    logFileName="",
    threads=3,
    solver="gurobi",
    timeLimit=None,
    optimizationSpecs="",
    warmstart=False,
)

Call the optimize function for a temporally aggregated MILP (so the model has to include hasIsBuiltBinaryVariables in all or some components). Fix the binary variables and run it again without temporal aggregation. Furthermore, a LP with relaxed binary variables can be solved to obtain both, an upper and lower bound for the fully resolved MILP.

Required arguments:

:param esM: energy system model to which the component should be added. Used for unit checks. :type esM: EnergySystemModel instance from the FINE package

Default arguments:

:param declaresOptimizationProblem: states if the optimization problem should be declared (True) or not (False).

(a) If true, the declareOptimizationProblem function is called and a pyomo ConcreteModel instance is built.
(b) If false a previously declared pyomo ConcreteModel instance is used.

|br| * the default value is True

:type declaresOptimizationProblem: boolean

:param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower bound of the problem. |br| * the default value is False :type declaresOptimizationProblem: boolean

:param numberOfTypicalPeriods: states the number of typical periods into which the time series data should be clustered. The number of time steps per period must be an integer multiple of the total number of considered time steps in the energy system.

.. note::
    Please refer to the tsam package documentation of the parameter noTypicalPeriods for more
    information.

|br| * the default value is 30

:type numberOfTypicalPeriods: strictly positive integer

:param numberOfTimeStepsPerPeriod: states the number of time steps per period |br| * the default value is 24 :type numberOfTimeStepsPerPeriod: strictly positive integer

:param clusterMethod: states the method which is used in the tsam package for clustering the time series data. Options are for example 'averaging','k_means','exact k_medoid' or 'hierarchical'.

.. note::
    Please refer to the tsam package documentation of the parameter clusterMethod for more information.

|br| * the default value is 'hierarchical'

:type clusterMethod: string

:param logFileName: logFileName is used for naming the log file of the optimization solver output if gurobi is used as the optimization solver. If the logFileName is given as an absolute path (e.g. logFileName = os.path.join(os.getcwd(), 'Results', 'logFileName.txt')) the log file will be stored in the specified directory. Otherwise, it will be stored by default in the directory where the executing python script is called. |br| * the default value is 'job' :type logFileName: string

:param threads: number of computational threads used for solving the optimization (solver dependent input) if gurobi is used as the solver. A value of 0 results in using all available threads. If a value larger than the available number of threads are chosen, the value will reset to the maximum number of threads. |br| * the default value is 3 :type threads: positive integer

:param solver: specifies which solver should solve the optimization problem (which of course has to be installed on the machine on which the model is run). |br| * the default value is 'gurobi' :type solver: string

:param timeLimit: if not specified as None, indicates the maximum solve time of the optimization problem in seconds (solver dependent input). The use of this parameter is suggested when running models in runtime restricted environments (such as clusters with job submission systems). If the runtime limitation is triggered before an optimal solution is available, the best solution obtained up until then (if available) is processed. |br| * the default value is None :type timeLimit: strictly positive integer or None

:param optimizationSpecs: specifies parameters for the optimization solver (see the respective solver documentation for more information). Example: 'LogToConsole=1 OptimalityTol=1e-6' |br| * the default value is an empty string ('') :type timeLimit: string

:param warmstart: specifies if a warm start of the optimization should be considered (not always supported by the solvers). |br| * the default value is False :type warmstart: boolean

Source code in fine/expansionModules/optimizeTSAmultiStage.py
def optimizeTSAmultiStage(
    esM,
    declaresOptimizationProblem=True,
    relaxIsBuiltBinary=False,
    numberOfTypicalPeriods=30,
    numberOfTimeStepsPerPeriod=24,
    clusterMethod="hierarchical",
    logFileName="",
    threads=3,
    solver="gurobi",
    timeLimit=None,
    optimizationSpecs="",
    warmstart=False,
):
    """Call the optimize function for a temporally aggregated MILP (so the model has to include
    hasIsBuiltBinaryVariables in all or some components). Fix the binary variables and run it again
    without temporal aggregation. Furthermore, a LP with relaxed binary variables can be solved to
    obtain both, an upper and lower bound for the fully resolved MILP.

    **Required arguments:**

    :param esM: energy system model to which the component should be added. Used for unit checks.
    :type esM: EnergySystemModel instance from the FINE package

    **Default arguments:**

    :param declaresOptimizationProblem: states if the optimization problem should be declared (True) or not (False).

        (a) If true, the declareOptimizationProblem function is called and a pyomo ConcreteModel instance is built.
        (b) If false a previously declared pyomo ConcreteModel instance is used.

        |br| * the default value is True
    :type declaresOptimizationProblem: boolean

    :param relaxIsBuiltBinary: states if the optimization problem should be solved as a relaxed LP to get the lower
        bound of the problem.
        |br| * the default value is False
    :type declaresOptimizationProblem: boolean

    :param numberOfTypicalPeriods: states the number of typical periods into which the time series data
        should be clustered. The number of time steps per period must be an integer multiple of the total
        number of considered time steps in the energy system.

        .. note::
            Please refer to the tsam package documentation of the parameter noTypicalPeriods for more
            information.

        |br| * the default value is 30
    :type numberOfTypicalPeriods: strictly positive integer

    :param numberOfTimeStepsPerPeriod: states the number of time steps per period
        |br| * the default value is 24
    :type numberOfTimeStepsPerPeriod: strictly positive integer

    :param clusterMethod: states the method which is used in the tsam package for clustering the time series
        data. Options are for example 'averaging','k_means','exact k_medoid' or 'hierarchical'.

        .. note::
            Please refer to the tsam package documentation of the parameter clusterMethod for more information.

        |br| * the default value is 'hierarchical'
    :type clusterMethod: string

    :param logFileName: logFileName is used for naming the log file of the optimization solver output
        if gurobi is used as the optimization solver.
        If the logFileName is given as an absolute path (e.g. logFileName = os.path.join(os.getcwd(),
        'Results', 'logFileName.txt')) the log file will be stored in the specified directory. Otherwise,
        it will be stored by default in the directory where the executing python script is called.
        |br| * the default value is 'job'
    :type logFileName: string

    :param threads: number of computational threads used for solving the optimization (solver dependent
        input) if gurobi is used as the solver. A value of 0 results in using all available threads. If
        a value larger than the available number of threads are chosen, the value will reset to the maximum
        number of threads.
        |br| * the default value is 3
    :type threads: positive integer

    :param solver: specifies which solver should solve the optimization problem (which of course has to be
        installed on the machine on which the model is run).
        |br| * the default value is 'gurobi'
    :type solver: string

    :param timeLimit: if not specified as None, indicates the maximum solve time of the optimization problem
        in seconds (solver dependent input). The use of this parameter is suggested when running models in
        runtime restricted environments (such as clusters with job submission systems). If the runtime
        limitation is triggered before an optimal solution is available, the best solution obtained up
        until then (if available) is processed.
        |br| * the default value is None
    :type timeLimit: strictly positive integer or None

    :param optimizationSpecs: specifies parameters for the optimization solver (see the respective solver
        documentation for more information). Example: 'LogToConsole=1 OptimalityTol=1e-6'
        |br| * the default value is an empty string ('')
    :type timeLimit: string

    :param warmstart: specifies if a warm start of the optimization should be considered
        (not always supported by the solvers).
        |br| * the default value is False
    :type warmstart: boolean
    """
    lowerBound = None

    if relaxIsBuiltBinary:
        esM.optimize(
            declaresOptimizationProblem=True,
            timeSeriesAggregation=False,
            relaxIsBuiltBinary=True,
            logFileName="relaxedProblem",
            threads=threads,
            solver=solver,
            timeLimit=timeLimit,
            optimizationSpecs=optimizationSpecs,
            warmstart=warmstart,
        )
        lowerBound = esM.objectiveValue

    esM.aggregateTemporally(
        numberOfTypicalPeriods=numberOfTypicalPeriods,
        numberOfTimeStepsPerPeriod=numberOfTimeStepsPerPeriod,
        segmentation=False,
        clusterMethod=clusterMethod,
        solver=solver,
        sortValues=True,
        rescaleClusterPeriods=True,
        representationMethod=None,
    )

    esM.optimize(
        declaresOptimizationProblem=True,
        timeSeriesAggregation=True,
        relaxIsBuiltBinary=False,
        logFileName="firstStage",
        threads=threads,
        solver=solver,
        timeLimit=timeLimit,
        optimizationSpecs=optimizationSpecs,
        warmstart=warmstart,
    )

    # Set the binary variables to the values resulting from the first optimization step
    fn.fixBinaryVariables(esM)

    esM.optimize(
        declaresOptimizationProblem=True,
        timeSeriesAggregation=False,
        relaxIsBuiltBinary=False,
        logFileName="secondStage",
        threads=threads,
        solver=solver,
        timeLimit=timeLimit,
        optimizationSpecs=optimizationSpecs,
        warmstart=False,
    )
    upperBound = esM.objectiveValue

    if lowerBound is not None:
        delta = upperBound - lowerBound
        gap = delta / upperBound
        esM.lowerBound, esM.upperBound = lowerBound, upperBound
        esM.gap = gap
        logger.info(
            "The real optimal value lies between %s and %s with a gap of %s%%.",
            round(lowerBound, 2),
            round(upperBound, 2),
            round(gap * 100, 2),
        )

plotLocationalColorMap

plotLocationalColorMap(
    esM,
    compName,
    locationsShapeFileName,
    indexColumn,
    ip=0,
    perArea=True,
    areaFactor=1000.0,
    crs="EPSG:3035",
    variableName="capacityVariablesOptimum",
    doSum=False,
    cmap="viridis",
    vmin=0,
    vmax=-1,
    zlabel=None,
    figsize=(6, 6),
    fontsize=12,
    save=False,
    fileName="capacity.png",
    dpi=200,
    **kwargs,
)

Plot the data of a component for each location.

Required arguments:

:param esM: considered energy system model :type esM: EnergySystemModel class instance

:param compName: component name :type compName: string

:param locationsShapeFileName: file name or path to a shape file :type locationsShapeFileName: string

:param indexColumn: name of the column in which the location indices are stored :type indexColumn: string

Default arguments:

:param ip: investment period |br| * the default value is 0 :type ip: int

:param perArea: indicates if the capacity should be given per area |br| * the default value is False :type perArea: boolean

:param areaFactor: meter * areaFactor = km --> areaFactor = 1e3 (--> capacity/km) |br| * the default value is 1e3 :type areaFactor: scalar > 0

:param crs: coordinate reference system |br| * the default value is 'EPSG:3035' :type crs: string

:param variableName: parameter for plotting installed capacity ('_capacityVariablesOptimum') or operation ('_operationVariablesOptimum'). In case of plotting the operation, set the parameter doSum to True. |br| * the default value is '_capacityVariablesOptimum' :type variableName: string

:param doSum: indicates if the variable has to be summarized for the location (e.g. for operation variables) |br| * the default value is False :type doSum: boolean

:param cmap: heat map (color map) (see matplotlib options) |br| * the default value is 'viridis' :type cmap: string

:param vmin: minimum value in heat map |br| * the default value is 0 :type vmin: integer

:param vmax: maximum value in heat map. If -1, vmax is set to the maximum value of the operation time series. |br| * the default value is -1 :type vmax: integer

:param zlabel: z-label of the plot |br| * the default value is 'operation' :type zlabel: string

:param figsize: figure size in inches |br| * the default value is (12,4) :type figsize: tuple of positive floats

:param fontsize: font size of the axis |br| * the default value is 12 :type fontsize: positive float

:param save: indicates if figure should be saved |br| * the default value is False :type save: boolean

:param fileName: output file name |br| * the default value is 'capacity.png' :type fileName: string

:param dpi: resolution in dots per inch |br| * the default value is 200 :type dpi: scalar > 0

Source code in fine/IOManagement/standardIO.py
def plotLocationalColorMap(
    esM,
    compName,
    locationsShapeFileName,
    indexColumn,
    ip=0,
    perArea=True,
    areaFactor=1e3,
    crs="EPSG:3035",
    variableName="capacityVariablesOptimum",
    doSum=False,
    cmap="viridis",
    vmin=0,
    vmax=-1,
    zlabel=None,
    figsize=(6, 6),
    fontsize=12,
    save=False,
    fileName="capacity.png",
    dpi=200,
    **kwargs,
):
    """Plot the data of a component for each location.

    **Required arguments:**

    :param esM: considered energy system model
    :type esM: EnergySystemModel class instance

    :param compName: component name
    :type compName: string

    :param locationsShapeFileName: file name or path to a shape file
    :type locationsShapeFileName: string

    :param indexColumn: name of the column in which the location indices are stored
    :type indexColumn: string

    **Default arguments:**

    :param ip: investment period
        |br| * the default value is 0
    :type ip: int

    :param perArea: indicates if the capacity should be given per area
        |br| * the default value is False
    :type perArea: boolean

    :param areaFactor: meter * areaFactor = km --> areaFactor = 1e3 (--> capacity/km)
        |br| * the default value is 1e3
    :type areaFactor: scalar > 0

    :param crs: coordinate reference system
        |br| * the default value is 'EPSG:3035'
    :type crs: string

    :param variableName: parameter for plotting installed capacity ('_capacityVariablesOptimum') or operation
        ('_operationVariablesOptimum'). In case of plotting the operation, set the parameter doSum to True.
        |br| * the default value is '_capacityVariablesOptimum'
    :type variableName: string

    :param doSum: indicates if the variable has to be summarized for the location (e.g. for operation
        variables)
        |br| * the default value is False
    :type doSum: boolean

    :param cmap: heat map (color map) (see matplotlib options)
        |br| * the default value is 'viridis'
    :type cmap: string

    :param vmin: minimum value in heat map
        |br| * the default value is 0
    :type vmin: integer

    :param vmax: maximum value in heat map. If -1, vmax is set to the maximum value of the operation time series.
        |br| * the default value is -1
    :type vmax: integer

    :param zlabel: z-label of the plot
        |br| * the default value is 'operation'
    :type zlabel: string

    :param figsize: figure size in inches
        |br| * the default value is (12,4)
    :type figsize: tuple of positive floats

    :param fontsize: font size of the axis
        |br| * the default value is 12
    :type fontsize: positive float

    :param save: indicates if figure should be saved
        |br| * the default value is False
    :type save: boolean

    :param fileName: output file name
        |br| * the default value is 'capacity.png'
    :type fileName: string

    :param dpi: resolution in dots per inch
        |br| * the default value is 200
    :type dpi: scalar > 0
    """
    data = esM.componentModelingDict[esM.componentNames[compName]].getOptimalValues(
        variableName, ip=ip
    )
    data = data["values"].loc[(compName)]

    if doSum:
        data = data.sum(axis=1)
    gdf = gpd.read_file(locationsShapeFileName).to_crs(crs)

    # Make sure the data and gdf indices match
    ## 1. Sort the indices to obtain same order
    data.sort_index(inplace=True)
    gdf.sort_values(indexColumn, inplace=True)

    ## 2. Take first 20 characters of the string for matching. (In gdfs usually long strings are cut in the end)
    gdf[indexColumn] = gdf[indexColumn].apply(lambda x: x[:20])
    data.index = data.index.str[:20]

    ## 3. Merge data on the indices of the gdf, additional (pseudo) regions in data are ignored
    data = pd.DataFrame(data)
    data = data.rename({data.columns.values[0]: "data"}, axis=1)
    gdf = pd.merge(gdf, data, left_on=indexColumn, right_index=True, how="left")
    gdf = gdf.fillna(0)

    ## 4. Print the names of the excluded (pseudo) regions
    regions_data = list(data.index)
    regions_gdf = list(gdf.loc[:, indexColumn])

    excluded_regions = [item for item in regions_data if item not in regions_gdf]

    if len(excluded_regions) > 0:
        logger.warning(
            "Missing regions: %s - %s. The following regions are not plotted as they are not contained in the provided shapefile: %s",
            compName,
            variableName,
            excluded_regions,
        )

    if perArea:
        gdf.loc[:, "data"] = gdf.loc[:, "data"] / (gdf.geometry.area / areaFactor**2)
        if zlabel is None:
            if isinstance(esM.getComponent(compName), fn.Conversion):
                unit = esM.getComponent(compName).physicalUnit
            else:
                unit = esM.commodityUnitsDict[esM.getComponent(compName).commodity]

            if areaFactor == 1e3:
                area_unit = "km$^2$"
            elif areaFactor == 1:
                area_unit = "m$^2$"
            else:
                raise NotImplementedError(
                    f"Area Factor not supported. Supported Area Factors {1},{1e3}"
                )

            unit = " [" + unit + "/" + area_unit + "]"
            zlabel = "Installed capacity \n" + unit + "\n"

    elif zlabel is None:
        if isinstance(esM.getComponent(compName), fn.Conversion):
            unit = esM.getComponent(compName).physicalUnit
        else:
            unit = esM.commodityUnitsDict[esM.getComponent(compName).commodity]
        zlabel = f"Installed capacity \n [ {unit} ] \n"

    vmax = gdf["data"].max() if vmax == -1 else vmax

    fig, ax = plt.subplots(1, 1, figsize=figsize, constrained_layout=True, **kwargs)
    ax.set_aspect("equal")
    ax.axis("off")

    gdf.plot(
        column="data",
        ax=ax,
        cmap=cmap,
        edgecolor="black",
        alpha=1,
        linewidth=0.2,
        vmin=vmin,
        vmax=vmax,
    )

    ## Create color bar
    sm1 = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=vmin, vmax=vmax))
    sm1._A = []
    cb1 = fig.colorbar(sm1, ax=ax, fraction=0.07, pad=0.05, shrink=0.5)
    label = (zlabel or "").strip()
    label = label.replace(" [", "\n[")
    cb1.ax.set_title(label, fontsize=fontsize, pad=6)
    cb1.ax.tick_params(labelsize=fontsize)

    if save:
        plt.savefig(fileName, dpi=dpi, bbox_inches="tight")

    return fig, ax

plotLocations

plotLocations(
    locationsShapeFileName,
    indexColumn,
    plotLocNames=False,
    crs="EPSG:3035",
    faceColor="none",
    edgeColor="black",
    fig=None,
    ax=None,
    linewidth=0.5,
    figsize=(6, 6),
    fontsize=12,
    save=False,
    fileName="",
    dpi=200,
    **kwargs,
)

Plot locations from a shape file.

Required arguments:

:param locationsShapeFileName: file name or path to a shape file :type locationsShapeFileName: string

:param indexColumn: name of the column in which the location indices are stored :type indexColumn: string

Default arguments:

:param plotLocNames: indicates if the names of the locations should be plotted |br| * the default value is False :type plotLocNames: boolean

:param crs: coordinate reference system |br| * the default value is 'EPSG:3035' :type crs: string

:param faceColor: face color of the plot |br| * the default value is 'none' :type faceColor: string

:param edgeColor: edge color of the plot |br| * the default value is 'black' :type edgeColor: string

:param fig: None or figure to which the plot should be added |br| * the default value is None :type fig: matplotlib Figure

:param ax: None or ax to which the plot should be added |br| * the default value is None :type ax: matplotlib Axis

:param linewidth: linewidth of the plot |br| * the default value is 0.5 :type linewidth: positive float

:param figsize: figure size in inches |br| * the default value is (6,6) :type figsize: tuple of positive floats

:param fontsize: font size of the axis |br| * the default value is 12 :type fontsize: positive float

:param save: indicates if figure should be saved |br| * the default value is False :type save: boolean

:param fileName: output file name |br| * the default value is 'operation.png' :type fileName: string

:param dpi: resolution in dots per inch |br| * the default value is 200 :type dpi: scalar > 0

Source code in fine/IOManagement/standardIO.py
def plotLocations(
    locationsShapeFileName,
    indexColumn,
    plotLocNames=False,
    crs="EPSG:3035",
    faceColor="none",
    edgeColor="black",
    fig=None,
    ax=None,
    linewidth=0.5,
    figsize=(6, 6),
    fontsize=12,
    save=False,
    fileName="",
    dpi=200,
    **kwargs,
):
    """Plot locations from a shape file.

    **Required arguments:**

    :param locationsShapeFileName: file name or path to a shape file
    :type locationsShapeFileName: string

    :param indexColumn: name of the column in which the location indices are stored
    :type indexColumn: string

    **Default arguments:**

    :param plotLocNames: indicates if the names of the locations should be plotted
        |br| * the default value is False
    :type plotLocNames: boolean

    :param crs: coordinate reference system
        |br| * the default value is 'EPSG:3035'
    :type crs: string

    :param faceColor: face color of the plot
        |br| * the default value is 'none'
    :type faceColor: string

    :param edgeColor: edge color of the plot
        |br| * the default value is 'black'
    :type edgeColor: string

    :param fig: None or figure to which the plot should be added
        |br| * the default value is None
    :type fig: matplotlib Figure

    :param ax: None or ax to which the plot should be added
        |br| * the default value is None
    :type ax: matplotlib Axis

    :param linewidth: linewidth of the plot
        |br| * the default value is 0.5
    :type linewidth: positive float

    :param figsize: figure size in inches
        |br| * the default value is (6,6)
    :type figsize: tuple of positive floats

    :param fontsize: font size of the axis
        |br| * the default value is 12
    :type fontsize: positive float

    :param save: indicates if figure should be saved
        |br| * the default value is False
    :type save: boolean

    :param fileName: output file name
        |br| * the default value is 'operation.png'
    :type fileName: string

    :param dpi: resolution in dots per inch
        |br| * the default value is 200
    :type dpi: scalar > 0
    """
    gdf = gpd.read_file(locationsShapeFileName).to_crs(crs)

    if ax is None:
        fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)

    ax.set_aspect("equal")
    ax.axis("off")
    gdf.plot(ax=ax, facecolor=faceColor, edgecolor=edgeColor, linewidth=linewidth)
    if plotLocNames:
        bbox_props = dict(boxstyle="round,pad=0.3", fc="w", ec="0.5", alpha=0.9)
        for ix, row in gdf.iterrows():
            locName = ix if indexColumn == "" else row[indexColumn]
            ax.annotate(
                text=locName,
                xy=(row.geometry.centroid.x, row.geometry.centroid.y),
                horizontalalignment="center",
                fontsize=fontsize,
                bbox=bbox_props,
            )

    fig.tight_layout()

    if save:
        plt.savefig(fileName, dpi=dpi, bbox_inches="tight")

    return fig, ax

plotOperation

plotOperation(
    esM,
    compName,
    loc,
    ip=0,
    locTrans=None,
    tMin=0,
    tMax=-1,
    variableName="operationVariablesOptimum",
    xlabel="time step",
    ylabel="operation time series",
    figsize=(12, 4),
    color="k",
    fontsize=12,
    save=False,
    fileName="operation.png",
    dpi=200,
    **kwargs,
)

Plot operation time series of a component at a location.

Required arguments:

:param esM: considered energy system model :type esM: EnergySystemModel class instance

:param compName: component name :type compName: string

:param loc: location :type loc: string

Default arguments:

:param ip: investment period |br| * the default value is 0 :type ip: int

:param locTrans: second location, required when Transmission components are plotted |br| * the default value is None :type locTrans: string

:param tMin: first time step to be plotted (starting from 0) |br| * the default value is 0 :type tMin: integer

:param tMax: last time step to be plotted |br| * the default value is -1 (i.e. the last available index) :type tMax: integer

:param variableName: name of the operation time series. Checkout the component model class to see which options are available. |br| * the default value is '_operationVariablesOptimum' :type variableName: string

:param xlabel: x-label of the plot |br| * the default value is 'time step' :type xlabel: string

:param ylabel: y-label of the plot |br| * the default value is 'operation time series' :type ylabel: string

:param figsize: figure size in inches |br| * the default value is (12,4) :type figsize: tuple of positive floats

:param color: color of the operation line |br| * the default value is 'k' :type color: string

:param fontsize: font size of the axis |br| * the default value is 12 :type fontsize: positive float

:param save: indicates if figure should be saved |br| * the default value is False :type save: boolean

:param fileName: output file name |br| * the default value is 'operation.png' :type fileName: string

:param dpi: resolution in dots per inch |br| * the default value is 200 :type dpi: scalar > 0

Source code in fine/IOManagement/standardIO.py
def plotOperation(
    esM,
    compName,
    loc,
    ip=0,
    locTrans=None,
    tMin=0,
    tMax=-1,
    variableName="operationVariablesOptimum",
    xlabel="time step",
    ylabel="operation time series",
    figsize=(12, 4),
    color="k",
    fontsize=12,
    save=False,
    fileName="operation.png",
    dpi=200,
    **kwargs,
):
    """Plot operation time series of a component at a location.

    **Required arguments:**

    :param esM: considered energy system model
    :type esM: EnergySystemModel class instance

    :param compName: component name
    :type compName: string

    :param loc: location
    :type loc: string

    **Default arguments:**

    :param ip: investment period
        |br| * the default value is 0
    :type ip: int

    :param locTrans: second location, required when Transmission components are plotted
        |br| * the default value is None
    :type locTrans: string

    :param tMin: first time step to be plotted (starting from 0)
        |br| * the default value is 0
    :type tMin: integer

    :param tMax: last time step to be plotted
        |br| * the default value is -1 (i.e. the last available index)
    :type tMax: integer

    :param variableName: name of the operation time series. Checkout the component model class to see which options
        are available.
        |br| * the default value is '_operationVariablesOptimum'
    :type variableName: string

    :param xlabel: x-label of the plot
        |br| * the default value is 'time step'
    :type xlabel: string

    :param ylabel: y-label of the plot
        |br| * the default value is 'operation time series'
    :type ylabel: string

    :param figsize: figure size in inches
        |br| * the default value is (12,4)
    :type figsize: tuple of positive floats

    :param color: color of the operation line
        |br| * the default value is 'k'
    :type color: string

    :param fontsize: font size of the axis
        |br| * the default value is 12
    :type fontsize: positive float

    :param save: indicates if figure should be saved
        |br| * the default value is False
    :type save: boolean

    :param fileName: output file name
        |br| * the default value is 'operation.png'
    :type fileName: string

    :param dpi: resolution in dots per inch
        |br| * the default value is 200
    :type dpi: scalar > 0
    """
    data = esM.componentModelingDict[esM.componentNames[compName]].getOptimalValues(
        variableName, ip=ip
    )
    if data is None:
        return None
    if locTrans is None:
        timeSeries = data["values"].loc[(compName, loc)].values
    else:
        timeSeries = data["values"].loc[(compName, loc, locTrans)].values

    fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)

    ax.grid(True)
    ax.plot(timeSeries[tMin:tMax], color=color)

    ax.tick_params(labelsize=fontsize)
    ax.set_ylabel(ylabel, fontsize=fontsize)
    ax.set_xlabel(xlabel, fontsize=fontsize)

    fig.tight_layout()

    if save:
        plt.savefig(fileName, dpi=dpi, bbox_inches="tight")

    return fig, ax

plotOperationColorMap

plotOperationColorMap(
    esM,
    compName,
    loc,
    ip=0,
    locTrans=None,
    nbPeriods=365,
    nbTimeStepsPerPeriod=24,
    variableName="operationVariablesOptimum",
    cmap="viridis",
    vmin=0,
    vmax=-1,
    xlabel="period",
    ylabel="timestep per period",
    zlabel="",
    figsize=(12, 4),
    fontsize=12,
    save=False,
    fileName="",
    xticks=None,
    yticks=None,
    xticklabels=None,
    yticklabels=None,
    monthlabels=False,
    dpi=200,
    pad=0.12,
    aspect=15,
    fraction=0.2,
    orientation="horizontal",
    **kwargs,
)

Plot operation time series of a component at a location.

Required arguments:

:param esM: considered energy system model :type esM: EnergySystemModel class instance

:param compName: component name :type compName: string

:param loc: location :type loc: string

Default arguments:

:param ip: investment period of transformation path analysis. :type ip: int

:param locTrans: second location, required when Transmission components are plotted |br| * the default value is None :type locTrans: string

:param nbPeriods: number of periods to be plotted |br| * the default value is 365 :type nbPeriods: integer

:param nbTimeStepsPerPeriod: time steps per period to be plotted (nbPeriods*nbTimeStepsPerPeriod=length of time series) |br| * the default value is 24 :type nbTimeStepsPerPeriod: integer

:param variableName: name of the operation time series. Checkout the component model class to see which options are available. |br| * the default value is '_operationVariablesOptimum' :type variableName: string

:param cmap: heat map (color map) (see matplotlib options) |br| * the default value is 'viridis' :type cmap: string

:param vmin: minimum value in heat map |br| * the default value is 0 :type vmin: integer

:param vmax: maximum value in heat map. If -1, vmax is set to the maximum value of the operation time series. |br| * the default value is -1 :type vmax: integer

:param xlabel: x-label of the plot |br| * the default value is 'day' :type xlabel: string

:param ylabel: y-label of the plot |br| * the default value is 'hour' :type ylabel: string

:param zlabel: z-label of the plot |br| * the default value is 'operation' :type zlabel: string

:param figsize: figure size in inches |br| * the default value is (12,4) :type figsize: tuple of positive floats

:param fontsize: font size of the axis |br| * the default value is 12 :type fontsize: positive float

:param save: indicates if figure should be saved |br| * the default value is False :type save: boolean

:param fileName: output file name |br| * the default value is 'operation.png' :type fileName: string

:param xticks: user specified ticks of the x axis |br| * the default value is None :type xticks: list

:param yticks: user specified ticks of the ý axis |br| * the default value is None :type yticks: list

:param xticklabels: user specified tick labels of the x axis |br| * the default value is None :type xticklabels: list

:param yticklabels: user specified tick labels of the ý axis |br| * the default value is None :type yticklabels: list

:param monthlabels: specifies if month labels are to be plotted (only works correctly if 365 days are specified as the number of periods) |br| * the default value is False :type monthlabels: boolean

:param dpi: resolution in dots per inch |br| * the default value is 200 :type dpi: scalar > 0

:param pad: pad parameter of colorbar |br| * the default value is 0.12 :type pad: float

:param aspect: aspect parameter of colorbar |br| * the default value is 15 :type aspect: float

:param fraction: fraction parameter of colorbar |br| * the default value is 0.2 :type fraction: float

:param orientation: orientation parameter of colorbar |br| * the default value is 'horizontal' :type orientation: float

Source code in fine/IOManagement/standardIO.py
def plotOperationColorMap(
    esM,
    compName,
    loc,
    ip=0,
    locTrans=None,
    nbPeriods=365,
    nbTimeStepsPerPeriod=24,
    variableName="operationVariablesOptimum",
    cmap="viridis",
    vmin=0,
    vmax=-1,
    xlabel="period",
    ylabel="timestep per period",
    zlabel="",
    figsize=(12, 4),
    fontsize=12,
    save=False,
    fileName="",
    xticks=None,
    yticks=None,
    xticklabels=None,
    yticklabels=None,
    monthlabels=False,
    dpi=200,
    pad=0.12,
    aspect=15,
    fraction=0.2,
    orientation="horizontal",
    **kwargs,
):
    """Plot operation time series of a component at a location.

    **Required arguments:**

    :param esM: considered energy system model
    :type esM: EnergySystemModel class instance

    :param compName: component name
    :type compName: string

    :param loc: location
    :type loc: string

    **Default arguments:**

    :param ip: investment period of transformation path analysis.
    :type ip: int

    :param locTrans: second location, required when Transmission components are plotted
        |br| * the default value is None
    :type locTrans: string

    :param nbPeriods: number of periods to be plotted
        |br| * the default value is 365
    :type nbPeriods: integer

    :param nbTimeStepsPerPeriod: time steps per period to be plotted (nbPeriods*nbTimeStepsPerPeriod=length of time
        series)
        |br| * the default value is 24
    :type nbTimeStepsPerPeriod: integer

    :param variableName: name of the operation time series. Checkout the component model class to see which options
        are available.
        |br| * the default value is '_operationVariablesOptimum'
    :type variableName: string

    :param cmap: heat map (color map) (see matplotlib options)
        |br| * the default value is 'viridis'
    :type cmap: string

    :param vmin: minimum value in heat map
        |br| * the default value is 0
    :type vmin: integer

    :param vmax: maximum value in heat map. If -1, vmax is set to the maximum value of the operation time series.
        |br| * the default value is -1
    :type vmax: integer

    :param xlabel: x-label of the plot
        |br| * the default value is 'day'
    :type xlabel: string

    :param ylabel: y-label of the plot
        |br| * the default value is 'hour'
    :type ylabel: string

    :param zlabel: z-label of the plot
        |br| * the default value is 'operation'
    :type zlabel: string

    :param figsize: figure size in inches
        |br| * the default value is (12,4)
    :type figsize: tuple of positive floats

    :param fontsize: font size of the axis
        |br| * the default value is 12
    :type fontsize: positive float

    :param save: indicates if figure should be saved
        |br| * the default value is False
    :type save: boolean

    :param fileName: output file name
        |br| * the default value is 'operation.png'
    :type fileName: string

    :param xticks: user specified ticks of the x axis
        |br| * the default value is None
    :type xticks: list

    :param yticks: user specified ticks of the ý axis
        |br| * the default value is None
    :type yticks: list

    :param xticklabels: user specified tick labels of the x axis
        |br| * the default value is None
    :type xticklabels: list

    :param yticklabels: user specified tick labels of the ý axis
        |br| * the default value is None
    :type yticklabels: list

    :param monthlabels: specifies if month labels are to be plotted (only works correctly if
        365 days are specified as the number of periods)
        |br| * the default value is False
    :type monthlabels: boolean

    :param dpi: resolution in dots per inch
        |br| * the default value is 200
    :type dpi: scalar > 0

    :param pad: pad parameter of colorbar
        |br| * the default value is 0.12
    :type pad: float

    :param aspect: aspect parameter of colorbar
        |br| * the default value is 15
    :type aspect: float

    :param fraction: fraction parameter of colorbar
        |br| * the default value is 0.2
    :type fraction: float

    :param orientation: orientation parameter of colorbar
        |br| * the default value is 'horizontal'
    :type orientation: float

    """
    isStorage = False

    if isinstance(esM.getComponent(compName), fn.Conversion):
        unit = esM.getComponent(compName).physicalUnit
    else:
        unit = esM.commodityUnitsDict[esM.getComponent(compName).commodity]

    if isinstance(esM.getComponent(compName), fn.Storage):
        isStorage = True
        unit = unit + "*h"

    data = esM.componentModelingDict[esM.componentNames[compName]].getOptimalValues(
        variableName, ip=ip
    )

    if locTrans is None:
        timeSeries = data["values"].loc[(compName, loc)].values
    else:
        timeSeries = data["values"].loc[(compName, loc, locTrans)].values
    timeSeries = timeSeries / esM.hoursPerTimeStep if not isStorage else timeSeries

    try:
        timeSeries = timeSeries.reshape(nbPeriods, nbTimeStepsPerPeriod).T
    except ValueError as e:
        raise ValueError(
            f"Could not reshape array. Your timeSeries has {len(timeSeries)} values and it is therefore not possible"
            + f" to reshape it to ({nbPeriods}, {nbTimeStepsPerPeriod}). Please correctly specify nbPeriods"
            + f" and nbTimeStepsPerPeriod The error was: {e}."
        )
    vmax = timeSeries.max() if vmax == -1 else vmax

    fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)

    ax.pcolormesh(
        range(nbPeriods + 1),
        range(nbTimeStepsPerPeriod + 1),
        timeSeries,
        cmap=cmap,
        vmin=vmin,
        vmax=vmax,
        **kwargs,
    )
    ax.axis([0, nbPeriods, 0, nbTimeStepsPerPeriod])
    ax.set_xlabel(xlabel, fontsize=fontsize)
    ax.set_ylabel(ylabel, fontsize=fontsize)
    ax.xaxis.set_label_position("top"), ax.xaxis.set_ticks_position("top")

    sm1 = plt.cm.ScalarMappable(cmap=cmap, norm=plt.Normalize(vmin=vmin, vmax=vmax))
    sm1._A = []
    cb1 = fig.colorbar(
        sm1, ax=ax, pad=pad, aspect=aspect, fraction=fraction, orientation=orientation
    )
    cb1.ax.tick_params(labelsize=fontsize)
    if zlabel != "":
        cb1.ax.set_xlabel(zlabel, size=fontsize)
    elif isStorage:
        cb1.ax.set_xlabel("Storage inventory" + " [" + unit + "]", size=fontsize)
    else:
        cb1.ax.set_xlabel("Operation" + " [" + unit + "]", size=fontsize)
    cb1.ax.xaxis.set_label_position("top")

    if xticks:
        ax.set_xticks(xticks)
    if yticks:
        ax.set_yticks(yticks)
    if xticklabels:
        ax.set_xticklabels(xticklabels, fontsize=fontsize)
    if yticklabels:
        ax.set_yticklabels(yticklabels, fontsize=fontsize)

    if monthlabels:
        xticks, xlabels = [], []
        for i in range(1, 13, 2):
            xlabels.append(datetime.date(2050, i + 1, 1).strftime("%b"))
            xticks.append(datetime.datetime(2050, i + 1, 1).timetuple().tm_yday)
            ax.set_xticks(xticks), ax.set_xticklabels(xlabels, fontsize=fontsize)

    fig.tight_layout()

    if save:
        plt.savefig(fileName, dpi=dpi, bbox_inches="tight")

    return fig, ax

plotPieChart

plotPieChart(
    locFilePath,
    results_df,
    Property_to_plot="capacity",
    indexColumn_in_shp="index",
    color_list=[
        "skyBlue",
        "green",
        "yellowGreen",
        "#FFB732",
        "yellow",
        "darkOrange",
        "#996300",
        "steelBlue",
        "darkBlue",
    ],
    scaling_factor=500,
    legend_fontsize=14,
)

Plot pie charts on a map.

Source code in fine/IOManagement/standardIO.py
def plotPieChart(
    locFilePath,
    results_df,
    Property_to_plot="capacity",
    indexColumn_in_shp="index",
    color_list=[
        "skyBlue",
        "green",
        "yellowGreen",
        "#FFB732",
        "yellow",
        "darkOrange",
        "#996300",
        "steelBlue",
        "darkBlue",
    ],
    scaling_factor=500,
    legend_fontsize=14,
):
    """Plot pie charts on a map."""
    # Import shapefile, add centroid information
    shapefile = gpd.read_file(locFilePath)
    shapefile["centroid"] = shapefile.geometry.centroid

    # Subset, change NAs to 0s, Transpose, set indexColumn name in the property data
    property_subset = results_df.iloc[
        results_df.index.get_level_values("Property") == Property_to_plot
    ]

    property_subset = property_subset.droplevel(["Property", "Unit"]).fillna(0)
    property_subset = property_subset.transpose()
    property_subset.index.set_names(names=indexColumn_in_shp, inplace=True)

    # Total property values in each region
    regional_property_sum = property_subset.sum(axis=1)

    fig, ax = plotLocations(
        locFilePath, plotLocNames=False, indexColumn=indexColumn_in_shp
    )
    ax.set_aspect("equal")

    Total_degree = 360

    for region in shapefile[indexColumn_in_shp]:  # LOOP OVER REGIONS
        centroid = shapefile.loc[
            shapefile[indexColumn_in_shp] == region, "centroid"
        ].iloc[0]

        xValue = float(centroid.x)
        yValue = float(centroid.y)
        total_property_value = regional_property_sum[region]

        theta1 = 0
        for i, component in enumerate(
            property_subset.columns
        ):  # LOOP OVER TECHNOLOGIES
            component_property_value = property_subset.loc[region, component]

            share = (component_property_value / total_property_value) * Total_degree

            theta2 = theta1 + share

            wedge = mpatches.Wedge(
                (xValue, yValue),
                total_property_value
                * (10 / property_subset.values.mean())
                * scaling_factor,  # radius
                theta1,  # theta1
                theta2,  # theta2
                fc=color_list[i],  # color
                lw=0.6,
                zorder=2,
                edgecolor="black",
            )
            theta1 = theta2

            ax.add_artist(wedge)

    # Legend
    handles = []
    for i, component in enumerate(property_subset.columns):
        component_patch = mpatches.Patch(color=color_list[i], label=component)

        handles.append(component_patch)

    ax.legend(
        handles=handles,
        bbox_to_anchor=(1.05, 1),
        loc="upper left",
        fontsize=legend_fontsize,
    )

plotTransmission

plotTransmission(
    esM,
    compName,
    transmissionShapeFileName,
    loc0,
    loc1,
    ip=0,
    crs="EPSG:3035",
    variableName="capacityVariablesOptimum",
    color="k",
    loc=7,
    alpha=0.5,
    ax=None,
    fig=None,
    linewidth=10,
    figsize=(6, 6),
    fontsize=12,
    save=False,
    fileName="",
    dpi=200,
    **kwargs,
)

Plot build transmission lines from a shape file.

Required arguments:

:param esM: considered energy system model :type esM: EnergySystemModel class instance

:param compName: component name :type compName: string

:param transmissionShapeFileName: file name or path to a shape file :type transmissionShapeFileName: string

:param loc0: name of the column in which the location indices are stored (e.g. start/end of line) :type loc0: string

:param loc1: name of the column in which the location indices are stored (e.g. end/start of line) :type loc1: string

Default arguments:

:param ip: investment periods |br| * the default value is 0 :type ip: int

:param crs: coordinate reference system |br| * the default value is 'EPSG:3035' :type crs: string

:param variableName: parameter for plotting installed capacity ('_capacityVariablesOptimum') or operation ('_operationVariablesOptimum'). |br| * the default value is '_capacityVariablesOptimum' :type variableName: string

:param color: color of the transmission line |br| * the default value is 'k' :type color: string

:param loc: location of the legend in the plot |br| * the default value is 7 :type loc: 0 <= integer <= 10

:param alpha: transparency of the legend |br| * the default value is 0.5 :type alpha: 0 <= scalar <= 1

:param fig: None or figure to which the plot should be added |br| * the default value is None :type fig: matplotlib Figure

:param ax: None or ax to which the plot should be added |br| * the default value is None :type ax: matplotlib Axis

:param linewidth: line width of the plot |br| * the default value is 0.5 :type linewidth: positive float

:param figsize: figure size in inches |br| * the default value is (6,6) :type figsize: tuple of positive floats

:param fontsize: font size of the axis |br| * the default value is 12 :type fontsize: positive float

:param save: indicates if figure should be saved |br| * the default value is False :type save: boolean

:param fileName: output file name |br| * the default value is 'operation.png' :type fileName: string

:param dpi: resolution in dots per inch |br| * the default value is 200 :type dpi: scalar > 0

Source code in fine/IOManagement/standardIO.py
def plotTransmission(
    esM,
    compName,
    transmissionShapeFileName,
    loc0,
    loc1,
    ip=0,
    crs="EPSG:3035",
    variableName="capacityVariablesOptimum",
    color="k",
    loc=7,
    alpha=0.5,
    ax=None,
    fig=None,
    linewidth=10,
    figsize=(6, 6),
    fontsize=12,
    save=False,
    fileName="",
    dpi=200,
    **kwargs,
):
    """Plot build transmission lines from a shape file.

    **Required arguments:**

    :param esM: considered energy system model
    :type esM: EnergySystemModel class instance

    :param compName: component name
    :type compName: string

    :param transmissionShapeFileName: file name or path to a shape file
    :type transmissionShapeFileName: string

    :param loc0: name of the column in which the location indices are stored (e.g. start/end of line)
    :type loc0: string

    :param loc1: name of the column in which the location indices are stored (e.g. end/start of line)
    :type loc1: string

    **Default arguments:**

    :param ip: investment periods
        |br| * the default value is 0
    :type ip: int

    :param crs: coordinate reference system
        |br| * the default value is 'EPSG:3035'
    :type crs: string

    :param variableName: parameter for plotting installed capacity ('_capacityVariablesOptimum') or operation
        ('_operationVariablesOptimum').
        |br| * the default value is '_capacityVariablesOptimum'
    :type variableName: string

    :param color: color of the transmission line
        |br| * the default value is 'k'
    :type color: string

    :param loc: location of the legend in the plot
        |br| * the default value is 7
    :type loc: 0 <= integer <= 10

    :param alpha: transparency of the legend
        |br| * the default value is 0.5
    :type alpha: 0 <= scalar <= 1

    :param fig: None or figure to which the plot should be added
        |br| * the default value is None
    :type fig: matplotlib Figure

    :param ax: None or ax to which the plot should be added
        |br| * the default value is None
    :type ax: matplotlib Axis

    :param linewidth: line width of the plot
        |br| * the default value is 0.5
    :type linewidth: positive float

    :param figsize: figure size in inches
        |br| * the default value is (6,6)
    :type figsize: tuple of positive floats

    :param fontsize: font size of the axis
        |br| * the default value is 12
    :type fontsize: positive float

    :param save: indicates if figure should be saved
        |br| * the default value is False
    :type save: boolean

    :param fileName: output file name
        |br| * the default value is 'operation.png'
    :type fileName: string

    :param dpi: resolution in dots per inch
        |br| * the default value is 200
    :type dpi: scalar > 0
    """
    data = esM.componentModelingDict[esM.componentNames[compName]].getOptimalValues(
        variableName, ip=ip
    )
    unit = esM.getComponentAttribute(compName, "commodityUnit")
    if data is None:
        return fig, ax
    cap = data["values"].loc[compName].copy()
    capMax = cap.max().max()
    if capMax == 0:
        return fig, ax
    cap = cap / capMax
    gdf = gpd.read_file(transmissionShapeFileName).to_crs(crs)

    if ax is None:
        fig, ax = plt.subplots(1, 1, figsize=figsize, **kwargs)

    ax.set_aspect("equal")
    ax.axis("off")
    for key, row in gdf.iterrows():
        capacity = cap.loc[row[loc0], row[loc1]]
        gdf[gdf.index == key].plot(ax=ax, color=color, linewidth=linewidth * capacity)

    lineMax = plt.Line2D(
        range(1),
        range(1),
        linewidth=linewidth,
        color=color,
        marker="_",
        label=f"{str(capMax):>4.4}" + " " + unit,
    )
    lineMax23 = plt.Line2D(
        range(1),
        range(1),
        linewidth=linewidth * 2 / 3,
        color=color,
        marker="_",
        label=f"{str(capMax * 2 / 3):>4.4}" + " " + unit,
    )
    lineMax13 = plt.Line2D(
        range(1),
        range(1),
        linewidth=linewidth * 1 / 3,
        color=color,
        marker="_",
        label=f"{str(capMax * 1 / 3):>4.4}" + " " + unit,
    )

    leg = ax.legend(
        handles=[lineMax, lineMax23, lineMax13], prop={"size": fontsize}, loc=loc
    )
    leg.get_frame().set_edgecolor("white")
    leg.get_frame().set_alpha(alpha)

    fig.tight_layout()

    if save:
        plt.savefig(fileName, dpi=dpi, bbox_inches="tight")

    return fig, ax