• 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

65.45
/src/multi_vector_simulator/C1_verification.py
1
"""
2
Module C1 - Verification
3
========================
4

5
Module C1 is used to validate the input data compiled in A1 or read in B0.
6

7
In A1/B0, the input parameters were parsed to str/bool/float/int. This module
8
tests whether the parameters are in correct value ranges:
9
- Display error message when wrong type
10
- Display error message when outside defined range
11
- Display error message when feed-in tariff > electricity price (would cause loop, see #119)
12

13
"""
14

15
import logging
1✔
16
import os
1✔
17

18
import pandas as pd
1✔
19

20
from multi_vector_simulator.utils.helpers import find_value_by_key
1✔
21

22
from multi_vector_simulator.utils.exceptions import (
1✔
23
    UnknownEnergyVectorError,
24
    DuplicateLabels,
25
)
26
from multi_vector_simulator.utils.constants import (
1✔
27
    PATH_INPUT_FILE,
28
    PATH_INPUT_FOLDER,
29
    PATH_OUTPUT_FOLDER,
30
    DISPLAY_OUTPUT,
31
    OVERWRITE,
32
    DEFAULT_WEIGHTS_ENERGY_CARRIERS,
33
)
34
from multi_vector_simulator.utils.constants_json_strings import (
1✔
35
    PROJECT_DURATION,
36
    DISCOUNTFACTOR,
37
    TAX,
38
    LABEL,
39
    CURR,
40
    DISPATCH_PRICE,
41
    SPECIFIC_COSTS_OM,
42
    DEVELOPMENT_COSTS,
43
    SPECIFIC_COSTS,
44
    AGE_INSTALLED,
45
    LIFETIME,
46
    INSTALLED_CAP,
47
    FILENAME,
48
    EFFICIENCY,
49
    EVALUATED_PERIOD,
50
    START_DATE,
51
    SOC_INITIAL,
52
    SOC_MAX,
53
    SOC_MIN,
54
    FEEDIN_TARIFF,
55
    MAXIMUM_CAP,
56
    SCENARIO_NAME,
57
    PROJECT_NAME,
58
    LONGITUDE,
59
    LATITUDE,
60
    PERIODS,
61
    COUNTRY,
62
    ENERGY_PRICE,
63
    ENERGY_CONSUMPTION,
64
    ENERGY_CONVERSION,
65
    ENERGY_PROVIDERS,
66
    ENERGY_PRODUCTION,
67
    ENERGY_BUSSES,
68
    ENERGY_STORAGE,
69
    STORAGE_CAPACITY,
70
    VALUE,
71
    ASSET_DICT,
72
    RENEWABLE_ASSET_BOOL,
73
    TIMESERIES,
74
    TIMESERIES_PEAK,
75
    ENERGY_VECTOR,
76
    PROJECT_DATA,
77
    LES_ENERGY_VECTOR_S,
78
    SIMULATION_ANNUITY,
79
    TIMESERIES_TOTAL,
80
    DISPATCHABILITY,
81
    OPTIMIZE_CAP,
82
    OUTPUT_POWER,
83
    OUTFLOW_DIRECTION,
84
    EMISSION_FACTOR,
85
    MAXIMUM_EMISSIONS,
86
    CONSTRAINTS,
87
    RENEWABLE_SHARE_DSO,
88
    DSO_PEAK_DEMAND_SUFFIX,
89
    DSO_FEEDIN_CAP,
90
)
91

92
# Necessary for check_for_label_duplicates()
93
from collections import Counter
1✔
94

95

96
def lookup_file(file_path, name):
1✔
97
    """
98
    Checks whether file specified in `file_path` exists.
99

100
    If it does not exist, a FileNotFoundError is raised.
101

102
    :param file_path: File name including path of file that is checked.
103
    :param name: Something referring to which component the file belongs. In\
104
    :func:`~.CO_data_processing.get_timeseries_multiple_flows` the label of the\
105
    asset is used.
106
    :return:
107
    """
108
    if os.path.isfile(file_path) is False:
1✔
109
        msg = (
1✔
110
            f"Missing file! The timeseries file '{file_path}' \nof asset "
111
            + f"{name} can not be found. Operation terminated."
112
        )
113
        raise FileNotFoundError(msg)
1✔
114

115

116
def check_for_label_duplicates(dict_values):
1✔
117
    """
118
    This function checks if any LABEL provided for the energy system model in dict_values is a duplicate.
119
    This is not allowed, as oemof can not build a model with identical labels.
120

121
    Parameters
122
    ----------
123
    dict_values: dict
124
        All simulation inputs
125

126
    Returns
127
    -------
128
    pass or error message: DuplicateLabels
129
    """
130
    values_of_label = find_value_by_key(dict_values, LABEL)
1✔
131
    count = Counter(values_of_label)
1✔
132
    msg = ""
1✔
133
    for item in count:
1✔
134
        if count[item] > 1:
1✔
135
            msg += f"Following asset label is not unique with {count[item]} occurrences: {item}. \n"
1✔
136
    if len(msg) > 1:
1✔
137
        msg += f"Please make sure that each label is only used once, as oemof otherwise can not build the model."
1✔
138
        raise DuplicateLabels(msg)
1✔
139

140

