• 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

80.93
/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
        define_sink(
1✔
294
            dict_values=dict_values,
295
            asset_key=excess_sink_name,
296
            price={VALUE: 0, UNIT: CURR + "/" + UNIT},
297
            inflow_direction=bus,
298
            energy_vector=energy_vector,
299
        )
300
        dict_values[ENERGY_BUSSES][bus].update({EXCESS_SINK: excess_sink_name})
1✔
301
        auto_sinks.append(excess_sink_name)
1✔
302
        logging.debug(
1✔
303
            f"Created excess sink for energy bus {bus}, connected to {ENERGY_VECTOR} {energy_vector}."
304
        )
305
    return auto_sinks
1✔
306

307

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

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

328
        # in case there is only one parameter provided (input bus and one output bus)
329
        if (
1✔
330
            FILENAME in dict_values[group][asset][EFFICIENCY]
331
            and HEADER in dict_values[group][asset][EFFICIENCY]
332
        ):
333
            receive_timeseries_from_csv(
×
334
                dict_values[SIMULATION_SETTINGS], dict_values[group][asset], EFFICIENCY,
335
            )
336
        # 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)
337
        # dictionaries with filenames and headers will be replaced by timeseries, scalars will be mantained
338
        elif isinstance(dict_values[group][asset][EFFICIENCY][VALUE], list):
1✔
339
            treat_multiple_flows(dict_values[group][asset], dict_values, EFFICIENCY)
×
340

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

351

352
def energyProduction(dict_values, group):
1✔
353
    """
354

355
    :param dict_values:
356
    :param group:
357
    :return:
358
    """
359
    for asset in dict_values[group]:
1✔
360
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
361
        evaluate_lifetime_costs(
1✔
362
            dict_values[SIMULATION_SETTINGS],
363
            dict_values[ECONOMIC_DATA],
364
            dict_values[group][asset],
365
        )
366

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

387

388
def energyStorage(dict_values, group):
1✔
389
    """
390

391
    :param dict_values:
392
    :param group:
393
    :return:
394
    """
395
    for asset in dict_values[group]:
1✔
396
        for subasset in [STORAGE_CAPACITY, INPUT_POWER, OUTPUT_POWER]:
1✔
397
            define_missing_cost_data(
1✔
398
                dict_values, dict_values[group][asset][subasset],
399
            )
400
            evaluate_lifetime_costs(
1✔
401
                dict_values[SIMULATION_SETTINGS],
402
                dict_values[ECONOMIC_DATA],
403
                dict_values[group][asset][subasset],
404
            )
405

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

432

433
def energyProviders(dict_values, group):
1✔
434
    """
435

436
    :param dict_values:
437
    :param group:
438
    :return:
439
    """
440
    # add sources and sinks depending on items in energy providers as pre-processing
441
    for asset in dict_values[group]:
1✔
442
        define_auxiliary_assets_of_energy_providers(dict_values, dso_name=asset)
1✔
443

444
        # Add lifetime capex (incl. replacement costs), calculate annuity
445
        # (incl. om), and simulation annuity to each asset
446
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
447
        evaluate_lifetime_costs(
1✔
448
            dict_values[SIMULATION_SETTINGS],
449
            dict_values[ECONOMIC_DATA],
450
            dict_values[group][asset],
451
        )
452

453

454
def energyConsumption(dict_values, group):
1✔
455
    """
456

457
    :param dict_values:
458
    :param group:
459
    :return:
460
    """
461
    for asset in dict_values[group]:
1✔
462
        define_missing_cost_data(dict_values, dict_values[group][asset])
1✔
463
        evaluate_lifetime_costs(
1✔
464
            dict_values[SIMULATION_SETTINGS],
465
            dict_values[ECONOMIC_DATA],
466
            dict_values[group][asset],
467
        )
468
        if INFLOW_DIRECTION not in dict_values[group][asset]:
1✔
469
            dict_values[group][asset].update(
×
470
                {INFLOW_DIRECTION: dict_values[group][asset][ENERGY_VECTOR]}
471
            )
472

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

489

490
def define_missing_cost_data(dict_values, dict_asset):
1✔
491
    """
492

493
    :param dict_values:
494
    :param dict_asset:
495
    :return:
496
    """
497

498
    # read timeseries with filename provided for variable costs.
499
    # if multiple dispatch_price are given for multiple busses, it checks if any v
500
    # alue is a timeseries
501
    if DISPATCH_PRICE in dict_asset:
1✔
502
        if isinstance(dict_asset[DISPATCH_PRICE][VALUE], dict):
1✔
503
            receive_timeseries_from_csv(
×
504
                dict_values[SIMULATION_SETTINGS], dict_asset, DISPATCH_PRICE,
505
            )
506
        elif isinstance(dict_asset[DISPATCH_PRICE][VALUE], list):
1✔
507
            treat_multiple_flows(dict_asset, dict_values, DISPATCH_PRICE)
×
508

509
    economic_data = dict_values[ECONOMIC_DATA]
1✔
510

511
    basic_costs = {
1✔
512
        OPTIMIZE_CAP: {VALUE: False, UNIT: TYPE_BOOL},
513
        UNIT: "?",
514
        INSTALLED_CAP: {VALUE: 0.0, UNIT: UNIT},
515
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
516
        SPECIFIC_COSTS: {VALUE: 0, UNIT: CURR + "/" + UNIT},
517
        SPECIFIC_COSTS_OM: {VALUE: 0, UNIT: CURR + "/" + UNIT_YEAR},
518
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + UNIT + "/" + UNIT_YEAR},
519
        LIFETIME: {VALUE: economic_data[PROJECT_DURATION][VALUE], UNIT: UNIT_YEAR,},
520
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR,},
521
    }
522

523
    # checks that an asset has all cost parameters needed for evaluation.
524
    # Adds standard values.
525
    str = ""
1✔
526
    for cost in basic_costs:
1✔
527
        if cost not in dict_asset:
1✔
528
            dict_asset.update({cost: basic_costs[cost]})
1✔
529
            str = str + " " + cost
1✔
530

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

534

535
def add_assets_to_asset_dict_of_connected_busses(dict_values):
1✔
536
    """
537
    This function adds the assets of the different asset groups to the asset dict of ENERGY_BUSSES.
538
    The asset groups are: ENERGY_CONVERSION, ENERGY_PRODUCTION, ENERGY_CONSUMPTION, ENERGY_PROVIDERS, ENERGY_STORAGE
539

540
    Parameters
541
    ----------
542
    dict_values: dict
543
        Dictionary with all simulation information
544

545
    Returns
546
    -------
547
    Extends dict_values[ENERGY_BUSSES] by an asset_dict that includes all connected assets.
548

549
    Notes
550
    -----
551
    Tested with:
552
    - C0.test_add_assets_to_asset_dict_of_connected_busses()
553
    """
554
    for group in [
1✔
555
        ENERGY_CONVERSION,
556
        ENERGY_PRODUCTION,
557
        ENERGY_CONSUMPTION,
558
        ENERGY_PROVIDERS,
559
        ENERGY_STORAGE,
560
    ]:
561
        for asset in dict_values[group]:
1✔
562
            add_asset_to_asset_dict_for_each_flow_direction(
1✔
563
                dict_values, dict_values[group][asset], asset
564
            )
565

566

567
def add_asset_to_asset_dict_for_each_flow_direction(dict_values, dict_asset, asset_key):
1✔
568
    """
569
    Add asset to the asset dict of the busses connected to the INPUT_DIRECTION and OUTPUT_DIRECTION of the asset.
570

571
    Parameters
572
    ----------
573
    dict_values: dict
574
        All simulation information
575

576
    dict_asset: dict
577
        All information of the current asset
578

579
    asset_key: str
580
        Key that calls the dict_asset from dict_values[asset_group][key]
581

582
    Returns
583
    -------
584
    Updated dict_values, with dict_values[ENERGY_BUSSES] now including asset dictionaries for each asset connected to a bus.
585

586
    Notes
587
    -----
588
    Tested with:
589
    - C0.test_add_asset_to_asset_dict_for_each_flow_direction()
590
    """
591

592
    # The asset needs to be added both to the inflow as well as the outflow bus:
593
    for direction in [INFLOW_DIRECTION, OUTFLOW_DIRECTION]:
1✔
594
        # Check if the asset has an INFLOW_DIRECTION or OUTFLOW_DIRECTION
