• Home
  • Features
  • Pricing
  • Docs
  • Announcements
  • Sign In

rl-institut / multi-vector-simulator / 4083659824

pending completion
4083659824

push

github

GitHub
Merge pull request #942 from rl-institut/feature/feedin-capping

86 of 86 new or added lines in 9 files covered. (100.0%)

5800 of 7524 relevant lines covered (77.09%)

0.77 hits per line

Source File
Press 'n' to go to next uncovered line, 'b' for previous

90.09
/src/multi_vector_simulator/E3_indicator_calculation.py
1
r"""
2
Module E3 - Indicator calculation
3
==================================
4

5
In module E3 the technical KPI are evaluated:
6
- calculate renewable share
7
- calculate degree of autonomy (DA)
8
- calculate degree of net zero energy (NZE)
9
- calculate total generation of each asset and total_internal_generation
10
- calculate total feedin electricity equivalent
11
- calculate energy flows between sectors
12
- calculate degree of sector coupling
13
- calculate onsite energy fraction (OEF)
14
- calculate onsite energy matching (OEM)
15
"""
16
import logging
1✔
17

18
from multi_vector_simulator.utils.constants import DEFAULT_WEIGHTS_ENERGY_CARRIERS
1✔
19
from multi_vector_simulator.utils.constants import PROJECT_DATA
1✔
20
from multi_vector_simulator.utils.constants_json_strings import (
1✔
21
    VALUE,
22
    LABEL,
23
    ECONOMIC_DATA,
24
    SIMULATION_SETTINGS,
25
    ENERGY_CONVERSION,
26
    ENERGY_PRODUCTION,
27
    ENERGY_PROVIDERS,
28
    ENERGY_CONSUMPTION,
29
    CONNECTED_FEEDIN_SINK,
30
    CRF,
31
    LES_ENERGY_VECTOR_S,
32
    EXCESS_SINK,
33
    ENERGY_VECTOR,
34
    KPI,
35
    KPI_SCALARS_DICT,
36
    KPI_UNCOUPLED_DICT,
37
    KPI_COST_MATRIX,
38
    KPI_SCALAR_MATRIX,
39
    LCOE_ASSET,
40
    COST_TOTAL,
41
    TOTAL_FLOW,
42
    RENEWABLE_ASSET_BOOL,
43
    RENEWABLE_SHARE_DSO,
44
    DSO_CONSUMPTION,
45
    DSO_FEEDIN,
46
    DSO_CONSUMPTION,
47
    TOTAL_RENEWABLE_GENERATION_IN_LES,
48
    TOTAL_NON_RENEWABLE_GENERATION_IN_LES,
49
    TOTAL_GENERATION_IN_LES,
50
    TOTAL_RENEWABLE_ENERGY_USE,
51
    TOTAL_NON_RENEWABLE_ENERGY_USE,
52
    RENEWABLE_FACTOR,
53
    RENEWABLE_SHARE_OF_LOCAL_GENERATION,
54
    TOTAL_DEMAND,
55
    TOTAL_EXCESS,
56
    TOTAL_FEEDIN,
57
    TOTAL_CONSUMPTION_FROM_PROVIDERS,
58
    SUFFIX_ELECTRICITY_EQUIVALENT,
59
    LCOeleq,
60
    ATTRIBUTED_COSTS,
61
    DEGREE_OF_SECTOR_COUPLING,
62
    DEGREE_OF_AUTONOMY,
63
    ONSITE_ENERGY_FRACTION,
64
    ONSITE_ENERGY_MATCHING,
65
    EMISSION_FACTOR,
66
    TOTAL_EMISSIONS,
67
    SPECIFIC_EMISSIONS_ELEQ,
68
    UNIT,
69
    UNIT_SPECIFIC_EMISSIONS,
70
    UNIT_EMISSIONS,
71
    DEGREE_OF_NZE,
72
)
73

74

75
def all_totals(dict_values):
1✔
76
    """Calculate sum of all cost parameters
77

78
    Parameters
79
    ----------
80

81
    dict_values :
82
        dict all input parameters and results up to E0
83

84
    Returns
85
    -------
86

87
    type
88
        List of all total cost parameters for the project
89

90
    Notes
91
    -----
92

93
    The totals are calculated for following parameters:
94
    - costs_total
95
    - costs_om_total
96
    - costs_investment_over_lifetime
97
    - costs_upfront_in_year_zero
98
    - costs_dispatch
99
    - costs_cost_om
100
    - annuity_total
101
    - annuity_om
102

103
    The levelized_cost_of_energy_of_asset are dropped from the list,
104
    as they do not hold any actual meaning for the whole energy system.
105
    The LCOE of the energy system is calculated seperately.
106
    """
107
    for column in dict_values[KPI][KPI_COST_MATRIX].columns:
1✔
108
        if column not in [LABEL, LCOE_ASSET]:
1✔
109
            dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
110
                {column: dict_values[KPI][KPI_COST_MATRIX][column].sum()}
111
            )
112

113

114
def total_demand_and_excess_each_sector(dict_values):
1✔
115
    """
116
    Calculation of the total demand and total excess of each sector
117

118
    Both in original energy carrier unit and electricity equivalent
119

120
    Parameters
121
    ----------
122
    dict_values :
123
        dict with all project input data and results up to E0
124

125
    Returns
126
    -------
127
    Updated KPI_SCALARS_DICT with
128
    - total demand of each energy carrier (original unit)
129
    - total demand of each energy carrier (electricity equivalent)
130
    - total demand in electricity equivalent
131
    - total excess of each energy carrier (original unit)
132
    - total excess of each energy carrier (electricity equivalent)
133
    - total excess in electricity equivalent
134

135
    Notes
136
    -----
137
    Tested with
138
    - test_add_levelized_cost_of_energy_carriers_one_sector()
139
    - test_add_levelized_cost_of_energy_carriers_two_sectors()
140
    - TestTechnicalKPI.renewable_factor_and_renewable_share_of_local_generation()
141
    """
142

143
    # Define empty dict to gather the total demand of each energy carrier
144
    total_demand_dict = {}
1✔
145
    total_excess_dict = {}
1✔
146
    for sector in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]:
1✔
147
        total_demand_dict.update({sector: 0})
1✔
148
        total_excess_dict.update({sector: 0})
1✔
149

150
    # determine all dso feedin sinks that should not be evaluated for the total demand
151
    dso_feedin_sinks = []
1✔
152
    for dso in dict_values[ENERGY_PROVIDERS]:
1✔
153
        dso_feedin_sinks.append(
1✔
154
            dict_values[ENERGY_PROVIDERS][dso][CONNECTED_FEEDIN_SINK]
155
        )
156

157
    # Loop though energy consumption assets to determine those that are demand
158
    for consumption_asset in dict_values[ENERGY_CONSUMPTION]:
1✔
159
        # Do not process feedin sinks neither for excess nor for demands
160
        if consumption_asset not in dso_feedin_sinks:
1✔
161
            # get name of energy carrier
162
            energy_carrier = dict_values[ENERGY_CONSUMPTION][consumption_asset][
1✔
163
                ENERGY_VECTOR
164
            ]
