• 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

80.97
/src/multi_vector_simulator/C0_data_processing.py
1
"""
2
Module C0 - Data processing
3
===========================
4

5
Module C0 prepares the data read from csv or json for simulation, ie. pre-processes it.
6
- Verify input values with C1
7
- Identify energyVectors and write them to project_data/sectors
8
- Create an excess sink for each bus
9
- Process start_date/simulation_duration to pd.datatimeindex (future: Also consider timesteplenghts)
10
- Add economic parameters to json with C2
11
- Calculate "simulation annuity" used in oemof model
12
- Add demand sinks to energyVectors (this should actually be changed and demand sinks should be added to bus relative to input_direction, also see issue #179)
13
- Translate input_directions/output_directions to bus names
14
- Add missing cost data to automatically generated objects (eg. DSO transformers)
15
- Read timeseries of assets and store into json (differ between one-column csv, multi-column csv)
16
- Read timeseries for parameter of an asset, eg. efficiency
17
- Parse list of inputs/outputs, eg. for chp
18
- Define dso sinks, sources, transformer stations (this will be changed due to bug #119), also for peak demand pricing
19
- Add a source if a conversion object is connected to a new input_direction (bug #186)
20
- Define all necessary energyBusses and add all assets that are connected to them specifically with asset name and label
21
- Multiply `maximumCap` of non-dispatchable sources by max(timeseries(kWh/kWp)) as the `maximumCap` is limiting the flow but we want to limit the installed capacity (see issue #446)
22
"""
23

24
import logging
1✔
25
import os
1✔
26
import sys
1✔
27
import pprint as pp
1✔
28
import pandas as pd
1✔
29
import warnings
1✔
30
from multi_vector_simulator.version import version_num
1✔
31

32
from multi_vector_simulator.utils.constants import (
1✔
33
    TIME_SERIES,
34
    PATH_INPUT_FOLDER,
35
    PATH_OUTPUT_FOLDER,
36
    TYPE_BOOL,
37
    FILENAME,
38
    HEADER,
39
    JSON_PROCESSED,
40
)
41

42
from multi_vector_simulator.utils.exceptions import MaximumCapValueInvalid
1✔
43

44
from multi_vector_simulator.utils.constants_json_strings import *
1✔
45
from multi_vector_simulator.utils.helpers import (
1✔
46
    peak_demand_bus_name,
47
    peak_demand_transformer_name,
48
)
49
from multi_vector_simulator.utils.exceptions import InvalidPeakDemandPricingPeriodsError
1✔
50
import multi_vector_simulator.B0_data_input_json as B0
1✔
51
import multi_vector_simulator.C1_verification as C1
1✔
52
import multi_vector_simulator.C2_economic_functions as C2
1✔
53

54

55
def all(dict_values):
1✔
56
    """
57
    Function executing all pre-processing steps necessary
58
    :param dict_values
59
    All input data in dict format
60

61
    :return Pre-processed dictionary with all input parameters
62

63
    """
64
    # Check if any asset label has duplicates
65
    C1.check_for_label_duplicates(dict_values)
1✔
66
    add_version_number_used(dict_values[SIMULATION_SETTINGS])
1✔
67
    B0.retrieve_date_time_info(dict_values[SIMULATION_SETTINGS])
1✔
68
    add_economic_parameters(dict_values[ECONOMIC_DATA])
1✔
69
    define_energy_vectors_from_busses(dict_values)
1✔
70
    C1.check_if_energy_vector_of_all_assets_is_valid(dict_values)
1✔
71

72
    ## Verify inputs
73
    # todo check whether input values can be true
74
    # C1.check_input_values(dict_values)
75
    # todo Check, whether files (demand, generation) are existing
76

77
    # Adds costs to each asset and sub-asset, adds time series to assets
78
    process_all_assets(dict_values)
1✔
79

80
    # check electricity price >= feed-in tariff todo: can be integrated into check_input_values() later
81
    C1.check_feedin_tariff_vs_energy_price(dict_values=dict_values)
1✔
82
    # check that energy supply costs are not lower than generation costs of any asset (of the same energy vector)
83
    C1.check_feedin_tariff_vs_levelized_cost_of_generation_of_production(dict_values)
1✔
84

85
    # check time series of non-dispatchable sources in range [0, 1]
86
    C1.check_non_dispatchable_source_time_series(dict_values)
1✔
87

88
    # display warning in case of maximum emissions constraint and no asset with zero emissions has no capacity limit
89
    C1.check_feasibility_of_maximum_emissions_constraint(dict_values)
1✔
90

91
    # display warning in case of emission_factor of provider > 0 while RE share = 100 %
92
    C1.check_emission_factor_of_providers(dict_values)
1✔
93

94
    # check efficiencies of storage capacity, raise error in case it is 0 and add a
95
    # warning in case it is <0.2 to help users to spot major change in #676
96
    C1.check_efficiency_of_storage_capacity(dict_values)
1✔
97

98
    # Perform basic (limited) check for module completeness
99
    C1.check_for_sufficient_assets_on_busses(dict_values)
1✔
100

101
    # just to be safe, run evaluation a second time
102
    C1.check_for_label_duplicates(dict_values)
1✔
103

104
    # check installed and maximum capacity of all conversion, generation and storage assets
105
    # connected to one bus is smaller than the maximum demand
106
    C1.check_energy_system_can_fulfill_max_demand(dict_values)
1✔
107

108

109
def add_version_number_used(simulation_settings):
1✔
110
    r"""
111
    Add version number to simulation settings
112

113
    Parameters
114
    ----------
115
    simulation_settings: dict
116
        Dict of simulation settings
117

118
    Returns
119
    -------
120
    Updated dict simulation_settings with `VERSION_NUM` equal to local version number.
121
    This version number will be added to the json output files.
122
    The automatic report generated in `F0` references the version number and date on its own accord.
123
    """
124
    simulation_settings.update({VERSION_NUM: version_num})
1✔
125

126

127
def define_energy_vectors_from_busses(dict_values):
1✔
128
    """
129
    Identifies all energyVectors used in the energy system by looking at the defined energyBusses.
130
    The EnergyVectors later will be used to distribute costs and KPI amongst the sectors
131

132
    Parameters
133
    ----------
134
    dict_values: dict
135
        All input data in dict format
136

137
    Returns
138
    -------
139
    Update dict[PROJECT_DATA] by included energyVectors (LES_ENERGY_VECTOR_S)
140

141
    Notes
142
    -----
143
    Function tested with
144
    -  C1.test_define_energy_vectors_from_busses
145
    """
146
    dict_of_energy_vectors = {}
1✔
147
    energy_vector_string = ""
1✔
148
    for bus in dict_values[ENERGY_BUSSES]:
1✔
149
        energy_vector_name = dict_values[ENERGY_BUSSES][bus][ENERGY_VECTOR]
1✔
150
        if energy_vector_name not in dict_of_energy_vectors.keys():
1✔
151
            C1.check_if_energy_vector_is_defined_in_DEFAULT_WEIGHTS_ENERGY_CARRIERS(
1✔
152
                energy_vector_name, ENERGY_BUSSES, bus
153
            )
154
            dict_of_energy_vectors.update(
1✔
155
                {energy_vector_name: energy_vector_name.replace("_", " ")}
156
            )
157
            energy_vector_string = energy_vector_string + energy_vector_name + ", "
1✔
158

159
    dict_values[PROJECT_DATA].update({LES_ENERGY_VECTOR_S: dict_of_energy_vectors})
1✔
160
    logging.info(
1✔
161
        f"The energy system modelled includes following energy vectors: {energy_vector_string[:-2]}",
162
    )
163

164

165
def add_economic_parameters(economic_parameters):
1✔
166
    """
167
    Update economic parameters with annuity factor and CRF
168

169
    Parameters
170
    ----------
171

172
    economic_parameters: dict
173
        Economic parameters of the simulation
174

175
    Returns
176
    -------
177

178
    Updated economic parameters
179

180
    Notes
181
    -----
182
    Function tested with test_add_economic_parameters()
183
    """
184

185
    economic_parameters.update(
1✔
186
        {
187
            ANNUITY_FACTOR: {
188
                VALUE: C2.annuity_factor(
189
                    economic_parameters[PROJECT_DURATION][VALUE],
190
                    economic_parameters[DISCOUNTFACTOR][VALUE],
191
                ),
192
                UNIT: "?",
193
            }
194
        }
195
    )
196
    # Calculate crf
197
    economic_parameters.update(
1✔
198
        {
199
            CRF: {
200
                VALUE: C2.crf(
201
                    economic_parameters[PROJECT_DURATION][VALUE],
202
                    economic_parameters[DISCOUNTFACTOR][VALUE],
203
                ),
204
                UNIT: "?",
205
            }
206
        }
207
    )
208

209

210
def process_all_assets(dict_values):
1✔
211
    """defines dict_values['energyBusses'] for later reference
212

213
    Processes all assets of the energy system by evaluating them, performing economic pre-calculations and validity checks.
214

215
    Parameters
216
    ----------
217

218
    dict_values: dict
219
        All simulation inputs
220

221
    Returns
222
    -------
223

224
    dict_values: dict
225
        Updated dict_values with pre-processes assets, including economic parameters, busses and auxiliary assets like excess sinks and all assets connected to the energyProviders.
226

227
    Notes
228
    -----
229

230
    Tested with:
231
    - test_C0_data_processing.test_process_all_assets_fixcost()
232
    """
233

234
    # Define all busses based on the in- and outflow directions of the assets in the input data
235
    add_assets_to_asset_dict_of_connected_busses(dict_values)
1✔
236
    # Define all excess sinks for each energy bus
237
    auto_sinks = define_excess_sinks(dict_values)
1✔
238

239
    # Needed for E3.total_demand_each_sector(), but location is not perfect as it is more about the model then the settings.
240
    # Decided against implementing a new major 1st level category in json to avoid an excessive datatree.
241
    dict_values[SIMULATION_SETTINGS].update({EXCESS_SINK: auto_sinks})
1✔
242

243
    # process all energyAssets:
244
    # Attention! Order of asset_groups important. for energyProviders/energyConversion sinks and sources
245
    # might be defined that have to be processed in energyProduction/energyConsumption
246

247
    # The values of the keys are functions!
248
    asset_group_list = {
1✔
249
        ENERGY_PROVIDERS: energyProviders,
250
        ENERGY_CONVERSION: energyConversion,
251
        ENERGY_STORAGE: energyStorage,
252
        ENERGY_PRODUCTION: energyProduction,
253
        ENERGY_CONSUMPTION: energyConsumption,
254
    }
255

256
    logging.debug("Pre-process fix project costs")
1✔
257
    for asset in dict_values[FIX_COST]:
1✔
258
        evaluate_lifetime_costs(
1✔
259
            dict_values[SIMULATION_SETTINGS],
260
            dict_values[ECONOMIC_DATA],
261
            dict_values[FIX_COST][asset],
262
        )
263

264
    for asset_group, asset_function in asset_group_list.items():
