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

rl-institut / multi-vector-simulator / 8870538658

28 Apr 2024 09:31PM UTC coverage: 75.582% (-1.4%) from 76.96%
8870538658

push

github

web-flow
Merge pull request #971 from rl-institut/fix/black-vulnerability

Fix/black vulnerability

26 of 29 new or added lines in 15 files covered. (89.66%)

826 existing lines in 21 files now uncovered.

5977 of 7908 relevant lines covered (75.58%)

0.76 hits per line

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

65.35
/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
    BETA,
91
)
92

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

96

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

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

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

116

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

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

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

141

142
def check_feedin_tariff_vs_levelized_cost_of_generation_of_production(dict_values):
1✔
143
    r"""
144
    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.
145

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

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

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

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

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

171
    This test does not cover cross-sectoral invalid feedin tariffs.
172
    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.
173
    """
174

175
    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✔
176
    warning_message_hint_maxcap = f"This will cause the optimization to result into the maximum capacity of this asset."
1✔
177
    warning_message_hint_dispatch = (
1✔
178
        f"No error expected but strange dispatch behaviour might occur."
179
    )
180

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

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

195
                # If energy production asset is a non-dispatchable source (PV plant)
196
                if (
1✔
197
                    dict_values[ENERGY_PRODUCTION][production_asset][DISPATCHABILITY]
198
                    is False
199
                ):
200
                    # Calculate cost per kWh generated
201
                    levelized_cost_of_generation = (
1✔
202
                        dict_values[ENERGY_PRODUCTION][production_asset][
203
                            SIMULATION_ANNUITY
204
                        ][VALUE]
205
                        / dict_values[ENERGY_PRODUCTION][production_asset][
206
                            TIMESERIES_TOTAL
207
                        ][VALUE]
208
                    )
209
                # If energy production asset is a dispatchable source (fuel source)
210
                else:
211
                    log_message_object += " (based on is dispatch price)"
1✔
212
                    # 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.
213
                    levelized_cost_of_generation = dict_values[ENERGY_PRODUCTION][
1✔
214
                        production_asset
215
                    ][DISPATCH_PRICE][VALUE]
216

217
                # Determine the margin between feedin tariff and generation costs
218
                diff = feedin_tariff[VALUE] - levelized_cost_of_generation
1✔
219
                # Get value of optimizeCap and maximumCap of production_asset
220
                optimize_cap = dict_values[ENERGY_PRODUCTION][production_asset][
1✔
221
                    OPTIMIZE_CAP
222
                ][VALUE]
223
                maximum_cap = dict_values[ENERGY_PRODUCTION][production_asset][
1✔
224
                    MAXIMUM_CAP
225
                ][VALUE]
226
                # If float/int values
227
                if isinstance(diff, float) or isinstance(diff, int):
1✔
228
                    if diff > 0:
1✔
229
                        # This can result in an unbound solution if optimizeCap is True and maximumCap is None
230
                        if optimize_cap is True and maximum_cap is None:
1✔
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
                            ):
UNCOV
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 optimize_cap is 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:
UNCOV
252
                    boolean = [
×
253
                        k > 0 for k in diff.values
254
                    ]  # True if there is an instance where feed-in tariff > electricity_price
UNCOV
255
                    if any(boolean) is True:
×
256
                        instances = sum(boolean)  # Count instances
×
257
                        # This can result in an unbound solution if optimizeCap is True and maximumCap is None
258
                        if optimize_cap is True and maximum_cap is None:
×
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
                            ):
UNCOV
264
                                logging.warning(msg)
×
265
                            else:
UNCOV
266
                                raise ValueError(msg)
×
267
                        # If maximumCap is not None the maximum capacity of the production asset will be installed
UNCOV
268
                        elif optimize_cap is 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:
UNCOV
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:
UNCOV
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
        if isinstance(feedin_tariff[VALUE], pd.Series):
1✔
UNCOV
306
            feedin_tariff = feedin_tariff[VALUE].values
×
307
        else:
308
            feedin_tariff = feedin_tariff[VALUE]
1✔
309

310
        if isinstance(electricity_price[VALUE], pd.Series):
1✔
UNCOV
311
            electricity_price = electricity_price[VALUE].values
×
312
        else:
313
            electricity_price = electricity_price[VALUE]
1✔
314

315
        diff = feedin_tariff - electricity_price