165
            # check if energy carrier in total_demand dict
166
            # (might be unnecessary, check where dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S] are defined)
167
            if energy_carrier not in total_demand_dict:
1✔
168
                logging.error(
×
169
                    f'Energy vector "{energy_carrier}" of asset {consumption_asset} not in known energy sectors. Please double check.'
170
                )
171
                total_demand_dict.update({energy_carrier: {}})
×
172

173
            # Evaluate excess
174
            if consumption_asset in dict_values[SIMULATION_SETTINGS][EXCESS_SINK]:
1✔
175
                total_excess_dict.update(
1✔
176
                    {
177
                        energy_carrier: total_excess_dict[energy_carrier]
178
                        + dict_values[ENERGY_CONSUMPTION][consumption_asset][
179
                            TOTAL_FLOW
180
                        ][VALUE]
181
                    }
182
                )
183
            # Evaluate demands
184
            else:
185
                total_demand_dict.update(
1✔
186
                    {
187
                        energy_carrier: total_demand_dict[energy_carrier]
188
                        + dict_values[ENERGY_CONSUMPTION][consumption_asset][
189
                            TOTAL_FLOW
190
                        ][VALUE]
191
                    }
192
                )
193

194
    # Append total demand in electricity equivalent to kpi
195
    calculate_electricity_equivalent_for_a_set_of_aggregated_values(
1✔
196
        dict_values, total_demand_dict, kpi_name=TOTAL_DEMAND
197
    )
198

199
    # Append total excess in electricity equivalent to kpi
200
    calculate_electricity_equivalent_for_a_set_of_aggregated_values(
1✔
201
        dict_values, total_excess_dict, kpi_name=TOTAL_EXCESS
202
    )
203

204

205
def calculate_electricity_equivalent_for_a_set_of_aggregated_values(
1✔
206
    dict_values, dict_of_aggregated_flows, kpi_name
207
):
208
    r"""
209
    Calculates the electricity equivalent for a dict of aggregated flows and writes it to the KPI
210

211
    Parameters
212
    ----------
213
    dict_values: dict
214
        All simulation parameters
215

216
    dict_of_aggregated_flows: dict
217
        Dict of aggragated flows, with keys of energy carriers.
218

219
    kpi_name: str
220
        Name of the KPI to write to the results
221

222
    Returns
223
    -------
224
    Updated dict_values.
225
    """
226

227
    # For totalling aggregated electricity equivalents
228
    total_electricity_equivalent = 0
1✔
229

230
    # Write total demand per energy carrier as well as its electricity equivalent to dict_values
231
    for energy_carrier in dict_of_aggregated_flows:
1✔
232
        # Define total aggregated flow of energy carrier in original unit
233
        dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
234
            {kpi_name + energy_carrier: dict_of_aggregated_flows[energy_carrier]}
235
        )
236
        # Define total aggregated flow of energy carrier in electricity equivalent
237
        dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
238
            {
239
                kpi_name
240
                + energy_carrier
241
                + SUFFIX_ELECTRICITY_EQUIVALENT: dict_of_aggregated_flows[
242
                    energy_carrier
243
                ]
244
                * DEFAULT_WEIGHTS_ENERGY_CARRIERS[energy_carrier][VALUE]
245
            }
246
        )
247
        # Add to total aggregated flow in electricity equivalent
248
        total_electricity_equivalent += dict_values[KPI][KPI_SCALARS_DICT][
1✔
249
            kpi_name + energy_carrier + SUFFIX_ELECTRICITY_EQUIVALENT
250
        ]
251

252
    # Define total aggregated flow in electricity equivalent
253
    dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
254
        {kpi_name + SUFFIX_ELECTRICITY_EQUIVALENT: total_electricity_equivalent}
255
    )
256

257
    logging.info(
1✔
258
        f"The {kpi_name+SUFFIX_ELECTRICITY_EQUIVALENT} of the LES is: {round(total_electricity_equivalent)} kWheleq."
259
    )
260

261
    return total_electricity_equivalent
1✔
262

263

264
def add_total_renewable_and_non_renewable_energy_origin(dict_values):
1✔
265
    """Identifies all renewable generation assets and summs up their total generation to total renewable generation
266

267
    Parameters
268
    ----------
269
    dict_values :
270
        dict with all project input data and results up to E0
271

272
    Returns
273
    -------
274
    type
275
        Updated dict_values with total internal/overall renewable and non-renewable energy origin
276

277
    Notes
278
    -----
279
    Tested with
280
    - test_total_renewable_and_non_renewable_origin_of_each_sector()
281
    """
282
    dict_values[KPI].update({KPI_UNCOUPLED_DICT: {}})
1✔
283

284
    renewable_origin = {}
1✔
285
    non_renewable_origin = {}
1✔
286
    for sector in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]:
1✔
287
        renewable_origin.update({sector: 0})
1✔
288
        non_renewable_origin.update({sector: 0})
1✔
289

290
    # Aggregate the total generation of non renewable and renewable energy in the LES
291
    for asset in dict_values[ENERGY_PRODUCTION]:
1✔
292
        if RENEWABLE_ASSET_BOOL in dict_values[ENERGY_PRODUCTION][asset]:
1✔
293
            sector = dict_values[ENERGY_PRODUCTION][asset][ENERGY_VECTOR]
1✔
294
            if (
1✔
295
                dict_values[ENERGY_PRODUCTION][asset][RENEWABLE_ASSET_BOOL][VALUE]
296
                is True
297
            ):
298
                renewable_origin[sector] += dict_values[ENERGY_PRODUCTION][asset][
1✔
299
                    TOTAL_FLOW
300
                ][VALUE]
301
            else:
302
                non_renewable_origin[sector] += dict_values[ENERGY_PRODUCTION][asset][
1✔
303
                    TOTAL_FLOW
304
                ][VALUE]
305

306
    dict_values[KPI][KPI_UNCOUPLED_DICT].update(
1✔
307
        {
308
            TOTAL_RENEWABLE_GENERATION_IN_LES: renewable_origin.copy(),
309
            TOTAL_NON_RENEWABLE_GENERATION_IN_LES: non_renewable_origin.copy(),
310
        }
311
    )
312

313
    # Aggregate the total use of non renewable and renewable energy at DSO level
314
    for dso in dict_values[ENERGY_PROVIDERS]:
1✔
315
        sector = dict_values[ENERGY_PROVIDERS][dso][ENERGY_VECTOR]
1✔
316

317
        # Get source connected to the specific DSO in question
318
        DSO_source_name = dso + DSO_CONSUMPTION
1✔
319

320
        # Add renewable share of energy consumption from DSO to the renewable origin (total)
321
        renewable_origin[sector] += (
1✔
322
            dict_values[ENERGY_PRODUCTION][DSO_source_name][TOTAL_FLOW][VALUE]
323
            * dict_values[ENERGY_PROVIDERS][dso][RENEWABLE_SHARE_DSO][VALUE]
324
        )
325