141
def check_feedin_tariff_vs_levelized_cost_of_generation_of_production(dict_values):
1✔
142
    r"""
143
    Raises error if feed-in tariff > levelized costs of generation for energy asset in ENERGY_PRODUCTION with capacity to be optimized and no maximum capacity constraint.
144

145
    This is not allowed, as oemof otherwise may be subjected to an unbound problem, ie.
146
    a business case in which an asset should be installed with infinite capacities to maximize revenue.
147

148
    In case of a set maximum capacity or no capacity optimization logging messages are logged.
149

150
    Parameters
151
    ----------
152
    dict_values : dict
153
        Contains all input data of the simulation.
154

155
    Returns
156
    -------
157
    Raises error message in case of feed-in tariff > levelized costs of generation for energy asset of any
158
    asset in ENERGY_PRODUCTION
159

160
    Notes
161
    -----
162
    Tested with:
163
    - C1.test_check_feedin_tariff_vs_levelized_cost_of_generation_of_production_non_dispatchable_not_greater_costs()
164
    - C1.test_check_feedin_tariff_vs_levelized_cost_of_generation_of_production_non_dispatchable_greater_costs()
165
    - C1.test_check_feedin_tariff_vs_levelized_cost_of_generation_of_production_dispatchable_higher_dispatch_price()
166
    - C1.test_check_feedin_tariff_vs_levelized_cost_of_generation_of_production_dispatchable_lower_dispatch_price()
167
    - C1.test_check_feedin_tariff_vs_levelized_cost_of_generation_of_production_non_dispatchable_greater_costs_with_maxcap()
168
    - C1.test_check_feedin_tariff_vs_levelized_cost_of_generation_of_production_non_dispatchable_greater_costs_dispatch_mode()
169

170
    This test does not cover cross-sectoral invalid feedin tariffs.
171
    Example: If there is very cheap electricity generation but a high H2 feedin tariff, then it might be a business case to install a large Electrolyzer, and the simulation would fail. In that case one should set bounds to the solution.
172
    """
173

174
    warning_message_hint_unbound = f"This may cause an unbound solution and terminate the optimization, if there are no additional costs in the supply line. If this happens, please check the costs of your assets or the feed-in tariff. If both are correct, consider setting a maximum capacity constraint (maximumCap) for the relevant assets."
1✔
175
    warning_message_hint_maxcap = f"This will cause the optimization to result into the maximum capacity of this asset."
1✔
176
    warning_message_hint_dispatch = (
1✔
177
        f"No error expected but strange dispatch behaviour might occur."
178
    )
179

180
    # Check if feed-in tariff of any provider is less then expected minimal levelized energy generation costs
181
    for provider in dict_values[ENERGY_PROVIDERS].keys():
1✔
182
        feedin_tariff = dict_values[ENERGY_PROVIDERS][provider][FEEDIN_TARIFF]
1✔
183
        energy_vector = dict_values[ENERGY_PROVIDERS][provider][ENERGY_VECTOR]
1✔
184

185
        # Loop though all produciton assets
186
        for production_asset in dict_values[ENERGY_PRODUCTION]:
1✔
187
            # Only compare those assets to the provider that serve the same energy vector
188
            if (
1✔
189
                dict_values[ENERGY_PRODUCTION][production_asset][ENERGY_VECTOR]
190
                == energy_vector
191
            ):
192
                log_message_object = f"levelized costs of generation for energy asset '{dict_values[ENERGY_PRODUCTION][production_asset][LABEL]}'"
1✔
193

194
                # If energy production asset is a non-dispatchable source (PV plant)
195
                if (
1✔
196
                    dict_values[ENERGY_PRODUCTION][production_asset][DISPATCHABILITY]
197
                    is False
198
                ):
199
                    # Calculate cost per kWh generated
200
                    levelized_cost_of_generation = (
1✔
201
                        dict_values[ENERGY_PRODUCTION][production_asset][
202
                            SIMULATION_ANNUITY
203
                        ][VALUE]
204
                        / dict_values[ENERGY_PRODUCTION][production_asset][
205
                            TIMESERIES_TOTAL
206
                        ][VALUE]
207
                    )
208
                # If energy production asset is a dispatchable source (fuel source)
209
                else:
210
                    log_message_object += " (based on is dispatch price)"
1✔
211
                    # Estimate costs based on dispatch price (this is the lower minimum, as actually o&m and investment costs would need to be added as well, but can not be added as the dispatch is not known yet.
212
                    levelized_cost_of_generation = dict_values[ENERGY_PRODUCTION][
1✔
213
                        production_asset
214
                    ][DISPATCH_PRICE][VALUE]
215

216
                # Determine the margin between feedin tariff and generation costs
217
                diff = feedin_tariff[VALUE] - levelized_cost_of_generation
1✔
218
                # Get value of optimizeCap and maximumCap of production_asset
219
                optimze_cap = dict_values[ENERGY_PRODUCTION][production_asset][
1✔
220
                    OPTIMIZE_CAP
221
                ][VALUE]
222
                maximum_cap = dict_values[ENERGY_PRODUCTION][production_asset][
1✔
223
                    MAXIMUM_CAP
224
                ][VALUE]
225
                # If float/int values
226
                if isinstance(diff, float) or isinstance(diff, int):
1✔
227
                    if diff > 0:
1✔
228
                        # This can result in an unbound solution if optimizeCap is True and maximumCap is None