1✔
316
        if isinstance(diff, float) or isinstance(diff, int):
1✔
317
            if diff > 0:
1✔
318

319
                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✔
320
                if DSO_FEEDIN_CAP in dict_values[ENERGY_PROVIDERS][provider]:
1✔
UNCOV
321
                    logging.warning(msg)
×
322
                else:
323
                    raise ValueError(msg)
1✔
324
            else:
325
                logging.debug(
1✔
326
                    f"Feed-in tariff < energy price for energy provider asset '{dict_values[ENERGY_PROVIDERS][provider][LABEL]}'"
327
                )
328
        else:
329
            boolean = [
×
330
                k > 0 for k in diff
331
            ]  # True if there is an instance where feed-in tariff > electricity_price
UNCOV
332
            if any(boolean) is True:
×
UNCOV
333
                instances = sum(boolean)  # Count instances
×
UNCOV
334
                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."
×
UNCOV
335
                if DSO_FEEDIN_CAP in dict_values[ENERGY_PROVIDERS][provider]:
×
UNCOV
336
                    logging.warning(msg)
×
337
                else:
UNCOV
338
                    raise ValueError(msg)
×
339
            else:
UNCOV
340
                logging.debug(
×
341
                    f"Feed-in tariff < energy price for energy provider asset '{dict_values[ENERGY_PROVIDERS][provider][LABEL]}'"
342
                )
343

344

345
def check_feasibility_of_maximum_emissions_constraint(dict_values):
1✔
346
    r"""
347
    Logs a logging.warning message in case the maximum emissions constraint could lead into an unbound problem.
348

349
    If the maximum emissions constraint is used it is checked whether there is any
350
    production asset with zero emissions that has a capacity to be optimized without
351
    maximum capacity constraint. If this is not the case a warning is logged.
352

353
    Parameters
354
    ----------
355
    dict_values : dict
356
        Contains all input data of the simulation.
357

358
    Returns
359
    -------
360
    Indirectly, logs a logging.warning message in case the maximum emissions constraint
361
    is used while no production with zero emissions is optimized without maximum capacity.
362

363
    Notes
364
    -----
365
    Tested with:
366
    - C1.test_check_feasibility_of_maximum_emissions_constraint_no_warning_no_constraint()
367
    - C1.test_check_feasibility_of_maximum_emissions_constraint_no_warning_although_emission_constraint()
368
    - C1.test_check_feasibility_of_maximum_emissions_constraint_maximumcap()
369
    - C1.test_check_feasibility_of_maximum_emissions_constraint_optimizeCap_is_False()
370
    - C1.test_check_feasibility_of_maximum_emissions_constraint_no_zero_emission_asset()
371

372
    """
373
    if dict_values[CONSTRAINTS][MAXIMUM_EMISSIONS][VALUE] is not None:
1✔
374
        count = 0
1✔
375
        for key, asset in dict_values[ENERGY_PRODUCTION].items():
1✔
376
            if (
1✔
377
                asset[EMISSION_FACTOR][VALUE] == 0
378
                and asset[OPTIMIZE_CAP][VALUE] == True
379
                and asset[MAXIMUM_CAP][VALUE] is None
380
            ):
381
                count += 1
1✔
382

383
        if count == 0:
1✔
384
            logging.warning(
1✔
385
                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."
386
            )
387

388

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

393
    This would affect the optimization if a maximum emissions contraint is used.
394
    Aditionally, it effects the KPIs connected to emissions.
395

396
    Parameters
397
    ----------
398
    dict_values : dict
399
        Contains all input data of the simulation.
400

401
    Returns
402
    -------
403
    Indirectly, logs a logging.warning message in case tthe grid has a renewable share
404
    of 100 % but an emission factor > 0.
405

406
    Notes
407
    -----
408
    Tested with:
409
    - C1.test_check_emission_factor_of_providers_no_warning_RE_share_lower_1()
410
    - C1.test_check_emission_factor_of_providers_no_warning_emission_factor_0()
411
    - C1.test_check_emission_factor_of_providers_warning()
412