326
        non_renewable_origin[sector] += dict_values[ENERGY_PRODUCTION][DSO_source_name][
1✔
327
            TOTAL_FLOW
328
        ][VALUE] * (1 - dict_values[ENERGY_PROVIDERS][dso][RENEWABLE_SHARE_DSO][VALUE])
329

330
    dict_values[KPI][KPI_UNCOUPLED_DICT].update(
1✔
331
        {
332
            TOTAL_RENEWABLE_ENERGY_USE: renewable_origin,
333
            TOTAL_NON_RENEWABLE_ENERGY_USE: non_renewable_origin,
334
        }
335
    )
336

337
    for sector_specific_kpi in [
1✔
338
        TOTAL_RENEWABLE_GENERATION_IN_LES,
339
        TOTAL_NON_RENEWABLE_GENERATION_IN_LES,
340
        TOTAL_RENEWABLE_ENERGY_USE,
341
        TOTAL_NON_RENEWABLE_ENERGY_USE,
342
    ]:
343
        weighting_for_sector_coupled_kpi(dict_values, sector_specific_kpi)
1✔
344
    # calculate total generation, renewable + non-renewable
345
    dict_values[KPI][KPI_SCALARS_DICT][TOTAL_GENERATION_IN_LES] = (
1✔
346
        dict_values[KPI][KPI_SCALARS_DICT][TOTAL_RENEWABLE_GENERATION_IN_LES]
347
        + dict_values[KPI][KPI_SCALARS_DICT][TOTAL_NON_RENEWABLE_GENERATION_IN_LES]
348
    )
349

350
    logging.info("Calculated renewable share of the LES.")
1✔
351

352

353
def add_renewable_share_of_local_generation(dict_values):
1✔
354
    """Determination of renewable share of local energy production
355

356
        Parameters
357
    ----------
358
    dict_values :
359
        dict with all project information and results, after applying add_total_renewable_and_non_renewable_energy_origin
360
    sector :
361
        Sector for which renewable share is being calculated
362

363
    Returns
364
    -------
365
    type
366
        updated dict_values with renewable share of each sector as well as the system-wide KPI
367

368
    Notes
369
    -----
370
    Updates the KPI with RENEWABLE_SHARE_OF_LOCAL_GENERATION for each sector as well as system-wide KPI.
371

372
    Tested with
373
    * test_renewable_share_of_local_generation_one_sector()
374
    * test_renewable_share_of_local_generation_two_sectors()
375
    * TestTechnicalKPI.renewable_factor_and_renewable_share_of_local_generation()
376
    """
377

378
    dict_renewable_share = {}
1✔
379
    for sector in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]:
1✔
380
        # Defines the total renewable energy as the renewable production within the LES
381
        total_res = dict_values[KPI][KPI_UNCOUPLED_DICT][
1✔
382
            TOTAL_RENEWABLE_GENERATION_IN_LES
383
        ][sector]
384
        # Defines the total non-renewable energy as the non-renewable production within the LES
385
        total_non_res = dict_values[KPI][KPI_UNCOUPLED_DICT][
1✔
386
            TOTAL_NON_RENEWABLE_GENERATION_IN_LES
387
        ][sector]
388
        # Calculates the renewable factor for the current sector
389
        dict_renewable_share.update(
1✔
390
            {sector: equation_renewable_share(total_res, total_non_res)}
391
        )
392
    # Updates the KPI matrix for the individual sectors
393
    dict_values[KPI][KPI_UNCOUPLED_DICT].update(
1✔
394
        {RENEWABLE_SHARE_OF_LOCAL_GENERATION: dict_renewable_share}
395
    )
396

397
    # Calculation of the system-wide renewable factor
398
    total_res = dict_values[KPI][KPI_SCALARS_DICT][TOTAL_RENEWABLE_GENERATION_IN_LES]
1✔
399
    total_non_res = dict_values[KPI][KPI_SCALARS_DICT][
1✔
400
        TOTAL_NON_RENEWABLE_GENERATION_IN_LES
401
    ]
402

403
    # Updates the system-wide KPI matrix
404
    dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
405
        {
406
            RENEWABLE_SHARE_OF_LOCAL_GENERATION: equation_renewable_share(
407
                total_res, total_non_res
408
            )
409
        }
410
    )
411

412

413
def add_renewable_factor(dict_values):
1✔
414
    """Determination of renewable share of one sector
415

416
    Parameters
417
    ----------
418
    dict_values :
419
        dict with all project information and results, after applying add_total_renewable_and_non_renewable_energy_origin
420

421
    Returns
422
    -------
423
    type
424
        updated dict_values with renewable factor of each sector as well as system-wide indicator
425

426
    Notes
427
    -----
428
    Updates the KPI with RENEWABLE_FACTOR for each sector as well as system-wide KPI.
429

430

431
    Tested with
432
    - test_renewable_factor_one_sector
433
    - test_renewable_factor_two_sectors
434
    - TestTechnicalKPI.renewable_factor_and_renewable_share_of_local_generation()
435
    """
436
    dict_renewable_share = {}
1✔
437
    # Loops though the sectors
438
    for sector in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]:
1✔
439
        # Defines the total renewable energy as the renewable influx into the system (generation and consumption)
440
        total_res = dict_values[KPI][KPI_UNCOUPLED_DICT][TOTAL_RENEWABLE_ENERGY_USE][
1✔
441
            sector
442
        ]
443
        # Defines the total non-renewable energy as the non-renewable influx into the system (generation and consumption)
444
        total_non_res = dict_values[KPI][KPI_UNCOUPLED_DICT][
1✔
445
            TOTAL_NON_RENEWABLE_ENERGY_USE
446
        ][sector]
447
        # Calculates the renewable factor for the current sector
448
        dict_renewable_share.update(
1✔
449
            {sector: equation_renewable_share(total_res, total_non_res)}
450
        )
451
    # Updates the KPI matrix for the individual sectors
452
    dict_values[KPI][KPI_UNCOUPLED_DICT].update(
1✔
453
        {RENEWABLE_FACTOR: dict_renewable_share}
454
    )
455

456
    # Calculation of the system-wide renewable factor
457
    total_res = dict_values[KPI][KPI_SCALARS_DICT][TOTAL_RENEWABLE_ENERGY_USE]
1✔
458
    total_non_res = dict_values[KPI][KPI_SCALARS_DICT][TOTAL_NON_RENEWABLE_ENERGY_USE]
1✔
459
    # Updates the system-wide KPI matrix
460
    dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
461
        {RENEWABLE_FACTOR: equation_renewable_share(total_res, total_non_res)}
462
    )
463

464