595
        if direction in dict_asset:
1✔
596
            bus = dict_asset[direction]
1✔
597
            # Check if a list ob busses is in INFLOW_DIRECTION or OUTFLOW_DIRECTION
598
            if isinstance(bus, list):
1✔
599
                # If true: All busses need to be checked
600
                bus_list = []
1✔
601
                # Checking each bus of the list
602
                for subbus in bus:
1✔
603
                    # Append bus name to bus_list
604
                    bus_list.append(subbus)
1✔
605
                    # Check if bus of the direction is already contained in energyBusses
606
                    add_asset_to_asset_dict_of_bus(
1✔
607
                        bus=subbus,
608
                        dict_values=dict_values,
609
                        asset_key=asset_key,
610
                        asset_label=dict_asset[LABEL],
611
                    )
612

613
            # If false: Only one bus
614
            else:
615
                # Check if bus of the direction is already contained in energyBusses
616
                add_asset_to_asset_dict_of_bus(
1✔
617
                    bus=bus,
618
                    dict_values=dict_values,
619
                    asset_key=asset_key,
620
                    asset_label=dict_asset[LABEL],
621
                )
622

623

624
def add_asset_to_asset_dict_of_bus(bus, dict_values, asset_key, asset_label):
1✔
625
    """
626
    Adds asset key and label to a bus defined by `energyBusses.csv`
627
    Sends an error message if the bus was not included in `energyBusses.csv`
628

629
    Parameters
630
    ----------
631
    dict_values: dict
632
        Dict of all simulation parameters
633

634
    bus: str
635
        A bus label
636

637
    asset_key: str
638
        Key with with an dict_asset would be called from dict_values[groups][key]
639

640
    asset_label: str
641
        Label of the asset
642

643
    Returns
644
    -------
645
    Updated dict_values[ENERGY_BUSSES] by adding an asset to the busses` ASSET DICT
646

647
    EnergyBusses now has following keys: LABEL, ENERGY_VECTOR, ASSET_DICT
648

649
    Notes
650
    -----
651
    Tested with:
652
    - C0.test_add_asset_to_asset_dict_of_bus()
653
    - C0.test_add_asset_to_asset_dict_of_bus_ValueError()
654
    """
655
    # If bus not defined in `energyBusses.csv` display error message
656
    if bus not in dict_values[ENERGY_BUSSES]:
1✔
657
        bus_string = ", ".join(map(str, dict_values[ENERGY_BUSSES].keys()))
1✔
658
        msg = (
1✔
659
            f"Asset {asset_key} has an inflow or outflow direction of {bus}. "
660
            f"This bus is not defined in `energyBusses.csv`: {bus_string}. "
661
            f"You may either have a typo in one of the files or need to add a bus to `energyBusses.csv`."
662
        )
663
        raise ValueError(msg)
1✔
664

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

669
    # Asset should added to respective bus
670
    dict_values[ENERGY_BUSSES][bus][ASSET_DICT].update({asset_key: asset_label})
1✔
671
    logging.debug(f"Added asset {asset_label} to bus {bus}")
1✔
672

673

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

678
    Parameters
679
    ----------
680
    dict_values: dict
681
        All simulation parameters
682

683
    dso_name: str
684
        the name of the energy provider asset
685

686
    Returns
687
    -------
688
    Updated dict_values
689

690
    Notes
691
    -----
692
    This function is tested with following pytests:
693
    - C0.test_define_auxiliary_assets_of_energy_providers()
694
    - C0.test_determine_months_in_a_peak_demand_pricing_period_not_valid()
695
    - C0.test_determine_months_in_a_peak_demand_pricing_period_valid()
696
    - C0.test_define_availability_of_peak_demand_pricing_assets_yearly()
697
    - C0.test_define_availability_of_peak_demand_pricing_assets_monthly()
698
    - C0.test_define_availability_of_peak_demand_pricing_assets_quarterly()
699
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_1_period()
700
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_2_periods()
701
    - C0.test_define_transformer_for_peak_demand_pricing()
702
    - C0.test_define_source()
703
    - C0.test_define_source_exception_unknown_bus()
704
    - C0.test_define_source_timeseries_not_None()
705
    - C0.test_define_source_price_not_None_but_with_scalar_value()
706
    - C0.test_define_sink() -> incomplete
707
    - C0.test_change_sign_of_feedin_tariff_positive_value()
708
    - C0.test_change_sign_of_feedin_tariff_negative_value()
709
    - C0.test_change_sign_of_feedin_tariff_zero()
710
    """
711

712
    dso_dict = dict_values[ENERGY_PROVIDERS][dso_name]
1✔
713

714
    number_of_pricing_periods = dso_dict[PEAK_DEMAND_PRICING_PERIOD][VALUE]
1✔
715

716
    months_in_a_period = determine_months_in_a_peak_demand_pricing_period(
1✔
717
        number_of_pricing_periods,
718
        dict_values[SIMULATION_SETTINGS][EVALUATED_PERIOD][VALUE],
719
    )
720

721
    dict_availability_timeseries = define_availability_of_peak_demand_pricing_assets(
1✔
722
        dict_values, number_of_pricing_periods, months_in_a_period,
723
    )
724

725
    list_of_dso_energyConversion_assets = add_a_transformer_for_each_peak_demand_pricing_period(
1✔
726
        dict_values, dso_dict, dict_availability_timeseries,
727
    )
728

729
    define_source(
1✔
730
        dict_values=dict_values,
731
        asset_key=dso_name + DSO_CONSUMPTION,
732
        outflow_direction=peak_demand_bus_name(dso_dict[OUTFLOW_DIRECTION]),
733
        price=dso_dict[ENERGY_PRICE],
734
        energy_vector=dso_dict[ENERGY_VECTOR],
735
        emission_factor=dso_dict[EMISSION_FACTOR],
736
    )
737
    dict_feedin = change_sign_of_feedin_tariff(dso_dict[FEEDIN_TARIFF], dso_name)
1✔
738

739
    inflow_bus_name = peak_demand_bus_name(dso_dict[INFLOW_DIRECTION], feedin=True)
1✔
740

741
    # define feed-in sink of the DSO
742
    define_sink(
1✔
743
        dict_values=dict_values,
744
        asset_key=dso_name + DSO_FEEDIN,
745
        price=dict_feedin,
746
        inflow_direction=inflow_bus_name,
747
        specific_costs={VALUE: 0, UNIT: CURR + "/" + UNIT},
748
        energy_vector=dso_dict[ENERGY_VECTOR],
749
    )
750
    dso_dict.update(
1✔
751
        {
752
            CONNECTED_CONSUMPTION_SOURCE: dso_name + DSO_CONSUMPTION,
753
            CONNECTED_PEAK_DEMAND_PRICING_TRANSFORMERS: list_of_dso_energyConversion_assets,
754
            CONNECTED_FEEDIN_SINK: dso_name + DSO_FEEDIN,
755
        }
756
    )
757

758

759
def change_sign_of_feedin_tariff(dict_feedin_tariff, dso):
1✔
760
    r"""
761
    Change the sign of the feed-in tariff.
762
    Additionally, prints a logging.warning in case of the feed-in tariff is entered as
763
    negative value in 'energyProviders.csv'.
764

765
    Parameters
766
    ----------
767
    dict_feedin_tariff: dict
768
        Dict of feedin tariff with Unit-value pair
769

770
    dso: str
771
        Name of the energy provider
772

773
    Returns
774
    -------
775
    dict_feedin_tariff: dict
776
        Dict of feedin tariff, to be used as input to C0.define_sink
777

778
    Notes
779
    -----
780
    Tested with:
781
    - C0.test_change_sign_of_feedin_tariff_positive_value()
782
    - C0.test_change_sign_of_feedin_tariff_negative_value()
783
    - C0.test_change_sign_of_feedin_tariff_zero()