1✔
265
        logging.info("Pre-processing all assets in asset group %s.", asset_group)
1✔
266
        # call asset function connected to current asset group (see asset_group_list)
267
        asset_function(dict_values, asset_group)
1✔
268

269
        logging.debug(
1✔
270
            "Finished pre-processing all assets in asset group %s.", asset_group
271
        )
272

273
    logging.info("Processed cost data and added economic values.")
1✔
274

275

276
def define_excess_sinks(dict_values):
1✔
277
    r"""
278
    Define energy excess sinks for each bus
279

280
    Parameters
281
    ----------
282
    dict_values: dict
283
        All simulation parameters
284

285
    Returns
286
    -------
287
    Updates dict_values
288
    """
289
    auto_sinks = []
1✔
290
    for bus in dict_values[ENERGY_BUSSES]:
1✔
291
        excess_sink_name = bus + EXCESS_SINK
1✔
292
        energy_vector = dict_values[ENERGY_BUSSES][bus][ENERGY_VECTOR]
1✔
293
        # TODO make this official if needed
294
        excess_price = dict_values[ENERGY_BUSSES][bus].get("price", 0)
1✔
295
        define_sink(
1✔
296
            dict_values=dict_values,
297
            asset_key=excess_sink_name,
298
            price={VALUE: excess_price, UNIT: CURR + "/" + UNIT},
299
            inflow_direction=bus,
300
            energy_vector=energy_vector,
301
            asset_type="excess",
302
        )
303
        dict_values[ENERGY_BUSSES][bus].update({EXCESS_SINK: excess_sink_name})
1✔
304
        auto_sinks.append(excess_sink_name)
1✔
305
        logging.debug(
1✔
306
            f"Created excess sink for energy bus {bus}, connected to {ENERGY_VECTOR} {energy_vector}."
307
        )
308
    return auto_sinks
1✔
309

310

311
def energyConversion(dict_values, group):
1✔
312
    """Add lifetime capex (incl. replacement costs), calculate annuity (incl. om), and simulation annuity to each asset
313

314
    :param dict_values:
315
    :param group:
316
    :return:
317
    """
318
    #
319
    for asset in dict_values[group]:
1✔
320
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
321
        evaluate_lifetime_costs(
1✔
322
            dict_values[SIMULATION_SETTINGS],
323
            dict_values[ECONOMIC_DATA],
324
            dict_values[group][asset],
325
        )
326
        # check if maximumCap exists and add it to dict_values
327
        process_maximum_cap_constraint(
1✔
328
            dict_values=dict_values, group=group, asset=asset
329
        )
330

331
        # in case there is only one parameter provided (input bus and one output bus)
332
        if (
1✔
333
            FILENAME in dict_values[group][asset][EFFICIENCY]
334
            and HEADER in dict_values[group][asset][EFFICIENCY]
335
        ):
UNCOV
336
            receive_timeseries_from_csv(
×
337
                dict_values[SIMULATION_SETTINGS],
338
                dict_values[group][asset],
339
                EFFICIENCY,
340
            )
341
        # in case there is more than one parameter provided (either (A) n input busses and 1 output bus or (B) 1 input bus and n output busses)
342
        # dictionaries with filenames and headers will be replaced by timeseries, scalars will be mantained
343
        elif isinstance(dict_values[group][asset][EFFICIENCY][VALUE], list):
1✔
UNCOV
344
            treat_multiple_flows(dict_values[group][asset], dict_values, EFFICIENCY)
×
345

346
            # same distinction of values provided with dictionaries (one input and one output) or list (multiple).
347
            # They can at turn be scalars, mantained, or timeseries
UNCOV
348
            logging.debug(
×
349
                "Asset %s has multiple input/output busses with a list of efficiencies. Reading list",
350
                dict_values[group][asset][LABEL],
351
            )
352
        else:
353
            logging.debug(f"Not loading {group} asset {asset} from file")
1✔
354
            compute_timeseries_properties(dict_values[group][asset])
1✔
355

356

357
def energyProduction(dict_values, group):
1✔
358
    """
359

360
    :param dict_values:
361
    :param group:
362
    :return:
363
    """
364
    for asset in dict_values[group]:
1✔
365
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
366
        evaluate_lifetime_costs(
1✔
367
            dict_values[SIMULATION_SETTINGS],
368
            dict_values[ECONOMIC_DATA],
369
            dict_values[group][asset],
370
        )
371

372
        if FILENAME in dict_values[group][asset]:
1✔
373
            if dict_values[group][asset][FILENAME] in ("None", None):
1✔
374
                dict_values[group][asset].update({DISPATCHABILITY: True})
1✔
375
            else:
376
                receive_timeseries_from_csv(
1✔
377
                    dict_values[SIMULATION_SETTINGS],
378
                    dict_values[group][asset],
379
                    "input",
380
                )
381
                # If Filename defines the generation timeseries, then we have an asset with a lack of dispatchability
382
                dict_values[group][asset].update({DISPATCHABILITY: False})
1✔
383
                process_normalized_installed_cap(dict_values, group, asset)
1✔
384
        else:
385
            logging.debug(
1✔
386
                f"Not loading {group} asset {asset} from a file, timeseries is provided"
387
            )
388
            compute_timeseries_properties(dict_values[group][asset])
1✔
389
        # check if maximumCap exists and add it to dict_values
390
        process_maximum_cap_constraint(dict_values, group, asset)
1✔
391

392

393
def energyStorage(dict_values, group):
1✔
394
    """
395

396
    :param dict_values:
397
    :param group:
398
    :return:
399
    """
400
    for asset in dict_values[group]:
1✔
401
        for subasset in [STORAGE_CAPACITY, INPUT_POWER, OUTPUT_POWER]:
1✔
402
            define_missing_cost_data(
1✔
403
                dict_values,
404
                dict_values[group][asset][subasset],
405
            )
406
            evaluate_lifetime_costs(
1✔
407
                dict_values[SIMULATION_SETTINGS],
408
                dict_values[ECONOMIC_DATA],
409
                dict_values[group][asset][subasset],
410
            )
411

412
            # check if parameters are provided as timeseries
413
            for parameter in [
1✔
414
                EFFICIENCY,
415
                SOC_MIN,
416
                SOC_MAX,
417
                THERM_LOSSES_REL,
418
                THERM_LOSSES_ABS,
419
            ]:
420
                if parameter in dict_values[group][asset][subasset] and (
1✔
421
                    FILENAME in dict_values[group][asset][subasset][parameter]
422
                    and HEADER in dict_values[group][asset][subasset][parameter]
423
                ):
UNCOV
424
                    receive_timeseries_from_csv(
×
425
                        dict_values[SIMULATION_SETTINGS],
426
                        dict_values[group][asset][subasset],
427
                        parameter,
428
                    )
429
                elif parameter in dict_values[group][asset][subasset] and isinstance(
1✔
430
                    dict_values[group][asset][subasset][parameter][VALUE], list
431
                ):
UNCOV
432
                    treat_multiple_flows(
×
433
                        dict_values[group][asset][subasset], dict_values, parameter
434
                    )
435
            # check if maximumCap exists and add it to dict_values
436
            process_maximum_cap_constraint(dict_values, group, asset, subasset)
1✔
437

438

439
def energyProviders(dict_values, group):
1✔
440
    """
441

442
    :param dict_values:
443
    :param group:
444
    :return:
445
    """
446
    # add sources and sinks depending on items in energy providers as pre-processing
447
    for asset in dict_values[group]:
1✔
448
        define_auxiliary_assets_of_energy_providers(dict_values, dso_name=asset)
1✔
449

450
        # Add lifetime capex (incl. replacement costs), calculate annuity
451
        # (incl. om), and simulation annuity to each asset
452
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
453
        evaluate_lifetime_costs(
1✔
454
            dict_values[SIMULATION_SETTINGS],
455
            dict_values[ECONOMIC_DATA],
456
            dict_values[group][asset],
457
        )
458

459

460
def energyConsumption(dict_values, group):
1✔
461
    """
462

463
    :param dict_values:
464
    :param group:
465
    :return:
466
    """
467
    for asset in dict_values[group]:
1✔
468
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
469
        evaluate_lifetime_costs(
1✔
470
            dict_values[SIMULATION_SETTINGS],
471
            dict_values[ECONOMIC_DATA],
472
            dict_values[group][asset],
473
        )
474
        if INFLOW_DIRECTION not in dict_values[group][asset]:
1✔
UNCOV
475
            dict_values[group][asset].update(
×
476
                {INFLOW_DIRECTION: dict_values[group][asset][ENERGY_VECTOR]}
477
            )
478

479
        if FILENAME in dict_values[group][asset]:
1✔
480
            dict_values[group][asset].update(
1✔
481
                {DISPATCHABILITY: {VALUE: False, UNIT: TYPE_BOOL}}
482
            )
483
            receive_timeseries_from_csv(
1✔
484
                dict_values[SIMULATION_SETTINGS],
485
                dict_values[group][asset],
486
                "input",
487
                is_demand_profile=True,
488
            )
489
        else:
490
            logging.debug(
1✔
491
                f"Not loading {group} asset {asset} from a file, timeseries is provided"
492
            )
493
            compute_timeseries_properties(dict_values[group][asset])
1✔
494

495

496
def define_missing_cost_data(dict_values, dict_asset):
1✔
497
    """
498

499
    :param dict_values:
500
    :param dict_asset:
501
    :return:
502
    """
503

504
    # read timeseries with filename provided for variable costs.
505
    # if multiple dispatch_price are given for multiple busses, it checks if any v
506
    # alue is a timeseries
507
    if DISPATCH_PRICE in dict_asset:
1✔
508
        if isinstance(dict_asset[DISPATCH_PRICE][VALUE], dict):
1✔
UNCOV
509
            receive_timeseries_from_csv(
×
510
                dict_values[SIMULATION_SETTINGS],
511
                dict_asset,
512
                DISPATCH_PRICE,
513
            )
514
        elif isinstance(dict_asset[DISPATCH_PRICE][VALUE], list):
1✔
UNCOV
515
            treat_multiple_flows(dict_asset, dict_values, DISPATCH_PRICE)
×
516

517
    economic_data = dict_values[ECONOMIC_DATA]
1✔
518

519
    basic_costs = {
1✔
520
        OPTIMIZE_CAP: {VALUE: False, UNIT: TYPE_BOOL},
521
        UNIT: "?",
522
        INSTALLED_CAP: {VALUE: 0.0, UNIT: UNIT},
523
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
524
        SPECIFIC_COSTS: {VALUE: 0, UNIT: CURR + "/" + UNIT},
525
        SPECIFIC_COSTS_OM: {VALUE: 0, UNIT: CURR + "/" + UNIT_YEAR},
526
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + UNIT + "/" + UNIT_YEAR},
527
        LIFETIME: {
528
            VALUE: economic_data[PROJECT_DURATION][VALUE],
529
            UNIT: UNIT_YEAR,
530
        },
531
        AGE_INSTALLED: {
532
            VALUE: 0,
533
            UNIT: UNIT_YEAR,
534
        },