465
def equation_renewable_share(total_res, total_non_res):
1✔
466
    r"""Calculates the renewable share
467

468
    Parameters
469
    ----------
470
    total_res :
471
        Renewable generation of a system
472

473
    total_non_res :
474
        Non-renewable generation of a system
475

476
    Returns
477
    -------
478
    type
479
        Renewable share
480

481
    Notes
482
    -----
483

484
    Used both to calculate RENEWABLE_FACTOR and RENEWABLE_SHARE_OF_LOCAL_GENERATION.
485

486
    Equation:
487

488
    .. math::
489
        RES = \frac{total_res}{total_non_res + total_res}
490

491
    The renewable share is relative to generation, but not consumption of energy, the renewable share can not be larger 1.
492
    If there is no generation or consumption from a DSO within an energyVector and supply is solely reached by energy conversion from another vector, the renewable share is defined to be zero.
493

494
    * renewable share = 1 - all energy in the energy system is of renewable origin
495
    * renewable share < 1 - part of the energy in the system is of renewable origin
496
    * renewable share = 0 - no energy is of renewable origin
497

498
    Tested with:
499

500
    - test_renewable_share_equation_no_generation()
501
    - test_renewable_share_equation_below_1()
502
    - test_renewable_share_equation_is_0()
503
    - test_renewable_share_equation_is_1()
504
    """
505

506
    if total_res + total_non_res > 0:
1✔
507
        renewable_share = total_res / (total_non_res + total_res)
1✔
508
    else:
509
        renewable_share = 0
1✔
510
    return renewable_share
1✔
511

512

513
def add_degree_of_autonomy(dict_values):
1✔
514
    """
515
    Determines degree of autonomy and adds KPI to dict_values
516

517
    Parameters
518
    ----------
519
    dict_values: dict
520
        dict with all project information and results,
521
        after applying total_renewable_and_non_renewable_energy_origin and
522
        total_demand_and_excess_each_sector
523

524
    Returns
525
    -------
526
    None
527
        updated dict_values with the degree of autonomy
528

529
    Tested with
530
    - test_add_degree_of_autonomy()
531
    """
532

533
    total_consumption_from_energy_provider = dict_values[KPI][KPI_SCALARS_DICT][
1✔
534
        TOTAL_CONSUMPTION_FROM_PROVIDERS + SUFFIX_ELECTRICITY_EQUIVALENT
535
    ]
536
    total_demand = dict_values[KPI][KPI_SCALARS_DICT][
1✔
537
        TOTAL_DEMAND + SUFFIX_ELECTRICITY_EQUIVALENT
538
    ]
539

540
    degree_of_autonomy = equation_degree_of_autonomy(
1✔
541
        total_consumption_from_energy_provider, total_demand
542
    )
543

544
    dict_values[KPI][KPI_SCALARS_DICT].update({DEGREE_OF_AUTONOMY: degree_of_autonomy})
1✔
545

546
    logging.debug(
1✔
547
        f"Calculated the {DEGREE_OF_AUTONOMY}: {round(degree_of_autonomy, 2)}"
548
    )
549
    logging.info(f"Calculated the {DEGREE_OF_AUTONOMY} of the LES.")
1✔
550

551

552
def equation_degree_of_autonomy(total_consumption_from_energy_provider, total_demand):
1✔
553
    r"""
554
    Calculates the degree of autonomy (DA).
555

556
    The degree of autonomy describes the relation of how much demand is supplied by local generation (as opposed to
557
    grid conumption) compared to the total demand of the system.
558

559
    Parameters
560
    ----------
561
    total_consumption_from_energy_provider: float
562
        total energy consumption from providers
563

564
    total_demand: float
565
        total demand
566

567
    Returns
568
    -------
569
    float
570
        degree of autonomy
571

572
    .. math::
573
        DA &=\frac{\sum_i {E_{demand} (i) \cdot w_i} - \sum_{i} {E_{consumption,provider,j} (j) \cdot w_j}}{\sum_i {E_{demand} (i) \cdot w_i}}
574

575
    A DA = 0 : Demand is fully supplied by DSO consumption
576
    DA = 1 : System is autonomous, ie. no DSO consumption is necessary
577

578
    Notice: As above, we apply a weighting based on Electricity Equivalent.
579

580
    Tested with
581
    - test_equation_degree_of_autonomy()
582
    """
583
    if total_demand == 0:
1✔
584
        degree_of_autonomy = 0
×
585
    else:
586
        degree_of_autonomy = (
1✔
587
            total_demand - total_consumption_from_energy_provider
588
        ) / total_demand
589

590
    return degree_of_autonomy
1✔
591

592

593
def add_degree_of_net_zero_energy(dict_values):
1✔
594
    """
595
    Determines degree of net zero energy (NZE) and adds KPI to dict_values.
596

597
    Parameters
598
    ----------
599
    dict_values: dict
600
        dict with all project information and results,
601
        after applying total_renewable_and_non_renewable_energy_origin and
602
        total_demand_and_excess_each_sector
603

604
    Returns
605
    -------
606
    None
607
        updated dict_values with the degree of net zero energy
608

609
    Notes
610
    -----
611
    As for other KPI, we apply a weighting based on Electricity Equivalent.
612

613
    Tested with
614
    - test_add_degree_of_net_zero_energy()
615
    """
616

617
    total_feedin = dict_values[KPI][KPI_SCALARS_DICT][
1✔
618
        TOTAL_FEEDIN + SUFFIX_ELECTRICITY_EQUIVALENT
619
    ]
620

621
    total_consumption_from_energy_provider = dict_values[KPI][KPI_SCALARS_DICT][
1✔
622
        TOTAL_CONSUMPTION_FROM_PROVIDERS + SUFFIX_ELECTRICITY_EQUIVALENT
623
    ]
624
    total_demand = dict_values[KPI][KPI_SCALARS_DICT][
1✔
625
        TOTAL_DEMAND + SUFFIX_ELECTRICITY_EQUIVALENT
626
    ]
627

628
    degree_of_nze = equation_degree_of_net_zero_energy(
1✔
629
        total_feedin, total_consumption_from_energy_provider, total_demand
630
    )
631

632
    dict_values[KPI][KPI_SCALARS_DICT].update({DEGREE_OF_NZE: degree_of_nze})
1✔
633

634
    logging.debug(f"Calculated the {DEGREE_OF_NZE}: {round(degree_of_nze, 2)}")
1✔
635
    logging.info(f"Calculated the {DEGREE_OF_NZE} of the LES.")
1✔
636

637

638
def equation_degree_of_net_zero_energy(
1✔
639
    total_feedin, total_grid_consumption, total_demand
640
):
641
    r"""
642
    Calculates the degree of net zero energy (NZE).
643

644
    In NZE systems import and export of energy is allowed while the balance over one
645
    year should be zero, thus the degree of net zero energy would be 1. The
646
    Degree of net zero energy indicates how close the system gets to the NZE ideal.
647
    If more energy is exported than imported it is plus-energy system (degree of NZE > 1).
648

649
    Parameters
650
    ----------
651
    total_feedin: float
652
        total grid feed-in in electricity equivalents
653

654
    total_grid_consumption: float
655
        total consumption from energy provider in electricity equivalents
656
    total_demand: float
657
        total demand in electricity equivalents
658

659
    Returns
660
    -------
661
    float
662
        degree of net zero energy
663

664
    Notes
665
    -----
666

667
    .. math::
668
        Degree of NZE &= 1 + \frac{(\sum_{i} {E_{grid feedin}(i)} \cdot w_i - E_{grid consumption} (i) \cdot w_i)}{\sum_i {E_{demand, i} \cdot w_i}}
669

670
    Degree of NZE = 1 : System is a net zero energy system, as E_feedin = E_grid_consumption
671
    Degree of NZE > 1 : system is a plus-energy system, as E_feedin > E_grid_consumption
672
    Degree of NZE < 1 : system does not reach net zero balance. The degree indicates by how much it fails to do so.
673
    Degree of NZE = 0 : system has no internal production, as E_dem = E_grid_consumption.
674

675
    Tested with
676
    - test_equation_degree_of_net_zero_energy()
677
    - test_equation_degree_of_net_zero_energy_is_zero()
678
    - test_equation_degree_of_net_zero_energy_is_one()
679
    - test_equation_degree_of_net_zero_energy_greater_one()
680

681
    """
