Completed
Push — dev ( 468d85...f4c061 )
by Patrik
23s queued 17s
created

offset_diesel_genset_nonconvex_investment   A

Complexity

Total Complexity 10

Size/Duplication

Total Lines 619
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 10
eloc 338
dl 0
loc 619
rs 10
c 0
b 0
f 0

2 Functions

Rating   Name   Duplication   Size   Complexity  
A get_data_from_file_path() 0 7 2
C main() 0 543 8
1
# -*- coding: utf-8 -*-
2
3
"""
4
General description
5
-------------------
6
This example focuses on the modelling of an OffsetConverters representing
7
a diesel generator in a hybrid mini-grid system considering its real efficiency
8
curve based on the min/max load and efficiency.
9
10
The system consists of the following components:
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
Code
23
----
24
Download source code: :download:`offset_diesel_genset_nonconvex_investment.py </../examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py>`
25
26
.. dropdown:: Click to display code
27
28
    .. literalinclude:: /../examples/offset_converter_example/offset_diesel_genset_nonconvex_investment.py
29
        :language: python
30
        :lines: 44-
31
32
Data
33
----
34
Download data: :download:`input_data.csv </../examples/offset_converter_example/diesel_genset_data.csv>`
35
36
Installation requirements
37
-------------------------
38
This example requires the version v0.5.x of oemof. Install by:
39
40
    pip install 'oemof.solph>=0.5,<0.6'
41
42
"""
43
44
__copyright__ = "oemof developer group"
45
__license__ = "MIT"
46
47
import os
48
import time
49
from datetime import datetime
50
from datetime import timedelta
51
52
import numpy as np
53
import pandas as pd
54
55
from oemof import solph
56
57
try:
58
    import matplotlib.pyplot as plt
59
except ImportError:
60
    plt = None
61
62
63
def get_data_from_file_path(file_path: str) -> pd.DataFrame:
64
    try:
65
        data = pd.read_csv(file_path)
66
    except FileNotFoundError:
67
        dir = os.path.dirname(os.path.abspath(__file__))
68
        data = pd.read_csv(dir + "/" + file_path)
69
    return data
70
71
72
def main(optimize=True):
73
    ##########################################################################
74
    # Initialize the energy system and calculate necessary parameters
75
    ##########################################################################
76
77
    # Start time for calculating the total elapsed time.
78
    start_simulation_time = time.time()
79
80
    start = "2022-01-01"
81
82
    # The maximum number of days depends on the given csv file.
83
    n_days = 5
84
    n_days_in_year = 365
85
86
    # Create date and time objects.
87
    start_date_obj = datetime.strptime(start, "%Y-%m-%d")
88
    start_date = start_date_obj.date()
89
    start_time = start_date_obj.time()
90
    start_datetime = datetime.combine(
91
        start_date_obj.date(), start_date_obj.time()
92
    )
93
    end_datetime = start_datetime + timedelta(days=n_days)
94
95
    # Import data.
96
    data = get_data_from_file_path("diesel_genset_data.csv")
97
98
    # Change the index of data to be able to select data based on the time range.
99
    data.index = pd.date_range(start="2022-01-01", periods=len(data), freq="h")
100
101
    # Choose the range of the solar potential and demand
102
    # based on the selected simulation period.
103
    solar_potential = data.SolarGen.loc[start_datetime:end_datetime]
104
    hourly_demand = data.Demand.loc[start_datetime:end_datetime]
105
    peak_solar_potential = solar_potential.max()
106
    peak_demand = hourly_demand.max()
107
108
    # Create the energy system.
109
    date_time_index = pd.date_range(
110
        start=start_date, periods=n_days * 24, freq="h"
111
    )
112
    energy_system = solph.EnergySystem(timeindex=date_time_index)
113
114
    # -------------------- BUSES --------------------
115
    # Create electricity and diesel buses.
116
    b_el_ac = solph.buses.Bus(label="electricity_ac")
117
    b_el_dc = solph.buses.Bus(label="electricity_dc")
118
    b_diesel = solph.buses.Bus(label="diesel")
119
120
    # -------------------- SOURCES --------------------
121
    diesel_cost = 0.65  # currency/l
122
    diesel_density = 0.846  # kg/l
123
    diesel_lhv = 11.83  # kWh/kg
124
    diesel_source = solph.components.Source(
125
        label="diesel_source",
126
        outputs={
127
            b_diesel: solph.flows.Flow(
128
                variable_costs=diesel_cost / diesel_density / diesel_lhv
129
            )
130
        },
131
    )
132
133
    # EPC stands for the equivalent periodical costs.