413
    """
414
    for key, asset in dict_values[ENERGY_PROVIDERS].items():
1✔
415
        if isinstance(asset[EMISSION_FACTOR][VALUE], str):
1✔
UNCOV
416
            raise TypeError(
×
417
                f"The emission factor of the provider asset {asset[LABEL]} is a string, this is likely due to a missing value in the csv input file 'energyProviders.csv', please check your input file for missing values or typos"
418
            )
419
        if asset[EMISSION_FACTOR][VALUE] > 0 and asset[RENEWABLE_SHARE_DSO][VALUE] == 1:
1✔
420
            logging.warning(
1✔
421
                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."
422
            )
423

424

425
def check_time_series_values_between_0_and_1(time_series):
1✔
426
    r"""
427
    Checks whether all values of `time_series` in [0, 1].
428

429
    Parameters
430
    ----------
431
    time_series : pd.Series
432
        Time series to be checked.
433

434
    Returns
435
    -------
436
    bool
437
        True if values of `time_series` within [0, 1], else False.
438

439
    """
440
    boolean = time_series.between(0, 1)
1✔
441

442
    return bool(boolean.all())
1✔
443

444

445
def check_non_dispatchable_source_time_series(dict_values):
1✔
446
    r"""
447
    Raises error if time series of non-dispatchable sources are not between [0, 1].
448

449
    Parameters
450
    ----------
451
    dict_values : dict
452
        Contains all input data of the simulation.
453

454
    Returns
455
    -------
456
    Indirectly, raises error message in case of time series of non-dispatchable sources
457
    not between [0, 1].
458

459
    """
460
    # go through all non-dispatchable sources
461
    for key, source in dict_values[ENERGY_PRODUCTION].items():
1✔
462
        if TIMESERIES in source and source[DISPATCHABILITY] is False:
1✔
463
            # check if values between 0 and 1
464
            result = check_time_series_values_between_0_and_1(
1✔
465
                time_series=source[TIMESERIES]
466
            )
467
            if result is False:
1✔
468
                logging.error(
1✔
469
                    f"{TIMESERIES} of non-dispatchable source {source[LABEL]} contains values out of bounds [0, 1]."
470
                )
471
                return False
1✔
472

473

474
def check_efficiency_of_storage_capacity(dict_values):
1✔
475
    r"""
476
    Raises error or logs a warning to help users to spot major change in PR #676.
477

478
    In #676 the `efficiency` of `storage capacity' in `storage_*.csv` was defined as the
479
    storages' efficiency/ability to hold charge over time. Before it was defined as
480
    loss rate.
481
    This function raises an error if efficiency of 'storage capacity' of one of the
482
    storages is 0 and logs a warning if efficiency of 'storage capacity' of one of the
483
    storages is <0.2.
484

485
    Parameters
486
    ----------
487
    dict_values : dict
488
        Contains all input data of the simulation.
489

490
    Notes
491
    -----
492
    Tested with:
493
    - test_check_efficiency_of_storage_capacity_is_0
494
    - test_check_efficiency_of_storage_capacity_is_btw_0_and_02
495
    - test_check_efficiency_of_storage_capacity_is_greater_02
496

497
    Returns
498
    -------
499
    Indirectly, raises error message in case of efficiency of 'storage capacity' is 0
500
    and logs warning message in case of efficiency of 'storage capacity' is <0.2.
501

502
    """
503
    # go through all storages
504
    for key, item in dict_values[ENERGY_STORAGE].items():
1✔
505
        eff = item[STORAGE_CAPACITY][EFFICIENCY][VALUE]
1✔
506
        if eff == 0:
1✔
507
            raise ValueError(
1✔
508
                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."
509
            )
510
        elif eff < 0.2:
1✔
511
            logging.warning(
1✔
512
                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."
513
            )
514

515

516
def check_input_values(dict_values):
1✔
517
    """
518

519
    :param dict_values:
520
    :return:
521
    """
UNCOV
522
    for asset_name in dict_values:
×
UNCOV
523
        if not (isinstance(dict_values[asset_name], dict)):
×
524
            # checking first layer of dict_values
525
            all_valid_intervals(asset_name, dict_values[asset_name], "")
×
526
        else:
527
            # logging.debug('Asset %s checked for validation.', asset_name)
UNCOV
528
            for sub_asset_name in dict_values[asset_name]:
×
UNCOV
529
                if not (isinstance(dict_values[asset_name][sub_asset_name], dict)):
×
530
                    # checking second layer of dict values
UNCOV
531
                    all_valid_intervals(
×
532
                        sub_asset_name,
533
                        dict_values[asset_name][sub_asset_name],
534
                        asset_name,
535
                    )
536
                else:
537
                    # logging.debug('\t Sub-asset %s checked for validation.', sub_asset_name)
UNCOV
538
                    for sub_sub_asset_name in dict_values[asset_name][sub_asset_name]:
×
UNCOV
539
                        if not (
×
540
                            isinstance(
541
                                dict_values[asset_name][sub_asset_name][
542
                                    sub_sub_asset_name
543
                                ],
544
                                dict,
545
                            )
546
                        ):
547
                            # checking third layer of dict values
548
                            all_valid_intervals(
×
549
                                sub_sub_asset_name,
550
                                dict_values[asset_name][sub_asset_name][
551
                                    sub_sub_asset_name
552
                                ],
553
                                asset_name + sub_asset_name,
554
                            )
555
                        else:
556
                            # logging.debug('\t\t Sub-sub-asset %s checked for validation.', sub_sub_asset_name)
UNCOV
557
                            logging.critical(
×
558
                                "Verification Error! Add another layer to evaluation."
559
                            )
560

UNCOV
561
    logging.info(
×
562
        "Input values have been verified. This verification can not replace a manual input parameter check."
563
    )
564

565

566
def all_valid_intervals(name, value, title):
1✔
567
    """
568
    Checks whether `value` of `name` is valid.
569

570
    Checks include the expected type and the expected range a parameter is
571
    supposed to be inside.
572

573
    :param name:
574
    :param value:
575
    :param title:
576
    :return:
577
    """
UNCOV
578
    valid_type_string = [
×
579
        PROJECT_NAME,
580
        SCENARIO_NAME,
581
        COUNTRY,
582
        "parent",
583
        "type",
584
        FILENAME,
585
        LABEL,
586
        CURR,
587
        PATH_OUTPUT_FOLDER,
588
        DISPLAY_OUTPUT,
589
        PATH_INPUT_FILE,
590
        PATH_INPUT_FOLDER,
591
        "sector",
592
    ]
593

UNCOV
594
    valid_type_int = [EVALUATED_PERIOD, "time_step", PERIODS]
×
595

UNCOV
596
    valid_type_timestamp = [START_DATE]
×
597

UNCOV
598
    valid_type_index = ["index"]
×
599

UNCOV
600
    valid_binary = ["optimize_cap", DSM, OVERWRITE]
×
601

UNCOV
602
    valid_intervals = {
×
603
        LONGITUDE: [-180, 180],
604
        LATITUDE: [-90, 90],
605
        LIFETIME: ["largerzero", "any"],
606
        AGE_INSTALLED: [0, "any"],
607
        INSTALLED_CAP: [0, "any"],
608
        MAXIMUM_CAP: [0, "any", None],
609
        SOC_MIN: [0, 1],
610
        SOC_MAX: [0, 1],
611
        SOC_INITIAL: [0, 1],
612
        "crate": [0, 1],
613
        EFFICIENCY: [0, 1],
614
        "electricity_cost_fix_annual": [0, "any"],
615
        "electricity_price_var_kWh": [0, "any"],
616
        "electricity_price_var_kW_monthly": [0, "any"],
617
        FEEDIN_TARIFF: [0, "any"],
618
        DEVELOPMENT_COSTS: [0, "any"],
619
        SPECIFIC_COSTS: [0, "any"],
620
        SPECIFIC_COSTS_OM: [0, "any"],
621
        DISPATCH_PRICE: [0, "any"],
622
        DISCOUNTFACTOR: [0, 1],
623
        PROJECT_DURATION: ["largerzero", "any"],
624
        TAX: [0, 1],
625
        BETA: [0, 1],
626
    }
627

UNCOV
628
    if name in valid_type_int:
×
UNCOV
629
        if not (isinstance(value, int)):
×
UNCOV
630
            logging.error(
×
631
                'Input error! Value %s/%s is not in recommended format "integer".',
632
                name,
633
                title,
634
            )
635

UNCOV
636
    elif name in valid_type_string:
×
UNCOV
637
        if not (isinstance(value, str)):
×
UNCOV
638
            logging.error(
×
639
                'Input error! Value %s/%s is not in recommended format "string".',
640
                name,
641
                title,
642
            )
643

UNCOV
644
    elif name in valid_type_index:
×
UNCOV
645
        if not (isinstance(value, pd.DatetimeIndex)):
×
UNCOV
646
            logging.error(
×
647
                'Input error! Value %s/%s is not in recommended format "pd.DatetimeIndex".',
648
                name,
649
                title,
650
            )
651

UNCOV
652
    elif name in valid_type_timestamp:
×
653
        if not (isinstance(value, pd.Timestamp)):
×
654
            logging.error(
×
655
                'Input error! Value %s/%s is not in recommended format "pd.DatetimeIndex".',
656
                name,
657
                title,
658
            )
659

UNCOV
660
    elif name in valid_binary:
×
UNCOV
661
        if not (value is True or value is False):
×
UNCOV
662
            logging.error(
×
663
                "Input error! Value %s/%s is neither True nor False.", name, title
664
            )
665

666
    elif name in valid_intervals:
×
UNCOV
667
        if name == SOC_INITIAL:
×
UNCOV
668
            if value is not None:
×
UNCOV
669
                if not (0 <= value and value <= 1):
×
UNCOV
670
                    logging.error(
×
671
                        "Input error! Value %s/%s should be None, or between 0 and 1.",
672
                        name,
673
                        title,
674
                    )
675
        else:
676

UNCOV
677
            if valid_intervals[name][0] == "largerzero":
×
UNCOV
678
                if value <= 0:
×
UNCOV
679
                    logging.error(
×
680
                        "Input error! Value %s/%s can not be to be smaller or equal to 0.",
681
                        name,
682
                        title,
683
                    )
684
            elif valid_intervals[name][0] == "nonzero":
×
685
                if value == 0:
×
686
                    logging.error("Input error! Value %s/%s can not be 0.", name, title)
×
UNCOV
687
            elif valid_intervals[name][0] == 0:
×
UNCOV
688
                if value < 0:
×
UNCOV
689
                    logging.error(
×
690
                        "Input error! Value %s/%s has to be larger than or equal to 0.",
691
                        name,
692
                        title,
693
                    )
694

UNCOV
695
            if valid_intervals[name][1] == "any":
×
UNCOV
696
                pass
×
UNCOV
697
            elif valid_intervals[name][1] == 1:
×
UNCOV
698
                if 1 < value:
×
UNCOV
699
                    logging.error(
×
700
                        "Input error! Value %s/%s can not be larger than 1.",
701
                        name,
702
                        title,
703
                    )
704

705
    else:
UNCOV
706
        logging.warning(
×
707
            "VALIDATION FAILED: Code does not define a valid range for value %s/%s",
708
            name,
709
            title,
710
        )
711

712

713
def check_if_energy_vector_of_all_assets_is_valid(dict_values):
1✔
714
    """
