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

rl-institut / multi-vector-simulator / 4084410217

pending completion
4084410217

push

github

pierre-francois.duc
Fix failing test

5928 of 7724 relevant lines covered (76.75%)

0.77 hits per line

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

65.49
/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
                            ):
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:
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
                        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
                            ):
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 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:
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
        if isinstance(feedin_tariff[VALUE], pd.Series):
1✔
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✔
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✔
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
332
            if any(boolean) is True:
×
333
                instances = sum(boolean)  # Count instances
×
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."
×
335
                if DSO_FEEDIN_CAP in dict_values[ENERGY_PROVIDERS][provider]:
×
336
                    logging.warning(msg)
×
337
                else:
338
                    raise ValueError(msg)
×
339
            else:
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 asset[EMISSION_FACTOR][VALUE] > 0 and asset[RENEWABLE_SHARE_DSO][VALUE] == 1:
1✔
416
            logging.warning(
1✔
417
                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."
418
            )
419

420

421
def check_time_series_values_between_0_and_1(time_series):
1✔
422
    r"""
423
    Checks whether all values of `time_series` in [0, 1].
424

425
    Parameters
426
    ----------
427
    time_series : pd.Series
428
        Time series to be checked.
429

430
    Returns
431
    -------
432
    bool
433
        True if values of `time_series` within [0, 1], else False.
434

435
    """
436
    boolean = time_series.between(0, 1)
1✔
437

438
    return bool(boolean.all())
1✔
439

440

441
def check_non_dispatchable_source_time_series(dict_values):
1✔
442
    r"""
443
    Raises error if time series of non-dispatchable sources are not between [0, 1].
444

445
    Parameters
446
    ----------
447
    dict_values : dict
448
        Contains all input data of the simulation.
449

450
    Returns
451
    -------
452
    Indirectly, raises error message in case of time series of non-dispatchable sources
453
    not between [0, 1].
454

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

469

470
def check_efficiency_of_storage_capacity(dict_values):
1✔
471
    r"""
472
    Raises error or logs a warning to help users to spot major change in PR #676.
473

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

481
    Parameters
482
    ----------
483
    dict_values : dict
484
        Contains all input data of the simulation.
485

486
    Notes
487
    -----
488
    Tested with:
489
    - test_check_efficiency_of_storage_capacity_is_0
490
    - test_check_efficiency_of_storage_capacity_is_btw_0_and_02
491
    - test_check_efficiency_of_storage_capacity_is_greater_02
492

493
    Returns
494
    -------
495
    Indirectly, raises error message in case of efficiency of 'storage capacity' is 0
496
    and logs warning message in case of efficiency of 'storage capacity' is <0.2.
497

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

511

512
def check_input_values(dict_values):
1✔
513
    """
514

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

557
    logging.info(
×
558
        "Input values have been verified. This verification can not replace a manual input parameter check."
559
    )
560

561

562
def all_valid_intervals(name, value, title):
1✔
563
    """
564
    Checks whether `value` of `name` is valid.
565

566
    Checks include the expected type and the expected range a parameter is
567
    supposed to be inside.
568

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

590
    valid_type_int = [EVALUATED_PERIOD, "time_step", PERIODS]
×
591

592
    valid_type_timestamp = [START_DATE]
×
593

594
    valid_type_index = ["index"]
×
595

596
    valid_binary = ["optimize_cap", DSM, OVERWRITE]
×
597

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

624
    if name in valid_type_int:
×
625
        if not (isinstance(value, int)):
×
626
            logging.error(
×
627
                'Input error! Value %s/%s is not in recommended format "integer".',
628
                name,
629
                title,
630
            )
631

632
    elif name in valid_type_string:
×
633
        if not (isinstance(value, str)):
×
634
            logging.error(
×
635
                'Input error! Value %s/%s is not in recommended format "string".',
636
                name,
637
                title,
638
            )
639

640
    elif name in valid_type_index:
×
641
        if not (isinstance(value, pd.DatetimeIndex)):
×
642
            logging.error(
×
643
                'Input error! Value %s/%s is not in recommended format "pd.DatetimeIndex".',
644
                name,
645
                title,
646
            )
647

648
    elif name in valid_type_timestamp:
×
649
        if not (isinstance(value, pd.Timestamp)):
×
650
            logging.error(
×
651
                'Input error! Value %s/%s is not in recommended format "pd.DatetimeIndex".',
652
                name,
653
                title,
654
            )
655

656
    elif name in valid_binary:
×
657
        if not (value is True or value is False):
×
658
            logging.error(
×
659
                "Input error! Value %s/%s is neither True nor False.", name, title
660
            )
661

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

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

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

701
    else:
702
        logging.warning(
×
703
            "VALIDATION FAILED: Code does not define a valid range for value %s/%s",
704
            name,
705
            title,
706
        )
707

708

709
def check_if_energy_vector_of_all_assets_is_valid(dict_values):
1✔
710
    """
711
    Validates for all assets, whether 'energyVector' is defined within DEFAULT_WEIGHTS_ENERGY_CARRIERS and within the energyBusses.
712

713
    Parameters
714
    ----------
715
    dict_values: dict
716
        All input data in dict format
717

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

745

746
def check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS(
1✔
747
    energy_carrier, asset_group, asset
748
):
749
    r"""
750
    Raises an error message if an energy vector is unknown.
751

752
    It then needs to be added to the DEFAULT_WEIGHTS_ENERGY_CARRIERS in constants.py
753

754
    Parameters
755
    ----------
756
    energy_carrier: str
757
        Name of the energy carrier
758

759
    asset_group: str
760
        Name of the asset group
761

762
    asset: str
763
        Name of the asset
764

765
    Returns
766
    -------
767
    None
768

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

782

783
def check_for_sufficient_assets_on_busses(dict_values):
1✔
784
    r"""
785
    Validation check for busses, to make sure a sufficient number of assets is connected.
786

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

793
    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.
794

795
    Parameters
796
    ----------
797
    dict_values: dict
798
        All simulation parameters
799

800
    Returns
801
    -------
802
    Logging error message if test fails
803

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

827
    return True
1✔
828

829

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

838
    Parameters
839
    ----------
840
    dict_values : dict
841
        Contains all input data of the simulation.
842

843
    Returns
844
    -------
845
    Indirectly, logs a logging.warning message if the installed and maximum capacities of
846
    conversion/generation/storage assets are less than the maximum demand, for each bus.
847

848
    Notes
849
    -----
850

851
    Tested with:
852
    - test_check_energy_system_can_fulfill_max_demand_sufficient_capacities()
853
    - test_check_energy_system_can_fulfill_max_demand_no_maximum_capacity()
854
    - test_check_energy_system_can_fulfill_max_demand_insufficient_capacities()
855
    - test_check_energy_system_can_fulfill_max_demand_with_storage()
856
    - test_check_energy_system_can_fulfill_max_demand_sufficient_dispatchable_production
857
    - test_check_energy_system_can_fulfill_max_demand_insufficient_dispatchable_production
858
    - test_check_energy_system_can_fulfill_max_demand_sufficient_non_dispatchable_production
859
    - test_check_energy_system_can_fulfill_max_demand_insufficient_non_dispatchable_production
860
    - test_check_energy_system_can_fulfill_max_demand_fails_mvs_runthrough
861

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

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

945
        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