535
    }
536

537
    # checks that an asset has all cost parameters needed for evaluation.
538
    # Adds standard values.
539
    str = ""
1✔
540
    for cost in basic_costs:
1✔
541
        if cost not in dict_asset:
1✔
542
            dict_asset.update({cost: basic_costs[cost]})
1✔
543
            str = str + " " + cost
1✔
544

545
    if len(str) > 1:
1✔
546
        logging.debug("Added basic costs to asset %s: %s", dict_asset[LABEL], str)
1✔
547

548

549
def add_assets_to_asset_dict_of_connected_busses(dict_values):
1✔
550
    """
551
    This function adds the assets of the different asset groups to the asset dict of ENERGY_BUSSES.
552
    The asset groups are: ENERGY_CONVERSION, ENERGY_PRODUCTION, ENERGY_CONSUMPTION, ENERGY_PROVIDERS, ENERGY_STORAGE
553

554
    Parameters
555
    ----------
556
    dict_values: dict
557
        Dictionary with all simulation information
558

559
    Returns
560
    -------
561
    Extends dict_values[ENERGY_BUSSES] by an asset_dict that includes all connected assets.
562

563
    Notes
564
    -----
565
    Tested with:
566
    - C0.test_add_assets_to_asset_dict_of_connected_busses()
567
    """
568
    for group in [
1✔
569
        ENERGY_CONVERSION,
570
        ENERGY_PRODUCTION,
571
        ENERGY_CONSUMPTION,
572
        ENERGY_PROVIDERS,
573
        ENERGY_STORAGE,
574
    ]:
575
        for asset in dict_values[group]:
1✔
576
            add_asset_to_asset_dict_for_each_flow_direction(
1✔
577
                dict_values, dict_values[group][asset], asset
578
            )
579

580

581
def add_asset_to_asset_dict_for_each_flow_direction(dict_values, dict_asset, asset_key):
1✔
582
    """
583
    Add asset to the asset dict of the busses connected to the INPUT_DIRECTION and OUTPUT_DIRECTION of the asset.
584

585
    Parameters
586
    ----------
587
    dict_values: dict
588
        All simulation information
589

590
    dict_asset: dict
591
        All information of the current asset
592

593
    asset_key: str
594
        Key that calls the dict_asset from dict_values[asset_group][key]
595

596
    Returns
597
    -------
598
    Updated dict_values, with dict_values[ENERGY_BUSSES] now including asset dictionaries for each asset connected to a bus.
599

600
    Notes
601
    -----
602
    Tested with:
603
    - C0.test_add_asset_to_asset_dict_for_each_flow_direction()
604
    """
605

606
    # The asset needs to be added both to the inflow as well as the outflow bus:
607
    for direction in [INFLOW_DIRECTION, OUTFLOW_DIRECTION]:
1✔
608
        # Check if the asset has an INFLOW_DIRECTION or OUTFLOW_DIRECTION
609
        if direction in dict_asset:
1✔
610
            bus = dict_asset[direction]
1✔
611
            # Check if a list ob busses is in INFLOW_DIRECTION or OUTFLOW_DIRECTION
612
            if isinstance(bus, list):
1✔
613
                # If true: All busses need to be checked
614
                bus_list = []
1✔
615
                # Checking each bus of the list
616
                for subbus in bus:
1✔
617
                    # Append bus name to bus_list
618
                    bus_list.append(subbus)
1✔
619
                    # Check if bus of the direction is already contained in energyBusses
620
                    add_asset_to_asset_dict_of_bus(
1✔
621
                        bus=subbus,
622
                        dict_values=dict_values,
623
                        asset_key=asset_key,
624
                        asset_label=dict_asset[LABEL],
625
                    )
626

627
            # If false: Only one bus
628
            else:
629
                # Check if bus of the direction is already contained in energyBusses
630
                add_asset_to_asset_dict_of_bus(
1✔
631
                    bus=bus,
632
                    dict_values=dict_values,
633
                    asset_key=asset_key,
634
                    asset_label=dict_asset[LABEL],
635
                )
636

637

638
def add_asset_to_asset_dict_of_bus(bus, dict_values, asset_key, asset_label):
1✔
639
    """
640
    Adds asset key and label to a bus defined by `energyBusses.csv`
641
    Sends an error message if the bus was not included in `energyBusses.csv`
642

643
    Parameters
644
    ----------
645
    dict_values: dict
646
        Dict of all simulation parameters
647

648
    bus: str
649
        A bus label
650

651
    asset_key: str
652
        Key with with an dict_asset would be called from dict_values[groups][key]
653

654
    asset_label: str
655
        Label of the asset
656

657
    Returns
658
    -------
659
    Updated dict_values[ENERGY_BUSSES] by adding an asset to the busses` ASSET DICT
660

661
    EnergyBusses now has following keys: LABEL, ENERGY_VECTOR, ASSET_DICT
662

663
    Notes
664
    -----
665
    Tested with:
666
    - C0.test_add_asset_to_asset_dict_of_bus()
667
    - C0.test_add_asset_to_asset_dict_of_bus_ValueError()
668
    """
669
    # If bus not defined in `energyBusses.csv` display error message
670
    if bus not in dict_values[ENERGY_BUSSES]:
1✔
671
        bus_string = ", ".join(map(str, dict_values[ENERGY_BUSSES].keys()))
1✔
672
        msg = (
1✔
673
            f"Asset {asset_key} has an inflow or outflow direction of {bus}. "
674
            f"This bus is not defined in `energyBusses.csv`: {bus_string}. "
675
            f"You may either have a typo in one of the files or need to add a bus to `energyBusses.csv`."
676
        )
677
        raise ValueError(msg)
1✔
678

679
    # If the EnergyBus has no ASSET_DICT to which the asset can be added later, add it
680
    if ASSET_DICT not in dict_values[ENERGY_BUSSES][bus]:
1✔
681
        dict_values[ENERGY_BUSSES][bus].update({ASSET_DICT: {}})
1✔
682

683
    # Asset should added to respective bus
684
    dict_values[ENERGY_BUSSES][bus][ASSET_DICT].update({asset_key: asset_label})
1✔
685
    logging.debug(f"Added asset {asset_label} to bus {bus}")
1✔
686

687

688
def define_auxiliary_assets_of_energy_providers(dict_values, dso_name):
1✔
689
    r"""
690
    Defines all sinks and sources that need to be added to model the transformer using assets of energyConsumption, energyProduction and energyConversion.
691

692
    Parameters
693
    ----------
694
    dict_values: dict
695
        All simulation parameters
696

697
    dso_name: str
698
        the name of the energy provider asset
699

700
    Returns
701
    -------
702
    Updated dict_values
703

704
    Notes
705
    -----
706
    This function is tested with following pytests:
707
    - C0.test_define_auxiliary_assets_of_energy_providers()
708
    - C0.test_determine_months_in_a_peak_demand_pricing_period_not_valid()
709
    - C0.test_determine_months_in_a_peak_demand_pricing_period_valid()
710
    - C0.test_define_availability_of_peak_demand_pricing_assets_yearly()
711
    - C0.test_define_availability_of_peak_demand_pricing_assets_monthly()
712
    - C0.test_define_availability_of_peak_demand_pricing_assets_quarterly()
713
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_1_period()
714
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_2_periods()
715
    - C0.test_define_transformer_for_peak_demand_pricing()
716
    - C0.test_define_source()
717
    - C0.test_define_source_exception_unknown_bus()
718
    - C0.test_define_source_timeseries_not_None()
719
    - C0.test_define_source_price_not_None_but_with_scalar_value()
720
    - C0.test_define_sink() -> incomplete
721
    - C0.test_change_sign_of_feedin_tariff_positive_value()
722
    - C0.test_change_sign_of_feedin_tariff_negative_value()
723
    - C0.test_change_sign_of_feedin_tariff_zero()
724
    """
725

726
    dso_dict = dict_values[ENERGY_PROVIDERS][dso_name]
1✔
727

728
    number_of_pricing_periods = dso_dict[PEAK_DEMAND_PRICING_PERIOD][VALUE]
1✔
729

730
    months_in_a_period = determine_months_in_a_peak_demand_pricing_period(
1✔
731
        number_of_pricing_periods,
732
        dict_values[SIMULATION_SETTINGS][EVALUATED_PERIOD][VALUE],
733
    )
734

735
    dict_availability_timeseries = define_availability_of_peak_demand_pricing_assets(
1✔
736
        dict_values,
737
        number_of_pricing_periods,
738
        months_in_a_period,
739
    )
740

741
    list_of_dso_energyConversion_assets = (
1✔
742
        add_a_transformer_for_each_peak_demand_pricing_period(
743
            dict_values,
744
            dso_dict,
745
            dict_availability_timeseries,
746
        )
747
    )
748

749
    define_source(
1✔
750
        dict_values=dict_values,
751
        asset_key=dso_name + DSO_CONSUMPTION,
752
        outflow_direction=peak_demand_bus_name(dso_dict[OUTFLOW_DIRECTION]),
753
        price=dso_dict[ENERGY_PRICE],
754
        energy_vector=dso_dict[ENERGY_VECTOR],
755
        emission_factor=dso_dict[EMISSION_FACTOR],
756
        asset_type=dso_dict.get(TYPE_ASSET),
757
    )
758
    dict_feedin = change_sign_of_feedin_tariff(dso_dict[FEEDIN_TARIFF], dso_name)
1✔
759

760
    inflow_bus_name = peak_demand_bus_name(dso_dict[INFLOW_DIRECTION], feedin=True)
1✔
761

762
    # define feed-in sink of the DSO
763
    define_sink(
1✔
764
        dict_values=dict_values,
765
        asset_key=dso_name + DSO_FEEDIN,
766
        price=dict_feedin,
767
        inflow_direction=inflow_bus_name,
768
        specific_costs={VALUE: 0, UNIT: CURR + "/" + UNIT},
769
        energy_vector=dso_dict[ENERGY_VECTOR],
770
        asset_type=dso_dict.get(TYPE_ASSET),
771
    )
772
    dso_dict.update(
1✔
773
        {
774
            CONNECTED_CONSUMPTION_SOURCE: dso_name + DSO_CONSUMPTION,
775
            CONNECTED_PEAK_DEMAND_PRICING_TRANSFORMERS: list_of_dso_energyConversion_assets,
776
            CONNECTED_FEEDIN_SINK: dso_name + DSO_FEEDIN,
777
        }
778
    )
779

780

781
def change_sign_of_feedin_tariff(dict_feedin_tariff, dso):
1✔
782
    r"""
783
    Change the sign of the feed-in tariff.
784
    Additionally, prints a logging.warning in case of the feed-in tariff is entered as
785
    negative value in 'energyProviders.csv'.
786

787
    Parameters
788
    ----------
789
    dict_feedin_tariff: dict
790
        Dict of feedin tariff with Unit-value pair
791

792
    dso: str
793
        Name of the energy provider
794

795
    Returns
796
    -------
797
    dict_feedin_tariff: dict
798
        Dict of feedin tariff, to be used as input to C0.define_sink
799

800
    Notes
801
    -----
802
    Tested with:
803
    - C0.test_change_sign_of_feedin_tariff_positive_value()
804
    - C0.test_change_sign_of_feedin_tariff_negative_value()
805
    - C0.test_change_sign_of_feedin_tariff_zero()
806
    """