229
                        if optimze_cap == True and maximum_cap is None:
1✔
230

231
                            msg = f"Feed-in tariff of {energy_vector} ({round(feedin_tariff[VALUE],4)}) > {log_message_object} with {round(levelized_cost_of_generation,4)}. {warning_message_hint_unbound}"
1✔
232
                            if (
1✔
233
                                DSO_FEEDIN_CAP
234
                                in dict_values[ENERGY_PROVIDERS][provider]
235
                            ):
236
                                logging.warning(msg)
×
237
                            else:
238
                                raise ValueError(msg)
1✔
239
                        # If maximumCap is not None the maximum capacity of the production asset will be installed
240
                        elif optimze_cap == True and maximum_cap is not None:
1✔
241
                            msg = f"Feed-in tariff of {energy_vector} ({round(feedin_tariff[VALUE],4)}) > {log_message_object} with {round(levelized_cost_of_generation,4)}. {warning_message_hint_maxcap}"
1✔
242
                            logging.warning(msg)
1✔
243
                        # If the capacity of the production asset is not optimized there is no unbound problem but strange dispatch behaviour might occur
244
                        else:
245
                            logging.debug(
1✔
246
                                f"Feed-in tariff of {energy_vector} ({round(feedin_tariff[VALUE],4)}) > {log_message_object} with {round(levelized_cost_of_generation,4)}. {warning_message_hint_dispatch}"
247
                            )
248
                    else:
249
                        logging.debug(f"Feed-in tariff < {log_message_object}.")
1✔
250
                # If provided as a timeseries
251
                else:
252
                    boolean = [
×
253
                        k > 0 for k in diff.values
254
                    ]  # True if there is an instance where feed-in tariff > electricity_price
255
                    if any(boolean) is True:
×
256
                        # This can result in an unbound solution if optimizeCap is True and maximumCap is None
257
                        if optimze_cap == True and maximum_cap is None:
×
258
                            instances = sum(boolean)  # Count instances
×
259
                            msg = f"Feed-in tariff of {energy_vector} > {log_message_object} in {instances} during the simulation time. {warning_message_hint_unbound}"
×
260
                            if (
×
261
                                DSO_FEEDIN_CAP
262
                                in dict_values[ENERGY_PROVIDERS][provider]
263
                            ):
264
                                logging.warning(msg)
×
265
                            else:
266
                                raise ValueError(msg)
×
267
                        # If maximumCap is not None the maximum capacity of the production asset will be installed
268
                        elif optimze_cap == True and maximum_cap is not None:
×
269
                            msg = f"Feed-in tariff of {energy_vector} > {log_message_object} in {instances} during the simulation time. {warning_message_hint_maxcap}"
×
270
                            logging.warning(msg)
×
271
                        # If the capacity of the production asset is not optimized there is no unbound problem but strange dispatch behaviour might occur
272
                        else:
273
                            logging.debug(
×
274
                                f"Feed-in tariff of {energy_vector} > {log_message_object} in {instances} during the simulation time. {warning_message_hint_dispatch}"
275
                            )
276
                    else:
277
                        logging.debug(f"Feed-in tariff < {log_message_object}.")
×
278

279

280
def check_feedin_tariff_vs_energy_price(dict_values):
1✔
281
    r"""
282
    Raises error if feed-in tariff > energy price of any asset in 'energyProvider.csv'.
283
    This is not allowed, as oemof otherwise is subjected to an unbound and unrealistic problem, eg. one where the owner should consume electricity to feed it directly back into the grid for its revenue.
284

285
    Parameters
286
    ----------
287
    dict_values : dict
288
        Contains all input data of the simulation.
289

290
    Returns
291
    -------
292
    Indirectly, raises error message in case of feed-in tariff > energy price of any
293
    asset in 'energyProvider.csv'.
294

295
    Notes
296
    -----
297
    Tested with:
298
    - C1.test_check_feedin_tariff_vs_energy_price_greater_energy_price()
299
    - C1.test_check_feedin_tariff_vs_energy_price_not_greater_energy_price()
300

301
    """
302
    for provider in dict_values[ENERGY_PROVIDERS].keys():
1✔
303
        feedin_tariff = dict_values[ENERGY_PROVIDERS][provider][FEEDIN_TARIFF]
1✔
304
        electricity_price = dict_values[ENERGY_PROVIDERS][provider][ENERGY_PRICE]
1✔
305
        diff = feedin_tariff[VALUE] - electricity_price[VALUE]
1✔
306
        if isinstance(diff, float) or isinstance(diff, int):
1✔
307
            if diff > 0:
1✔
308

309
                msg = f"Feed-in tariff > energy price for the energy provider asset '{dict_values[ENERGY_PROVIDERS][provider][LABEL]}' would cause an unbound solution and terminate the optimization. Please reconsider your feed-in tariff and energy price."
1✔
310
                if DSO_FEEDIN_CAP in dict_values[ENERGY_PROVIDERS][provider]:
1✔
311
                    logging.warning(msg)
×
312
                else:
313
                    raise ValueError(msg)
1✔
314
            else:
315
                logging.debug(
1✔
316
                    f"Feed-in tariff < energy price for energy provider asset '{dict_values[ENERGY_PROVIDERS][provider][LABEL]}'"
317
                )
318
        else:
319
            boolean = [
×
320
                k > 0 for k in diff.values
321
            ]  # True if there is an instance where feed-in tariff > electricity_price
322
            if any(boolean) is True:
×
323
                instances = sum(boolean)  # Count instances
×
324
                msg = f"Feed-in tariff > energy price during {instances} timesteps of the simulation for the energy provider asset '{dict_values[ENERGY_PROVIDERS][provider][LABEL]}'. This would cause an unbound solution and terminate the optimization. Please reconsider your feed-in tariff and energy price."
×
325
                if DSO_FEEDIN_CAP in dict_values[ENERGY_PROVIDERS][provider]:
×
326
                    logging.warning(msg)
×
327
                else:
328
                    raise ValueError(msg)
×
329
            else:
330
                logging.debug(
×
331
                    f"Feed-in tariff < energy price for energy provider asset '{dict_values[ENERGY_PROVIDERS][provider][LABEL]}'"
332
                )
333

334

335
def check_feasibility_of_maximum_emissions_constraint(dict_values):
1✔
336
    r"""
337
    Logs a logging.warning message in case the maximum emissions constraint could lead into an unbound problem.
338

339
    If the maximum emissions constraint is used it is checked whether there is any
340
    production asset with zero emissions that has a capacity to be optimized without
341
    maximum capacity constraint. If this is not the case a warning is logged.
342

343
    Parameters
344
    ----------
345
    dict_values : dict
346
        Contains all input data of the simulation.
347

348
    Returns
349
    -------
350
    Indirectly, logs a logging.warning message in case the maximum emissions constraint
351
    is used while no production with zero emissions is optimized without maximum capacity.
352

353
    Notes
354
    -----
355
    Tested with:
356
    - C1.test_check_feasibility_of_maximum_emissions_constraint_no_warning_no_constraint()
357
    - C1.test_check_feasibility_of_maximum_emissions_constraint_no_warning_although_emission_constraint()
358
    - C1.test_check_feasibility_of_maximum_emissions_constraint_maximumcap()
359
    - C1.test_check_feasibility_of_maximum_emissions_constraint_optimizeCap_is_False()
360
    - C1.test_check_feasibility_of_maximum_emissions_constraint_no_zero_emission_asset()
361

362
    """
363
    if dict_values[CONSTRAINTS][MAXIMUM_EMISSIONS][VALUE] is not None:
1✔
364
        count = 0
1✔
365
        for key, asset in dict_values[ENERGY_PRODUCTION].items():
1✔
366
            if (
1✔
367
                asset[EMISSION_FACTOR][VALUE] == 0
368
                and asset[OPTIMIZE_CAP][VALUE] == True
369
                and asset[MAXIMUM_CAP][VALUE] is None
370
            ):
371
                count += 1
1✔
372

373
        if count == 0:
1✔
374
            logging.warning(
1✔
375
                f"When the maximum emissions constraint is used and no production asset with zero emissions is optimized without maximum capacity this could result into an unbound problem. If this happens you can either raise the allowed maximum emissions or make sure you have enough production capacity with low emissions to cover the demand."
376
            )
377

378

379
def check_emission_factor_of_providers(dict_values):
1✔
380
    r"""
381
    Logs a logging.warning message in case the grid has a renewable share of 100 % but an emission factor > 0.
382

383
    This would affect the optimization if a maximum emissions contraint is used.
384
    Aditionally, it effects the KPIs connected to emissions.
385

386
    Parameters
387
    ----------
388
    dict_values : dict
389
        Contains all input data of the simulation.
390

391
    Returns
392
    -------
393
    Indirectly, logs a logging.warning message in case tthe grid has a renewable share
394
    of 100 % but an emission factor > 0.
395

396
    Notes
397
    -----
398
    Tested with:
399
    - C1.test_check_emission_factor_of_providers_no_warning_RE_share_lower_1()
400
    - C1.test_check_emission_factor_of_providers_no_warning_emission_factor_0()
401
    - C1.test_check_emission_factor_of_providers_warning()
402

403
    """
404
    for key, asset in dict_values[ENERGY_PROVIDERS].items():
1✔
405
        if asset[EMISSION_FACTOR][VALUE] > 0 and asset[RENEWABLE_SHARE_DSO][VALUE] == 1:
1✔
406
            logging.warning(
1✔
407
                f"The renewable share of provider {key} is {asset[RENEWABLE_SHARE_DSO][VALUE] * 100} % while its emission_factor is >0. Check if this is what you intended to define."
408
            )
409

410

411
def check_time_series_values_between_0_and_1(time_series):
1✔
412
    r"""
413
    Checks whether all values of `time_series` in [0, 1].
414

415
    Parameters
416
    ----------
417
    time_series : pd.Series
418
        Time series to be checked.
419

420
    Returns
421
    -------
422
    bool
423
        True if values of `time_series` within [0, 1], else False.
424

425
    """
426
    boolean = time_series.between(0, 1)
1✔
427

428
    return bool(boolean.all())
1✔
429

430

431
def check_non_dispatchable_source_time_series(dict_values):
1✔
432
    r"""
433
    Raises error if time series of non-dispatchable sources are not between [0, 1].
434

435
    Parameters
436
    ----------
437
    dict_values : dict
438
        Contains all input data of the simulation.
439

440
    Returns
441
    -------
442
    Indirectly, raises error message in case of time series of non-dispatchable sources
443
    not between [0, 1].
444

445
    """