682
    if total_demand == 0:
1✔
683
        degree_of_nze = 1
×
684
    else:
685
        degree_of_nze = 1 + (total_feedin - total_grid_consumption) / total_demand
1✔
686

687
    return degree_of_nze
1✔
688

689

690
def add_degree_of_sector_coupling(dict_values):
1✔
691
    r"""
692
    Determines the aggregated flows in between the sectors and the Degree of Sector Coupling.
693

694
    Takes into account the value of different energy carriers.
695

696
    Parameters
697
    ----------
698
    dict_values: dict
699
        dictionary with all project inputs and results, specifically the energyConversion assets and the outputs.
700

701
    Returns
702
    -------
703
    Energy equivalent of total conversion flows:
704

705
    .. math::
706
        E_{conversion,eq} = \sum_{i}{E_{conversion} (i) \cdot w_i}
707
        with i are conversion assets
708

709
    """
710
    # todo actually only flows that transform an energy carrier from one energy vector to the next should be added
711
    # maybe energyBusses helps?
712
    total_flow_of_energy_conversion_equivalent = 0
×
713
    for asset in dict_values[ENERGY_CONVERSION]:
×
714
        sector = dict_values[ENERGY_CONVERSION][asset][ENERGY_VECTOR]
×
715
        total_flow_of_energy_conversion_equivalent += (
×
716
            dict_values[ENERGY_CONVERSION][asset][TOTAL_FLOW][VALUE]
717
            * DEFAULT_WEIGHTS_ENERGY_CARRIERS[sector][VALUE]
718
        )
719

720
    dict_values[KPI][KPI_SCALARS_DICT].update(
×
721
        {"total_energy_conversion_flow": total_flow_of_energy_conversion_equivalent}
722
    )
723
    logging.debug("Determined total energy conversion flow in electricity equivalent.")
×
724

725
    # Calculate degree of sector coupling
726
    degree_of_sector_coupling = equation_degree_of_sector_coupling(
×
727
        total_flow_of_energy_conversion_equivalent,
728
        dict_values[KPI][KPI_SCALARS_DICT][
729
            TOTAL_DEMAND + SUFFIX_ELECTRICITY_EQUIVALENT
730
        ],
731
    )
732
    dict_values[KPI][KPI_SCALARS_DICT].update(
×
733
        {DEGREE_OF_SECTOR_COUPLING: degree_of_sector_coupling}
734
    )
735
    logging.debug(
×
736
        f"Calculated the {DEGREE_OF_SECTOR_COUPLING}: {round(degree_of_sector_coupling)}"
737
    )
738
    logging.info(f"Calculated the {DEGREE_OF_SECTOR_COUPLING} for the LES.")
×
739
    return total_flow_of_energy_conversion_equivalent
×
740

741

742
def equation_degree_of_sector_coupling(
1✔
743
    total_flow_of_energy_conversion_equivalent, total_demand_equivalent
744
):
745
    r"""Calculates degree of sector coupling.
746

747
    Parameters
748
    ----------
749
    total_flow_of_energy_conversion_equivalent: float
750
        Energy equivalent of total conversion flows
751

752
    total_demand_equivalent: float
753
        Energy equivalent of total energy demand
754

755
    Returns
756
    -------
757
    float
758
        Degree of sector coupling based on conversion flows and energy demands in electricity equivalent.
759

760
    .. math::
761
       DSC=\frac{\sum_{i,j}{E_{conversion} (i,j) \cdot w_i}}{\sum_i {E_{demand} (i) \cdot w_i}}
762

763
        with i,j \epsilon [Electricity,H2…]
764

765
    """
766
    degree_of_sector_coupling = (
×
767
        total_flow_of_energy_conversion_equivalent / total_demand_equivalent
768
    )
769
    return degree_of_sector_coupling
×
770

771

772
def add_total_feedin_electricity_equivalent(dict_values):
1✔
773
    """
774
    Determines the total grid feed-in with weighting of electricity equivalent.
775

776
    Parameters
777
    ----------
778
    dict_values: dict
779
        dict with all project information and results
780

781
    Returns
782
    -------
783
    None
784
        updated dict_values with KPI : total feedin
785

786
    Tested with
787
    - test_add_total_feedin_electricity_equivalent()
788
    - test_add_total_feedin_electricity_equivalent_two_providers_one_energy_carrier
789
    """
790

791
    total_feedin_dict = {}
1✔
792
    # Get source connected to the specific DSO in question
793
    for dso in dict_values[ENERGY_PROVIDERS]:
1✔
794
        # load total flow into the dso sink
795
        feedin_sink = str(dso + DSO_FEEDIN)
1✔
796
        energy_carrier = dict_values[ENERGY_CONSUMPTION][feedin_sink][ENERGY_VECTOR]
1✔
797
        if energy_carrier not in total_feedin_dict:
1✔
798
            total_feedin_dict.update({energy_carrier: 0})
1✔
799

800
        total_feedin_dict[energy_carrier] += dict_values[ENERGY_CONSUMPTION][
1✔
801
            feedin_sink
802
        ][TOTAL_FLOW][VALUE]
803

804
    # Append total feedin in electricity equivalent to kpi
805
    calculate_electricity_equivalent_for_a_set_of_aggregated_values(
1✔
806
        dict_values, total_feedin_dict, kpi_name=TOTAL_FEEDIN
807
    )
808

809

810
def add_total_consumption_from_provider_electricity_equivalent(dict_values):
1✔
811
    """
812
    Determines the total consumption from energy providers with weighting of electricity equivalent.
813

814
    Parameters
815
    ----------
816
    dict_values: dict
817
        dict with all project information and results
818

819
    Returns
820
    -------
821
    None
822
        updated dict_values with KPI :
823
        - TOTAL_CONSUMPTION_FROM_PROVIDERS + electricity,
824
        - TOTAL_CONSUMPTION_FROM_PROVIDERS + electricity + SUFFIX_ELECTRICITY_EQUIVALENT
825
        - TOTAL_CONSUMPTION_FROM_PROVIDERS + SUFFIX_ELECTRICITY_EQUIVALENT
826

827
    Notes
828
    -----
829
    Tested with:
830
    - E3.test_add_total_consumption_from_provider_electricity_equivalent()
831
    - E3.test_add_total_consumption_from_provider_electricity_equivalent_two_providers_one_energy_carrier
832
    """
833

834
    total_consumption_dict = {}