807

808
    if isinstance(dict_feedin_tariff[VALUE], pd.Series) is False:
1✔
809
        if dict_feedin_tariff[VALUE] > 0:
1✔
810
            # Add a debug message in case the feed-in is interpreted as revenue-inducing.
811
            logging.debug(
1✔
812
                f"The {FEEDIN_TARIFF} of {dso} is positive, which means that feeding into the grid results in a revenue stream."
813
            )
814
        elif dict_feedin_tariff[VALUE] == 0:
1✔
815
            # Add a warning msg in case the feedin induces expenses rather than revenue
816
            logging.warning(
1✔
817
                f"The {FEEDIN_TARIFF} of {dso} is 0, which means that there is no renumeration for feed-in to the grid. Potentially, this can lead to random dispatch into feed-in and excess sinks."
818
            )
819
        elif dict_feedin_tariff[VALUE] < 0:
1✔
820
            # Add a warning msg in case the feedin induces expenses rather than revenue
821
            logging.warning(
1✔
822
                f"The {FEEDIN_TARIFF} of {dso} is negative, which means that payments are necessary to be allowed to feed-into the grid. If you intended a revenue stream, set the feedin tariff to a positive value."
823
            )
824
        else:
825
            pass
1✔
826
    else:
UNCOV
827
        if (dict_feedin_tariff[VALUE] < 0).any():
×
828
            # Add a warning msg in case the feedin induces expenses rather than revenue
UNCOV
829
            ts_info = ", ".join(
×
830
                dict_feedin_tariff[VALUE]
831
                .loc[dict_feedin_tariff[VALUE] < 0]
832
                .index.astype(str)
833
            )
UNCOV
834
            logging.warning(
×
835
                f"The {FEEDIN_TARIFF} of {dso} is 0 for the following timestamps:\n{ts_info}\n. This means that there is no renumeration for feed-in to the grid. Potentially, this can lead to random dispatch into feed-in and excess sinks."
836
            )
UNCOV
837
        elif (dict_feedin_tariff[VALUE] < 0).any():
×
838
            # Add a warning msg in case the feedin induces expenses rather than revenue
UNCOV
839
            ts_info = ", ".join(
×
840
                dict_feedin_tariff[VALUE]
841
                .loc[dict_feedin_tariff[VALUE] < 0]
842
                .index.astype(str)
843
            )
UNCOV
844
            logging.warning(
×
845
                f"The {FEEDIN_TARIFF} of {dso} is negative for the following timestamps:\n{ts_info}\n. A negative value means that payments are necessary to be allowed to feed-into the grid. If you intended a revenue stream, set the feedin tariff to a positive value."
846
            )
847

848
    dict_feedin_tariff = {
1✔
849
        VALUE: -dict_feedin_tariff[VALUE],
850
        UNIT: dict_feedin_tariff[UNIT],
851
    }
852
    return dict_feedin_tariff
1✔
853

854

855
def define_availability_of_peak_demand_pricing_assets(
1✔
856
    dict_values, number_of_pricing_periods, months_in_a_period
857
):
858
    r"""
859
    Determined the availability timeseries for the later to be defined dso assets for taking into account the peak demand pricing periods.
860

861
    Parameters
862
    ----------
863
    dict_values: dict
864
        All simulation inputs
865
    number_of_pricing_periods: int
866
        Number of pricing periods in a year. Valid: 1,2,3,4,6,12
867
    months_in_a_period: int
868
        Duration of a period
869

870
    Returns
871
    -------
872
    dict_availability_timeseries: dict
873
        Dict with all availability timeseries for each period
874

875
    """
876
    dict_availability_timeseries = {}
1✔
877
    for period in range(1, number_of_pricing_periods + 1):
1✔
878
        availability_in_period = pd.Series(
1✔
879
            0, index=dict_values[SIMULATION_SETTINGS][TIME_INDEX]
880
        )
881
        time_period = pd.date_range(
1✔
882
            # Period start
883
            start=dict_values[SIMULATION_SETTINGS][START_DATE]
884
            + pd.DateOffset(months=(period - 1) * months_in_a_period),
885
            # Period end, with months_in_a_period durartion
886
            end=dict_values[SIMULATION_SETTINGS][START_DATE]
887
            + pd.DateOffset(months=(period) * months_in_a_period, hours=-1),
888
            freq=str(dict_values[SIMULATION_SETTINGS][TIMESTEP][VALUE]) + UNIT_MINUTE,
889
        )
890

891
        availability_in_period = availability_in_period.add(
1✔
892
            pd.Series(1, index=time_period), fill_value=0
893
        ).loc[dict_values[SIMULATION_SETTINGS][TIME_INDEX]]
894
        dict_availability_timeseries.update({period: availability_in_period})
1✔
895

896
    return dict_availability_timeseries
1✔
897

898

899
def add_a_transformer_for_each_peak_demand_pricing_period(
1✔
900
    dict_values, dict_dso, dict_availability_timeseries
901
):
902
    r"""
903
    Adds transformers that are supposed to model the peak_demand_pricing periods for each period.
904
    This is changed compared to MVS 0.3.0, as there a peak demand pricing period was added by adding a source, not a transformer.
905

906
    Parameters
907
    ----------
908
    dict_values: dict
909
        dict with all simulation parameters
910

911
    dict_dso: dict
912
        dict with all info on the specific dso at hand
913

914
    dict_availability_timeseries: dict
915
        dict with all availability timeseries for each period
916

917
    Returns
918
    -------
919
    list_of_dso_energyConversion_assets: list
920
        List of names of newly added energy conversion assets,
921

922
    Updated dict_values with a transformer for each peak demand pricing period
923

924
    Notes
925
    -----
926

927
    Tested by:
928
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_1_period
929
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_2_periods
930
    """
931

932
    list_of_dso_energyConversion_assets = []
1✔
933
    for key in dict_availability_timeseries.keys():
1✔
934

935
        if len(dict_availability_timeseries.keys()) > 1:
1✔
936
            transformer_name = peak_demand_transformer_name(
1✔
937
                dict_dso[LABEL], peak_number=key
938
            )
939
        else:
940
            transformer_name = peak_demand_transformer_name(dict_dso[LABEL])
1✔
941

942
        define_transformer_for_peak_demand_pricing(
1✔
943
            dict_values=dict_values,
944
            dict_dso=dict_dso,
945
            transformer_name=transformer_name,
946
            timeseries_availability=dict_availability_timeseries[key],
947
        )
948

949
        list_of_dso_energyConversion_assets.append(transformer_name)
1✔
950

951
    logging.debug(
1✔
952
        f"The peak demand pricing price of {dict_dso[PEAK_DEMAND_PRICING][VALUE]} {dict_values[ECONOMIC_DATA][CURR]} "
953
        f"is set as specific_costs_om of the peak demand pricing transformers of the DSO."
954
    )
955
    return list_of_dso_energyConversion_assets
1✔
956

957

958
def determine_months_in_a_peak_demand_pricing_period(
1✔
959
    number_of_pricing_periods, simulation_period_lenght
960
):
961
    r"""
962
    Check if the number of peak demand pricing periods is valid.
963
    Warns user that in case the number of periods exceeds 1 but the simulation time is not a year,
964
    there could be an unexpected number of timeseries considered.
965
    Raises error if number of peak demand pricing periods is not valid.
966

967
    Parameters
968
    ----------
969
    number_of_pricing_periods: int
970
        Defined in csv, is number of pricing periods within a year
971
    simulation_period_lenght: int
972
        Defined in csv, is number of days of the simulation
973

974
    Returns
975
    -------
976
    months_in_a_period: float
977
        Number of months that make a period, will be used to determine availability of dso assets
978
    """
979

980
    # check number of pricing periods - if >1 the simulation has to cover a whole year!
981
    if number_of_pricing_periods > 1:
1✔
982
        if simulation_period_lenght != 365:
1✔
983
            logging.debug(
1✔
984
                f"\n Message for dev: Following warning is not technically true, "
985
                f"as the evaluation period has to approximately be "
986
                f"larger than 365/peak demand pricing periods (see #331)."
987
            )
988
            logging.warning(
1✔
989
                f"You have chosen a number of peak demand pricing periods > 1."
990
                f"Please be advised that if you are not simulating for a year (365d)"
991
                f"an possibly unexpected number of periods will be considered."
992
            )
993

994
    if number_of_pricing_periods not in [1, 2, 3, 4, 6, 12]:
1✔
995
        raise InvalidPeakDemandPricingPeriodsError(
1✔
996
            f"You have defined a number of peak demand pricing periods of {number_of_pricing_periods}. "
997
            f"Acceptable values are, however: 1 (yearly), 2 (half-yearly), 3 (each trimester), 4 (quarterly), 6 (every two months) and 1 (monthly)."
998
        )
999

1000
    # defines the number of months that one period constists of.
1001
    months_in_a_period = 12 / number_of_pricing_periods
1✔
1002
    logging.info(
1✔
1003
        "Peak demand pricing is taking place %s times per year, ie. every %s "
1004
        "months.",
1005
        number_of_pricing_periods,
1006
        months_in_a_period,
1007
    )
1008
    return months_in_a_period
1✔
1009

1010

1011
def define_transformer_for_peak_demand_pricing(
1✔
1012
    dict_values, dict_dso, transformer_name, timeseries_availability
1013
):
1014
    r"""
1015
    Defines a transformer for peak demand pricing in energyConverion
1016

1017
    Parameters
1018
    ----------
1019
    dict_values: dict
1020
        All simulation parameters
1021

1022
    dict_dso: dict
1023
        All values connected to the DSO
1024

1025
    transformer_name: str
1026
        label of the transformer to be added
1027

1028
    timeseries_availability: pd.Series
1029
        Timeseries of transformer availability. Introduced to cover peak demand pricing.
1030

1031
    Returns
1032
    -------
1033
    Updated dict_values with newly added transformer asset in the energyConversion asset group.
1034
    """
1035

1036
    dso_consumption_transformer = {
1✔
1037
        LABEL: transformer_name,
1038
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1039
        INSTALLED_CAP: {VALUE: 0, UNIT: dict_dso[UNIT]},
1040
        INFLOW_DIRECTION: peak_demand_bus_name(dict_dso[INFLOW_DIRECTION]),
1041
        OUTFLOW_DIRECTION: dict_dso[OUTFLOW_DIRECTION],
1042
        AVAILABILITY_DISPATCH: timeseries_availability,
1043
        EFFICIENCY: {VALUE: 1, UNIT: "factor"},
1044
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
1045
        SPECIFIC_COSTS: {
1046
            VALUE: 0,
1047
            UNIT: CURR + "/" + dict_dso[UNIT],
1048
        },
1049
        # the demand pricing is only applied to consumption
1050
        SPECIFIC_COSTS_OM: {
1051
            VALUE: dict_dso[PEAK_DEMAND_PRICING][VALUE],
1052
            UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_YEAR,
1053
        },
1054
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_HOUR},
1055
        OEMOF_ASSET_TYPE: OEMOF_TRANSFORMER,