446
    # go through all non-dispatchable sources
447
    for key, source in dict_values[ENERGY_PRODUCTION].items():
1✔
448
        if TIMESERIES in source and source[DISPATCHABILITY] is False:
1✔
449
            # check if values between 0 and 1
450
            result = check_time_series_values_between_0_and_1(
1✔
451
                time_series=source[TIMESERIES]
452
            )
453
            if result is False:
1✔
454
                logging.error(
1✔
455
                    f"{TIMESERIES} of non-dispatchable source {source[LABEL]} contains values out of bounds [0, 1]."
456
                )
457
                return False
1✔
458

459

460
def check_efficiency_of_storage_capacity(dict_values):
1✔
461
    r"""
462
    Raises error or logs a warning to help users to spot major change in PR #676.
463

464
    In #676 the `efficiency` of `storage capacity' in `storage_*.csv` was defined as the
465
    storages' efficiency/ability to hold charge over time. Before it was defined as
466
    loss rate.
467
    This function raises an error if efficiency of 'storage capacity' of one of the
468
    storages is 0 and logs a warning if efficiency of 'storage capacity' of one of the
469
    storages is <0.2.
470

471
    Parameters
472
    ----------
473
    dict_values : dict
474
        Contains all input data of the simulation.
475

476
    Notes
477
    -----
478
    Tested with:
479
    - test_check_efficiency_of_storage_capacity_is_0
480
    - test_check_efficiency_of_storage_capacity_is_btw_0_and_02
481
    - test_check_efficiency_of_storage_capacity_is_greater_02
482

483
    Returns
484
    -------
485
    Indirectly, raises error message in case of efficiency of 'storage capacity' is 0
486
    and logs warning message in case of efficiency of 'storage capacity' is <0.2.
487

488
    """
489
    # go through all storages
490
    for key, item in dict_values[ENERGY_STORAGE].items():
1✔
491
        eff = item[STORAGE_CAPACITY][EFFICIENCY][VALUE]
1✔
492
        if eff == 0:
1✔
493
            raise ValueError(
1✔
494
                f"You might use an old input file! The efficiency of the storage capacity of '{item[LABEL]}' is {eff}, although it should represent the ability of the storage to hold charge over time; check PR #676."
495
            )
496
        elif eff < 0.2:
1✔
497
            logging.warning(
1✔
498
                f"You might use an old input file! The efficiency of the storage capacity of '{item[LABEL]}' is {eff}, although it should represent the ability of the storage to hold charge over time; check PR #676."
499
            )
500

501

502
def check_input_values(dict_values):
1✔
503
    """
504

505
    :param dict_values:
506
    :return:
507
    """
508
    for asset_name in dict_values:
×
509
        if not (isinstance(dict_values[asset_name], dict)):
×
510
            # checking first layer of dict_values
511
            all_valid_intervals(asset_name, dict_values[asset_name], "")
×
512
        else:
513
            # logging.debug('Asset %s checked for validation.', asset_name)
514
            for sub_asset_name in dict_values[asset_name]:
×
515
                if not (isinstance(dict_values[asset_name][sub_asset_name], dict)):
×
516
                    # checking second layer of dict values
517
                    all_valid_intervals(
×
518
                        sub_asset_name,
519
                        dict_values[asset_name][sub_asset_name],
520
                        asset_name,
521
                    )
522
                else:
523
                    # logging.debug('\t Sub-asset %s checked for validation.', sub_asset_name)
524
                    for sub_sub_asset_name in dict_values[asset_name][sub_asset_name]:
×
525
                        if not (
×
526
                            isinstance(
527
                                dict_values[asset_name][sub_asset_name][
528
                                    sub_sub_asset_name
529
                                ],
530
                                dict,
531
                            )
532
                        ):
533
                            # checking third layer of dict values
534
                            all_valid_intervals(
×
535
                                sub_sub_asset_name,
536
                                dict_values[asset_name][sub_asset_name][
537
                                    sub_sub_asset_name
538
                                ],
539
                                asset_name + sub_asset_name,
540
                            )
541
                        else:
542
                            # logging.debug('\t\t Sub-sub-asset %s checked for validation.', sub_sub_asset_name)
543
                            logging.critical(
×
544
                                "Verification Error! Add another layer to evaluation."
545
                            )
546

547
    logging.info(
×
548
        "Input values have been verified. This verification can not replace a manual input parameter check."
549
    )
550

551

552
def all_valid_intervals(name, value, title):
1✔
553
    """
554
    Checks whether `value` of `name` is valid.
555

556
    Checks include the expected type and the expected range a parameter is
557
    supposed to be inside.
558

559
    :param name:
560
    :param value:
561
    :param title:
562
    :return:
563
    """
564
    valid_type_string = [
×
565
        PROJECT_NAME,
566
        SCENARIO_NAME,
567
        COUNTRY,
568
        "parent",
569
        "type",
570
        FILENAME,
571
        LABEL,
572
        CURR,
573
        PATH_OUTPUT_FOLDER,
574
        DISPLAY_OUTPUT,
575
        PATH_INPUT_FILE,
576
        PATH_INPUT_FOLDER,
577
        "sector",
578
    ]
579