1✔
835
    # Get source connected to the specific DSO in question
836
    for dso in dict_values[ENERGY_PROVIDERS]:
1✔
837
        # load total flow into the dso sink
838
        consumption_source = str(dso + DSO_CONSUMPTION)
1✔
839
        energy_carrier = dict_values[ENERGY_PRODUCTION][consumption_source][
1✔
840
            ENERGY_VECTOR
841
        ]
842
        if energy_carrier not in total_consumption_dict:
1✔
843
            total_consumption_dict.update({energy_carrier: 0})
1✔
844

845
        total_consumption_dict[energy_carrier] += dict_values[ENERGY_PRODUCTION][
1✔
846
            consumption_source
847
        ][TOTAL_FLOW][VALUE]
848

849
    # Append total feedin in electricity equivalent to kpi
850
    calculate_electricity_equivalent_for_a_set_of_aggregated_values(
1✔
851
        dict_values, total_consumption_dict, kpi_name=TOTAL_CONSUMPTION_FROM_PROVIDERS
852
    )
853

854

855
def add_onsite_energy_fraction(dict_values):
1✔
856
    """
857
    Determines onsite energy fraction (OEF), i.e. self-consumption, and adds KPI to dict_values
858

859
    Parameters
860
    ----------
861
    dict_values: dict
862
        dict with all project information and results
863
        after applying total_renewable_and_non_renewable_energy_origin
864
    
865
    Returns
866
    -------
867
    None
868
        updated dict_values with onsite energy fraction KPI
869

870
    Tested with
871
    - test_add_onsite_energy_fraction()
872
    """
873

874
    total_generation = dict_values[KPI][KPI_SCALARS_DICT][TOTAL_GENERATION_IN_LES]
1✔
875

876
    total_feedin = dict_values[KPI][KPI_SCALARS_DICT][
1✔
877
        TOTAL_FEEDIN + SUFFIX_ELECTRICITY_EQUIVALENT
878
    ]
879

880
    # calculate onsite energy fraction
881
    onsite_energy_fraction = equation_onsite_energy_fraction(
1✔
882
        total_generation, total_feedin
883
    )
884

885
    # save KPI  onsite energy fraction into KPI_SCALARS_DICT
886
    dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
887
        {ONSITE_ENERGY_FRACTION: onsite_energy_fraction}
888
    )
889
    logging.debug(
1✔
890
        f"Calculated the {ONSITE_ENERGY_FRACTION}: {round(onsite_energy_fraction, 2)}"
891
    )
892
    logging.info(f"Calculated the {ONSITE_ENERGY_FRACTION} of the LES.")
1✔
893

894

895
def equation_onsite_energy_fraction(total_generation, total_feedin):
1✔
896
    r"""
897
    Calculates onsite energy fraction (OEF), i.e. self-consumption.
898

899
    OEF describes the fraction of all locally generated energy that is consumed
900
    by the system itself.
901

902
    Parameters
903
    ----------
904
    total_generation: float
905
        Energy equivalent of total generation flows
906
    total_feedin: float
907
        Total feed into the grid
908

909
    Returns
910
    -------
911
    float
912
        Onsite energy fraction.
913

914
    .. math::
915
            OEF &=\frac{\sum_{i} {E_{generation} (i) \cdot w_i} - E_{gridfeedin}(i) \cdot w_i}{\sum_{i} {E_{generation} (i) \cdot w_i}}
916
            &OEF \epsilon \text{[0,1]}
917

918
    Tested with
919
    - test_equation_onsite_energy_fraction()
920
    """
921

922
    if total_generation != 0:
1✔
923
        onsite_energy_fraction = (total_generation - total_feedin) / total_generation
1✔
924
    else:
925
        # TODO find a better way to deal with this
926
        onsite_energy_fraction = 0
×
927
        logging.warning(
×
928
            "The total local energy generation is zero, therefore the onsite energy fraction cannot be calculated and is set to 0"
929
        )
930

931
    return onsite_energy_fraction
1✔
932

933

934
def add_onsite_energy_matching(dict_values):
1✔
935
    """
936
    Determines onsite energy matching (OEM), i.e. self-sufficiency, and adds KPI to dict_values
937

938
    Parameters
939
    ----------
940
    dict_values: dict
941
        dict with all project information and results
942
        after applying total_renewable_and_non_renewable_energy_origin and
943
        total_demand_and_excess_each_sector and
944
        add_onsite_energy_fraction
945
    
946
    Returns
947
    -------
948
    None
949
        updated dict_values with onsite energy matching KPI
950

951
    Tested with
952
    - test_add_onsite_energy_matching()
953
    """
954

955
    total_generation = dict_values[KPI][KPI_SCALARS_DICT][TOTAL_GENERATION_IN_LES]
1✔
956

957
    total_feedin = dict_values[KPI][KPI_SCALARS_DICT][
1✔
958
        TOTAL_FEEDIN + SUFFIX_ELECTRICITY_EQUIVALENT
959
    ]
960

961
    total_excess = dict_values[KPI][KPI_SCALARS_DICT][
1✔
962
        TOTAL_EXCESS + SUFFIX_ELECTRICITY_EQUIVALENT
963
    ]
964

965
    total_demand = dict_values[KPI][KPI_SCALARS_DICT][
1✔
966
        TOTAL_DEMAND + SUFFIX_ELECTRICITY_EQUIVALENT
967
    ]
968

969
    # calculate onsite energy matching
970
    onsite_energy_matching = equation_onsite_energy_matching(
1✔
971
        total_generation, total_feedin, total_excess, total_demand
972
    )
973
    # save KPI onsite energy matching to KPI Scalars
974
    dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
975
        {ONSITE_ENERGY_MATCHING: onsite_energy_matching}
976
    )
977
    logging.debug(
1✔
978
        f"Calculated the {ONSITE_ENERGY_MATCHING}: {round(onsite_energy_matching, 2)}"
979
    )
980
    logging.info(f"Calculated the {ONSITE_ENERGY_MATCHING} of the LES.")
1✔
981

982

983
def equation_onsite_energy_matching(
1✔
984
    total_generation, total_feedin, total_excess, total_demand
985
):
986
    r"""
987
    Calculates onsite energy matching (OEM), i.e. self-sufficiency.
988

989
    OEM describes the fraction of the total demand that can be
990
    covered by the locally generated energy.
991

992
    Parameters
993
    ----------
994
    total_generation: float
995
        Energy equivalent of total conversion flows
996
    total_feedin: float
997
        Total feed into the grid
998
    total_excess: float
999
        Total Excess energy
1000
    total_demand: float
1001
        Total demand
1002

1003
    Returns
1004
    -------
1005
    Onsite energy matching.
1006

1007
    .. math::
1008
            OEM &=\frac{\sum_{i} {E_{generation} (i) \cdot w_i} - E_{gridfeedin}(i) \cdot w_i - E_{excess}(i) \cdot w_i}{\sum_i {E_{demand} (i) \cdot w_i}}
1009

1010
            &OEM \epsilon \text{[0,1]}
1011

1012
    Tested with
1013
    - test_equation_onsite_energy_matching()
1014
    """