784
    """
785

786
    if isinstance(dict_feedin_tariff[VALUE], pd.Series) is False:
1✔
787
        if dict_feedin_tariff[VALUE] > 0:
1✔
788
            # Add a debug message in case the feed-in is interpreted as revenue-inducing.
789
            logging.debug(
1✔
790
                f"The {FEEDIN_TARIFF} of {dso} is positive, which means that feeding into the grid results in a revenue stream."
791
            )
792
        elif dict_feedin_tariff[VALUE] == 0:
1✔
793
            # Add a warning msg in case the feedin induces expenses rather than revenue
794
            logging.warning(
1✔
795
                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."
796
            )
797
        elif dict_feedin_tariff[VALUE] < 0:
1✔
798
            # Add a warning msg in case the feedin induces expenses rather than revenue
799
            logging.warning(
1✔
800
                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."
801
            )
802
        else:
803
            pass
1✔
804
    else:
805
        if (dict_feedin_tariff[VALUE] < 0).any():
×
806
            # Add a warning msg in case the feedin induces expenses rather than revenue
807
            ts_info = ", ".join(
×
808
                dict_feedin_tariff[VALUE]
809
                .loc[dict_feedin_tariff[VALUE] < 0]
810
                .index.astype(str)
811
            )
812
            logging.warning(
×
813
                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."
814
            )
815
        elif (dict_feedin_tariff[VALUE] < 0).any():
×
816
            # Add a warning msg in case the feedin induces expenses rather than revenue
817
            ts_info = ", ".join(
×
818
                dict_feedin_tariff[VALUE]
819
                .loc[dict_feedin_tariff[VALUE] < 0]
820
                .index.astype(str)
821
            )
822
            logging.warning(
×
823
                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."
824
            )
825

826
    dict_feedin_tariff = {
1✔
827
        VALUE: -dict_feedin_tariff[VALUE],
828
        UNIT: dict_feedin_tariff[UNIT],
829
    }
830
    return dict_feedin_tariff
1✔
831

832

833
def define_availability_of_peak_demand_pricing_assets(
1✔
834
    dict_values, number_of_pricing_periods, months_in_a_period
835
):
836
    r"""
837
    Determined the availability timeseries for the later to be defined dso assets for taking into account the peak demand pricing periods.
838

839
    Parameters
840
    ----------
841
    dict_values: dict
842
        All simulation inputs
843
    number_of_pricing_periods: int
844
        Number of pricing periods in a year. Valid: 1,2,3,4,6,12
845
    months_in_a_period: int
846
        Duration of a period
847

848
    Returns
849
    -------
850
    dict_availability_timeseries: dict
851
        Dict with all availability timeseries for each period
852

853
    """
854
    dict_availability_timeseries = {}
1✔
855
    for period in range(1, number_of_pricing_periods + 1):
1✔
856
        availability_in_period = pd.Series(
1✔
857
            0, index=dict_values[SIMULATION_SETTINGS][TIME_INDEX]
858
        )
859
        time_period = pd.date_range(
1✔
860
            # Period start
861
            start=dict_values[SIMULATION_SETTINGS][START_DATE]
862
            + pd.DateOffset(months=(period - 1) * months_in_a_period),
863
            # Period end, with months_in_a_period durartion
864
            end=dict_values[SIMULATION_SETTINGS][START_DATE]
865
            + pd.DateOffset(months=(period) * months_in_a_period, hours=-1),
866
            freq=str(dict_values[SIMULATION_SETTINGS][TIMESTEP][VALUE]) + UNIT_MINUTE,
867
        )
868

869
        availability_in_period = availability_in_period.add(
1✔
870
            pd.Series(1, index=time_period), fill_value=0
871
        ).loc[dict_values[SIMULATION_SETTINGS][TIME_INDEX]]
872
        dict_availability_timeseries.update({period: availability_in_period})
1✔
873

874
    return dict_availability_timeseries
1✔
875

876

877
def add_a_transformer_for_each_peak_demand_pricing_period(
1✔
878
    dict_values, dict_dso, dict_availability_timeseries
879
):
880
    r"""
881
    Adds transformers that are supposed to model the peak_demand_pricing periods for each period.
882
    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.
883

884
    Parameters
885
    ----------
886
    dict_values: dict
887
        dict with all simulation parameters
888

889
    dict_dso: dict
890
        dict with all info on the specific dso at hand
891

892
    dict_availability_timeseries: dict
893
        dict with all availability timeseries for each period
894

895
    Returns
896
    -------
897
    list_of_dso_energyConversion_assets: list
898
        List of names of newly added energy conversion assets,
899

900
    Updated dict_values with a transformer for each peak demand pricing period
901

902
    Notes
903
    -----
904

905
    Tested by:
906
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_1_period
907
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_2_periods
908
    """
909

910
    list_of_dso_energyConversion_assets = []
1✔
911
    for key in dict_availability_timeseries.keys():
1✔
912

913
        if len(dict_availability_timeseries.keys()) > 1:
1✔
914
            transformer_name = peak_demand_transformer_name(
1✔
915
                dict_dso[LABEL], peak_number=key
916
            )
917
        else:
918
            transformer_name = peak_demand_transformer_name(dict_dso[LABEL])
1✔
919

920
        define_transformer_for_peak_demand_pricing(
1✔
921
            dict_values=dict_values,
922
            dict_dso=dict_dso,
923
            transformer_name=transformer_name,
924
            timeseries_availability=dict_availability_timeseries[key],
925
        )
926

927
        list_of_dso_energyConversion_assets.append(transformer_name)
1✔
928

929
    logging.debug(
1✔
930
        f"The peak demand pricing price of {dict_dso[PEAK_DEMAND_PRICING][VALUE]} {dict_values[ECONOMIC_DATA][CURR]} "
931
        f"is set as specific_costs_om of the peak demand pricing transformers of the DSO."
932
    )
933
    return list_of_dso_energyConversion_assets
1✔
934

935

936
def determine_months_in_a_peak_demand_pricing_period(
1✔
937
    number_of_pricing_periods, simulation_period_lenght
938
):
939
    r"""
940
    Check if the number of peak demand pricing periods is valid.
941
    Warns user that in case the number of periods exceeds 1 but the simulation time is not a year,
942
    there could be an unexpected number of timeseries considered.
943
    Raises error if number of peak demand pricing periods is not valid.
944

945
    Parameters
946
    ----------
947
    number_of_pricing_periods: int
948
        Defined in csv, is number of pricing periods within a year
949
    simulation_period_lenght: int
950
        Defined in csv, is number of days of the simulation
951

952
    Returns
953
    -------
954
    months_in_a_period: float
955
        Number of months that make a period, will be used to determine availability of dso assets
956
    """
957

958
    # check number of pricing periods - if >1 the simulation has to cover a whole year!
959
    if number_of_pricing_periods > 1:
1✔
960
        if simulation_period_lenght != 365:
1✔
961
            logging.debug(
1✔
962
                f"\n Message for dev: Following warning is not technically true, "
963
                f"as the evaluation period has to approximately be "
964
                f"larger than 365/peak demand pricing periods (see #331)."
965
            )
966
            logging.warning(
1✔
967
                f"You have chosen a number of peak demand pricing periods > 1."
968
                f"Please be advised that if you are not simulating for a year (365d)"
969
                f"an possibly unexpected number of periods will be considered."
970
            )
971

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

978
    # defines the number of months that one period constists of.
979
    months_in_a_period = 12 / number_of_pricing_periods
1✔
980
    logging.info(
1✔
981
        "Peak demand pricing is taking place %s times per year, ie. every %s "
982
        "months.",
983
        number_of_pricing_periods,
984
        months_in_a_period,
985
    )
986
    return months_in_a_period
1✔
987

988

989
def define_transformer_for_peak_demand_pricing(
1✔
990
    dict_values, dict_dso, transformer_name, timeseries_availability
991
):
992
    r"""
993
    Defines a transformer for peak demand pricing in energyConverion
994

995
    Parameters
996
    ----------
997
    dict_values: dict
998
        All simulation parameters
999

1000
    dict_dso: dict
1001
        All values connected to the DSO
1002

1003
    transformer_name: str
1004
        label of the transformer to be added
1005

1006
    timeseries_availability: pd.Series
1007
        Timeseries of transformer availability. Introduced to cover peak demand pricing.
1008

1009
    Returns
1010
    -------
1011
    Updated dict_values with newly added transformer asset in the energyConversion asset group.