1056
        ENERGY_VECTOR: dict_dso[ENERGY_VECTOR],
1057
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR},
1058
        TYPE_ASSET: dict_dso.get(TYPE_ASSET),
1059
    }
1060

1061
    dict_values[ENERGY_CONVERSION].update(
1✔
1062
        {transformer_name: dso_consumption_transformer}
1063
    )
1064

1065
    logging.debug(
1✔
1066
        f"Model for peak demand pricing on consumption side: Adding transfomer {transformer_name}."
1067
    )
1068

1069
    transformer_name = transformer_name.replace(DSO_CONSUMPTION, DSO_FEEDIN)
1✔
1070
    dso_feedin_transformer = {
1✔
1071
        LABEL: transformer_name,
1072
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1073
        INSTALLED_CAP: {VALUE: 0, UNIT: dict_dso[UNIT]},
1074
        INFLOW_DIRECTION: dict_dso[INFLOW_DIRECTION],
1075
        OUTFLOW_DIRECTION: peak_demand_bus_name(
1076
            dict_dso[INFLOW_DIRECTION], feedin=True
1077
        ),
1078
        AVAILABILITY_DISPATCH: timeseries_availability,
1079
        EFFICIENCY: {VALUE: 1, UNIT: "factor"},
1080
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
1081
        SPECIFIC_COSTS: {
1082
            VALUE: 0,
1083
            UNIT: CURR + "/" + dict_dso[UNIT],
1084
        },
1085
        # the demand pricing is only applied to consumption
1086
        SPECIFIC_COSTS_OM: {
1087
            VALUE: 0,
1088
            UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_YEAR,
1089
        },
1090
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_HOUR},
1091
        OEMOF_ASSET_TYPE: OEMOF_TRANSFORMER,
1092
        ENERGY_VECTOR: dict_dso[ENERGY_VECTOR],
1093
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR},
1094
        TYPE_ASSET: dict_dso.get(TYPE_ASSET),
1095
        # LIFETIME: {VALUE: 100, UNIT: UNIT_YEAR},
1096
    }
1097
    if dict_dso.get(DSO_FEEDIN_CAP, None) is not None:
1✔
UNCOV
1098
        dso_feedin_transformer[MAXIMUM_CAP] = {
×
1099
            VALUE: dict_dso[DSO_FEEDIN_CAP][VALUE],
1100
            UNIT: dict_dso[UNIT],
1101
        }
1102

UNCOV
1103
        logging.info(
×
1104
            f"Capping {dict_dso[LABEL]} feedin with maximum capacity {dict_dso[DSO_FEEDIN_CAP][VALUE]}"
1105
        )
1106

1107
    dict_values[ENERGY_CONVERSION].update({transformer_name: dso_feedin_transformer})
1✔
1108

1109
    logging.debug(
1✔
1110
        f"Model for peak demand pricing on feedin side: Adding transfomer {transformer_name}."
1111
    )
1112

1113

1114
def define_source(
1✔
1115
    dict_values,
1116
    asset_key,
1117
    outflow_direction,
1118
    energy_vector,
1119
    emission_factor,
1120
    price=None,
1121
    timeseries=None,
1122
    asset_type=None,
1123
):
1124
    r"""
1125
    Defines a source with default input values. If kwargs are given, the default values are overwritten.
1126

1127
    Parameters
1128
    ----------
1129
    dict_values: dict
1130
        Dictionary to which source should be added, with all simulation parameters
1131

1132
    asset_key: str
1133
        key under which the asset is stored in the asset group
1134

1135
    energy_vector: str
1136
        Energy vector the new asset should belong to
1137

1138
    emission_factor : dict
1139
        Dict with a unit-value pair of the emission factor of the new asset
1140

1141
    price: dict
1142
        Dict with a unit-value pair of the dispatch price of the source.
1143
        The value can also be defined though FILENAME and HEADER, making the value of the price a timeseries.
1144
        Default: None
1145

1146
    timeseries: pd.Dataframe
1147
        Timeseries defining the availability of the source. Currently not used.
1148
        Default: None
1149

1150
    Returns
1151
    -------
1152
    Updates dict_values[ENERGY_BUSSES] if outflow_direction not in it
1153
    Standard source defined as:
1154

1155
    Notes
1156
    -----
1157
    The pytests for this function are not complete. It is started with:
1158
    - C0.test_define_source()
1159
    - C0.test_define_source_exception_unknown_bus()
1160
    - C0.test_define_source_timeseries_not_None()
1161
    - C0.test_define_source_price_not_None_but_with_scalar_value()
1162
    Missing:
1163
    - C0.test_define_source_price_not_None_but_timeseries(), ie. value defined by FILENAME and HEADER
1164
    """
1165
    default_source_dict = {
1✔
1166
        OEMOF_ASSET_TYPE: OEMOF_SOURCE,
1167
        LABEL: asset_key,
1168
        OUTFLOW_DIRECTION: outflow_direction,
1169
        DISPATCHABILITY: True,
1170
        LIFETIME: {
1171
            VALUE: dict_values[ECONOMIC_DATA][PROJECT_DURATION][VALUE],
1172
            UNIT: UNIT_YEAR,
1173
        },
1174
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1175
        MAXIMUM_CAP: {VALUE: None, UNIT: "?"},
1176
        AGE_INSTALLED: {
1177
            VALUE: 0,
1178
            UNIT: UNIT_YEAR,
1179
        },
1180
        ENERGY_VECTOR: energy_vector,
1181
        EMISSION_FACTOR: emission_factor,
1182
        TYPE_ASSET: asset_type,
1183
    }
1184

1185
    if outflow_direction not in dict_values[ENERGY_BUSSES]:
1✔
1186
        dict_values[ENERGY_BUSSES].update(
1✔
1187
            {
1188
                outflow_direction: {
1189
                    LABEL: outflow_direction,
1190
                    ENERGY_VECTOR: energy_vector,
1191
                    ASSET_DICT: {asset_key: asset_key},
1192
                }
1193
            }
1194
        )
1195

1196
    if price is not None:
1✔
1197

1198
        if FILENAME in price and HEADER in price:
1✔
UNCOV
1199
            price.update(
×
1200
                {
1201
                    VALUE: get_timeseries_multiple_flows(
1202
                        dict_values[SIMULATION_SETTINGS],
1203
                        default_source_dict,
1204
                        price[FILENAME],
1205
                        price[HEADER],
1206
                    )
1207
                }
1208
            )
1209
        determine_dispatch_price(dict_values, price, default_source_dict)
1✔
1210

1211
    if timeseries is not None:
1✔
1212
        # This part is currently not used.
1213
        default_source_dict.update({DISPATCHABILITY: False})
1✔
1214
        logging.debug(
1✔
1215
            f"{default_source_dict[LABEL]} can provide a total generation of {sum(timeseries.values)}"
1216
        )
1217
        default_source_dict[OPTIMIZE_CAP].update({VALUE: True})
1✔
1218
        default_source_dict.update(
1✔
1219
            {
1220
                TIMESERIES_PEAK: {VALUE: max(timeseries), UNIT: "kW"},
1221
                TIMESERIES_NORMALIZED: timeseries / max(timeseries),
1222
            }
1223
        )
1224
        if DISPATCH_PRICE in default_source_dict and max(timeseries) != 0:
1✔
1225
            default_source_dict[DISPATCH_PRICE].update(
1✔
1226
                {VALUE: default_source_dict[DISPATCH_PRICE][VALUE] / max(timeseries)}
1227
            )
1228

1229
    dict_values[ENERGY_PRODUCTION].update({asset_key: default_source_dict})
1✔
1230

1231
    logging.info(
1✔
1232
        f"Asset {default_source_dict[LABEL]} was added to the energyProduction assets."
1233
    )
1234

1235
    apply_function_to_single_or_list(
1✔
1236
        function=add_asset_to_asset_dict_of_bus,
1237
        parameter=outflow_direction,
1238
        dict_values=dict_values,
1239
        asset_key=asset_key,
1240
        asset_label=default_source_dict[LABEL],
1241
    )
1242

1243

1244
def determine_dispatch_price(dict_values, price, source):
1✔
1245
    """
1246
    This function needs to be re-evaluated.
1247

1248
    Parameters
1249
    ----------
1250
    dict_values
1251
    price
1252
    source
1253

1254
    Returns
1255
    -------
1256

1257
    """
1258
    # check if multiple busses are provided
1259
    # for each bus, read time series for dispatch_price if a file name has been
1260
    # provided in energy price
1261
    if isinstance(price[VALUE], list):
1✔
UNCOV
1262
        source.update({DISPATCH_PRICE: {VALUE: [], UNIT: price[UNIT]}})
×
UNCOV
1263
        values_info = []
×
UNCOV
1264
        for element in price[VALUE]:
×
UNCOV
1265
            if isinstance(element, dict):
×
UNCOV
1266
                source[DISPATCH_PRICE][VALUE].append(
×
1267
                    get_timeseries_multiple_flows(
1268
                        dict_values[SIMULATION_SETTINGS],
1269
                        source,
1270
                        element[FILENAME],
1271
                        element[HEADER],
1272
                    )
1273
                )
UNCOV
1274
                values_info.append(element)
×
1275
            else:
UNCOV
1276
                source[DISPATCH_PRICE][VALUE].append(element)
×
UNCOV
1277
        if len(values_info) > 0:
×
UNCOV
1278
            source[DISPATCH_PRICE]["values_info"] = values_info
×
1279

1280
    elif isinstance(price[VALUE], dict):
1✔
UNCOV
1281
        source.update(
×
1282
            {
1283
                DISPATCH_PRICE: {
1284
                    VALUE: {
1285
                        FILENAME: price[VALUE][FILENAME],
1286
                        HEADER: price[VALUE][HEADER],
1287
                    },
1288
                    UNIT: price[UNIT],
1289
                }
1290
            }
1291
        )
UNCOV
1292
        receive_timeseries_from_csv(
×
1293
            dict_values[SIMULATION_SETTINGS], source, DISPATCH_PRICE
1294
        )
1295
    else:
1296
        source.update({DISPATCH_PRICE: {VALUE: price[VALUE], UNIT: price[UNIT]}})
1✔
1297

1298
    if type(source[DISPATCH_PRICE][VALUE]) == pd.Series:
1✔
UNCOV
1299
        logging.debug(
×
1300
            f"{source[LABEL]} was created, with a price defined as a timeseries (average: {source[DISPATCH_PRICE][VALUE].mean()})."
1301
        )
1302
    else:
1303
        logging.debug(
1✔
1304
            f"{source[LABEL]} was created, with a price of {source[DISPATCH_PRICE][VALUE]}."
1305
        )
1306

1307