1015
    if total_demand == 0:
1✔
1016
        total_demand = total_feedin
×
1017
    if total_demand != 0:
1✔
1018
        onsite_energy_matching = (
1✔
1019
            total_generation - total_feedin - total_excess
1020
        ) / total_demand
1021
    else:
1022
        onsite_energy_matching = 0
×
1023
    return onsite_energy_matching
1✔
1024

1025

1026
def calculate_emissions_from_flow(dict_asset):
1✔
1027
    r"""
1028
    Calculates the total emissions of the asset in 'dict_asset' in kg per year.
1029

1030
    Parameters
1031
    ----------
1032
    dict_asset : dict
1033
        Contains information about the asset.
1034

1035
    Notes
1036
    -----
1037
    Tested with:
1038
    - E3.test_calculate_emissions_from_flow()
1039
    - E3.test_calculate_emissions_from_flow_zero_emissions
1040

1041
    Returns
1042
    -------
1043
    None
1044
        Updated `dict_asset` with TOTAL_EMISSIONS of the asset in kgCO2eq/a (UNIT_EMISSIONS).
1045

1046
    """
1047
    emissions = dict_asset[TOTAL_FLOW][VALUE] * dict_asset[EMISSION_FACTOR][VALUE]
1✔
1048
    dict_asset.update({TOTAL_EMISSIONS: {VALUE: emissions, UNIT: UNIT_EMISSIONS}})
1✔
1049

1050

1051
def add_total_emissions(dict_values):
1✔
1052
    r"""
1053
    Calculates the total emission of the energy system in kgCO2eq/a and adds KPI to `dict_values`.
1054

1055
    Parameters
1056
    ----------
1057
    dict_values: dict
1058
        All simulation inputs and results
1059

1060
    Returns
1061
    -------
1062
    None
1063
        Updated `dict_values` with TOTAL_EMISSIONS of the energy system in kgCO2eq/a
1064
        (UNIT_EMISSIONS).
1065

1066
    Notes
1067
    -----
1068

1069
    Tested with:
1070
    - E3.test_add_total_emissions()
1071

1072
    """
1073
    # sum up emissions of all assets [kgCO2eq/a]
1074
    emissions = dict_values[KPI][KPI_SCALAR_MATRIX][TOTAL_EMISSIONS].sum()  # data frame
1✔
1075
    dict_values[KPI][KPI_SCALARS_DICT].update({TOTAL_EMISSIONS: emissions})
1✔
1076
    logging.debug(
1✔
1077
        f"Calculated the {TOTAL_EMISSIONS}: {round(emissions, 2)} {UNIT_EMISSIONS}."
1078
    )
1079
    logging.info(f"Calculated the {TOTAL_EMISSIONS} ({UNIT_EMISSIONS}) of the LES.")
1✔
1080

1081

1082
def add_specific_emissions_per_electricity_equivalent(dict_values):
1✔
1083
    r"""
1084
    Calculates the specific emissions of the energy system per kWheleq and adds KPI to `dict_values`.
1085

1086
    Parameters
1087
    ----------
1088
    dict_values: dict
1089
        All simulation inputs and results including TOTAL_EMISSIONS calculated in
1090
        `E3.calculate_emissions_from_flow`.
1091

1092
    Notes
1093
    -----
1094
    This funtion is run after `E3.calculate_emissions_from_flow`.
1095

1096
    Tested with:
1097
    - E3.test_add_specific_emissions_per_electricity_equivalent()
1098

1099
    Returns
1100
    -------
1101
    None
1102
        Updated `dict_values` with SPECIFIC_EMISSIONS_ELEQ in kgCO2eq/kWheleq (UNIT_SPECIFIC_EMISSIONS).
1103

1104
    """
1105
    # emissions per kWheleq
1106
    total_demand = dict_values[KPI][KPI_SCALARS_DICT][
1✔
1107
        TOTAL_DEMAND + SUFFIX_ELECTRICITY_EQUIVALENT
1108
    ]
1109
    if total_demand == 0:
1✔
1110
        emissions_kWheleq = 0
×
1111
    else:
1112
        emissions_kWheleq = (
1✔
1113
            dict_values[KPI][KPI_SCALARS_DICT][TOTAL_EMISSIONS] / total_demand
1114
        )
1115
    dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
1116
        {SPECIFIC_EMISSIONS_ELEQ: emissions_kWheleq}
1117
    )
1118
    logging.debug(
1✔
1119
        f"Calculated the {SPECIFIC_EMISSIONS_ELEQ}: {round(emissions_kWheleq, 2)} {UNIT_SPECIFIC_EMISSIONS}."
1120
    )
1121
    logging.info(
1✔
1122
        f"Calculated the {SPECIFIC_EMISSIONS_ELEQ} ({UNIT_SPECIFIC_EMISSIONS}) of the LES."
1123
    )
1124

1125

1126
def add_levelized_cost_of_energy_carriers(dict_values):
1✔
1127
    r"""
1128
    Adds levelized costs of all energy carriers and overall system to the scalar KPI.
1129

1130
    Parameters
1131
    ----------
1132
    dict_values: dict
1133
        All simulation inputs and results
1134

1135
    Returns
1136
    -------
1137
    Updated KPI_SCALAR_DICT: Add `ATTRIBUTED_COSTS` and `LCOeleq` for each energy carrier as well as `LCOeleq` for overall energy system
1138

1139
    Notes
1140
    -----
1141

1142
    Tested with:
1143
        - test_E3_indicator_calculation.test_add_levelized_cost_of_energy_carriers_one_sector()
1144
        - test_E3_indicator_calculation.test_add_levelized_cost_of_energy_carriers_two_sectors()
1145
    """
1146
    # Abbreviate costs accessed
1147
    NPC = dict_values[KPI][KPI_SCALARS_DICT][COST_TOTAL]
1✔
1148

1149
    # Get total demand in electricity equivalent
1150
    total_demand_electricity_equivalent = dict_values[KPI][KPI_SCALARS_DICT][
1✔
1151
        TOTAL_DEMAND + SUFFIX_ELECTRICITY_EQUIVALENT
1152
    ]
1153

1154
    # Loop through all energy carriers
1155
    for energy_carrier in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]:
1✔
1156
        # Get energy carrier specific values
1157
        energy_carrier_label = TOTAL_DEMAND + energy_carrier
1✔
1158
        total_flow_energy_carrier = dict_values[KPI][KPI_SCALARS_DICT][
1✔
1159
            energy_carrier_label
1160
        ]
1161
        total_flow_energy_carrier_eleq = dict_values[KPI][KPI_SCALARS_DICT][
1✔
1162
            energy_carrier_label + SUFFIX_ELECTRICITY_EQUIVALENT
1163
        ]
1164
        # Calculate LCOE of energy carrier
1165
        (
1✔
1166
            lcoe_energy_carrier,
1167
            attributed_costs,
1168
        ) = equation_levelized_cost_of_energy_carrier(
1169
            cost_total=NPC,
1170
            crf=dict_values[ECONOMIC_DATA][CRF][VALUE],
1171
            total_flow_energy_carrier_eleq=total_flow_energy_carrier_eleq,
1172
            total_flow_energy_carrier=total_flow_energy_carrier,
1173
            total_demand_electricity_equivalent=total_demand_electricity_equivalent,
1174
        )