1012
    """
1013

1014
    dso_consumption_transformer = {
1✔
1015
        LABEL: transformer_name,
1016
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1017
        INSTALLED_CAP: {VALUE: 0, UNIT: dict_dso[UNIT]},
1018
        INFLOW_DIRECTION: peak_demand_bus_name(dict_dso[INFLOW_DIRECTION]),
1019
        OUTFLOW_DIRECTION: dict_dso[OUTFLOW_DIRECTION],
1020
        AVAILABILITY_DISPATCH: timeseries_availability,
1021
        EFFICIENCY: {VALUE: 1, UNIT: "factor"},
1022
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
1023
        SPECIFIC_COSTS: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT],},
1024
        # the demand pricing is split between consumption and feedin
1025
        SPECIFIC_COSTS_OM: {
1026
            VALUE: dict_dso[PEAK_DEMAND_PRICING][VALUE] / 2,
1027
            UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_YEAR,
1028
        },
1029
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_HOUR},
1030
        OEMOF_ASSET_TYPE: OEMOF_TRANSFORMER,
1031
        ENERGY_VECTOR: dict_dso[ENERGY_VECTOR],
1032
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR},
1033
    }
1034

1035
    dict_values[ENERGY_CONVERSION].update(
1✔
1036
        {transformer_name: dso_consumption_transformer}
1037
    )
1038

1039
    logging.debug(
1✔
1040
        f"Model for peak demand pricing on consumption side: Adding transfomer {transformer_name}."
1041
    )
1042

1043
    transformer_name = transformer_name.replace(DSO_CONSUMPTION, DSO_FEEDIN)
1✔
1044
    dso_feedin_transformer = {
1✔
1045
        LABEL: transformer_name,
1046
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1047
        INSTALLED_CAP: {VALUE: 0, UNIT: dict_dso[UNIT]},
1048
        INFLOW_DIRECTION: dict_dso[INFLOW_DIRECTION],
1049
        OUTFLOW_DIRECTION: peak_demand_bus_name(
1050
            dict_dso[INFLOW_DIRECTION], feedin=True
1051
        ),
1052
        AVAILABILITY_DISPATCH: timeseries_availability,
1053
        EFFICIENCY: {VALUE: 1, UNIT: "factor"},
1054
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
1055
        SPECIFIC_COSTS: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT],},
1056
        # the demand pricing is split between consumption and feedin
1057
        SPECIFIC_COSTS_OM: {
1058
            VALUE: dict_dso[PEAK_DEMAND_PRICING][VALUE] / 2,
1059
            UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_YEAR,
1060
        },
1061
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_HOUR},
1062
        OEMOF_ASSET_TYPE: OEMOF_TRANSFORMER,
1063
        ENERGY_VECTOR: dict_dso[ENERGY_VECTOR],
1064
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR},
1065
        # LIFETIME: {VALUE: 100, UNIT: UNIT_YEAR},
1066
    }
1067
    if dict_dso.get(DSO_FEEDIN_CAP, None) is not None:
1✔
1068
        dso_feedin_transformer[MAXIMUM_CAP] = {
×
1069
            VALUE: dict_dso[DSO_FEEDIN_CAP][VALUE],
1070
            UNIT: dict_dso[UNIT],
1071
        }
1072

1073
        logging.info(
×
1074
            f"Capping {dict_dso[LABEL]} feedin with maximum capacity {dict_dso[DSO_FEEDIN_CAP][VALUE]}"
1075
        )
1076

1077
    dict_values[ENERGY_CONVERSION].update({transformer_name: dso_feedin_transformer})
1✔
1078

1079
    logging.debug(
1✔
1080
        f"Model for peak demand pricing on feedin side: Adding transfomer {transformer_name}."
1081
    )
1082

1083

1084
def define_source(
1✔
1085
    dict_values,
1086
    asset_key,
1087
    outflow_direction,
1088
    energy_vector,
1089
    emission_factor,
1090
    price=None,
1091
    timeseries=None,
1092
):
1093
    r"""
1094
    Defines a source with default input values. If kwargs are given, the default values are overwritten.
1095

1096
    Parameters
1097
    ----------
1098
    dict_values: dict
1099
        Dictionary to which source should be added, with all simulation parameters
1100

1101
    asset_key: str
1102
        key under which the asset is stored in the asset group
1103

1104
    energy_vector: str
1105
        Energy vector the new asset should belong to
1106

1107
    emission_factor : dict
1108
        Dict with a unit-value pair of the emission factor of the new asset
1109

1110
    price: dict
1111
        Dict with a unit-value pair of the dispatch price of the source.
1112
        The value can also be defined though FILENAME and HEADER, making the value of the price a timeseries.
1113
        Default: None
1114

1115
    timeseries: pd.Dataframe
1116
        Timeseries defining the availability of the source. Currently not used.
1117
        Default: None
1118

1119
    Returns
1120
    -------
1121
    Updates dict_values[ENERGY_BUSSES] if outflow_direction not in it
1122
    Standard source defined as:
1123

1124
    Notes
1125
    -----
1126
    The pytests for this function are not complete. It is started with:
1127
    - C0.test_define_source()
1128
    - C0.test_define_source_exception_unknown_bus()
1129
    - C0.test_define_source_timeseries_not_None()
1130
    - C0.test_define_source_price_not_None_but_with_scalar_value()
1131
    Missing:
1132
    - C0.test_define_source_price_not_None_but_timeseries(), ie. value defined by FILENAME and HEADER
1133
    """
1134
    default_source_dict = {
1✔
1135
        OEMOF_ASSET_TYPE: OEMOF_SOURCE,
1136
        LABEL: asset_key,
1137
        OUTFLOW_DIRECTION: outflow_direction,
1138
        DISPATCHABILITY: True,
1139
        LIFETIME: {
1140
            VALUE: dict_values[ECONOMIC_DATA][PROJECT_DURATION][VALUE],
1141
            UNIT: UNIT_YEAR,
1142
        },
1143
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1144
        MAXIMUM_CAP: {VALUE: None, UNIT: "?"},
1145
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR,},
1146
        ENERGY_VECTOR: energy_vector,
1147
        EMISSION_FACTOR: emission_factor,
1148
    }
1149

1150
    if outflow_direction not in dict_values[ENERGY_BUSSES]:
1✔
1151
        dict_values[ENERGY_BUSSES].update(
1✔
1152
            {
1153
                outflow_direction: {
1154
                    LABEL: outflow_direction,
1155
                    ENERGY_VECTOR: energy_vector,
1156
                    ASSET_DICT: {asset_key: asset_key},
1157
                }
1158
            }
1159
        )
1160

1161
    if price is not None:
1✔
1162

1163
        if FILENAME in price and HEADER in price:
1✔
1164
            price.update(
×
1165
                {
1166
                    VALUE: get_timeseries_multiple_flows(
1167
                        dict_values[SIMULATION_SETTINGS],
1168
                        default_source_dict,
1169
                        price[FILENAME],
1170
                        price[HEADER],
1171
                    )
1172
                }
1173
            )
1174
        determine_dispatch_price(dict_values, price, default_source_dict)
1✔
1175

1176
    if timeseries is not None:
1✔
1177
        # This part is currently not used.
1178
        default_source_dict.update({DISPATCHABILITY: False})
1✔
1179
        logging.debug(
1✔
1180
            f"{default_source_dict[LABEL]} can provide a total generation of {sum(timeseries.values)}"
1181
        )
1182
        default_source_dict[OPTIMIZE_CAP].update({VALUE: True})
1✔
1183
        default_source_dict.update(
1✔
1184
            {
1185
                TIMESERIES_PEAK: {VALUE: max(timeseries), UNIT: "kW"},
1186
                TIMESERIES_NORMALIZED: timeseries / max(timeseries),
1187
            }
1188
        )
1189
        if DISPATCH_PRICE in default_source_dict and max(timeseries) != 0:
1✔
1190
            default_source_dict[DISPATCH_PRICE].update(
1✔
1191
                {VALUE: default_source_dict[DISPATCH_PRICE][VALUE] / max(timeseries)}
1192
            )
1193

1194
    dict_values[ENERGY_PRODUCTION].update({asset_key: default_source_dict})
1✔
1195

1196
    logging.info(
1✔
1197
        f"Asset {default_source_dict[LABEL]} was added to the energyProduction assets."
1198
    )
1199

1200
    apply_function_to_single_or_list(
1✔
1201
        function=add_asset_to_asset_dict_of_bus,
1202
        parameter=outflow_direction,
1203
        dict_values=dict_values,
1204
        asset_key=asset_key,
1205
        asset_label=default_source_dict[LABEL],
1206
    )
1207

1208

1209
def determine_dispatch_price(dict_values, price, source):
1✔
1210
    """