580
    valid_type_int = [EVALUATED_PERIOD, "time_step", PERIODS]
×
581

582
    valid_type_timestamp = [START_DATE]
×
583

584
    valid_type_index = ["index"]
×
585

586
    valid_binary = ["optimize_cap", DSM, OVERWRITE]
×
587

588
    valid_intervals = {
×
589
        LONGITUDE: [-180, 180],
590
        LATITUDE: [-90, 90],
591
        LIFETIME: ["largerzero", "any"],
592
        AGE_INSTALLED: [0, "any"],
593
        INSTALLED_CAP: [0, "any"],
594
        MAXIMUM_CAP: [0, "any", None],
595
        SOC_MIN: [0, 1],
596
        SOC_MAX: [0, 1],
597
        SOC_INITIAL: [0, 1],
598
        "crate": [0, 1],
599
        EFFICIENCY: [0, 1],
600
        "electricity_cost_fix_annual": [0, "any"],
601
        "electricity_price_var_kWh": [0, "any"],
602
        "electricity_price_var_kW_monthly": [0, "any"],
603
        FEEDIN_TARIFF: [0, "any"],
604
        DEVELOPMENT_COSTS: [0, "any"],
605
        SPECIFIC_COSTS: [0, "any"],
606
        SPECIFIC_COSTS_OM: [0, "any"],
607
        DISPATCH_PRICE: [0, "any"],
608
        DISCOUNTFACTOR: [0, 1],
609
        PROJECT_DURATION: ["largerzero", "any"],
610
        TAX: [0, 1],
611
    }
612

613
    if name in valid_type_int:
×
614
        if not (isinstance(value, int)):
×
615
            logging.error(
×
616
                'Input error! Value %s/%s is not in recommended format "integer".',
617
                name,
618
                title,
619
            )
620

621
    elif name in valid_type_string:
×
622
        if not (isinstance(value, str)):
×
623
            logging.error(
×
624
                'Input error! Value %s/%s is not in recommended format "string".',
625
                name,
626
                title,
627
            )
628

629
    elif name in valid_type_index:
×
630
        if not (isinstance(value, pd.DatetimeIndex)):
×
631
            logging.error(
×
632
                'Input error! Value %s/%s is not in recommended format "pd.DatetimeIndex".',
633
                name,
634
                title,
635
            )
636

637
    elif name in valid_type_timestamp:
×
638
        if not (isinstance(value, pd.Timestamp)):
×
639
            logging.error(
×
640
                'Input error! Value %s/%s is not in recommended format "pd.DatetimeIndex".',
641
                name,
642
                title,
643
            )
644

645
    elif name in valid_binary:
×
646
        if not (value is True or value is False):
×
647
            logging.error(
×
648
                "Input error! Value %s/%s is neither True nor False.", name, title
649
            )
650

651
    elif name in valid_intervals:
×
652
        if name == SOC_INITIAL:
×
653
            if value is not None:
×
654
                if not (0 <= value and value <= 1):
×
655
                    logging.error(
×
656
                        "Input error! Value %s/%s should be None, or between 0 and 1.",
657
                        name,
658
                        title,
659
                    )
660
        else:
661

662
            if valid_intervals[name][0] == "largerzero":
×
663
                if value <= 0:
×
664
                    logging.error(
×
665
                        "Input error! Value %s/%s can not be to be smaller or equal to 0.",
666
                        name,
667
                        title,
668
                    )
669
            elif valid_intervals[name][0] == "nonzero":
×
670
                if value == 0:
×
671
                    logging.error("Input error! Value %s/%s can not be 0.", name, title)
×
672
            elif valid_intervals[name][0] == 0:
×
673
                if value < 0:
×
674
                    logging.error(
×
675
                        "Input error! Value %s/%s has to be larger than or equal to 0.",
676
                        name,
677
                        title,
678
                    )
679

680
            if valid_intervals[name][1] == "any":
×
681
                pass
×
682
            elif valid_intervals[name][1] == 1:
×
683
                if 1 < value:
×
684
                    logging.error(
×
685
                        "Input error! Value %s/%s can not be larger than 1.",
686
                        name,
687
                        title,
688
                    )
689

690
    else:
691
        logging.warning(
×
692
            "VALIDATION FAILED: Code does not define a valid range for value %s/%s",
693
            name,
694
            title,
695
        )
696

697

698
def check_if_energy_vector_of_all_assets_is_valid(dict_values):
1✔
699
    """
700
    Validates for all assets, whether 'energyVector' is defined within DEFAULT_WEIGHTS_ENERGY_CARRIERS and within the energyBusses.
701

702
    Parameters
703
    ----------
704
    dict_values: dict
705
        All input data in dict format
706

707
    Notes
708
    -----
709
    Function tested with
710
    - test_add_economic_parameters()
711
    - test_check_if_energy_vector_of_all_assets_is_valid_fails
712
    - test_check_if_energy_vector_of_all_assets_is_valid_passes
713
    """
714
    for level1 in dict_values.keys():
1✔
715
        for level2 in dict_values[level1].keys():
1✔
716
            if (
1✔
717
                isinstance(dict_values[level1][level2], dict)
718
                and ENERGY_VECTOR in dict_values[level1][level2].keys()
719
            ):
720
                energy_vector_name = dict_values[level1][level2][ENERGY_VECTOR]
