Passed
Pull Request — dev (#850)
by Uwe
01:30
created

main()   C

Complexity

Conditions 5

Size

Total Lines 449
Code Lines 266

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 266
dl 0
loc 449
rs 6.5333
c 0
b 0
f 0
cc 5
nop 0

How to fix   Long Method   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
# -*- coding: utf-8 -*-
2
3
"""
4
General description
5
-------------------
6
This example illustrates the combination of Investment and NonConvex options
7
applied to a diesel generator in a hybrid mini-grid system.
8
9
There are the following components:
10
11
    - pv: solar potential to generate electricity
12
    - diesel_source: input diesel for the diesel genset
13
    - diesel_genset: generates ac electricity
14
    - rectifier: converts generated ac electricity from the diesel genset
15
                 to dc electricity
16
    - inverter: converts generated dc electricity from the pv to ac electricity
17
    - battery: stores the generated dc electricity
18
    - demand_el: ac electricity demand (given as a separate *.csv file)
19
    - excess_el: allows for some electricity overproduction
20
21
22
23
Installation requirements
24
-------------------------
25
This example requires the version v0.5.x of oemof.solph. Install by:
26
27
    pip install 'oemof.solph>=0.5,<0.6'
28
29
"""
30
31
__copyright__ = "oemof developer group"
32
__license__ = "MIT"
33
34
import numpy as np
35
import os
36
import pandas as pd
37
import time
38
from datetime import datetime, timedelta
39
from oemof import solph
40
import warnings
41
42
try:
43
    import matplotlib.pyplot as plt
44
except ImportError:
45
    plt = None
46
47
48
def main():
49
    ##########################################################################
50
    # Initialize the energy system and calculate necessary parameters
51
    ##########################################################################
52
53
    # Start time for calculating the total elapsed time.
54
    start_simulation_time = time.time()
55
56
    start = "2022-01-01"
57
58
    # The maximum number of days depends on the given *.csv file.
59
    n_days = 10
60
    n_days_in_year = 365
61
62
    # Create date and time objects.
63
    start_date_obj = datetime.strptime(start, "%Y-%m-%d")
64
    start_date = start_date_obj.date()
65
    start_datetime = datetime.combine(
66
        start_date_obj.date(), start_date_obj.time()
67
    )
68
    end_datetime = start_datetime + timedelta(days=n_days)
69
70
    # Import data.
71
    filename = os.path.join(os.getcwd(), "solar_generation.csv")
72
    try:
73
        data = pd.read_csv(filename)
74
    except FileNotFoundError:
75
        msg = "Data file not found: {0}. Only one value used!"
76
        warnings.warn(msg.format(filename), UserWarning)
77
        data = pd.DataFrame({"pv": [0.3], "wind": [0.6], "demand_el": [500]})
78
79
    # Change the index of data to be able to select data based on the time
80
    # range.
81
    data.index = pd.date_range(start="2022-01-01", periods=len(data), freq="H")
82
83
    # Choose the range of the solar potential and demand
84
    # based on the selected simulation period.
85
    solar_potential = data.SolarGen.loc[start_datetime:end_datetime]
86
    hourly_demand = data.Demand.loc[start_datetime:end_datetime]
87
    peak_solar_potential = solar_potential.max()
88
    peak_demand = hourly_demand.max()
89
90
    # Create the energy system.
91
    date_time_index = solph.create_time_index(
92
        number=n_days * 24, start=start_date
93
    )
94
    energysystem = solph.EnergySystem(
95
        timeindex=date_time_index, infer_last_interval=False
96
    )
97
98
    # -------------------- BUSES --------------------
99
    # Create electricity and diesel buses.
100
    b_el_ac = solph.buses.Bus(label="electricity_ac")
101
    b_el_dc = solph.buses.Bus(label="electricity_dc")
102
    b_diesel = solph.buses.Bus(label="diesel")
103
104
    # -------------------- SOURCES --------------------
105
    diesel_cost = 0.65  # currency/l
106
    diesel_density = 0.846  # kg/l
107
    diesel_lhv = 11.83  # kWh/kg
108
    diesel_source = solph.components.Source(
109
        label="diesel_source",
110
        outputs={
111
            b_diesel: solph.flows.Flow(
112
                variable_costs=diesel_cost / diesel_density / diesel_lhv
113
            )
114
        },
115
    )
116
117
    # EPC stands for the equivalent periodical costs.
118
    epc_pv = 152.62  # currency/kW/year
119
    pv = solph.components.Source(
120
        label="pv",
121
        outputs={
122
            b_el_dc: solph.flows.Flow(
123
                fix=solar_potential / peak_solar_potential,
124
                nominal_value=None,
125
                investment=solph.Investment(
126
                    ep_costs=epc_pv * n_days / n_days_in_year
127
                ),
128
                variable_costs=0,
129
            )
130
        },
131
    )
132
133
    # -------------------- TRANSFORMERS --------------------
134
    # The diesel genset assumed to have a fixed efficiency of 33%.
135
136
    # The output power of the diesel genset can only vary between
137
    # the given minimum and maximum loads, which represent the fraction
138
    # of the optimal capacity obtained from the optimization.
139
140
    epc_diesel_genset = 84.80  # currency/kW/year
141
    variable_cost_diesel_genset = 0.045  # currency/kWh
142
    min_load = 0.2
143
    max_load = 1.0
144
    diesel_genset = solph.components.Transformer(
145
        label="diesel_genset",
146
        inputs={b_diesel: solph.flows.Flow()},
147
        outputs={
148
            b_el_ac: solph.flows.Flow(
149
                nominal_value=None,
150
                variable_costs=variable_cost_diesel_genset,
151
                min=min_load,
152
                max=max_load,
153
                investment=solph.Investment(
154
                    ep_costs=epc_diesel_genset * n_days / n_days_in_year,
155
                    maximum=2 * peak_demand,
156
                ),
157
                nonconvex=solph.NonConvex(),
158
            )
159
        },
160
        conversion_factors={b_el_ac: 0.33},
161
    )
162
163
    # The rectifier assumed to have a fixed efficiency of 98%.
164
    epc_rectifier = 62.35  # currency/kW/year
165
    rectifier = solph.components.Transformer(
166
        label="rectifier",
167
        inputs={
168
            b_el_ac: solph.flows.Flow(
169
                nominal_value=None,
170
                investment=solph.Investment(
171
                    ep_costs=epc_rectifier * n_days / n_days_in_year
172
                ),
173
                variable_costs=0,
174
            )
175
        },
176
        outputs={b_el_dc: solph.flows.Flow()},
177
        conversion_factor={
178
            b_el_dc: 0.98,
179
        },
180
    )
181
182
    # The inverter assumed to have a fixed efficiency of 98%.
183
    epc_inverter = 62.35  # currency/kW/year
184
    inverter = solph.components.Transformer(
185
        label="inverter",
186
        inputs={
187
            b_el_dc: solph.flows.Flow(
188
                nominal_value=None,
189
                investment=solph.Investment(
190
                    ep_costs=epc_inverter * n_days / n_days_in_year
191
                ),
192
                variable_costs=0,
193
            )
194
        },
195
        outputs={b_el_ac: solph.flows.Flow()},
196
        conversion_factor={
197
            b_el_ac: 0.98,
198
        },
199
    )
200
201
    # -------------------- STORAGE --------------------
202
    epc_battery = 101.00  # currency/kWh/year
203
    battery = solph.components.GenericStorage(
204
        label="battery",
205
        nominal_storage_capacity=None,
206
        investment=solph.Investment(
207
            ep_costs=epc_battery * n_days / n_days_in_year
208
        ),
209
        inputs={b_el_dc: solph.flows.Flow(variable_costs=0)},
210
        outputs={
211
            b_el_dc: solph.flows.Flow(investment=solph.Investment(ep_costs=0))
212
        },
213
        initial_storage_level=0.0,
214
        min_storage_level=0.0,
215
        max_storage_level=1.0,
216
        balanced=True,
217
        inflow_conversion_factor=0.9,
218
        outflow_conversion_factor=0.9,
219
        invest_relation_input_capacity=1,
220
        invest_relation_output_capacity=0.5,
221
    )
222
223
    # -------------------- SINKS --------------------
224
    demand_el = solph.components.Sink(
225
        label="electricity_demand",
226
        inputs={
227
            b_el_ac: solph.flows.Flow(
228
                fix=hourly_demand / peak_demand,
229
                nominal_value=peak_demand,
230
            )
231
        },
232
    )
233
234
    excess_el = solph.components.Sink(
235
        label="excess_el",
236
        inputs={b_el_dc: solph.flows.Flow()},
237
    )
238
239
    # Add all objects to the energy system.
240
    energysystem.add(
241
        pv,
242
        diesel_source,
243
        b_el_dc,
244
        b_el_ac,
245
        b_diesel,
246
        inverter,
247
        rectifier,
248
        diesel_genset,
249
        battery,
250
        demand_el,
251
        excess_el,
252
    )
253
254
    ##########################################################################
255
    # Optimise the energy system
256
    ##########################################################################
257
258
    # The higher the MipGap or ratioGap, the faster the solver would converge,
259
    # but the less accurate the results would be.
260
    solver_option = {"gurobi": {"MipGap": "0.02"}, "cbc": {"ratioGap": "0.02"}}
261
    solver = "cbc"
262
263
    model = solph.Model(energysystem)
264
    model.solve(
265
        solver=solver,
266
        solve_kwargs={"tee": True},
267
        cmdline_options=solver_option[solver],
268
    )
269
270
    # End of the calculation time.
271
    end_simulation_time = time.time()
272
273
    ##########################################################################
274
    # Process the results
275
    ##########################################################################
276
277
    results = solph.processing.results(model)
278
279
    results_pv = solph.views.node(results=results, node="pv")
280
    results_diesel_source = solph.views.node(
281
        results=results, node="diesel_source"
282
    )
283
    results_diesel_genset = solph.views.node(
284
        results=results, node="diesel_genset"
285
    )
286
    results_inverter = solph.views.node(results=results, node="inverter")
287
    results_rectifier = solph.views.node(results=results, node="rectifier")
288
    results_battery = solph.views.node(results=results, node="battery")
289
    results_demand_el = solph.views.node(
290
        results=results, node="electricity_demand"
291
    )
292
    results_excess_el = solph.views.node(results=results, node="excess_el")
293
294
    # -------------------- SEQUENCES (DYNAMIC) --------------------
295
    # Hourly demand profile.
296
    sequences_demand = results_demand_el["sequences"][
297
        (("electricity_ac", "electricity_demand"), "flow")
298
    ]
299
300
    # Hourly profiles for solar potential and pv production.
301
    sequences_pv = results_pv["sequences"][(("pv", "electricity_dc"), "flow")]
302
303
    # Hourly profiles for diesel consumption and electricity production
304
    # in the diesel genset.
305
    # The 'flow' from oemof is in kWh and must be converted to
306
    # kg by dividing it by the lower heating value and then to
307
    # liter by dividing it by the diesel density.
308
    sequences_diesel_consumption = (
309
        results_diesel_source["sequences"][
310
            (("diesel_source", "diesel"), "flow")
311
        ]
312
        / diesel_lhv
313
        / diesel_density
314
    )
315
316
    # Hourly profiles for electricity production in the diesel genset.
317
    sequences_diesel_genset = results_diesel_genset["sequences"][
318
        (("diesel_genset", "electricity_ac"), "flow")
319
    ]
320
321
    # Hourly profiles for excess ac and dc electricity production.
322
    sequences_excess = results_excess_el["sequences"][
323
        (("electricity_dc", "excess_el"), "flow")
324
    ]
325
326
    # -------------------- SCALARS (STATIC) --------------------
327
    capacity_diesel_genset = results_diesel_genset["scalars"][
328
        (("diesel_genset", "electricity_ac"), "invest")
329
    ]
330
331
    # Define a tolerance to force 'too close' numbers to the `min_load`
332
    # and to 0 to be the same as the `min_load` and 0.
333
    tol = 1e-8
334
    load_diesel_genset = sequences_diesel_genset / capacity_diesel_genset
335
    sequences_diesel_genset[np.abs(load_diesel_genset) < tol] = 0
336
    sequences_diesel_genset[np.abs(load_diesel_genset - min_load) < tol] = (
337
        min_load * capacity_diesel_genset
338
    )
339
    sequences_diesel_genset[np.abs(load_diesel_genset - max_load) < tol] = (
340
        max_load * capacity_diesel_genset
341
    )
342
343
    capacity_pv = results_pv["scalars"][(("pv", "electricity_dc"), "invest")]
344
    capacity_inverter = results_inverter["scalars"][
345
        (("electricity_dc", "inverter"), "invest")
346
    ]
347
    capacity_rectifier = results_rectifier["scalars"][
348
        (("electricity_ac", "rectifier"), "invest")
349
    ]
350
    capacity_battery = results_battery["scalars"][
351
        (("electricity_dc", "battery"), "invest")
352
    ]
353
354
    total_cost_component = (
355
        (
356
            epc_diesel_genset * capacity_diesel_genset
357
            + epc_pv * capacity_pv
358
            + epc_rectifier * capacity_rectifier
359
            + epc_inverter * capacity_inverter
360
            + epc_battery * capacity_battery
361
        )
362
        * n_days
363
        / n_days_in_year
364
    )
365
366
    # The only componnet with the variable cost is the diesl genset
367
    total_cost_variable = (
368
        variable_cost_diesel_genset * sequences_diesel_genset.sum(axis=0)
369
    )
370
371
    total_cost_diesel = diesel_cost * sequences_diesel_consumption.sum(axis=0)
372
    total_revenue = (
373
        total_cost_component + total_cost_variable + total_cost_diesel
374
    )
375
    total_demand = sequences_demand.sum(axis=0)
376
377
    # Levelized cost of electricity in the system in currency's Cent per kWh.
378
    lcoe = 100 * total_revenue / total_demand
379
380
    # The share of renewable energy source used to cover the demand.
381
    res = (
382
        100
383
        * sequences_pv.sum(axis=0)
384
        / (sequences_diesel_genset.sum(axis=0) + sequences_pv.sum(axis=0))
385
    )
386
387
    # The amount of excess electricity (which must probably be dumped).
388
    excess_rate = (
389
        100
390
        * sequences_excess.sum(axis=0)
391
        / (sequences_excess.sum(axis=0) + sequences_demand.sum(axis=0))
392
    )
393
394
    ##########################################################################
395
    # Print the results in the terminal
396
    ##########################################################################
397
398
    print("\n" + 50 * "*")
399
    print(
400
        f"Simulation Time:\t {end_simulation_time-start_simulation_time:.2f} s"
401
    )
402
    print(50 * "*")
403
    print(f"Peak Demand:\t {sequences_demand.max():.0f} kW")
404
    print(f"LCOE:\t\t {lcoe:.2f} cent/kWh")
405
    print(f"RES:\t\t {res:.0f}%")
406
    print(f"Excess:\t\t {excess_rate:.1f}% of the total production")
407
    print(50 * "*")
408
    print("Optimal Capacities:")
409
    print("-------------------")
410
    print(f"Diesel Genset:\t {capacity_diesel_genset:.0f} kW")
411
    print(f"PV:\t\t {capacity_pv:.0f} kW")
412
    print(f"Battery:\t {capacity_battery:.0f} kW")
413
    print(f"Inverter:\t {capacity_inverter:.0f} kW")
414
    print(f"Rectifier:\t {capacity_rectifier:.0f} kW")
415
    print(50 * "*")
416
417
    ##########################################################################
418
    # Plot the duration curve for the diesel genset
419
    ##########################################################################
420
421
    if plt is not None:
422
423
        # Create the duration curve for the diesel genset.
424
        fig, ax = plt.subplots(figsize=(10, 5))
425
426
        # Sort the power generated by the diesel genset in descending order.
427
        diesel_genset_duration_curve = np.sort(sequences_diesel_genset)[::-1]
428
429
        percentage = (
430
            100
431
            * np.arange(1, len(diesel_genset_duration_curve) + 1)
432
            / len(diesel_genset_duration_curve)
433
        )
434
435
        ax.scatter(
436
            percentage,
437
            diesel_genset_duration_curve,
438
            color="dodgerblue",
439
            marker="+",
440
        )
441
442
        # Plot a horizontal line representing the minimum load
443
        ax.axhline(
444
            y=min_load * capacity_diesel_genset,
445
            color="crimson",
446
            linestyle="--",
447
        )
448
        min_load_annotation_text = (
449
            f"minimum load: {min_load * capacity_diesel_genset:0.2f} kW"
450
        )
451
        ax.annotate(
452
            min_load_annotation_text,
453
            xy=(100, min_load * capacity_diesel_genset),
454
            xytext=(0, 5),
455
            textcoords="offset pixels",
456
            horizontalalignment="right",
457
            verticalalignment="bottom",
458
        )
459
460
        # Plot a horizontal line representing the maximum load
461
        ax.axhline(
462
            y=max_load * capacity_diesel_genset,
463
            color="crimson",
464
            linestyle="--",
465
        )
466
        max_load_annotation_text = (
467
            f"maximum load: {max_load * capacity_diesel_genset:0.2f} kW"
468
        )
469
        ax.annotate(
470
            max_load_annotation_text,
471
            xy=(100, max_load * capacity_diesel_genset),
472
            xytext=(0, -5),
473
            textcoords="offset pixels",
474
            horizontalalignment="right",
475
            verticalalignment="top",
476
        )
477
478
        ax.set_title(
479
            "Duration Curve for the Diesel Genset Electricity Production",
480
            fontweight="bold",
481
        )
482
        ax.set_ylabel("diesel genset production [kW]")
483
        ax.set_xlabel("percentage of annual operation [%]")
484
485
        # Create the second axis on the right side of the diagram
486
        # representing the operation load of the diesel genset.
487
        second_ax = ax.secondary_yaxis(
488
            "right",
489
            functions=(
490
                lambda x: x / capacity_diesel_genset * 100,
491
                lambda x: x * capacity_diesel_genset / 100,
492
            ),
493
        )
494
        second_ax.set_ylabel("diesel genset operation load [%]")
495
496
        plt.show()
497
498
499
if __name__ == "__main__":
500
    main()
501