134
    epc_pv = 152.62  # currency/kW/year
135
    pv = solph.components.Source(
136
        label="pv",
137
        outputs={
138
            b_el_dc: solph.flows.Flow(
139
                fix=solar_potential / peak_solar_potential,
140
                nominal_capacity=solph.Investment(
141
                    ep_costs=epc_pv * n_days / n_days_in_year
142
                ),
143
                variable_costs=0,
144
            )
145
        },
146
    )
147
148
    # -------------------- CONVERTERS --------------------
149
    # The diesel genset does not have a fixed nominal capacity and will be
150
    # optimized using the minimum and maximum loads and efficiencies.
151
    # In this case, the `Investment` and `NonConvex` attributes must be used. The
152
    # combination of these two attributes is utilized in the
153
    # `InvestNonConvexFlowBlock`.
154
155
    # If the nominal capacity of the genset is already known, only the `NonConvex`
156
    # attribute should be defined, and therefore, the `NonConvexFlowBlock` will
157
    # be used.
158
159
    # Specify the minimum and maximum loads and the corresponding efficiencies
160
    # for the diesel genset.
161
    min_load = 0.2
162
    max_load = 1.0
163
    min_efficiency = 0.20
164
    max_efficiency = 0.33
165
166
    # Calculate the two polynomial coefficients, i.e. the y-intersection and the
167
    # slope of the linear equation. There is a conveniece function for that
168
    # in solph:
169
    slope, offset = solph.components.slope_offset_from_nonconvex_output(
170
        max_load, min_load, max_efficiency, min_efficiency
171
    )
172
173
    epc_diesel_genset = 84.80  # currency/kW/year
174
    variable_cost_diesel_genset = 0.045  # currency/kWh
175
    diesel_genset = solph.components.OffsetConverter(
176
        label="diesel_genset",
177
        inputs={b_diesel: solph.flows.Flow()},
178
        outputs={
179
            b_el_ac: solph.flows.Flow(
180
                variable_costs=variable_cost_diesel_genset,
181
                min=min_load,
182
                max=max_load,
183
                nominal_capacity=solph.Investment(
184
                    ep_costs=epc_diesel_genset * n_days / n_days_in_year,
185
                    maximum=2 * peak_demand,
186
                ),
187
                nonconvex=solph.NonConvex(),
188
            ),
189
        },
190
        conversion_factors={b_diesel: slope},
191
        normed_offsets={b_diesel: offset},
192
    )
193
194
    # The rectifier assumed to have a fixed efficiency of 98%.
195
    epc_rectifier = 62.35  # currency/kW/year
196
    rectifier = solph.components.Converter(
197
        label="rectifier",
198
        inputs={
199
            b_el_ac: solph.flows.Flow(
200
                nominal_capacity=solph.Investment(
201
                    ep_costs=epc_rectifier * n_days / n_days_in_year
202
                ),
203
                variable_costs=0,
204
            )
205
        },
206
        outputs={b_el_dc: solph.flows.Flow()},
207
        conversion_factors={
208
            b_el_dc: 0.98,
209
        },
210
    )
211
212
    # The inverter assumed to have a fixed efficiency of 98%.
213
    epc_inverter = 62.35  # currency/kW/year
214
    inverter = solph.components.Converter(
215
        label="inverter",
216
        inputs={
217
            b_el_dc: solph.flows.Flow(
218
                nominal_capacity=solph.Investment(
219
                    ep_costs=epc_inverter * n_days / n_days_in_year
220
                ),
221
                variable_costs=0,
222
            )
223
        },
224
        outputs={b_el_ac: solph.flows.Flow()},
225
        conversion_factors={
226
            b_el_ac: 0.98,
227
        },
228
    )
229
230
    # -------------------- STORAGE --------------------
231
    epc_battery = 101.00  # currency/kWh/year
232
    battery = solph.components.GenericStorage(
233
        label="battery",
234
        nominal_capacity=solph.Investment(
235
            ep_costs=epc_battery * n_days / n_days_in_year
236
        ),
237
        inputs={b_el_dc: solph.flows.Flow(variable_costs=0)},
238
        outputs={
239
            b_el_dc: solph.flows.Flow(
240
                nominal_capacity=solph.Investment(ep_costs=0)
241
            )
242
        },
243
        initial_storage_level=0.0,
244
        min_storage_level=0.0,
245
        max_storage_level=1.0,
246
        balanced=True,
247
        inflow_conversion_factor=0.9,
248
        outflow_conversion_factor=0.9,
249
        invest_relation_input_capacity=1,
250
        invest_relation_output_capacity=0.5,
251
    )
252
253
    # -------------------- SINKS --------------------
254
    demand_el = solph.components.Sink(
255
        label="electricity_demand",
256
        inputs={
257
            b_el_ac: solph.flows.Flow(
258
                fix=hourly_demand / peak_demand,
259
                nominal_capacity=peak_demand,
260
            )
261
        },
262
    )
263
264
    excess_el = solph.components.Sink(
265
        label="excess_el",
266
        inputs={b_el_dc: solph.flows.Flow()},
267
    )
268
269
    # Add all objects to the energy system.
270
    energy_system.add(
271
        pv,
272
        diesel_source,
273
        b_el_dc,
274
        b_el_ac,
275
        b_diesel,
276
        inverter,
277
        rectifier,
278
        diesel_genset,
279
        battery,
280
        demand_el,
281
        excess_el,
282
    )
283
284
    ##########################################################################
285
    # Optimise the energy system
286
    ##########################################################################
287
288
    if optimize is False:
289
        return energy_system
290
291
    # The higher the MipGap or ratioGap, the faster the solver would converge,
292
    # but the less accurate the results would be.
293
    solver_option = {"gurobi": {"MipGap": "0.02"}, "cbc": {"ratioGap": "0.02"}}
294
    solver = "cbc"
295
296
    model = solph.Model(energy_system)
297
    model.solve(
298
        solver=solver,
299
        solve_kwargs={"tee": True},
300
        cmdline_options=solver_option[solver],
301
    )
302
303
    # End of the calculation time.
304
    end_simulation_time = time.time()
305
306
    ##########################################################################
307
    # Process the results
308
    ##########################################################################
309
310
    results = solph.processing.results(model)
311
312
    results_pv = solph.views.node(results=results, node="pv")
313
    results_diesel_source = solph.views.node(
314
        results=results, node="diesel_source"
315
    )
316
    results_diesel_genset = solph.views.node(
317
        results=results, node="diesel_genset"
318
    )
319
    results_inverter = solph.views.node(results=results, node="inverter")
320
    results_rectifier = solph.views.node(results=results, node="rectifier")
321
    results_battery = solph.views.node(results=results, node="battery")
322
    results_demand_el = solph.views.node(
323
        results=results, node="electricity_demand"
324
    )
325
    results_excess_el = solph.views.node(results=results, node="excess_el")
326
327
    # -------------------- SEQUENCES (DYNAMIC) --------------------
328
    # Hourly demand profile.
329
    sequences_demand = results_demand_el["sequences"][
330
        (("electricity_ac", "electricity_demand"), "flow")
331
    ]
332
333
    # Hourly profiles for solar potential and pv production.
334
    sequences_pv = results_pv["sequences"][(("pv", "electricity_dc"), "flow")]
335
336
    # Hourly profiles for diesel consumption and electricity production
337
    # in the diesel genset.
338
    # The 'flow' from oemof is in kWh and must be converted to
339
    # kg by dividing it by the lower heating value and then to
340
    # liter by dividing it by the diesel density.
341
    sequences_diesel_consumption = (
342
        results_diesel_source["sequences"][
343
            (("diesel_source", "diesel"), "flow")
344
        ]
345
        / diesel_lhv
346
        / diesel_density
347
    )
348
349
    # Hourly profile for the kWh of the diesel provided for the diesel genset
350
    sequences_diesel_consumption_kwh = results_diesel_source["sequences"][
351
        (("diesel_source", "diesel"), "flow")
352
    ]
353
354
    sequences_diesel_genset = results_diesel_genset["sequences"][
355
        (("diesel_genset", "electricity_ac"), "flow")
356
    ]
357
358
    # Hourly profiles for inverted electricity from dc to ac.
359
    sequences_inverter = results_inverter["sequences"][
360
        (("inverter", "electricity_ac"), "flow")
361
    ]
362
363
    # Hourly profiles for inverted electricity from ac to dc.
364
    sequences_rectifier = results_rectifier["sequences"][
365
        (("rectifier", "electricity_dc"), "flow")
366
    ]
367
368
    # Hourly profiles for excess ac and dc electricity production.
369
    sequences_excess = results_excess_el["sequences"][
370
        (("electricity_dc", "excess_el"), "flow")
371
    ]
372
373
    # -------------------- SCALARS (STATIC) --------------------
374
    capacity_diesel_genset = results_diesel_genset["scalars"][
375
        (("diesel_genset", "electricity_ac"), "invest")
376
    ]
377
378
    # Define a tolerance to force 'too close' numbers to the `min_load`
379
    # and to 0 to be the same as the `min_load` and 0.
380
    # Load is defined here in percentage.
381
    tol = 1e-8
382
    load_diesel_genset = sequences_diesel_genset / capacity_diesel_genset * 100
383
    sequences_diesel_genset[np.abs(load_diesel_genset) < tol] = 0
384
    sequences_diesel_genset[np.abs(load_diesel_genset - min_load) < tol] = (
385
        min_load * capacity_diesel_genset
386
    )
387
    sequences_diesel_genset[np.abs(load_diesel_genset - max_load) < tol] = (
388
        max_load * capacity_diesel_genset
389
    )
390
391
    # Calculate the efficiency of the diesel genset.
392
    # If the load is equal to 0, the efficiency will also be 0.
393
    # Efficiency is defined here in percentage.
394
    efficiency_diesel_genset = np.zeros(len(sequences_diesel_genset))
395
    for i in range(len(sequences_diesel_genset)):
396
        if sequences_diesel_consumption_kwh[i] != 0:
397
            efficiency_diesel_genset[i] = (
398
                sequences_diesel_genset[i]
399
                / sequences_diesel_consumption_kwh[i]
400
                * 100
401
            )
402
403
    capacity_pv = results_pv["scalars"][(("pv", "electricity_dc"), "invest")]
404
    capacity_inverter = results_inverter["scalars"][
405
        (("electricity_dc", "inverter"), "invest")
406
    ]
407
    capacity_rectifier = results_rectifier["scalars"][
408
        (("electricity_ac", "rectifier"), "invest")
409
    ]
410
    capacity_battery = results_battery["scalars"][
411
        (("electricity_dc", "battery"), "invest")
412
    ]
413
414
    total_cost_component = (
415
        (
416
            epc_diesel_genset * capacity_diesel_genset
417
            + epc_pv * capacity_pv
418
            + epc_rectifier * capacity_rectifier
419
            + epc_inverter * capacity_inverter
420
            + epc_battery * capacity_battery
421
        )
422
        * n_days
423
        / n_days_in_year
424
    )
425
426
    # The only componnet with the variable cost is the diesl genset
427
    total_cost_variable = (
428
        variable_cost_diesel_genset * sequences_diesel_genset.sum(axis=0)
429
    )
430
431
    total_cost_diesel = diesel_cost * sequences_diesel_consumption.sum(axis=0)
432
    total_revenue = (
433
        total_cost_component + total_cost_variable + total_cost_diesel
434
    )
435
    total_demand = sequences_demand.sum(axis=0)
436
437
    # Levelized cost of electricity in the system in currency's Cent per kWh.
438
    lcoe = 100 * total_revenue / total_demand
439
440
    # The share of renewable energy source used to cover the demand.
441
    res = (
442
        100
443
        * sequences_pv.sum(axis=0)
444
        / (sequences_diesel_genset.sum(axis=0) + sequences_pv.sum(axis=0))
445
    )
446
447
    # The amount of excess electricity (which must probably be dumped).
448
    excess_rate = (
449
        100
450
        * sequences_excess.sum(axis=0)
451
        / (sequences_excess.sum(axis=0) + sequences_demand.sum(axis=0))
452
    )
453
454
    ##########################################################################
455
    # Print the results in the terminal
456
    ##########################################################################
457
458
    if __name__ == "__main__":
459
        print("\n" + 50 * "*")
460
        print(
461
            f"Simulation Time: {end_simulation_time-start_simulation_time:.2f} s"
462
        )
463
        print(50 * "*")
464
        print(f"Peak Demand:\t {sequences_demand.max():.0f} kW")
465
        print(f"LCOE:\t\t {lcoe:.2f} cent/kWh")
466
        print(f"RES:\t\t {res:.0f}%")
467
        print(f"Excess:\t\t {excess_rate:.1f}% of the total production")
468
        print(50 * "*")
469
        print("Optimal Capacities:")
470
        print("-------------------")
471
        print(f"Diesel Genset:\t {capacity_diesel_genset:.0f} kW")
472
        print(f"PV:\t\t {capacity_pv:.0f} kW")
473
        print(f"Battery:\t {capacity_battery:.0f} kW")
474
        print(f"Inverter:\t {capacity_inverter:.0f} kW")
475
        print(f"Rectifier:\t {capacity_rectifier:.0f} kW")
476
        print(50 * "*")
477
478
        ##########################################################################
479
        # Plot the duration curve for the diesel genset
480
        ##########################################################################
481
482
        if plt is not None:
483
            # Create the duration curve for the diesel genset.
484
            fig1, ax = plt.subplots(figsize=(10, 5))
485
486
            # Sort the power generated by the diesel genset in a descending order.
487
            diesel_genset_duration_curve = np.sort(sequences_diesel_genset)[
488
                ::-1
489
            ]
490
491
            percentage = (
492
                100
493
                * np.arange(1, len(diesel_genset_duration_curve) + 1)
494
                / len(diesel_genset_duration_curve)
495
            )
496
497
            ax.scatter(
498
                percentage,
499
                diesel_genset_duration_curve,
500
                color="dodgerblue",
501
                marker="+",
502
            )
503
504
            # Plot a horizontal line representing the minimum load
505
            ax.axhline(
506
                y=min_load * capacity_diesel_genset,
507
                color="crimson",
508
                linestyle="--",
509
            )
510
            min_load_annotation_text = (
511
                f"minimum load: {min_load * capacity_diesel_genset:0.2f} kW"
512
            )
513
            ax.annotate(
514
                min_load_annotation_text,
515
                xy=(100, min_load * capacity_diesel_genset),
516
                xytext=(0, 5),
517
                textcoords="offset pixels",
518
                horizontalalignment="right",
519
                verticalalignment="bottom",
520
            )
521
522
            # Plot a horizontal line representing the maximum load
523
            ax.axhline(
524
                y=max_load * capacity_diesel_genset,
525
                color="crimson",
526
                linestyle="--",
527
            )
528
            max_load_annotation_text = (
529
                f"maximum load: {max_load * capacity_diesel_genset:0.2f} kW"
530
            )
531
            ax.annotate(
532
                max_load_annotation_text,
533
                xy=(100, max_load * capacity_diesel_genset),
534
                xytext=(0, -5),
535
                textcoords="offset pixels",
536
                horizontalalignment="right",
537
                verticalalignment="top",
538
            )
539
540
            ax.set_title(
541
                "Duration Curve for the Diesel Genset Electricity Production",
542
                fontweight="bold",
543
            )
544
            ax.set_ylabel("diesel genset production [kW]")
545
            ax.set_xlabel("percentage of annual operation [%]")
546
547
            # Create the second axis on the right side of the diagram
548
            # representing the operation load of the diesel genset.
549
            second_ax = ax.secondary_yaxis(
550
                "right",
551
                functions=(
552
                    lambda x: x / capacity_diesel_genset * 100,
0 ignored issues
show
introduced by
The variable capacity_diesel_genset does not seem to be defined for all execution paths.
Loading history...
553
                    lambda x: x * capacity_diesel_genset / 100,
0 ignored issues
show
introduced by
The variable capacity_diesel_genset does not seem to be defined for all execution paths.
Loading history...
554
                ),
555
            )
556
            second_ax.set_ylabel("diesel genset operation load [%]")
557
558
            #######################################################################
559
            # Plot the efficiency curve for the diesel genset
560
            #######################################################################
561
562
            fig2, ax = plt.subplots(figsize=(10, 5))
563
            ax.scatter(
564
                load_diesel_genset,
565
                efficiency_diesel_genset,
566
                marker="+",
567
            )
568
569
            # Plot a horizontal line representing the minimum efficiency
570
            ax.axhline(
571
                y=min_efficiency * 100,
572
                color="crimson",
573
                linestyle="--",
574
            )
575
            min_efficiency_annotation_text = (
576
                f"minimum efficiency: {min_efficiency*100:0.0f}%"
577
            )
578
            ax.annotate(
579
                min_efficiency_annotation_text,
580
                xy=(100, min_efficiency * 100),
581
                xytext=(0, 10),
582
                textcoords="offset pixels",
583
                horizontalalignment="right",
584
                verticalalignment="bottom",
585
            )
586
587
            # Plot a horizontal line representing the maximum efficiency
588
            ax.axhline(
589
                y=max_efficiency * 100,
590
                color="crimson",
591
                linestyle="--",
592
            )
593
            max_efficiency_annotation_text = (
594
                f"maximum efficiency: {max_efficiency*100:0.0f}%"
595
            )
596
            ax.annotate(
597
                max_efficiency_annotation_text,
598
                xy=(100, max_efficiency * 100),
599
                xytext=(0, 10),
600
                textcoords="offset pixels",
601
                horizontalalignment="right",
602
                verticalalignment="bottom",
603
            )
604
605
            ax.set_title(
606
                "Efficiency Curve for Different Loads of the Diesel Genset",
607
                fontweight="bold",
608
            )
609
            ax.set_ylabel("efficiency [%]")
610
            ax.set_xlabel("diesel genset load [%]")
611
612
            ax.set_xlim(min_load * 100 - 5, max_load * 100 + 5)
613
614
            plt.show()
615
616
617
if __name__ == "__main__":
618
    main()
619