715
    Validates for all assets, whether 'energyVector' is defined within DEFAULT_WEIGHTS_ENERGY_CARRIERS and within the energyBusses.
716

717
    Parameters
718
    ----------
719
    dict_values: dict
720
        All input data in dict format
721

722
    Notes
723
    -----
724
    Function tested with
725
    - test_add_economic_parameters()
726
    - test_check_if_energy_vector_of_all_assets_is_valid_fails
727
    - test_check_if_energy_vector_of_all_assets_is_valid_passes
728
    """
729
    for level1 in dict_values.keys():
1✔
730
        for level2 in dict_values[level1].keys():
1✔
731
            if (
1✔
732
                isinstance(dict_values[level1][level2], dict)
733
                and ENERGY_VECTOR in dict_values[level1][level2].keys()
734
            ):
735
                energy_vector_name = dict_values[level1][level2][ENERGY_VECTOR]
1✔
736
                if (
1✔
737
                    energy_vector_name
738
                    not in dict_values[PROJECT_DATA][LES_ENERGY_VECTOR_S]
739
                ):
740
                    raise ValueError(
1✔
741
                        f"Asset {level2} of asset group {level1} has an energy vector ({energy_vector_name}) that is not defined within the energyBusses. "
742
                        f"This prohibits proper processing of the assets dispatch."
743
                        f"Please check for typos or define another bus, as this hints at the energy system being faulty."
744
                    )
UNCOV
745
                    C1.check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS(
×
746
                        energy_vector_name, level1, level2
747
                    )
748

749

750
def check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS(
1✔
751
    energy_carrier, asset_group, asset
752
):
753
    r"""
754
    Raises an error message if an energy vector is unknown.
755

756
    It then needs to be added to the DEFAULT_WEIGHTS_ENERGY_CARRIERS in constants.py