1✔
721
                if (
1✔
722
                    energy_vector_name
723
                    not in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]
724
                ):
725
                    raise ValueError(
1✔
726
                        f"Asset {level2} of asset group {level1} has an energy vector ({energy_vector_name}) that is not defined within the energyBusses. "
727
                        f"This prohibits proper processing of the assets dispatch."
728
                        f"Please check for typos or define another bus, as this hints at the energy system being faulty."
729
                    )
730
                    C1.check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS(
×
731
                        energy_vector_name, level1, level2
732
                    )
733

734

735
def check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS(
1✔
736
    energy_carrier, asset_group, asset
737
):
738
    r"""
739
    Raises an error message if an energy vector is unknown.
740

741
    It then needs to be added to the DEFAULT_WEIGHTS_ENERGY_CARRIERS in constants.py
742

743
    Parameters
744
    ----------
745
    energy_carrier: str
746
        Name of the energy carrier
747

748
    asset_group: str
749
        Name of the asset group
750

751
    asset: str
752
        Name of the asset
753

754
    Returns
755
    -------
756
    None
757

758
    Notes
759
    -----
760
    Tested with:
761
    - test_check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS_pass()
762
    - test_check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS_fails()
763
    """
764
    if energy_carrier not in DEFAULT_WEIGHTS_ENERGY_CARRIERS:
1✔
765
        raise UnknownEnergyVectorError(
1✔
766
            f"The energy carrier {energy_carrier} of asset group {asset_group}, asset {asset} is unknown, "
767
            f"as it is not defined within the DEFAULT_WEIGHTS_ENERGY_CARRIERS."
768
            f"Please check the energy carrier, or update the DEFAULT_WEIGHTS_ENERGY_CARRIERS in contants.py (dev)."
769
        )
770

771

772
def check_for_sufficient_assets_on_busses(dict_values):
1✔
773
    r"""
774
    Validation check for busses, to make sure a sufficient number of assets is connected.
775

776
    Each bus has to has to have 3 or more assets connected to it. The reasoning is that each bus needs:
777
    - One asset for inflow into the bus
778
    - One asset for outflow from the bus
779
    - One energy excess asset
780
    Note, however, that this test does not check whether the assets actually serve that function, so there might be false negatives: The test can for example pass, if there are two output assets, one excess asset but no input asset, which would represent a non-sensical combination.
781

782
    On the bus created for the peak demand pricing function (name includes `DSO_PEAK_DEMAND_SUFFIX`) no excess sinks are added, and therefore the rule does not have to be applied to this bus.
783

784
    Parameters
785
    ----------
786
    dict_values: dict
787
        All simulation parameters
788

789
    Returns
790
    -------
791
    Logging error message if test fails
792

793
    Notes
794
    -----
795
    This function is tested with:
796
    - test_C1_verification.test_check_for_sufficient_assets_on_busses_example_bus_passes()
797
    - test_C1_verification.test_check_for_sufficient_assets_on_busses_example_bus_fails()
798
    - test_C1_verification.test_check_for_sufficient_assets_on_busses_skipped_for_peak_demand_pricing_bus()
799
    """
800
    for bus in dict_values[ENERGY_BUSSES]:
1✔
801
        if (
1✔
802
            len(dict_values[ENERGY_BUSSES][bus][ASSET_DICT]) < 3
803
            and DSO_PEAK_DEMAND_SUFFIX not in bus
804
            and DSO_FEEDIN_CAP not in bus
805
        ):
806
            asset_string = ", ".join(
1✔
807
                map(str, dict_values[ENERGY_BUSSES][bus][ASSET_DICT].keys())
808
            )
809
            logging.error(
1✔
810
                f"Energy system bus {bus} has too few assets connected to it. "
811
                f"The minimal number of assets that need to be connected "
812
                f"so that the bus is not a dead end should be two, excluding the excess sink. "
813
                f"These are the connected assets: {asset_string}"
814
            )
815

816
    return True
1✔
817

818

819
def check_energy_system_can_fulfill_max_demand(dict_values):
1✔
820
    r"""
821
    Helps to do oemof-solph termination debugging: Logs a logging.warning message if the aggregated installed capacity and maximum capacity (if applicable)
822
    of all conversion, generation and storage assets connected to one bus is smaller than the maximum demand.
823
    The check is applied to each bus of the energy system. Check passes when the potential peak supply is
824
    larger then or equal to the peak demand on the bus, or if the maximum capacity of an asset is set to
825
    None when optimizing.
826

827
    Parameters
828
    ----------
829
    dict_values : dict
830
        Contains all input data of the simulation.
831

832
    Returns
833
    -------
834
    Indirectly, logs a logging.warning message if the installed and maximum capacities of
835
    conversion/generation/storage assets are less than the maximum demand, for each bus.
836

837
    Notes
838
    -----
839

840
    Tested with:
841
    - test_check_energy_system_can_fulfill_max_demand_sufficient_capacities()
842
    - test_check_energy_system_can_fulfill_max_demand_no_maximum_capacity()
843
    - test_check_energy_system_can_fulfill_max_demand_insufficient_capacities()
844
    - test_check_energy_system_can_fulfill_max_demand_with_storage()
845
    - test_check_energy_system_can_fulfill_max_demand_sufficient_dispatchable_production
846
    - test_check_energy_system_can_fulfill_max_demand_insufficient_dispatchable_production
847
    - test_check_energy_system_can_fulfill_max_demand_sufficient_non_dispatchable_production
848
    - test_check_energy_system_can_fulfill_max_demand_insufficient_non_dispatchable_production
849
    - test_check_energy_system_can_fulfill_max_demand_fails_mvs_runthrough
850

851
    """