1211
    This function needs to be re-evaluated.
1212

1213
    Parameters
1214
    ----------
1215
    dict_values
1216
    price
1217
    source
1218

1219
    Returns
1220
    -------
1221

1222
    """
1223
    # check if multiple busses are provided
1224
    # for each bus, read time series for dispatch_price if a file name has been
1225
    # provided in energy price
1226
    if isinstance(price[VALUE], list):
1✔
1227
        source.update({DISPATCH_PRICE: {VALUE: [], UNIT: price[UNIT]}})
×
1228
        values_info = []
×
1229
        for element in price[VALUE]:
×
1230
            if isinstance(element, dict):
×
1231
                source[DISPATCH_PRICE][VALUE].append(
×
1232
                    get_timeseries_multiple_flows(
1233
                        dict_values[SIMULATION_SETTINGS],
1234
                        source,
1235
                        element[FILENAME],
1236
                        element[HEADER],
1237
                    )
1238
                )
1239
                values_info.append(element)
×
1240
            else:
1241
                source[DISPATCH_PRICE][VALUE].append(element)
×
1242
        if len(values_info) > 0:
×
1243
            source[DISPATCH_PRICE]["values_info"] = values_info
×
1244

1245
    elif isinstance(price[VALUE], dict):
1✔
1246
        source.update(
×
1247
            {
1248
                DISPATCH_PRICE: {
1249
                    VALUE: {
1250
                        FILENAME: price[VALUE][FILENAME],
1251
                        HEADER: price[VALUE][HEADER],
1252
                    },
1253
                    UNIT: price[UNIT],
1254
                }
1255
            }
1256
        )
1257
        receive_timeseries_from_csv(
×
1258
            dict_values[SIMULATION_SETTINGS], source, DISPATCH_PRICE
1259
        )
1260
    else:
1261
        source.update({DISPATCH_PRICE: {VALUE: price[VALUE], UNIT: price[UNIT]}})
1✔
1262

1263
    if type(source[DISPATCH_PRICE][VALUE]) == pd.Series:
1✔
1264
        logging.debug(
×
1265
            f"{source[LABEL]} was created, with a price defined as a timeseries (average: {source[DISPATCH_PRICE][VALUE].mean()})."
1266
        )
1267
    else:
1268
        logging.debug(
1✔
1269
            f"{source[LABEL]} was created, with a price of {source[DISPATCH_PRICE][VALUE]}."
1270
        )
1271

1272

1273
def define_sink(
1✔
1274
    dict_values, asset_key, price, inflow_direction, energy_vector, **kwargs
1275
):
1276
    r"""
1277
    This automatically defines a sink for an oemof-sink object. The sinks are added to the energyConsumption assets.
1278

1279
    Parameters
1280
    ----------
1281
    dict_values: dict
1282
        All information of the simulation
1283

1284
    asset_key: str
1285
        label of the asset to be generated
1286

1287
    price: float
1288
        Price of dispatch of the asset
1289

1290
    inflow_direction: str
1291
        Direction from which energy is provided to the sink
1292

1293
    kwargs: Misc
1294
        Common parameters:
1295
        -
1296

1297
    Returns
1298
    -------
1299
    Updates dict_values[ENERGY_BUSSES] if outflow_direction not in it
1300
    Updates dict_values[ENERGY_CONSUMPTION] with a new sink
1301

1302
    Notes
1303
    -----
1304
    Examples:
1305
    - Used to define excess sinks for all energyBusses
1306
    - Used to define feed-in sink for each DSO
1307

1308
    The pytests for this function are not complete. It is started with:
1309
    - C0.test_define_sink() and only the assertion messages are missing
1310
    """
1311

1312
    # create a dictionary for the sink
1313
    sink = {
1✔
1314
        OEMOF_ASSET_TYPE: OEMOF_SINK,
1315
        LABEL: asset_key,
1316
        INFLOW_DIRECTION: inflow_direction,
1317
        # OPEX_VAR: {VALUE: price, UNIT: CURR + "/" + UNIT},
1318
        LIFETIME: {
1319
            VALUE: dict_values[ECONOMIC_DATA][PROJECT_DURATION][VALUE],
1320
            UNIT: UNIT_YEAR,
1321
        },
1322
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR,},
1323
        ENERGY_VECTOR: energy_vector,
1324
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1325
        DISPATCHABILITY: {VALUE: True, UNIT: TYPE_BOOL},
1326
    }
1327

1328
    if inflow_direction not in dict_values[ENERGY_BUSSES]:
1✔
1329
        dict_values[ENERGY_BUSSES].update(
1✔
1330
            {
1331
                inflow_direction: {
1332
                    LABEL: inflow_direction,
1333
                    ENERGY_VECTOR: energy_vector,
1334
                    ASSET_DICT: {asset_key: asset_key},
1335
                }
1336
            }
1337
        )
1338

1339
    if energy_vector is None:
1✔
1340
        raise ValueError(
×
1341
            f"The {ENERGY_VECTOR} of the automatically defined sink {asset_key} is invalid: {energy_vector}."
1342
        )
1343

1344
    # check if multiple busses are provided
1345
    # for each bus, read time series for dispatch_price if a file name has been provided in feedin tariff
1346
    if isinstance(price[VALUE], list):
1✔
1347
        sink.update({DISPATCH_PRICE: {VALUE: [], UNIT: price[UNIT]}})
×
1348
        values_info = []
×
1349
        for element in price[VALUE]:
×
1350
            if isinstance(element, dict):
×
1351
                timeseries = get_timeseries_multiple_flows(
×
1352
                    dict_values[SIMULATION_SETTINGS],
1353
                    sink,
1354
                    element[FILENAME],
1355
                    element[HEADER],
1356
                )
1357
                # todo this should be moved to C0.change_sign_of_feedin_tariff when #354 is solved
1358
                if DSO_FEEDIN in asset_key:
×
1359
                    sink[DISPATCH_PRICE][VALUE].append([-i for i in timeseries])
×
1360
                else:
1361
                    sink[DISPATCH_PRICE][VALUE].append(timeseries)
×
1362
            else:
1363
                sink[DISPATCH_PRICE][VALUE].append(element)
×
1364
        if len(values_info) > 0:
×
1365
            sink[DISPATCH_PRICE]["values_info"] = values_info
×
1366

1367
    elif isinstance(price[VALUE], dict):
1✔
1368
        sink.update(
×
1369
            {
1370
                DISPATCH_PRICE: {
1371
                    VALUE: {
1372
                        FILENAME: price[VALUE][FILENAME],
1373
                        HEADER: price[VALUE][HEADER],
1374
                    },
1375
                    UNIT: price[UNIT],
1376
                }
1377
            }
1378
        )
1379
        receive_timeseries_from_csv(
×
1380
            dict_values[SIMULATION_SETTINGS], sink, DISPATCH_PRICE
1381
        )
1382
        # todo this should be moved to C0.change_sign_of_feedin_tariff when #354 is solved
1383
        if (
×
1384
            DSO_FEEDIN in asset_key
1385
        ):  # change into negative value if this is a feedin sink
1386
            sink[DISPATCH_PRICE].update(
×
1387
                {VALUE: [-i for i in sink[DISPATCH_PRICE][VALUE]]}
1388
            )
1389
    else:
1390
        sink.update({DISPATCH_PRICE: {VALUE: price[VALUE], UNIT: price[UNIT]}})
1✔
1391

1392
    for item in [SPECIFIC_COSTS, SPECIFIC_COSTS_OM]:
1✔
1393
        if item in kwargs:
1✔
1394
            sink.update(
1✔
1395
                {item: kwargs[item],}
1396
            )
1397

1398
    # update dictionary
1399
    dict_values[ENERGY_CONSUMPTION].update({asset_key: sink})
1✔
1400

1401
    # If multiple input busses exist
1402
    apply_function_to_single_or_list(
1✔
1403
        function=add_asset_to_asset_dict_of_bus,
1404
        parameter=inflow_direction,
1405
        dict_values=dict_values,
1406
        asset_key=asset_key,
1407
        asset_label=sink[LABEL],
1408
    )
1409

1410

1411
def apply_function_to_single_or_list(function, parameter, **kwargs):
1✔
1412
    """
1413
    Applies function to a paramter or to a list of parameters and returns resut
1414

1415
    Parameters
1416
    ----------
1417
    function: func
1418
        Function to be applied to a parameter
1419

1420
    parameter: float/str/boolean or list
1421
        Parameter, either float/str/boolean or list to be evaluated
1422
    kwargs
1423
        Miscellaneous arguments for function to be called
1424

1425
    Returns
1426
    -------
1427
    Processed parameter (single) or list of processed para<meters
1428
    """
1429
    if isinstance(parameter, list):
1✔
1430
        parameter_processed = []
1✔
1431
        for parameter_item in parameter:
1✔
1432
            parameter_processed.append(function(parameter_item, **kwargs))
1✔
1433
    else:
1434
        parameter_processed = function(parameter, **kwargs)
1✔
1435

1436
    return parameter_processed
1✔
1437

1438

1439
def evaluate_lifetime_costs(settings, economic_data, dict_asset):
1✔
1440
    r"""
1441
    Evaluates specific costs of an asset over the project lifetime. This includes:
1442
    - LIFETIME_PRICE_DISPATCH (C2.determine_lifetime_price_dispatch)
1443
    - LIFETIME_SPECIFIC_COST
1444
    - LIFETIME_SPECIFIC_COST_OM
1445
    - ANNUITY_SPECIFIC_INVESTMENT_AND_OM
1446
    - SIMULATION_ANNUITY
1447

1448
    The DEVELOPMENT_COSTS are not processed here, as they are not necessary for the optimization.
1449

1450
    Parameters
1451
    ----------
1452
    settings: dict
1453
        dict of simulation settings, including:
1454
        - EVALUATED_PERIOD
1455

1456
    economic_data: dict
1457
        dict of economic data of the simulation, including
1458
        - project duration (PROJECT_DURATION)
1459
        - discount factor (DISCOUNTFACTOR)
1460
        - tax (TAX)
1461
        - CRF
1462
        - ANNUITY_FACTOR
1463

1464
    dict_asset: dict
1465
        dict of all asset parameters, including
1466
        - SPECIFIC_COSTS
1467
        - SPECIFIC_COSTS_OM
1468
        - LIFETIME
1469

1470
    Returns
1471
    -------
1472
    Updates asset dict with
1473
    - LIFETIME_PRICE_DISPATCH (C2.determine_lifetime_price_dispatch)
1474
    - LIFETIME_SPECIFIC_COST
1475
    - LIFETIME_SPECIFIC_COST_OM
1476
    - ANNUITY_SPECIFIC_INVESTMENT_AND_OM
1477
    - SIMULATION_ANNUITY
1478
    - SPECIFIC_REPLACEMENT_COSTS_INSTALLED
1479
    - SPECIFIC_REPLACEMENT_COSTS_OPTIMIZED
1480
    Notes
1481
    -----
1482

1483
    Tested with:
1484
    - test_evaluate_lifetime_costs_adds_all_parameters()
1485
    - Test_Economic_KPI.test_benchmark_Economic_KPI_C2_E2()
1486

1487
    """
1488
    if DISPATCH_PRICE in dict_asset:
1✔
1489
        C2.determine_lifetime_price_dispatch(dict_asset, economic_data)
1✔
1490

1491
    (
1✔
1492
        specific_capex,
1493
        specific_replacement_costs_optimized,
1494
        specific_replacement_costs_already_installed,
1495
    ) = C2.capex_from_investment(
1496
        investment_t0=dict_asset[SPECIFIC_COSTS][VALUE],
1497
        lifetime=dict_asset[LIFETIME][VALUE],
1498
        project_life=economic_data[PROJECT_DURATION][VALUE],
1499
        discount_factor=economic_data[DISCOUNTFACTOR][VALUE],
1500
        tax=economic_data[TAX][VALUE],
1501
        age_of_asset=dict_asset[AGE_INSTALLED][VALUE],
1502
        asset_label=dict_asset[LABEL],
1503
    )
1504

1505
    dict_asset.update(
1✔
1506
        {
1507
            LIFETIME_SPECIFIC_COST: {
1508
                VALUE: specific_capex,
1509
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1510
            }
1511
        }
1512
    )
1513

1514
    dict_asset.update(
1✔
1515
        {
1516
            SPECIFIC_REPLACEMENT_COSTS_OPTIMIZED: {
1517
                VALUE: specific_replacement_costs_optimized,
1518
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1519
            }
1520
        }
1521
    )
1522

1523
    dict_asset.update(
1✔
1524
        {
1525
            SPECIFIC_REPLACEMENT_COSTS_INSTALLED: {
1526
                VALUE: specific_replacement_costs_already_installed,
1527
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1528
            }
1529
        }
1530
    )
1531

1532
    # Annuities of components including opex AND capex #
1533
    dict_asset.update(
1✔
1534
        {
1535
            ANNUITY_SPECIFIC_INVESTMENT_AND_OM: {
1536
                VALUE: C2.annuity(
1537
                    dict_asset[LIFETIME_SPECIFIC_COST][VALUE],
1538
                    economic_data[CRF][VALUE],
1539
                )
1540
                + dict_asset[SPECIFIC_COSTS_OM][VALUE],  # changes from dispatch_price
1541
                UNIT: dict_asset[LIFETIME_SPECIFIC_COST][UNIT] + "/" + UNIT_YEAR,
1542
            }
1543
        }
1544
    )
1545

1546
    dict_asset.update(
1✔
1547
        {
1548
            LIFETIME_SPECIFIC_COST_OM: {
1549
                VALUE: dict_asset[SPECIFIC_COSTS_OM][VALUE]
1550
                * economic_data[ANNUITY_FACTOR][VALUE],
1551
                UNIT: dict_asset[SPECIFIC_COSTS_OM][UNIT][:-2],
1552
            }
1553
        }
1554
    )
1555

1556
    dict_asset.update(
1✔
1557
        {
1558
            SIMULATION_ANNUITY: {
1559
                VALUE: C2.simulation_annuity(
1560
                    dict_asset[ANNUITY_SPECIFIC_INVESTMENT_AND_OM][VALUE],
1561
                    settings[EVALUATED_PERIOD][VALUE],
1562
                ),
1563
                UNIT: CURR + "/" + UNIT + "/" + EVALUATED_PERIOD,
1564
            }
1565
        }
1566
    )
1567

1568

1569
# read timeseries. 2 cases are considered: Input type is related to demand or generation profiles,
1570
# so additional values like peak, total or average must be calculated. Any other type does not need this additional info.
1571
def receive_timeseries_from_csv(
1✔
1572
    settings, dict_asset, input_type, is_demand_profile=False
1573
):
1574
    """
1575

1576
    :param settings:
1577
    :param dict_asset:
1578
    :param type:
1579
    :return:
1580
    """
1581

1582
    load_from_timeseries_instead_of_file = False
1✔
1583

1584
    if input_type == "input" and "input" in dict_asset:
1✔
1585
        file_name = dict_asset[input_type][FILENAME]
×
1586
        header = dict_asset[input_type][HEADER]
×
1587
        unit = dict_asset[input_type][UNIT]
×
1588
    elif FILENAME in dict_asset:
1✔
1589
        # todo this input/file_name thing is a workaround and has to be improved in the future
1590
        # if only filename is given here, then only one column can be in the csv
1591
        file_name = dict_asset[FILENAME]
1✔
1592
        unit = dict_asset[UNIT] + "/" + UNIT_HOUR
1✔
1593
    elif FILENAME in dict_asset.get(input_type, []):
1✔
1594
        file_name = dict_asset[input_type][FILENAME]
×
1595
        header = dict_asset[input_type][HEADER]
×
1596
        unit = dict_asset[input_type][UNIT]
×
1597
    else:
1598
        load_from_timeseries_instead_of_file = True
1✔
1599
        file_name = ""
1✔
1600

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

1603
    if os.path.exists(file_path) is False or os.path.isfile(file_path) is False:
1✔
1604
        msg = (
1✔
1605
            f"Missing file! The timeseries file '{file_path}' \nof asset "
1606
            + f"{dict_asset[LABEL]} can not be found."
1607
        )
1608
        logging.warning(msg + " Looking for {TIMESERIES} entry.")
1✔
1609
        # if the file is not found
1610
        load_from_timeseries_instead_of_file = True
1✔
1611

1612
    else:
1613
        data_set = pd.read_csv(file_path, sep=",", keep_default_na=True)
1✔
1614

1615
    # If loading the data from the file does not work (file not present), the data might be
1616
    # already present in dict_values under TIMESERIES
1617
    if load_from_timeseries_instead_of_file is False:
1✔
1618
        if FILENAME in dict_asset:
1✔
1619
            header = data_set.columns[0]
1✔
1620
        series_values = data_set[header]
1✔
1621
    else:
1622
        if TIMESERIES in dict_asset:
1✔
1623
            series_values = dict_asset[TIMESERIES]
1✔
1624
        else:
1625
            raise FileNotFoundError(msg)
×
1626

1627
    if len(series_values.index) == settings[PERIODS]:
1✔
1628
        if input_type == "input":
1✔
1629
            timeseries = pd.Series(series_values.values, index=settings[TIME_INDEX])
1✔
1630
            timeseries = replace_nans_in_timeseries_with_0(
1✔
1631
                timeseries, dict_asset[LABEL]
1632
            )
1633
            dict_asset.update({TIMESERIES: timeseries})
1✔
1634
        else:
1635
            timeseries = pd.Series(series_values.values, index=settings[TIME_INDEX])
×
1636
            timeseries = replace_nans_in_timeseries_with_0(
×
1637
                timeseries, dict_asset[LABEL] + "(" + input_type + ")"
1638
            )
1639
            dict_asset[input_type][VALUE] = timeseries
×
1640

1641
        logging.debug("Added timeseries of %s (%s).", dict_asset[LABEL], file_path)
1✔
1642
    elif len(series_values.index) >= settings[PERIODS]:
1✔
1643
        if input_type == "input":
1✔
1644
            dict_asset.update(
1✔
1645
                {
1646
                    TIMESERIES: pd.Series(
1647
                        series_values[0 : len(settings[TIME_INDEX])].values,
1648
                        index=settings[TIME_INDEX],
1649
                    )
1650
                }
1651
            )
1652
        else:
1653
            dict_asset[input_type][VALUE] = pd.Series(
×
1654
                series_values[0 : len(settings[TIME_INDEX])].values,
1655
                index=settings[TIME_INDEX],
1656
            )
1657

1658
        logging.info(
1✔
1659
            "Provided timeseries of %s (%s) longer than evaluated period. "
1660
            "Excess data dropped.",
1661
            dict_asset[LABEL],
1662
            file_path,
1663
        )
1664

1665
    elif len(series_values.index) <= settings[PERIODS]:
×
1666
        logging.critical(
×
1667
            "Input error! "
1668
            "Provided timeseries of %s (%s) shorter than evaluated period. "
1669
            "Operation terminated",
1670
            dict_asset[LABEL],
1671
            file_path,
1672
        )
1673
        sys.exit()
×
1674

1675
    if input_type == "input":
1✔
1676
        compute_timeseries_properties(dict_asset)
1✔
1677

1678

1679
def replace_nans_in_timeseries_with_0(timeseries, label):
1✔
1680
    """Replaces nans in the timeseries (if any) with 0
1681

1682
    Parameters
1683
    ----------
1684

1685
    timeseries: pd.Series
1686
        demand or resource timeseries in dict_asset (having nan value(s) if any),
1687
        also of parameters that are not defined as scalars but as timeseries
1688

1689
    label: str
1690
        Contains user-defined information about the timeseries to be printed into the eventual error message
1691

1692
    Returns
1693
    -------
1694
    timeseries: pd.Series
1695
        timeseries without NaN values
1696

1697
    Notes
1698
    -----
1699
    Function tested with
1700
    - C0.test_replace_nans_in_timeseries_with_0()
1701
    """
1702
    if sum(pd.isna(timeseries)) > 0:
1✔
1703
        incidents = sum(pd.isna(timeseries))
1✔
1704
        logging.warning(
1✔
1705
            f"A number of {incidents} NaN value(s) found in the {TIMESERIES} of {label}. Changing NaN value(s) to 0."
1706
        )
1707
        timeseries = timeseries.fillna(0)
1✔
1708
    return timeseries
1✔
1709

1710

1711
def compute_timeseries_properties(dict_asset):
1✔
1712
    """Compute peak, aggregation, average and normalize timeseries
1713

1714
    Parameters
1715
    ----------
1716
    dict_asset: dict
1717
        dict of all asset parameters, must contain TIMESERIES key
1718

1719
    Returns
1720
    -------
1721
    None
1722
    Add TIMESERIES_PEAK, TIMESERIES_TOTAL, TIMESERIES_AVERAGE and TIMESERIES_NORMALIZED
1723
    to dict_asset
1724

1725
    Notes
1726
    -----
1727
    Function tested with
1728
    - C0.test_compute_timeseries_properties_TIMESERIES_in_dict_asset()
1729
    - C0.test_compute_timeseries_properties_TIMESERIES_not_in_dict_asset()
1730
    """
1731

1732
    if TIMESERIES in dict_asset:
1✔
1733
        timeseries = dict_asset[TIMESERIES]
1✔
1734
        unit = dict_asset[UNIT]
1✔
1735

1736
        dict_asset.update(
1✔
1737
            {
1738
                TIMESERIES_PEAK: {VALUE: max(timeseries), UNIT: unit,},
1739
                TIMESERIES_TOTAL: {VALUE: sum(timeseries), UNIT: unit,},
1740
                TIMESERIES_AVERAGE: {
1741
                    VALUE: sum(timeseries) / len(timeseries),
1742
                    UNIT: unit,
1743
                },
1744
            }
1745
        )
1746

1747
        logging.debug("Normalizing timeseries of %s.", dict_asset[LABEL])
1✔
1748
        dict_asset.update(
1✔
1749
            {TIMESERIES_NORMALIZED: timeseries / dict_asset[TIMESERIES_PEAK][VALUE]}
1750
        )
1751
        # just to be sure!
1752
        if any(dict_asset[TIMESERIES_NORMALIZED].values) > 1:
1✔
1753
            logging.error(
×
1754
                f"{dict_asset[LABEL]} normalized timeseries has values greater than 1."
1755
            )
1756
        if any(dict_asset[TIMESERIES_NORMALIZED].values) < 0:
1✔
1757
            logging.error(
×
1758
                f"{dict_asset[LABEL]} normalized timeseries has negative values."
1759
            )
1760

1761

1762
def treat_multiple_flows(dict_asset, dict_values, parameter):
1✔
1763
    """
1764
    This function consider the case a technical parameter on the json file has a list of values because multiple
1765
    inputs or outputs busses are considered.
1766
    Parameters
1767
    ----------
1768
    dict_values:
1769
    dictionary of current values of the asset
1770
    parameter:
1771
    usually efficiency. Different efficiencies will be given if an asset has multiple inputs or outputs busses,
1772
    so a list must be considered.
1773

1774
    Returns
1775
    -------
1776

1777
    """
1778
    updated_values = []
×
1779
    values_info = (
×
1780
        []
1781
    )  # filenames and headers will be stored to allow keeping track of the timeseries generation
1782
    for element in dict_asset[parameter][VALUE]:
×
1783
        if isinstance(element, dict):
×
1784
            updated_values.append(
×
1785
                get_timeseries_multiple_flows(
1786
                    dict_values[SIMULATION_SETTINGS],
1787
                    dict_asset,
1788
                    element[FILENAME],
1789
                    element[HEADER],
1790
                )
1791
            )
1792
            values_info.append(element)
×
1793
        else:
1794
            updated_values.append(element)
×
1795
    dict_asset[parameter][VALUE] = updated_values
×
1796
    if len(values_info) > 0:
×
1797
        dict_asset[parameter].update({"values_info": values_info})
×
1798

1799

1800
# reads timeseries specifically when the need comes from a multiple or output busses situation
1801
# returns the timeseries. Does not update any dictionary
1802
def get_timeseries_multiple_flows(settings, dict_asset, file_name, header):
1✔
1803
    """
1804

1805
    Parameters
1806
    ----------
1807
    dict_asset:
1808
    dictionary of the asset
1809
    file_name:
1810
    name of the file to read the time series
1811
    header:
1812
    name of the column where the timeseries is provided
1813

1814
    Returns
1815
    -------
1816

1817
    """
1818
    file_path = os.path.join(settings[PATH_INPUT_FOLDER], TIME_SERIES, file_name)
×
1819
    C1.lookup_file(file_path, dict_asset[LABEL])
×
1820

1821
    # TODO if FILENAME is not defined
1822

1823
    data_set = pd.read_csv(file_path, sep=",")
×
1824
    if len(data_set.index) == settings[PERIODS]:
×
1825
        return pd.Series(data_set[header].values, index=settings[TIME_INDEX])
×
1826
    elif len(data_set.index) >= settings[PERIODS]:
×
1827
        return pd.Series(
×
1828
            data_set[header][0 : len(settings[TIME_INDEX])].values,
1829
            index=settings[TIME_INDEX],
1830
        )
1831
    elif len(data_set.index) <= settings[PERIODS]:
×
1832
        logging.critical(
×
1833
            "Input error! "
1834
            "Provided timeseries of %s (%s) shorter then evaluated period. "
1835
            "Operation terminated",
1836
            dict_asset[LABEL],
1837
            file_path,
1838
        )
1839
        sys.exit()
×
1840

1841

1842
def process_maximum_cap_constraint(dict_values, group, asset, subasset=None):
1✔
1843
    # ToDo: should function be split into separate processing and validation functions?
1844
    """
1845
    Processes the maximumCap constraint depending on its value.
1846

1847
    * If MaximumCap not in asset dict: MaximumCap is None
1848
    * If MaximumCap < installedCap: invalid, MaximumCapValueInvalid raised
1849
    * If MaximumCap == 0: invalid, MaximumCap is None
1850
    * If group == energyProduction and filename not in asset_dict (dispatchable assets): pass
1851
    * If group == energyProduction and filename in asset_dict (non-dispatchable assets): MaximumCapNormalized == MaximumCap*peak(timeseries), MaximumAddCapNormalized == MaximumAddCap*peak(timeseries)
1852

1853
    Parameters
1854
    ----------
1855
    dict_values: dict
1856
        dictionary of all assets
1857

1858
    group: str
1859
        Group that the asset belongs to (str). Used to acces sub-asset data and for error messages.
1860

1861
    asset: str
1862
        asset name
1863

1864
    subasset: str or None
1865
        subasset name.
1866
        Default: None.
1867

1868
    Notes
1869
    -----
1870
    Tested with:
1871
    - test_process_maximum_cap_constraint_maximumCap_undefined()
1872
    - test_process_maximum_cap_constraint_maximumCap_is_None()
1873
    - test_process_maximum_cap_constraint_maximumCap_is_int()
1874
    - test_process_maximum_cap_constraint_maximumCap_is_float()
1875
    - test_process_maximum_cap_constraint_maximumCap_is_0()
1876
    - test_process_maximum_cap_constraint_maximumCap_is_int_smaller_than_installed_cap()
1877
    - test_process_maximum_cap_constraint_group_is_ENERGY_PRODUCTION_fuel_source()
1878
    - test_process_maximum_cap_constraint_group_is_ENERGY_PRODUCTION_non_dispatchable_asset()
1879
    - test_process_maximum_cap_constraint_subasset()
1880

1881
    Returns
1882
    -------
1883
    Updates the asset dictionary.
1884

1885
    * Unit of MaximumCap is asset unit
1886

1887
    """
1888
    if subasset is None:
1✔
1889
        asset_dict = dict_values[group][asset]
1✔
1890
    else:
1891
        asset_dict = dict_values[group][asset][subasset]
1✔
1892

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

1896
    # check if a maximumCap is defined
1897
    if MAXIMUM_CAP not in asset_dict:
1✔
1898
        asset_dict.update({MAXIMUM_CAP: {VALUE: None}})
1✔
1899
    else:
1900
        if asset_dict[MAXIMUM_CAP][VALUE] is not None:
1✔
1901
            # maximum additional capacity = maximum total capacity - installed capacity
1902
            max_add_cap = (
1✔
1903
                asset_dict[MAXIMUM_CAP][VALUE] - asset_dict[INSTALLED_CAP][VALUE]
1904
            )
1905
            # include the maximumAddCap parameter to the asset dictionary
1906
            asset_dict[MAXIMUM_ADD_CAP].update({VALUE: max_add_cap})
1✔
1907
            # raise error if maximumCap is smaller than installedCap and is not set to zero
1908
            if (
1✔
1909
                asset_dict[MAXIMUM_CAP][VALUE] < asset_dict[INSTALLED_CAP][VALUE]
1910
                and asset_dict[MAXIMUM_CAP][VALUE] != 0
1911
            ):
1912
                message = (
1✔
1913
                    f"The stated total maximumCap in {group} {asset} is smaller than the "
1914
                    f"installedCap ({asset_dict[MAXIMUM_CAP][VALUE]}/{asset_dict[INSTALLED_CAP][VALUE]}). Please enter a greater maximumCap."
1915
                )
1916
                raise MaximumCapValueInvalid(message)
1✔
1917

1918
            # set maximumCap to None if it is zero
1919
            if asset_dict[MAXIMUM_CAP][VALUE] == 0:
1✔
1920
                message = (
1✔
1921
                    f"The stated maximumCap of zero in {group} {asset} is invalid."
1922
                    "For this simulation, the maximumCap will be "
1923
                    "disregarded and not be used in the simulation."
1924
                )
1925
                warnings.warn(UserWarning(message))
1✔
1926
                logging.warning(message)
1✔
1927
                asset_dict[MAXIMUM_CAP][VALUE] = None
1✔
1928

1929
            # adapt maximumCap and maximumAddCap of non-dispatchable sources
1930
            if (
1✔
1931
                group == ENERGY_PRODUCTION
1932
                and asset_dict.get(DISPATCHABILITY, True) is False
1933
                and asset_dict[MAXIMUM_CAP][VALUE] is not None
1934
            ):
1935
                max_cap_norm = (
1✔
1936
                    asset_dict[MAXIMUM_CAP][VALUE] * asset_dict[TIMESERIES_PEAK][VALUE]
1937
                )
1938
                asset_dict.update(
1✔
1939
                    {
1940
                        MAXIMUM_CAP_NORMALIZED: {
1941
                            VALUE: max_cap_norm,
1942
                            UNIT: asset_dict[UNIT],
1943
                        }
1944
                    }
1945
                )
1946
                logging.debug(
1✔
1947
                    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."
1948
                )
1949
                max_add_cap_norm = (
1✔
1950
                    asset_dict[MAXIMUM_ADD_CAP][VALUE]
1951
                    * asset_dict[TIMESERIES_PEAK][VALUE]
1952
                )
1953
                asset_dict.update(
1✔
1954
                    {
1955
                        MAXIMUM_ADD_CAP_NORMALIZED: {
1956
                            VALUE: max_add_cap_norm,
1957
                            UNIT: asset_dict[UNIT],
1958
                        }
1959
                    }
1960
                )
1961
                logging.debug(
1✔
1962
                    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."
1963
                )
1964

1965
    asset_dict[MAXIMUM_CAP].update({UNIT: asset_dict[UNIT]})
1✔
1966

1967

1968
def process_normalized_installed_cap(dict_values, group, asset, subasset=None):
1✔
1969
    """
1970
    Processes the normalized installed capacity value based on the installed capacity value and the chosen timeseries.
1971

1972
    Parameters
1973
    ----------
1974
    dict_values: dict
1975
        dictionary of all assets
1976

1977
    group: str
1978
        Group that the asset belongs to (str). Used to acces sub-asset data and for error messages.
1979

1980
    asset: str
1981
        asset name
1982

1983
    subasset: str or None
1984
        subasset name.
1985
        Default: None.
1986

1987
    Notes
1988
    -----
1989
    Tested with:
1990
    - test_process_normalized_installed_cap()
1991

1992
    Returns
1993
    -------
1994
    Updates the asset dictionary with the normalizedInstalledCap value.
1995

1996
    """
1997
    if subasset is None:
1✔
1998
        asset_dict = dict_values[group][asset]
1✔
1999
    else:
2000
        asset_dict = dict_values[group][asset][subasset]
×
2001

2002
    if asset_dict[FILENAME] is not None:
1✔
2003
        inst_cap_norm = (
1✔
2004
            asset_dict[INSTALLED_CAP][VALUE] * asset_dict[TIMESERIES_PEAK][VALUE]
2005
        )
2006
        asset_dict.update(
1✔
2007
            {INSTALLED_CAP_NORMALIZED: {VALUE: inst_cap_norm, UNIT: asset_dict[UNIT]}}
2008
        )
2009
        logging.debug(
1✔
2010
            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]})."
2011
        )
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