1308
def define_sink(
1✔
1309
    dict_values,
1310
    asset_key,
1311
    price,
1312
    inflow_direction,
1313
    energy_vector,
1314
    asset_type=None,
1315
    **kwargs,
1316
):
1317
    r"""
1318
    This automatically defines a sink for an oemof-sink object. The sinks are added to the energyConsumption assets.
1319

1320
    Parameters
1321
    ----------
1322
    dict_values: dict
1323
        All information of the simulation
1324

1325
    asset_key: str
1326
        label of the asset to be generated
1327

1328
    price: float
1329
        Price of dispatch of the asset
1330

1331
    inflow_direction: str
1332
        Direction from which energy is provided to the sink
1333

1334
    kwargs: Misc
1335
        Common parameters:
1336
        -
1337

1338
    Returns
1339
    -------
1340
    Updates dict_values[ENERGY_BUSSES] if outflow_direction not in it
1341
    Updates dict_values[ENERGY_CONSUMPTION] with a new sink
1342

1343
    Notes
1344
    -----
1345
    Examples:
1346
    - Used to define excess sinks for all energyBusses
1347
    - Used to define feed-in sink for each DSO
1348

1349
    The pytests for this function are not complete. It is started with:
1350
    - C0.test_define_sink() and only the assertion messages are missing
1351
    """
1352

1353
    # create a dictionary for the sink
1354
    sink = {
1✔
1355
        OEMOF_ASSET_TYPE: OEMOF_SINK,
1356
        LABEL: asset_key,
1357
        INFLOW_DIRECTION: inflow_direction,
1358
        # OPEX_VAR: {VALUE: price, UNIT: CURR + "/" + UNIT},
1359
        LIFETIME: {
1360
            VALUE: dict_values[ECONOMIC_DATA][PROJECT_DURATION][VALUE],
1361
            UNIT: UNIT_YEAR,
1362
        },
1363
        AGE_INSTALLED: {
1364
            VALUE: 0,
1365
            UNIT: UNIT_YEAR,
1366
        },
1367
        ENERGY_VECTOR: energy_vector,
1368
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1369
        DISPATCHABILITY: {VALUE: True, UNIT: TYPE_BOOL},
1370
        TYPE_ASSET: asset_type,
1371
    }
1372

1373
    if inflow_direction not in dict_values[ENERGY_BUSSES]:
1✔
1374
        dict_values[ENERGY_BUSSES].update(
1✔
1375
            {
1376
                inflow_direction: {
1377
                    LABEL: inflow_direction,
1378
                    ENERGY_VECTOR: energy_vector,
1379
                    ASSET_DICT: {asset_key: asset_key},
1380
                }
1381
            }
1382
        )
1383

1384
    if energy_vector is None:
1✔
1385
        raise ValueError(
×
1386
            f"The {ENERGY_VECTOR} of the automatically defined sink {asset_key} is invalid: {energy_vector}."
1387
        )
1388

1389
    # check if multiple busses are provided
1390
    # for each bus, read time series for dispatch_price if a file name has been provided in feedin tariff
1391
    if isinstance(price[VALUE], list):
1✔
1392
        sink.update({DISPATCH_PRICE: {VALUE: [], UNIT: price[UNIT]}})
×
UNCOV
1393
        values_info = []
×
UNCOV
1394
        for element in price[VALUE]:
×
UNCOV
1395
            if isinstance(element, dict):
×
UNCOV
1396
                timeseries = get_timeseries_multiple_flows(
×
1397
                    dict_values[SIMULATION_SETTINGS],
1398
                    sink,
1399
                    element[FILENAME],
1400
                    element[HEADER],
1401
                )
1402
                # todo this should be moved to C0.change_sign_of_feedin_tariff when #354 is solved
UNCOV
1403
                if DSO_FEEDIN in asset_key:
×
UNCOV
1404
                    sink[DISPATCH_PRICE][VALUE].append([-i for i in timeseries])
×
1405
                else:
UNCOV
1406
                    sink[DISPATCH_PRICE][VALUE].append(timeseries)
×
1407
            else:
UNCOV
1408
                sink[DISPATCH_PRICE][VALUE].append(element)
×
UNCOV
1409
        if len(values_info) > 0:
×
UNCOV
1410
            sink[DISPATCH_PRICE]["values_info"] = values_info
×
1411

1412
    elif isinstance(price[VALUE], dict):
1✔
UNCOV
1413
        sink.update(
×
1414
            {
1415
                DISPATCH_PRICE: {
1416
                    VALUE: {
1417
                        FILENAME: price[VALUE][FILENAME],
1418
                        HEADER: price[VALUE][HEADER],
1419
                    },
1420
                    UNIT: price[UNIT],
1421
                }
1422
            }
1423
        )
UNCOV
1424
        receive_timeseries_from_csv(
×
1425
            dict_values[SIMULATION_SETTINGS], sink, DISPATCH_PRICE
1426
        )
1427
        # todo this should be moved to C0.change_sign_of_feedin_tariff when #354 is solved
UNCOV
1428
        if (
×
1429
            DSO_FEEDIN in asset_key
1430
        ):  # change into negative value if this is a feedin sink
UNCOV
1431
            sink[DISPATCH_PRICE].update(
×
1432
                {VALUE: [-i for i in sink[DISPATCH_PRICE][VALUE]]}
1433
            )
1434
    else:
1435
        sink.update({DISPATCH_PRICE: {VALUE: price[VALUE], UNIT: price[UNIT]}})
1✔
1436

1437
    for item in [SPECIFIC_COSTS, SPECIFIC_COSTS_OM]:
1✔
1438
        if item in kwargs:
1✔
1439
            sink.update(
1✔
1440
                {
1441
                    item: kwargs[item],
1442
                }
1443
            )
1444

1445
    # update dictionary
1446
    dict_values[ENERGY_CONSUMPTION].update({asset_key: sink})
1✔
1447

1448
    # If multiple input busses exist
1449
    apply_function_to_single_or_list(
1✔
1450
        function=add_asset_to_asset_dict_of_bus,
1451
        parameter=inflow_direction,
1452
        dict_values=dict_values,
1453
        asset_key=asset_key,
1454
        asset_label=sink[LABEL],
1455
    )
1456

1457

1458
def apply_function_to_single_or_list(function, parameter, **kwargs):
1✔
1459
    """
1460
    Applies function to a paramter or to a list of parameters and returns resut
1461

1462
    Parameters
1463
    ----------
1464
    function: func
1465
        Function to be applied to a parameter
1466

1467
    parameter: float/str/boolean or list
1468
        Parameter, either float/str/boolean or list to be evaluated
1469
    kwargs
1470
        Miscellaneous arguments for function to be called
1471

1472
    Returns
1473
    -------
1474
    Processed parameter (single) or list of processed para<meters
1475
    """
1476
    if isinstance(parameter, list):
1✔
1477
        parameter_processed = []
1✔
1478
        for parameter_item in parameter:
1✔
1479
            parameter_processed.append(function(parameter_item, **kwargs))
1✔
1480
    else:
1481
        parameter_processed = function(parameter, **kwargs)
1✔
1482

1483
    return parameter_processed
1✔
1484

1485

1486
def evaluate_lifetime_costs(settings, economic_data, dict_asset):
1✔
1487
    r"""
1488
    Evaluates specific costs of an asset over the project lifetime. This includes:
1489
    - LIFETIME_PRICE_DISPATCH (C2.determine_lifetime_price_dispatch)
1490
    - LIFETIME_SPECIFIC_COST
1491
    - LIFETIME_SPECIFIC_COST_OM
1492
    - ANNUITY_SPECIFIC_INVESTMENT_AND_OM
1493
    - SIMULATION_ANNUITY
1494

1495
    The DEVELOPMENT_COSTS are not processed here, as they are not necessary for the optimization.
1496

1497
    Parameters
1498
    ----------
1499
    settings: dict
1500
        dict of simulation settings, including:
1501
        - EVALUATED_PERIOD
1502

1503
    economic_data: dict
1504
        dict of economic data of the simulation, including
1505
        - project duration (PROJECT_DURATION)
1506
        - discount factor (DISCOUNTFACTOR)
1507
        - tax (TAX)
1508
        - CRF
1509
        - ANNUITY_FACTOR
1510

1511
    dict_asset: dict
1512
        dict of all asset parameters, including
1513
        - SPECIFIC_COSTS
1514
        - SPECIFIC_COSTS_OM
1515
        - LIFETIME
1516

1517
    Returns
1518
    -------
1519
    Updates asset dict with
1520
    - LIFETIME_PRICE_DISPATCH (C2.determine_lifetime_price_dispatch)
1521
    - LIFETIME_SPECIFIC_COST
1522
    - LIFETIME_SPECIFIC_COST_OM
1523
    - ANNUITY_SPECIFIC_INVESTMENT_AND_OM
1524
    - SIMULATION_ANNUITY
1525
    - SPECIFIC_REPLACEMENT_COSTS_INSTALLED
1526
    - SPECIFIC_REPLACEMENT_COSTS_OPTIMIZED
1527
    Notes
1528
    -----
1529

1530
    Tested with:
1531
    - test_evaluate_lifetime_costs_adds_all_parameters()
1532
    - Test_Economic_KPI.test_benchmark_Economic_KPI_C2_E2()
1533

1534
    """
1535
    if DISPATCH_PRICE in dict_asset:
1✔
1536
        C2.determine_lifetime_price_dispatch(dict_asset, economic_data)
1✔
1537

1538
    (
1✔
1539
        specific_capex,
1540
        specific_replacement_costs_optimized,
1541
        specific_replacement_costs_already_installed,
1542
    ) = C2.capex_from_investment(
1543
        investment_t0=dict_asset[SPECIFIC_COSTS][VALUE],
1544
        lifetime=dict_asset[LIFETIME][VALUE],
1545
        project_life=economic_data[PROJECT_DURATION][VALUE],
1546
        discount_factor=economic_data[DISCOUNTFACTOR][VALUE],
1547
        tax=economic_data[TAX][VALUE],
1548
        age_of_asset=dict_asset[AGE_INSTALLED][VALUE],
1549
        asset_label=dict_asset[LABEL],
1550
    )
1551

1552
    dict_asset.update(
1✔
1553
        {
1554
            LIFETIME_SPECIFIC_COST: {
1555
                VALUE: specific_capex,
1556
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1557
            }
1558
        }
1559
    )
1560

1561
    dict_asset.update(
1✔
1562
        {
1563
            SPECIFIC_REPLACEMENT_COSTS_OPTIMIZED: {
1564
                VALUE: specific_replacement_costs_optimized,
1565
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1566
            }
1567
        }
1568
    )
1569

1570
    dict_asset.update(
1✔
1571
        {
1572
            SPECIFIC_REPLACEMENT_COSTS_INSTALLED: {
1573
                VALUE: specific_replacement_costs_already_installed,
1574
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1575
            }
1576
        }
1577
    )
1578

1579
    # Annuities of components including opex AND capex #