852
    for bus in dict_values[ENERGY_BUSSES]:
1✔
853
        pass_check = False
1✔
854
        opt_cap_storage = False
1✔
855
        peak_demand = 0
1✔
856
        peak_generation = 0
1✔
857
        for item in dict_values[ENERGY_BUSSES][bus][ASSET_DICT]:
1✔
858
            # filters out excess energy sinks, leaving only actual demand profiles
859
            if item in dict_values[ENERGY_CONSUMPTION]:
1✔
860
                if (
1✔
861
                    dict_values[ENERGY_CONSUMPTION][item][DISPATCHABILITY][VALUE]
862
                    is False
863
                ):
864
                    peak_demand += dict_values[ENERGY_CONSUMPTION][item][
1✔
865
                        TIMESERIES_PEAK
866
                    ][VALUE]
867
            # Only add capacity of conversion assets that can contribute to supply
868
            if (
1✔
869
                item in dict_values[ENERGY_CONVERSION]
870
                and dict_values[ENERGY_CONVERSION][item][OUTFLOW_DIRECTION] == bus
871
            ):
872
                peak_generation += dict_values[ENERGY_CONVERSION][item][INSTALLED_CAP][
1✔
873
                    VALUE
874
                ]
875
                if dict_values[ENERGY_CONVERSION][item][OPTIMIZE_CAP][VALUE] is True:
1✔
876
                    if dict_values[ENERGY_CONVERSION][item][MAXIMUM_CAP][VALUE] is None:
1✔
877
                        pass_check = True
1✔
878
                    else:
879
                        peak_generation += dict_values[ENERGY_CONVERSION][item][
1✔
880
                            MAXIMUM_CAP
881
                        ][VALUE]
882

883
            # Add potential generation of energy production assets
884
            if item in dict_values[ENERGY_PRODUCTION]:
1✔
885
                # Effective generation of asset dependent on the peak of timeseries
886
                if TIMESERIES_PEAK in dict_values[ENERGY_PRODUCTION][item]:
1✔
887
                    # This is the case for all non-dispatchable assets
888
                    factor = dict_values[ENERGY_PRODUCTION][item][TIMESERIES_PEAK][
1✔
889
                        VALUE
890
                    ]
891
                else:
892
                    # This is the case for all dispatchable assets, ie. fuel sources defined in `energyProduction.csv`
893
                    factor = 1
1✔
894
                peak_generation += (
1✔
895
                    dict_values[ENERGY_PRODUCTION][item][INSTALLED_CAP][VALUE] * factor
896
                )
897
                if dict_values[ENERGY_PRODUCTION][item][OPTIMIZE_CAP][VALUE] is True:
1✔
898
                    if dict_values[ENERGY_PRODUCTION][item][MAXIMUM_CAP][VALUE] is None:
1✔
899
                        pass_check = True
1✔
900
                    else:
901
                        # Effective generation of asset dependent on the peak of timeseries
902
                        peak_generation += (
1✔
903
                            dict_values[ENERGY_PRODUCTION][item][MAXIMUM_CAP][VALUE]
904
                            * factor
905
                        )
906
            if item in dict_values[ENERGY_STORAGE]:
1✔
907
                peak_generation += dict_values[ENERGY_STORAGE][item][OUTPUT_POWER][
1✔
908
                    INSTALLED_CAP
909
                ][VALUE]
910
                if dict_values[ENERGY_STORAGE][item][OPTIMIZE_CAP][VALUE] is True:
1✔
911
                    # unlike the conversion/production assets, no maximum capacities are defined for
912
                    # storage assets and therefore as soon as a storage asset is connected, the check
913
                    # should pass
914
                    pass_check = True
1✔
915
                    opt_cap_storage = True
1✔
916
        if peak_generation < peak_demand and pass_check is False:
1✔
917
            logging.warning(
1✔
918
                f"The assets of {bus} might have insufficient capacities to fulfill"
919
                f" the maximum demand ({round(peak_generation)}<{round(peak_demand)})."
920
            )
921
        elif opt_cap_storage is True:
1✔
922
            logging.debug(
1✔
923
                f"The check for assets having sufficient capacities to fulfill the"
924
                f" maximum demand has successfully passed for bus {bus} (peak generation: {peak_generation}, peak demand: {peak_demand}, unlimited storage capacities in optimization: {opt_cap_storage}). At least partially, this also happens because there is a storage asset that is being optimized."
925
                f" However, this check does not determine if the storage can be sufficiently"
926
                f" charged by the other assets in the energy system to ensure a viable supply for each timestep."
927
            )
928
        else:
929
            logging.debug(
1✔
930
                f"The check for assets having sufficient capacities to fulfill the"
931
                f" maximum demand has successfully passed for bus {bus} (peak generation: {peak_generation}, peak demand: {peak_demand}, unlimited capacities in optimization: {pass_check})."
932
            )
933

934
        return peak_generation, peak_demand
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