1175

1176
        # Update dict_values with ATTRIBUTED_COSTS and LCOE_energy_carrier
1177
        dict_values[KPI][KPI_SCALARS_DICT].update(
1✔
1178
            {
1179
                ATTRIBUTED_COSTS + energy_carrier: attributed_costs,
1180
                LCOeleq + energy_carrier: lcoe_energy_carrier,
1181
            }
1182
        )
1183
        logging.debug(
1✔
1184
            f"Determined {LCOeleq} {energy_carrier}: {round(lcoe_energy_carrier, 2)}"
1185
        )
1186

1187
    # Total demand
1188
    lcoe_energy_carrier, attributed_costs = equation_levelized_cost_of_energy_carrier(
1✔
1189
        cost_total=NPC,
1190
        crf=dict_values[ECONOMIC_DATA][CRF][VALUE],
1191
        total_flow_energy_carrier_eleq=total_demand_electricity_equivalent,
1192
        total_flow_energy_carrier=total_demand_electricity_equivalent,
1193
        total_demand_electricity_equivalent=total_demand_electricity_equivalent,
1194
    )
1195
    dict_values[KPI][KPI_SCALARS_DICT].update({LCOeleq: lcoe_energy_carrier})
1✔
1196
    logging.debug(f"Determined {LCOeleq}: {round(lcoe_energy_carrier, 2)}")
1✔
1197
    logging.info("Calculated LCOE of the energy system.")
1✔
1198

1199

1200
def equation_levelized_cost_of_energy_carrier(
1✔
1201
    cost_total,
1202
    crf,
1203
    total_flow_energy_carrier_eleq,
1204
    total_demand_electricity_equivalent,
1205
    total_flow_energy_carrier,
1206
):
1207
    r"""
1208
    Calculates LCOE of each energy carrier of the system.
1209

1210
    Based on distributing the NPC of the energy system over the total weighted energy demand of the local energy system.
1211
    This avoids that a conversion asset has to be defined as being used for a specific sector only,
1212
    or that an energy production asset (eg. electricity) which is mainly used for powering energy conversion assets for another energy carrier (eg. H2)
1213
    are increasing the costs of the first energy carrier (electricity),
1214
    eventhough the costs should be attributed to the costs of the end-use of generation.
1215

1216
    Parameters
1217
    ----------
1218
    cost_total: float
1219

1220
    crf: float
1221

1222
    total_flow_energy_carrier_eleq: float
1223

1224
    total_demand_electricity_equivalent: float
1225

1226
    total_flow_energy_carrier:float
1227

1228
    Returns
1229
    -------
1230
        lcoe_energy_carrier: float
1231
            Levelized costs of an energy carrier in a sector coupled system
1232

1233
        attributed_costs: float
1234
            Costs attributed to a specific energy carrier
1235

1236
    Notes
1237
    -----
1238
    Please refer to the conference paper presented at the CIRED Workshop Berlin (see readthedocs) for more detail.
1239

1240
    The costs attributed to an energy carrier are calculated from the ratio of electricity equivalent of the energy carrier demand in focus to the electricity equivalent of the total demand:
1241

1242
    .. math:: attributed costs = NPC \cdot \frac{Total electricity equivalent of energy carrier demand}{Total electricity equivalent of demand}
1243

1244
    The LCOE sets these attributed costs in relation to the energy carrier demand (in its original unit):
1245

1246
    .. math:: LCOE energy carrier = \frac{attributed costs \cdot CRF}{total energy carrier demand}
1247

1248
    Tested with:
1249
    - test_equation_levelized_cost_of_energy_carrier_total_demand_electricity_equivalent_larger_0_total_flow_energy_carrier_larger_0()
1250
    - test_equation_levelized_cost_of_energy_carrier_total_demand_electricity_equivalent_larger_0_total_flow_energy_carrier_is_0()
1251
    - test_equation_levelized_cost_of_energy_carrier_total_demand_electricity_equivalent_is_0_total_flow_energy_carrier_is_0()
1252
    """
1253
    attributed_costs = 0
1✔
1254

1255
    if total_demand_electricity_equivalent > 0:
1✔
1256
        attributed_costs = (
1✔
1257
            cost_total
1258
            * total_flow_energy_carrier_eleq
1259
            / total_demand_electricity_equivalent
1260
        )
1261

1262
    if total_flow_energy_carrier > 0:
1✔
1263
        lcoe_energy_carrier = attributed_costs * crf / total_flow_energy_carrier
1✔
1264
    else:
1265
        lcoe_energy_carrier = 0
1✔
1266
    return lcoe_energy_carrier, attributed_costs
1✔
1267

1268

1269
def weighting_for_sector_coupled_kpi(dict_values, kpi_name):
1✔
1270
    """Calculates the weighted kpi for a specific kpi_name both for a single sector and system-wide
1271

1272
    Parameters
1273
    ----------
1274
    dict_values :
1275
        dict with all project information and results, including KPI_UNCOUPLED_DICT with the specifc kpi_name in question
1276
    kpi_name :
1277
        str with the kpi which should be weighted
1278

1279
    Returns
1280
    -------
1281
    type
1282
        Append specific KPI that describes sector-coupled system to dict_values[KPI][KPI_SCALARS_DICT]
1283
        Appends specific KPI in energy equivalent to each sector to dict_values[KPI][KPI_UNCOUPLED_DICT]
1284
    """
1285
    total_energy_equivalent = 0
1✔
1286
    dict_energy_equivalents_per_sector = {}
1✔
1287

1288
    for sector in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]:
1✔
1289
        if sector in DEFAULT_WEIGHTS_ENERGY_CARRIERS:
1✔
1290
            energy_equivalent = (
1✔
1291
                dict_values[KPI][KPI_UNCOUPLED_DICT][kpi_name][sector]
1292
                * DEFAULT_WEIGHTS_ENERGY_CARRIERS[sector][VALUE]
1293
            )
1294
            total_energy_equivalent += energy_equivalent
1✔
1295
            dict_energy_equivalents_per_sector.update({sector: energy_equivalent})
1✔
1296
        else:
1297
            raise ValueError(
1✔
1298
                f"The electricity equivalent value of energy carrier {sector} is not defined. "
1299
                f"Please add this information to the variable DEFAULT_WEIGHTS_ENERGY_CARRIERS in constants.py."
1300
            )
1301

1302
    # Describes the energy equivalent of the kpi in question, eg. the renewable generation, so that comparison is easier
1303
    dict_values[KPI][KPI_UNCOUPLED_DICT].update(
1✔
1304
        {kpi_name + SUFFIX_ELECTRICITY_EQUIVALENT: dict_energy_equivalents_per_sector}
1305
    )
1306
    # Describes system wide total of the energy equivalent of the kpi
1307
    dict_values[KPI][KPI_SCALARS_DICT].update({kpi_name: total_energy_equivalent})
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc