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

diesel_genset_nonconvex_investment   A

Complexity

Total Complexity 5

Size/Duplication

Total Lines 512
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 5
eloc 276
dl 0
loc 512
rs 10
c 0
b 0
f 0

1 Function

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