757

758
    Parameters
759
    ----------
760
    energy_carrier: str
761
        Name of the energy carrier
762

763
    asset_group: str
764
        Name of the asset group
765

766
    asset: str
767
        Name of the asset
768

769
    Returns
770
    -------
771
    None
772

773
    Notes
774
    -----
775
    Tested with:
776
    - test_check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS_pass()
777
    - test_check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS_fails()
778
    """
779
    if energy_carrier not in DEFAULT_WEIGHTS_ENERGY_CARRIERS:
1✔
780
        raise UnknownEnergyVectorError(
1✔
781
            f"The energy carrier {energy_carrier} of asset group {asset_group}, asset {asset} is unknown, "
782
            f"as it is not defined within the DEFAULT_WEIGHTS_ENERGY_CARRIERS."
783
            f"Please check the energy carrier, or update the DEFAULT_WEIGHTS_ENERGY_CARRIERS in contants.py (dev)."
784
        )
785

786

787
def check_for_sufficient_assets_on_busses(dict_values):
1✔
788
    r"""
789
    Validation check for busses, to make sure a sufficient number of assets is connected.
790

791
    Each bus has to has to have 3 or more assets connected to it. The reasoning is that each bus needs:
792
    - One asset for inflow into the bus
793
    - One asset for outflow from the bus
794
    - One energy excess asset
795
    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.
796

797
    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.
798

799
    Parameters
800
    ----------
801
    dict_values: dict
802
        All simulation parameters
803

804
    Returns
805
    -------
806
    Logging error message if test fails
807

808
    Notes
809
    -----
810
    This function is tested with:
811
    - test_C1_verification.test_check_for_sufficient_assets_on_busses_example_bus_passes()
812
    - test_C1_verification.test_check_for_sufficient_assets_on_busses_example_bus_fails()
813
    - test_C1_verification.test_check_for_sufficient_assets_on_busses_skipped_for_peak_demand_pricing_bus()
814
    """
815
    for bus in dict_values[ENERGY_BUSSES]:
1✔
816
        if (
1✔
817
            len(dict_values[ENERGY_BUSSES][bus][ASSET_DICT]) < 3
818
            and DSO_PEAK_DEMAND_SUFFIX not in bus
819
            and DSO_FEEDIN_CAP not in bus
820
        ):
821
            asset_string = ", ".join(
1✔
822
                map(str, dict_values[ENERGY_BUSSES][bus][ASSET_DICT].keys())
823
            )
824
            logging.error(
1✔
825
                f"Energy system bus {bus} has too few assets connected to it. "
826
                f"The minimal number of assets that need to be connected "
827
                f"so that the bus is not a dead end should be two, excluding the excess sink. "
828
                f"These are the connected assets: {asset_string}"
829
            )
830

831
    return True
1✔
832

833

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

842
    Parameters
843
    ----------
844
    dict_values : dict
845
        Contains all input data of the simulation.
846

847
    Returns
848
    -------
849
    Indirectly, logs a logging.warning message if the installed and maximum capacities of
850
    conversion/generation/storage assets are less than the maximum demand, for each bus.
851

852
    Notes
853
    -----
854

855
    Tested with:
856
    - test_check_energy_system_can_fulfill_max_demand_sufficient_capacities()
857
    - test_check_energy_system_can_fulfill_max_demand_no_maximum_capacity()
858
    - test_check_energy_system_can_fulfill_max_demand_insufficient_capacities()
859
    - test_check_energy_system_can_fulfill_max_demand_with_storage()
860
    - test_check_energy_system_can_fulfill_max_demand_sufficient_dispatchable_production
861
    - test_check_energy_system_can_fulfill_max_demand_insufficient_dispatchable_production
862
    - test_check_energy_system_can_fulfill_max_demand_sufficient_non_dispatchable_production
863
    - test_check_energy_system_can_fulfill_max_demand_insufficient_non_dispatchable_production
864
    - test_check_energy_system_can_fulfill_max_demand_fails_mvs_runthrough
865

866
    """
867
    for bus in dict_values[ENERGY_BUSSES]:
1✔
868
        pass_check = False
1✔
869
        opt_cap_storage = False
1✔
870
        peak_demand = 0
1✔
871
        peak_generation = 0
1✔
872
        for item in dict_values[ENERGY_BUSSES][bus][ASSET_DICT]:
1✔
873
            # filters out excess energy sinks, leaving only actual demand profiles
874
            if item in dict_values[ENERGY_CONSUMPTION]:
1✔
875
                if (
1✔
876
                    dict_values[ENERGY_CONSUMPTION][item][DISPATCHABILITY][VALUE]
877
                    is False
878
                ):
879
                    peak_demand += dict_values[ENERGY_CONSUMPTION][item][
1✔
880
                        TIMESERIES_PEAK
881
                    ][VALUE]
882
            # Only add capacity of conversion assets that can contribute to supply
883
            if (
1✔
884
                item in dict_values[ENERGY_CONVERSION]
885
                and dict_values[ENERGY_CONVERSION][item][OUTFLOW_DIRECTION] == bus
886
            ):
887
                peak_generation += dict_values[ENERGY_CONVERSION][item][INSTALLED_CAP][
1✔
888
                    VALUE
889
                ]
890
                if dict_values[ENERGY_CONVERSION][item][OPTIMIZE_CAP][VALUE] is True:
1✔
891
                    if dict_values[ENERGY_CONVERSION][item][MAXIMUM_CAP][VALUE] is None:
1✔
892
                        pass_check = True
1✔
893
                    else:
894
                        peak_generation += dict_values[ENERGY_CONVERSION][item][
1✔
895
                            MAXIMUM_CAP
896
                        ][VALUE]
897

898
            # Add potential generation of energy production assets
899
            if item in dict_values[ENERGY_PRODUCTION]:
1✔
900
                # Effective generation of asset dependent on the peak of timeseries
901
                if TIMESERIES_PEAK in dict_values[ENERGY_PRODUCTION][item]:
1✔
902
                    # This is the case for all non-dispatchable assets
903
                    factor = dict_values[ENERGY_PRODUCTION][item][TIMESERIES_PEAK][
1✔
904
                        VALUE
905
                    ]
906
                else:
907
                    # This is the case for all dispatchable assets, ie. fuel sources defined in `energyProduction.csv`
908
                    factor = 1
1✔
909
                peak_generation += (
1✔
910
                    dict_values[ENERGY_PRODUCTION][item][INSTALLED_CAP][VALUE] * factor
911
                )
912
                if dict_values[ENERGY_PRODUCTION][item][OPTIMIZE_CAP][VALUE] is True:
1✔
913
                    if dict_values[ENERGY_PRODUCTION][item][MAXIMUM_CAP][VALUE] is None:
1✔
914
                        pass_check = True
1✔
915
                    else:
916
                        # Effective generation of asset dependent on the peak of timeseries
917
                        peak_generation += (
1✔
918
                            dict_values[ENERGY_PRODUCTION][item][MAXIMUM_CAP][VALUE]
919
                            * factor
920
                        )
921
            if item in dict_values[ENERGY_STORAGE]:
1✔
922
                peak_generation += dict_values[ENERGY_STORAGE][item][OUTPUT_POWER][
1✔
923
                    INSTALLED_CAP
924
                ][VALUE]
925
                if dict_values[ENERGY_STORAGE][item][OPTIMIZE_CAP][VALUE] is True:
1✔
926
                    # unlike the conversion/production assets, no maximum capacities are defined for
927
                    # storage assets and therefore as soon as a storage asset is connected, the check
928
                    # should pass
929
                    pass_check = True
1✔
930
                    opt_cap_storage = True
1✔
931
        if peak_generation < peak_demand and pass_check is False:
1✔
932
            logging.warning(
1✔
933
                f"The assets of {bus} might have insufficient capacities to fulfill"
934
                f" the maximum demand ({round(peak_generation)}<{round(peak_demand)})."
935
            )
936
        elif opt_cap_storage is True:
1✔
937
            logging.debug(
1✔
938
                f"The check for assets having sufficient capacities to fulfill the"
939
                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."
940
                f" However, this check does not determine if the storage can be sufficiently"
941
                f" charged by the other assets in the energy system to ensure a viable supply for each timestep."
942
            )
943
        else:
944
            logging.debug(
1✔
945
                f"The check for assets having sufficient capacities to fulfill the"
946
                f" maximum demand has successfully passed for bus {bus} (peak generation: {peak_generation}, peak demand: {peak_demand}, unlimited capacities in optimization: {pass_check})."
947
            )
948

949
        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

© 2026 Coveralls, Inc