1580
    dict_asset.update(
1✔
1581
        {
1582
            ANNUITY_SPECIFIC_INVESTMENT_AND_OM: {
1583
                VALUE: C2.annuity(
1584
                    dict_asset[LIFETIME_SPECIFIC_COST][VALUE],
1585
                    economic_data[CRF][VALUE],
1586
                )
1587
                + dict_asset[SPECIFIC_COSTS_OM][VALUE],  # changes from dispatch_price
1588
                UNIT: dict_asset[LIFETIME_SPECIFIC_COST][UNIT] + "/" + UNIT_YEAR,
1589
            }
1590
        }
1591
    )
1592

1593
    dict_asset.update(
1✔
1594
        {
1595
            LIFETIME_SPECIFIC_COST_OM: {
1596
                VALUE: dict_asset[SPECIFIC_COSTS_OM][VALUE]
1597
                * economic_data[ANNUITY_FACTOR][VALUE],
1598
                UNIT: dict_asset[SPECIFIC_COSTS_OM][UNIT][:-2],
1599
            }
1600
        }
1601
    )
1602

1603
    dict_asset.update(
1✔
1604
        {
1605
            SIMULATION_ANNUITY: {
1606
                VALUE: C2.simulation_annuity(
1607
                    dict_asset[ANNUITY_SPECIFIC_INVESTMENT_AND_OM][VALUE],
1608
                    settings[EVALUATED_PERIOD][VALUE],
1609
                ),
1610
                UNIT: CURR + "/" + UNIT + "/" + EVALUATED_PERIOD,
1611
            }
1612
        }
1613
    )
1614

1615

1616
# read timeseries. 2 cases are considered: Input type is related to demand or generation profiles,
1617
# so additional values like peak, total or average must be calculated. Any other type does not need this additional info.
1618
def receive_timeseries_from_csv(
1✔
1619
    settings, dict_asset, input_type, is_demand_profile=False
1620
):
1621
    """
1622

1623
    :param settings:
1624
    :param dict_asset:
1625
    :param type:
1626
    :return:
1627
    """
1628

1629
    load_from_timeseries_instead_of_file = False
1✔
1630

1631
    if input_type == "input" and "input" in dict_asset:
1✔
UNCOV
1632
        file_name = dict_asset[input_type][FILENAME]
×
1633
        header = dict_asset[input_type][HEADER]
×
UNCOV
1634
        unit = dict_asset[input_type][UNIT]
×
1635
    elif FILENAME in dict_asset:
1✔
1636
        # todo this input/file_name thing is a workaround and has to be improved in the future
1637
        # if only filename is given here, then only one column can be in the csv
1638
        file_name = dict_asset[FILENAME]
1✔
1639
        unit = dict_asset[UNIT] + "/" + UNIT_HOUR
1✔
1640
    elif FILENAME in dict_asset.get(input_type, []):
1✔
UNCOV
1641
        file_name = dict_asset[input_type][FILENAME]
×
UNCOV
1642
        header = dict_asset[input_type][HEADER]
×
1643
        unit = dict_asset[input_type][UNIT]
×
1644
    else:
1645
        load_from_timeseries_instead_of_file = True
1✔
1646
        file_name = ""
1✔
1647

1648
    file_path = os.path.join(settings[PATH_INPUT_FOLDER], TIME_SERIES, file_name)
1✔
1649

1650
    if os.path.exists(file_path) is False or os.path.isfile(file_path) is False:
1✔
1651
        msg = (
1✔
1652
            f"Missing file! The timeseries file '{file_path}' \nof asset "
1653
            + f"{dict_asset[LABEL]} can not be found."
1654
        )
1655
        logging.warning(msg + " Looking for {TIMESERIES} entry.")
1✔
1656
        # if the file is not found
1657
        load_from_timeseries_instead_of_file = True
1✔
1658

1659
    else:
1660
        data_set = pd.read_csv(file_path, sep=",", keep_default_na=True)
1✔
1661

1662
    # If loading the data from the file does not work (file not present), the data might be
1663
    # already present in dict_values under TIMESERIES
1664
    if load_from_timeseries_instead_of_file is False:
1✔
1665
        if FILENAME in dict_asset:
1✔
1666
            header = data_set.columns[0]
1✔
1667
        series_values = data_set[header]
1✔
1668
    else:
1669
        if TIMESERIES in dict_asset:
1✔
1670
            series_values = dict_asset[TIMESERIES]
1✔
1671
        else:
UNCOV
1672
            raise FileNotFoundError(msg)
×
1673

1674
    if len(series_values.index) == settings[PERIODS]:
1✔
1675
        if input_type == "input":
1✔
1676
            timeseries = pd.Series(series_values.values, index=settings[TIME_INDEX])
1✔
1677
            timeseries = replace_nans_in_timeseries_with_0(
1✔
1678
                timeseries, dict_asset[LABEL]
1679
            )
1680
            dict_asset.update({TIMESERIES: timeseries})
1✔
1681
        else:
UNCOV
1682
            timeseries = pd.Series(series_values.values, index=settings[TIME_INDEX])
×
UNCOV
1683
            timeseries = replace_nans_in_timeseries_with_0(
×
1684
                timeseries, dict_asset[LABEL] + "(" + input_type + ")"
1685
            )
UNCOV
1686
            dict_asset[input_type][VALUE] = timeseries
×
1687

1688
        logging.debug("Added timeseries of %s (%s).", dict_asset[LABEL], file_path)
1✔
1689
    elif len(series_values.index) >= settings[PERIODS]:
1✔
1690
        if input_type == "input":
1✔
1691
            dict_asset.update(
1✔
1692
                {
1693
                    TIMESERIES: pd.Series(
1694
                        series_values[0 : len(settings[TIME_INDEX])].values,
1695
                        index=settings[TIME_INDEX],
1696
                    )
1697
                }
1698
            )
1699
        else:
UNCOV
1700
            dict_asset[input_type][VALUE] = pd.Series(
×
1701
                series_values[0 : len(settings[TIME_INDEX])].values,
1702
                index=settings[TIME_INDEX],
1703
            )
1704

1705
        logging.info(
1✔
1706
            "Provided timeseries of %s (%s) longer than evaluated period. "
1707
            "Excess data dropped.",
1708
            dict_asset[LABEL],
1709
            file_path,
1710
        )
1711

UNCOV
1712
    elif len(series_values.index) <= settings[PERIODS]:
×
UNCOV
1713
        logging.critical(
×
1714
            "Input error! "
1715
            "Provided timeseries of %s (%s) shorter than evaluated period. "
1716
            "Operation terminated",
1717
            dict_asset[LABEL],
1718
            file_path,
1719
        )
UNCOV
1720
        sys.exit()
×
1721

1722
    if input_type == "input":
1✔
1723
        compute_timeseries_properties(dict_asset)
1✔
1724

1725

1726
def replace_nans_in_timeseries_with_0(timeseries, label):
1✔
1727
    """Replaces nans in the timeseries (if any) with 0
1728

1729
    Parameters
1730
    ----------
1731

1732
    timeseries: pd.Series
1733
        demand or resource timeseries in dict_asset (having nan value(s) if any),
1734
        also of parameters that are not defined as scalars but as timeseries
1735

1736
    label: str
1737
        Contains user-defined information about the timeseries to be printed into the eventual error message
1738

1739
    Returns
1740
    -------
1741
    timeseries: pd.Series
1742
        timeseries without NaN values
1743

1744
    Notes
1745
    -----
1746
    Function tested with
1747
    - C0.test_replace_nans_in_timeseries_with_0()
1748
    """
1749
    if sum(pd.isna(timeseries)) > 0:
1✔
1750
        incidents = sum(pd.isna(timeseries))
1✔
1751
        logging.warning(
1✔
1752
            f"A number of {incidents} NaN value(s) found in the {TIMESERIES} of {label}. Changing NaN value(s) to 0."
1753
        )
1754
        timeseries = timeseries.fillna(0)
1✔
1755
    return timeseries
1✔
1756

1757

1758
def compute_timeseries_properties(dict_asset):
1✔
1759
    """Compute peak, aggregation, average and normalize timeseries
1760

1761
    Parameters
1762
    ----------
1763
    dict_asset: dict
1764
        dict of all asset parameters, must contain TIMESERIES key
1765

1766
    Returns
1767
    -------
1768
    None
1769
    Add TIMESERIES_PEAK, TIMESERIES_TOTAL, TIMESERIES_AVERAGE and TIMESERIES_NORMALIZED
1770
    to dict_asset
1771

1772
    Notes
1773
    -----
1774
    Function tested with
1775
    - C0.test_compute_timeseries_properties_TIMESERIES_in_dict_asset()
1776
    - C0.test_compute_timeseries_properties_TIMESERIES_not_in_dict_asset()
1777
    """
1778

1779
    if TIMESERIES in dict_asset:
1✔
1780
        timeseries = dict_asset[TIMESERIES]
1✔
1781
        unit = dict_asset[UNIT]
1✔
1782

1783
        dict_asset.update(
1✔
1784
            {
1785
                TIMESERIES_PEAK: {
1786
                    VALUE: max(timeseries),
1787
                    UNIT: unit,
1788
                },
1789
                TIMESERIES_TOTAL: {
1790
                    VALUE: sum(timeseries),
1791
                    UNIT: unit,
1792
                },
1793
                TIMESERIES_AVERAGE: {
1794
                    VALUE: sum(timeseries) / len(timeseries),
1795
                    UNIT: unit,
1796
                },
1797
            }
1798
        )
1799

1800
        logging.debug("Normalizing timeseries of %s.", dict_asset[LABEL])
1✔
1801
        dict_asset.update(
1✔
1802
            {TIMESERIES_NORMALIZED: timeseries / dict_asset[TIMESERIES_PEAK][VALUE]}
1803
        )
1804
        # just to be sure!
1805
        if any(dict_asset[TIMESERIES_NORMALIZED].values) > 1:
1✔
1806
            logging.error(
×
1807
                f"{dict_asset[LABEL]} normalized timeseries has values greater than 1."
1808
            )
1809
        if any(dict_asset[TIMESERIES_NORMALIZED].values) < 0:
1✔
1810
            logging.error(
×
1811
                f"{dict_asset[LABEL]} normalized timeseries has negative values."
1812
            )
1813

1814

1815
def treat_multiple_flows(dict_asset, dict_values, parameter):
1✔
1816
    """
1817
    This function consider the case a technical parameter on the json file has a list of values because multiple
1818
    inputs or outputs busses are considered.
1819
    Parameters
1820
    ----------
1821
    dict_values:
1822
    dictionary of current values of the asset
1823
    parameter:
1824
    usually efficiency. Different efficiencies will be given if an asset has multiple inputs or outputs busses,
1825
    so a list must be considered.
1826

1827
    Returns
1828
    -------
1829

1830
    """
UNCOV
1831
    updated_values = []
×
1832
    values_info = (
×
1833
        []
1834
    )  # filenames and headers will be stored to allow keeping track of the timeseries generation
UNCOV
1835
    for element in dict_asset[parameter][VALUE]:
×
UNCOV
1836
        if isinstance(element, dict):
×
1837
            updated_values.append(
×
1838
                get_timeseries_multiple_flows(
1839
                    dict_values[SIMULATION_SETTINGS],
1840
                    dict_asset,
1841
                    element[FILENAME],
1842
                    element[HEADER],
1843
                )
1844
            )
1845
            values_info.append(element)
×
1846
        else:
UNCOV
1847
            updated_values.append(element)
×
UNCOV
1848
    dict_asset[parameter][VALUE] = updated_values
×
UNCOV
1849
    if len(values_info) > 0:
×
UNCOV
1850
        dict_asset[parameter].update({"values_info": values_info})
×
1851

1852

1853
# reads timeseries specifically when the need comes from a multiple or output busses situation
1854
# returns the timeseries. Does not update any dictionary
1855
def get_timeseries_multiple_flows(settings, dict_asset, file_name, header):
1✔
1856
    """
1857

1858
    Parameters
1859
    ----------
1860
    dict_asset:
1861
    dictionary of the asset
1862
    file_name:
1863
    name of the file to read the time series
1864
    header:
1865
    name of the column where the timeseries is provided
1866

1867
    Returns
1868
    -------
1869

1870
    """
UNCOV
1871
    file_path = os.path.join(settings[PATH_INPUT_FOLDER], TIME_SERIES, file_name)
×
UNCOV
1872
    C1.lookup_file(file_path, dict_asset[LABEL])
×
1873

1874
    # TODO if FILENAME is not defined
1875

UNCOV
1876
    data_set = pd.read_csv(file_path, sep=",")
×
UNCOV
1877
    if len(data_set.index) == settings[PERIODS]:
×
UNCOV
1878
        return pd.Series(data_set[header].values, index=settings[TIME_INDEX])
×
UNCOV
1879
    elif len(data_set.index) >= settings[PERIODS]:
×
UNCOV
1880
        return pd.Series(
×
1881
            data_set[header][0 : len(settings[TIME_INDEX])].values,
1882
            index=settings[TIME_INDEX],
1883
        )
UNCOV
1884
    elif len(data_set.index) <= settings[PERIODS]:
×
UNCOV
1885
        logging.critical(
×
1886
            "Input error! "
1887
            "Provided timeseries of %s (%s) shorter then evaluated period. "
1888
            "Operation terminated",
1889
            dict_asset[LABEL],
1890
            file_path,
1891
        )
UNCOV
1892
        sys.exit()
×
1893

1894

1895
def process_maximum_cap_constraint(dict_values, group, asset, subasset=None):
1✔
1896
    # ToDo: should function be split into separate processing and validation functions?
1897
    """
1898
    Processes the maximumCap constraint depending on its value.
1899

1900
    * If MaximumCap not in asset dict: MaximumCap is None
1901
    * If MaximumCap < installedCap: invalid, MaximumCapValueInvalid raised
1902
    * If MaximumCap == 0: invalid, MaximumCap is None
1903
    * If group == energyProduction and filename not in asset_dict (dispatchable assets): pass
1904
    * If group == energyProduction and filename in asset_dict (non-dispatchable assets): MaximumCapNormalized == MaximumCap*peak(timeseries), MaximumAddCapNormalized == MaximumAddCap*peak(timeseries)
1905

1906
    Parameters
1907
    ----------
1908
    dict_values: dict
1909
        dictionary of all assets
1910

1911
    group: str
1912
        Group that the asset belongs to (str). Used to acces sub-asset data and for error messages.
1913

1914
    asset: str
1915
        asset name
1916

1917
    subasset: str or None
1918
        subasset name.
1919
        Default: None.
1920

1921
    Notes
1922
    -----
1923
    Tested with:
1924
    - test_process_maximum_cap_constraint_maximumCap_undefined()
1925
    - test_process_maximum_cap_constraint_maximumCap_is_None()
1926
    - test_process_maximum_cap_constraint_maximumCap_is_int()
1927
    - test_process_maximum_cap_constraint_maximumCap_is_float()
1928
    - test_process_maximum_cap_constraint_maximumCap_is_0()
1929
    - test_process_maximum_cap_constraint_maximumCap_is_int_smaller_than_installed_cap()
1930
    - test_process_maximum_cap_constraint_group_is_ENERGY_PRODUCTION_fuel_source()
1931
    - test_process_maximum_cap_constraint_group_is_ENERGY_PRODUCTION_non_dispatchable_asset()
1932
    - test_process_maximum_cap_constraint_subasset()
1933

1934
    Returns
1935
    -------
1936
    Updates the asset dictionary.
1937

1938
    * Unit of MaximumCap is asset unit
1939

1940
    """
1941
    if subasset is None:
1✔
1942
        asset_dict = dict_values[group][asset]
1✔
1943
    else:
1944
        asset_dict = dict_values[group][asset][subasset]
1✔
1945

1946
    # include the maximumAddCap parameter to the asset dictionary
1947
    asset_dict.update({MAXIMUM_ADD_CAP: {VALUE: None, UNIT: asset_dict[UNIT]}})
1✔
1948

1949
    # check if a maximumCap is defined
1950
    if MAXIMUM_CAP not in asset_dict:
1✔
1951
        asset_dict.update({MAXIMUM_CAP: {VALUE: None}})
1✔
1952
    else:
1953
        if asset_dict[MAXIMUM_CAP][VALUE] is not None:
1✔
1954
            # maximum additional capacity = maximum total capacity - installed capacity
1955
            max_add_cap = (
1✔
1956
                asset_dict[MAXIMUM_CAP][VALUE] - asset_dict[INSTALLED_CAP][VALUE]
1957
            )
1958
            # include the maximumAddCap parameter to the asset dictionary
1959
            asset_dict[MAXIMUM_ADD_CAP].update({VALUE: max_add_cap})
1✔
1960
            # raise error if maximumCap is smaller than installedCap and is not set to zero
1961
            if (
1✔
1962
                asset_dict[MAXIMUM_CAP][VALUE] < asset_dict[INSTALLED_CAP][VALUE]
1963
                and asset_dict[MAXIMUM_CAP][VALUE] != 0
1964
            ):
1965
                message = (
1✔
1966
                    f"The stated total maximumCap in {group} {asset} is smaller than the "
1967
                    f"installedCap ({asset_dict[MAXIMUM_CAP][VALUE]}/{asset_dict[INSTALLED_CAP][VALUE]}). Please enter a greater maximumCap."
1968
                )
1969
                raise MaximumCapValueInvalid(message)
1✔
1970

1971
            # set maximumCap to None if it is zero
1972
            if asset_dict[MAXIMUM_CAP][VALUE] == 0:
1✔
1973
                message = (
1✔
1974
                    f"The stated maximumCap of zero in {group} {asset} is invalid."
1975
                    "For this simulation, the maximumCap will be "
1976
                    "disregarded and not be used in the simulation."
1977
                )
1978
                warnings.warn(UserWarning(message))
1✔
1979
                logging.warning(message)
1✔
1980
                asset_dict[MAXIMUM_CAP][VALUE] = None
1✔
1981

1982
            # adapt maximumCap and maximumAddCap of non-dispatchable sources
1983
            if (
1✔
1984
                group == ENERGY_PRODUCTION
1985
                and asset_dict.get(DISPATCHABILITY, True) is False
1986
                and asset_dict[MAXIMUM_CAP][VALUE] is not None
1987
            ):
1988
                max_cap_norm = (
1✔
1989
                    asset_dict[MAXIMUM_CAP][VALUE] * asset_dict[TIMESERIES_PEAK][VALUE]
1990
                )
1991
                asset_dict.update(
1✔
1992
                    {
1993
                        MAXIMUM_CAP_NORMALIZED: {
1994
                            VALUE: max_cap_norm,
1995
                            UNIT: asset_dict[UNIT],
1996
                        }
1997
                    }
1998
                )
1999
                logging.debug(
1✔
2000
                    f"Parameter {MAXIMUM_CAP} of asset '{asset_dict[LABEL]}' was multiplied by the peak value of {TIMESERIES} to obtain {MAXIMUM_CAP_NORMALIZED}. This was done as the aimed constraint is to limit the power, not the flow."
2001
                )
2002
                max_add_cap_norm = (
1✔
2003
                    asset_dict[MAXIMUM_ADD_CAP][VALUE]
2004
                    * asset_dict[TIMESERIES_PEAK][VALUE]
2005
                )
2006
                asset_dict.update(
1✔
2007
                    {
2008
                        MAXIMUM_ADD_CAP_NORMALIZED: {
2009
                            VALUE: max_add_cap_norm,
2010
                            UNIT: asset_dict[UNIT],
2011
                        }
2012
                    }
2013
                )
2014
                logging.debug(
1✔
2015
                    f"Parameter {MAXIMUM_ADD_CAP} of asset '{asset_dict[LABEL]}' was multiplied by the peak value of {TIMESERIES} to obtain {MAXIMUM_ADD_CAP_NORMALIZED}. This was done as the aimed constraint is to limit the power, not the flow."
2016
                )
2017

2018
    asset_dict[MAXIMUM_CAP].update({UNIT: asset_dict[UNIT]})
1✔
2019

2020

2021
def process_normalized_installed_cap(dict_values, group, asset, subasset=None):
1✔
2022
    """
2023
    Processes the normalized installed capacity value based on the installed capacity value and the chosen timeseries.
2024

2025
    Parameters
2026
    ----------
2027
    dict_values: dict
2028
        dictionary of all assets
2029

2030
    group: str
2031
        Group that the asset belongs to (str). Used to acces sub-asset data and for error messages.
2032

2033
    asset: str
2034
        asset name
2035

2036
    subasset: str or None
2037
        subasset name.
2038
        Default: None.
2039

2040
    Notes
2041
    -----
2042
    Tested with:
2043
    - test_process_normalized_installed_cap()
2044

2045
    Returns
2046
    -------
2047
    Updates the asset dictionary with the normalizedInstalledCap value.
2048

2049
    """
2050
    if subasset is None:
1✔
2051
        asset_dict = dict_values[group][asset]
1✔
2052
    else:
UNCOV
2053
        asset_dict = dict_values[group][asset][subasset]
×
2054

2055
    if asset_dict[FILENAME] is not None:
1✔
2056
        inst_cap_norm = (
1✔
2057
            asset_dict[INSTALLED_CAP][VALUE] * asset_dict[TIMESERIES_PEAK][VALUE]
2058
        )
2059
        asset_dict.update(
1✔
2060
            {INSTALLED_CAP_NORMALIZED: {VALUE: inst_cap_norm, UNIT: asset_dict[UNIT]}}
2061
        )
2062
        logging.debug(
1✔
2063
            f"Parameter {INSTALLED_CAP} ({asset_dict[INSTALLED_CAP][VALUE]}) of asset '{asset_dict[LABEL]}' was multiplied by the peak value of {TIMESERIES} to obtain {INSTALLED_CAP_NORMALIZED}  ({asset_dict[INSTALLED_CAP_NORMALIZED][VALUE]})."
2064
        )
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