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

rl-institut / multi-vector-simulator / 4083659824

pending completion
4083659824

push

github

GitHub
Merge pull request #942 from rl-institut/feature/feedin-capping

86 of 86 new or added lines in 9 files covered. (100.0%)

5800 of 7524 relevant lines covered (77.09%)

0.77 hits per line

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

81.99
/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
    #todo This only works if the feedin tariff is not defined as a timeseries
766
    Parameters
767
    ----------
768
    dict_feedin_tariff: dict
769
        Dict of feedin tariff with Unit-value pair
770

771
    dso: str
772
        Name of the energy provider
773

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

779
    Notes
780
    -----
781
    Tested with:
782
    - C0.test_change_sign_of_feedin_tariff_positive_value()
783
    - C0.test_change_sign_of_feedin_tariff_negative_value()
784
    - C0.test_change_sign_of_feedin_tariff_zero()
785
    """
786
    if dict_feedin_tariff[VALUE] > 0:
1✔
787
        # Add a debug message in case the feed-in is interpreted as revenue-inducing.
788
        logging.debug(
1✔
789
            f"The {FEEDIN_TARIFF} of {dso} is positive, which means that feeding into the grid results in a revenue stream."
790
        )
791
    elif dict_feedin_tariff[VALUE] == 0:
1✔
792
        # Add a warning msg in case the feedin induces expenses rather than revenue
793
        logging.warning(
1✔
794
            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."
795
        )
796
    elif dict_feedin_tariff[VALUE] < 0:
1✔
797
        # Add a warning msg in case the feedin induces expenses rather than revenue
798
        logging.warning(
1✔
799
            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."
800
        )
801
    else:
802
        pass
803

804
    dict_feedin_tariff = {
1✔
805
        VALUE: -dict_feedin_tariff[VALUE],
806
        UNIT: dict_feedin_tariff[UNIT],
807
    }
808
    return dict_feedin_tariff
1✔
809

810

811
def define_availability_of_peak_demand_pricing_assets(
1✔
812
    dict_values, number_of_pricing_periods, months_in_a_period
813
):
814
    r"""
815
    Determined the availability timeseries for the later to be defined dso assets for taking into account the peak demand pricing periods.
816

817
    Parameters
818
    ----------
819
    dict_values: dict
820
        All simulation inputs
821
    number_of_pricing_periods: int
822
        Number of pricing periods in a year. Valid: 1,2,3,4,6,12
823
    months_in_a_period: int
824
        Duration of a period
825

826
    Returns
827
    -------
828
    dict_availability_timeseries: dict
829
        Dict with all availability timeseries for each period
830

831
    """
832
    dict_availability_timeseries = {}
1✔
833
    for period in range(1, number_of_pricing_periods + 1):
1✔
834
        availability_in_period = pd.Series(
1✔
835
            0, index=dict_values[SIMULATION_SETTINGS][TIME_INDEX]
836
        )
837
        time_period = pd.date_range(
1✔
838
            # Period start
839
            start=dict_values[SIMULATION_SETTINGS][START_DATE]
840
            + pd.DateOffset(months=(period - 1) * months_in_a_period),
841
            # Period end, with months_in_a_period durartion
842
            end=dict_values[SIMULATION_SETTINGS][START_DATE]
843
            + pd.DateOffset(months=(period) * months_in_a_period, hours=-1),
844
            freq=str(dict_values[SIMULATION_SETTINGS][TIMESTEP][VALUE]) + UNIT_MINUTE,
845
        )
846

847
        availability_in_period = availability_in_period.add(
1✔
848
            pd.Series(1, index=time_period), fill_value=0
849
        ).loc[dict_values[SIMULATION_SETTINGS][TIME_INDEX]]
850
        dict_availability_timeseries.update({period: availability_in_period})
1✔
851

852
    return dict_availability_timeseries
1✔
853

854

855
def add_a_transformer_for_each_peak_demand_pricing_period(
1✔
856
    dict_values, dict_dso, dict_availability_timeseries
857
):
858
    r"""
859
    Adds transformers that are supposed to model the peak_demand_pricing periods for each period.
860
    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.
861

862
    Parameters
863
    ----------
864
    dict_values: dict
865
        dict with all simulation parameters
866

867
    dict_dso: dict
868
        dict with all info on the specific dso at hand
869

870
    dict_availability_timeseries: dict
871
        dict with all availability timeseries for each period
872

873
    Returns
874
    -------
875
    list_of_dso_energyConversion_assets: list
876
        List of names of newly added energy conversion assets,
877

878
    Updated dict_values with a transformer for each peak demand pricing period
879

880
    Notes
881
    -----
882

883
    Tested by:
884
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_1_period
885
    - C0.test_add_a_transformer_for_each_peak_demand_pricing_period_2_periods
886
    """
887

888
    list_of_dso_energyConversion_assets = []
1✔
889
    for key in dict_availability_timeseries.keys():
1✔
890

891
        if len(dict_availability_timeseries.keys()) > 1:
1✔
892
            transformer_name = peak_demand_transformer_name(
1✔
893
                dict_dso[LABEL], peak_number=key
894
            )
895
        else:
896
            transformer_name = peak_demand_transformer_name(dict_dso[LABEL])
1✔
897

898
        define_transformer_for_peak_demand_pricing(
1✔
899
            dict_values=dict_values,
900
            dict_dso=dict_dso,
901
            transformer_name=transformer_name,
902
            timeseries_availability=dict_availability_timeseries[key],
903
        )
904

905
        list_of_dso_energyConversion_assets.append(transformer_name)
1✔
906

907
    logging.debug(
1✔
908
        f"The peak demand pricing price of {dict_dso[PEAK_DEMAND_PRICING][VALUE]} {dict_values[ECONOMIC_DATA][CURR]} "
909
        f"is set as specific_costs_om of the peak demand pricing transformers of the DSO."
910
    )
911
    return list_of_dso_energyConversion_assets
1✔
912

913

914
def determine_months_in_a_peak_demand_pricing_period(
1✔
915
    number_of_pricing_periods, simulation_period_lenght
916
):
917
    r"""
918
    Check if the number of peak demand pricing periods is valid.
919
    Warns user that in case the number of periods exceeds 1 but the simulation time is not a year,
920
    there could be an unexpected number of timeseries considered.
921
    Raises error if number of peak demand pricing periods is not valid.
922

923
    Parameters
924
    ----------
925
    number_of_pricing_periods: int
926
        Defined in csv, is number of pricing periods within a year
927
    simulation_period_lenght: int
928
        Defined in csv, is number of days of the simulation
929

930
    Returns
931
    -------
932
    months_in_a_period: float
933
        Number of months that make a period, will be used to determine availability of dso assets
934
    """
935

936
    # check number of pricing periods - if >1 the simulation has to cover a whole year!
937
    if number_of_pricing_periods > 1:
1✔
938
        if simulation_period_lenght != 365:
1✔
939
            logging.debug(
1✔
940
                f"\n Message for dev: Following warning is not technically true, "
941
                f"as the evaluation period has to approximately be "
942
                f"larger than 365/peak demand pricing periods (see #331)."
943
            )
944
            logging.warning(
1✔
945
                f"You have chosen a number of peak demand pricing periods > 1."
946
                f"Please be advised that if you are not simulating for a year (365d)"
947
                f"an possibly unexpected number of periods will be considered."
948
            )
949

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

956
    # defines the number of months that one period constists of.
957
    months_in_a_period = 12 / number_of_pricing_periods
1✔
958
    logging.info(
1✔
959
        "Peak demand pricing is taking place %s times per year, ie. every %s "
960
        "months.",
961
        number_of_pricing_periods,
962
        months_in_a_period,
963
    )
964
    return months_in_a_period
1✔
965

966

967
def define_transformer_for_peak_demand_pricing(
1✔
968
    dict_values, dict_dso, transformer_name, timeseries_availability
969
):
970
    r"""
971
    Defines a transformer for peak demand pricing in energyConverion
972

973
    Parameters
974
    ----------
975
    dict_values: dict
976
        All simulation parameters
977

978
    dict_dso: dict
979
        All values connected to the DSO
980

981
    transformer_name: str
982
        label of the transformer to be added
983

984
    timeseries_availability: pd.Series
985
        Timeseries of transformer availability. Introduced to cover peak demand pricing.
986

987
    Returns
988
    -------
989
    Updated dict_values with newly added transformer asset in the energyConversion asset group.
990
    """
991

992
    dso_consumption_transformer = {
1✔
993
        LABEL: transformer_name,
994
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
995
        INSTALLED_CAP: {VALUE: 0, UNIT: dict_dso[UNIT]},
996
        INFLOW_DIRECTION: peak_demand_bus_name(dict_dso[INFLOW_DIRECTION]),
997
        OUTFLOW_DIRECTION: dict_dso[OUTFLOW_DIRECTION],
998
        AVAILABILITY_DISPATCH: timeseries_availability,
999
        EFFICIENCY: {VALUE: 1, UNIT: "factor"},
1000
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
1001
        SPECIFIC_COSTS: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT],},
1002
        # the demand pricing is split between consumption and feedin
1003
        SPECIFIC_COSTS_OM: {
1004
            VALUE: dict_dso[PEAK_DEMAND_PRICING][VALUE] / 2,
1005
            UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_YEAR,
1006
        },
1007
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_HOUR},
1008
        OEMOF_ASSET_TYPE: OEMOF_TRANSFORMER,
1009
        ENERGY_VECTOR: dict_dso[ENERGY_VECTOR],
1010
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR},
1011
    }
1012

1013
    dict_values[ENERGY_CONVERSION].update(
1✔
1014
        {transformer_name: dso_consumption_transformer}
1015
    )
1016

1017
    logging.debug(
1✔
1018
        f"Model for peak demand pricing on consumption side: Adding transfomer {transformer_name}."
1019
    )
1020

1021
    transformer_name = transformer_name.replace(DSO_CONSUMPTION, DSO_FEEDIN)
1✔
1022
    dso_feedin_transformer = {
1✔
1023
        LABEL: transformer_name,
1024
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1025
        INSTALLED_CAP: {VALUE: 0, UNIT: dict_dso[UNIT]},
1026
        INFLOW_DIRECTION: dict_dso[INFLOW_DIRECTION],
1027
        OUTFLOW_DIRECTION: peak_demand_bus_name(
1028
            dict_dso[INFLOW_DIRECTION], feedin=True
1029
        ),
1030
        AVAILABILITY_DISPATCH: timeseries_availability,
1031
        EFFICIENCY: {VALUE: 1, UNIT: "factor"},
1032
        DEVELOPMENT_COSTS: {VALUE: 0, UNIT: CURR},
1033
        SPECIFIC_COSTS: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT],},
1034
        # the demand pricing is split between consumption and feedin
1035
        SPECIFIC_COSTS_OM: {
1036
            VALUE: dict_dso[PEAK_DEMAND_PRICING][VALUE] / 2,
1037
            UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_YEAR,
1038
        },
1039
        DISPATCH_PRICE: {VALUE: 0, UNIT: CURR + "/" + dict_dso[UNIT] + "/" + UNIT_HOUR},
1040
        OEMOF_ASSET_TYPE: OEMOF_TRANSFORMER,
1041
        ENERGY_VECTOR: dict_dso[ENERGY_VECTOR],
1042
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR},
1043
        # LIFETIME: {VALUE: 100, UNIT: UNIT_YEAR},
1044
    }
1045
    if dict_dso.get(DSO_FEEDIN_CAP, None) is not None:
1✔
1046
        dso_feedin_transformer[MAXIMUM_CAP] = {
×
1047
            VALUE: dict_dso[DSO_FEEDIN_CAP][VALUE],
1048
            UNIT: dict_dso[UNIT],
1049
        }
1050

1051
        logging.info(
×
1052
            f"Capping {dict_dso[LABEL]} feedin with maximum capacity {dict_dso[DSO_FEEDIN_CAP][VALUE]}"
1053
        )
1054

1055
    dict_values[ENERGY_CONVERSION].update({transformer_name: dso_feedin_transformer})
1✔
1056

1057
    logging.debug(
1✔
1058
        f"Model for peak demand pricing on feedin side: Adding transfomer {transformer_name}."
1059
    )
1060

1061

1062
def define_source(
1✔
1063
    dict_values,
1064
    asset_key,
1065
    outflow_direction,
1066
    energy_vector,
1067
    emission_factor,
1068
    price=None,
1069
    timeseries=None,
1070
):
1071
    r"""
1072
    Defines a source with default input values. If kwargs are given, the default values are overwritten.
1073

1074
    Parameters
1075
    ----------
1076
    dict_values: dict
1077
        Dictionary to which source should be added, with all simulation parameters
1078

1079
    asset_key: str
1080
        key under which the asset is stored in the asset group
1081

1082
    energy_vector: str
1083
        Energy vector the new asset should belong to
1084

1085
    emission_factor : dict
1086
        Dict with a unit-value pair of the emission factor of the new asset
1087

1088
    price: dict
1089
        Dict with a unit-value pair of the dispatch price of the source.
1090
        The value can also be defined though FILENAME and HEADER, making the value of the price a timeseries.
1091
        Default: None
1092

1093
    timeseries: pd.Dataframe
1094
        Timeseries defining the availability of the source. Currently not used.
1095
        Default: None
1096

1097
    Returns
1098
    -------
1099
    Updates dict_values[ENERGY_BUSSES] if outflow_direction not in it
1100
    Standard source defined as:
1101

1102
    Notes
1103
    -----
1104
    The pytests for this function are not complete. It is started with:
1105
    - C0.test_define_source()
1106
    - C0.test_define_source_exception_unknown_bus()
1107
    - C0.test_define_source_timeseries_not_None()
1108
    - C0.test_define_source_price_not_None_but_with_scalar_value()
1109
    Missing:
1110
    - C0.test_define_source_price_not_None_but_timeseries(), ie. value defined by FILENAME and HEADER
1111
    """
1112
    default_source_dict = {
1✔
1113
        OEMOF_ASSET_TYPE: OEMOF_SOURCE,
1114
        LABEL: asset_key,
1115
        OUTFLOW_DIRECTION: outflow_direction,
1116
        DISPATCHABILITY: True,
1117
        LIFETIME: {
1118
            VALUE: dict_values[ECONOMIC_DATA][PROJECT_DURATION][VALUE],
1119
            UNIT: UNIT_YEAR,
1120
        },
1121
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1122
        MAXIMUM_CAP: {VALUE: None, UNIT: "?"},
1123
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR,},
1124
        ENERGY_VECTOR: energy_vector,
1125
        EMISSION_FACTOR: emission_factor,
1126
    }
1127

1128
    if outflow_direction not in dict_values[ENERGY_BUSSES]:
1✔
1129
        dict_values[ENERGY_BUSSES].update(
1✔
1130
            {
1131
                outflow_direction: {
1132
                    LABEL: outflow_direction,
1133
                    ENERGY_VECTOR: energy_vector,
1134
                    ASSET_DICT: {asset_key: asset_key},
1135
                }
1136
            }
1137
        )
1138

1139
    if price is not None:
1✔
1140
        if FILENAME in price and HEADER in price:
1✔
1141
            price.update(
×
1142
                {
1143
                    VALUE: get_timeseries_multiple_flows(
1144
                        dict_values[SIMULATION_SETTINGS],
1145
                        default_source_dict,
1146
                        price[FILENAME],
1147
                        price[HEADER],
1148
                    )
1149
                }
1150
            )
1151
        determine_dispatch_price(dict_values, price, default_source_dict)
1✔
1152

1153
    if timeseries is not None:
1✔
1154
        # This part is currently not used.
1155
        default_source_dict.update({DISPATCHABILITY: False})
1✔
1156
        logging.debug(
1✔
1157
            f"{default_source_dict[LABEL]} can provide a total generation of {sum(timeseries.values)}"
1158
        )
1159
        default_source_dict[OPTIMIZE_CAP].update({VALUE: True})
1✔
1160
        default_source_dict.update(
1✔
1161
            {
1162
                TIMESERIES_PEAK: {VALUE: max(timeseries), UNIT: "kW"},
1163
                TIMESERIES_NORMALIZED: timeseries / max(timeseries),
1164
            }
1165
        )
1166
        if DISPATCH_PRICE in default_source_dict and max(timeseries) != 0:
1✔
1167
            default_source_dict[DISPATCH_PRICE].update(
1✔
1168
                {VALUE: default_source_dict[DISPATCH_PRICE][VALUE] / max(timeseries)}
1169
            )
1170

1171
    dict_values[ENERGY_PRODUCTION].update({asset_key: default_source_dict})
1✔
1172

1173
    logging.info(
1✔
1174
        f"Asset {default_source_dict[LABEL]} was added to the energyProduction assets."
1175
    )
1176

1177
    apply_function_to_single_or_list(
1✔
1178
        function=add_asset_to_asset_dict_of_bus,
1179
        parameter=outflow_direction,
1180
        dict_values=dict_values,
1181
        asset_key=asset_key,
1182
        asset_label=default_source_dict[LABEL],
1183
    )
1184

1185

1186
def determine_dispatch_price(dict_values, price, source):
1✔
1187
    """
1188
    This function needs to be re-evaluated.
1189

1190
    Parameters
1191
    ----------
1192
    dict_values
1193
    price
1194
    source
1195

1196
    Returns
1197
    -------
1198

1199
    """
1200
    # check if multiple busses are provided
1201
    # for each bus, read time series for dispatch_price if a file name has been
1202
    # provided in energy price
1203
    if isinstance(price[VALUE], list):
1✔
1204
        source.update({DISPATCH_PRICE: {VALUE: [], UNIT: price[UNIT]}})
×
1205
        values_info = []
×
1206
        for element in price[VALUE]:
×
1207
            if isinstance(element, dict):
×
1208
                source[DISPATCH_PRICE][VALUE].append(
×
1209
                    get_timeseries_multiple_flows(
1210
                        dict_values[SIMULATION_SETTINGS],
1211
                        source,
1212
                        element[FILENAME],
1213
                        element[HEADER],
1214
                    )
1215
                )
1216
                values_info.append(element)
×
1217
            else:
1218
                source[DISPATCH_PRICE][VALUE].append(element)
×
1219
        if len(values_info) > 0:
×
1220
            source[DISPATCH_PRICE]["values_info"] = values_info
×
1221

1222
    elif isinstance(price[VALUE], dict):
1✔
1223
        source.update(
×
1224
            {
1225
                DISPATCH_PRICE: {
1226
                    VALUE: {
1227
                        FILENAME: price[VALUE][FILENAME],
1228
                        HEADER: price[VALUE][HEADER],
1229
                    },
1230
                    UNIT: price[UNIT],
1231
                }
1232
            }
1233
        )
1234
        receive_timeseries_from_csv(
×
1235
            dict_values[SIMULATION_SETTINGS], source, DISPATCH_PRICE
1236
        )
1237
    else:
1238
        source.update({DISPATCH_PRICE: {VALUE: price[VALUE], UNIT: price[UNIT]}})
1✔
1239

1240
    if type(source[DISPATCH_PRICE][VALUE]) == pd.Series:
1✔
1241
        logging.debug(
×
1242
            f"{source[LABEL]} was created, with a price defined as a timeseries (average: {source[DISPATCH_PRICE][VALUE].mean()})."
1243
        )
1244
    else:
1245
        logging.debug(
1✔
1246
            f"{source[LABEL]} was created, with a price of {source[DISPATCH_PRICE][VALUE]}."
1247
        )
1248

1249

1250
def define_sink(
1✔
1251
    dict_values, asset_key, price, inflow_direction, energy_vector, **kwargs
1252
):
1253
    r"""
1254
    This automatically defines a sink for an oemof-sink object. The sinks are added to the energyConsumption assets.
1255

1256
    Parameters
1257
    ----------
1258
    dict_values: dict
1259
        All information of the simulation
1260

1261
    asset_key: str
1262
        label of the asset to be generated
1263

1264
    price: float
1265
        Price of dispatch of the asset
1266

1267
    inflow_direction: str
1268
        Direction from which energy is provided to the sink
1269

1270
    kwargs: Misc
1271
        Common parameters:
1272
        -
1273

1274
    Returns
1275
    -------
1276
    Updates dict_values[ENERGY_BUSSES] if outflow_direction not in it
1277
    Updates dict_values[ENERGY_CONSUMPTION] with a new sink
1278

1279
    Notes
1280
    -----
1281
    Examples:
1282
    - Used to define excess sinks for all energyBusses
1283
    - Used to define feed-in sink for each DSO
1284

1285
    The pytests for this function are not complete. It is started with:
1286
    - C0.test_define_sink() and only the assertion messages are missing
1287
    """
1288

1289
    # create a dictionary for the sink
1290
    sink = {
1✔
1291
        OEMOF_ASSET_TYPE: OEMOF_SINK,
1292
        LABEL: asset_key,
1293
        INFLOW_DIRECTION: inflow_direction,
1294
        # OPEX_VAR: {VALUE: price, UNIT: CURR + "/" + UNIT},
1295
        LIFETIME: {
1296
            VALUE: dict_values[ECONOMIC_DATA][PROJECT_DURATION][VALUE],
1297
            UNIT: UNIT_YEAR,
1298
        },
1299
        AGE_INSTALLED: {VALUE: 0, UNIT: UNIT_YEAR,},
1300
        ENERGY_VECTOR: energy_vector,
1301
        OPTIMIZE_CAP: {VALUE: True, UNIT: TYPE_BOOL},
1302
        DISPATCHABILITY: {VALUE: True, UNIT: TYPE_BOOL},
1303
    }
1304

1305
    if inflow_direction not in dict_values[ENERGY_BUSSES]:
1✔
1306
        dict_values[ENERGY_BUSSES].update(
1✔
1307
            {
1308
                inflow_direction: {
1309
                    LABEL: inflow_direction,
1310
                    ENERGY_VECTOR: energy_vector,
1311
                    ASSET_DICT: {asset_key: asset_key},
1312
                }
1313
            }
1314
        )
1315

1316
    if energy_vector is None:
1✔
1317
        raise ValueError(
×
1318
            f"The {ENERGY_VECTOR} of the automatically defined sink {asset_key} is invalid: {energy_vector}."
1319
        )
1320

1321
    # check if multiple busses are provided
1322
    # for each bus, read time series for dispatch_price if a file name has been provided in feedin tariff
1323
    if isinstance(price[VALUE], list):
1✔
1324
        sink.update({DISPATCH_PRICE: {VALUE: [], UNIT: price[UNIT]}})
×
1325
        values_info = []
×
1326
        for element in price[VALUE]:
×
1327
            if isinstance(element, dict):
×
1328
                timeseries = get_timeseries_multiple_flows(
×
1329
                    dict_values[SIMULATION_SETTINGS],
1330
                    sink,
1331
                    element[FILENAME],
1332
                    element[HEADER],
1333
                )
1334
                # todo this should be moved to C0.change_sign_of_feedin_tariff when #354 is solved
1335
                if DSO_FEEDIN in asset_key:
×
1336
                    sink[DISPATCH_PRICE][VALUE].append([-i for i in timeseries])
×
1337
                else:
1338
                    sink[DISPATCH_PRICE][VALUE].append(timeseries)
×
1339
            else:
1340
                sink[DISPATCH_PRICE][VALUE].append(element)
×
1341
        if len(values_info) > 0:
×
1342
            sink[DISPATCH_PRICE]["values_info"] = values_info
×
1343

1344
    elif isinstance(price[VALUE], dict):
1✔
1345
        sink.update(
×
1346
            {
1347
                DISPATCH_PRICE: {
1348
                    VALUE: {
1349
                        FILENAME: price[VALUE][FILENAME],
1350
                        HEADER: price[VALUE][HEADER],
1351
                    },
1352
                    UNIT: price[UNIT],
1353
                }
1354
            }
1355
        )
1356
        receive_timeseries_from_csv(
×
1357
            dict_values[SIMULATION_SETTINGS], sink, DISPATCH_PRICE
1358
        )
1359
        # todo this should be moved to C0.change_sign_of_feedin_tariff when #354 is solved
1360
        if (
×
1361
            DSO_FEEDIN in asset_key
1362
        ):  # change into negative value if this is a feedin sink
1363
            sink[DISPATCH_PRICE].update(
×
1364
                {VALUE: [-i for i in sink[DISPATCH_PRICE][VALUE]]}
1365
            )
1366
    else:
1367
        sink.update({DISPATCH_PRICE: {VALUE: price[VALUE], UNIT: price[UNIT]}})
1✔
1368

1369
    for item in [SPECIFIC_COSTS, SPECIFIC_COSTS_OM]:
1✔
1370
        if item in kwargs:
1✔
1371
            sink.update(
1✔
1372
                {item: kwargs[item],}
1373
            )
1374

1375
    # update dictionary
1376
    dict_values[ENERGY_CONSUMPTION].update({asset_key: sink})
1✔
1377

1378
    # If multiple input busses exist
1379
    apply_function_to_single_or_list(
1✔
1380
        function=add_asset_to_asset_dict_of_bus,
1381
        parameter=inflow_direction,
1382
        dict_values=dict_values,
1383
        asset_key=asset_key,
1384
        asset_label=sink[LABEL],
1385
    )
1386

1387

1388
def apply_function_to_single_or_list(function, parameter, **kwargs):
1✔
1389
    """
1390
    Applies function to a paramter or to a list of parameters and returns resut
1391

1392
    Parameters
1393
    ----------
1394
    function: func
1395
        Function to be applied to a parameter
1396

1397
    parameter: float/str/boolean or list
1398
        Parameter, either float/str/boolean or list to be evaluated
1399
    kwargs
1400
        Miscellaneous arguments for function to be called
1401

1402
    Returns
1403
    -------
1404
    Processed parameter (single) or list of processed para<meters
1405
    """
1406
    if isinstance(parameter, list):
1✔
1407
        parameter_processed = []
1✔
1408
        for parameter_item in parameter:
1✔
1409
            parameter_processed.append(function(parameter_item, **kwargs))
1✔
1410
    else:
1411
        parameter_processed = function(parameter, **kwargs)
1✔
1412

1413
    return parameter_processed
1✔
1414

1415

1416
def evaluate_lifetime_costs(settings, economic_data, dict_asset):
1✔
1417
    r"""
1418
    Evaluates specific costs of an asset over the project lifetime. This includes:
1419
    - LIFETIME_PRICE_DISPATCH (C2.determine_lifetime_price_dispatch)
1420
    - LIFETIME_SPECIFIC_COST
1421
    - LIFETIME_SPECIFIC_COST_OM
1422
    - ANNUITY_SPECIFIC_INVESTMENT_AND_OM
1423
    - SIMULATION_ANNUITY
1424

1425
    The DEVELOPMENT_COSTS are not processed here, as they are not necessary for the optimization.
1426

1427
    Parameters
1428
    ----------
1429
    settings: dict
1430
        dict of simulation settings, including:
1431
        - EVALUATED_PERIOD
1432

1433
    economic_data: dict
1434
        dict of economic data of the simulation, including
1435
        - project duration (PROJECT_DURATION)
1436
        - discount factor (DISCOUNTFACTOR)
1437
        - tax (TAX)
1438
        - CRF
1439
        - ANNUITY_FACTOR
1440

1441
    dict_asset: dict
1442
        dict of all asset parameters, including
1443
        - SPECIFIC_COSTS
1444
        - SPECIFIC_COSTS_OM
1445
        - LIFETIME
1446

1447
    Returns
1448
    -------
1449
    Updates asset dict with
1450
    - LIFETIME_PRICE_DISPATCH (C2.determine_lifetime_price_dispatch)
1451
    - LIFETIME_SPECIFIC_COST
1452
    - LIFETIME_SPECIFIC_COST_OM
1453
    - ANNUITY_SPECIFIC_INVESTMENT_AND_OM
1454
    - SIMULATION_ANNUITY
1455
    - SPECIFIC_REPLACEMENT_COSTS_INSTALLED
1456
    - SPECIFIC_REPLACEMENT_COSTS_OPTIMIZED
1457
    Notes
1458
    -----
1459

1460
    Tested with:
1461
    - test_evaluate_lifetime_costs_adds_all_parameters()
1462
    - Test_Economic_KPI.test_benchmark_Economic_KPI_C2_E2()
1463

1464
    """
1465
    if DISPATCH_PRICE in dict_asset:
1✔
1466
        C2.determine_lifetime_price_dispatch(dict_asset, economic_data)
1✔
1467

1468
    (
1✔
1469
        specific_capex,
1470
        specific_replacement_costs_optimized,
1471
        specific_replacement_costs_already_installed,
1472
    ) = C2.capex_from_investment(
1473
        investment_t0=dict_asset[SPECIFIC_COSTS][VALUE],
1474
        lifetime=dict_asset[LIFETIME][VALUE],
1475
        project_life=economic_data[PROJECT_DURATION][VALUE],
1476
        discount_factor=economic_data[DISCOUNTFACTOR][VALUE],
1477
        tax=economic_data[TAX][VALUE],
1478
        age_of_asset=dict_asset[AGE_INSTALLED][VALUE],
1479
        asset_label=dict_asset[LABEL],
1480
    )
1481

1482
    dict_asset.update(
1✔
1483
        {
1484
            LIFETIME_SPECIFIC_COST: {
1485
                VALUE: specific_capex,
1486
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1487
            }
1488
        }
1489
    )
1490

1491
    dict_asset.update(
1✔
1492
        {
1493
            SPECIFIC_REPLACEMENT_COSTS_OPTIMIZED: {
1494
                VALUE: specific_replacement_costs_optimized,
1495
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1496
            }
1497
        }
1498
    )
1499

1500
    dict_asset.update(
1✔
1501
        {
1502
            SPECIFIC_REPLACEMENT_COSTS_INSTALLED: {
1503
                VALUE: specific_replacement_costs_already_installed,
1504
                UNIT: dict_asset[SPECIFIC_COSTS][UNIT],
1505
            }
1506
        }
1507
    )
1508

1509
    # Annuities of components including opex AND capex #
1510
    dict_asset.update(
1✔
1511
        {
1512
            ANNUITY_SPECIFIC_INVESTMENT_AND_OM: {
1513
                VALUE: C2.annuity(
1514
                    dict_asset[LIFETIME_SPECIFIC_COST][VALUE],
1515
                    economic_data[CRF][VALUE],
1516
                )
1517
                + dict_asset[SPECIFIC_COSTS_OM][VALUE],  # changes from dispatch_price
1518
                UNIT: dict_asset[LIFETIME_SPECIFIC_COST][UNIT] + "/" + UNIT_YEAR,
1519
            }
1520
        }
1521
    )
1522

1523
    dict_asset.update(
1✔
1524
        {
1525
            LIFETIME_SPECIFIC_COST_OM: {
1526
                VALUE: dict_asset[SPECIFIC_COSTS_OM][VALUE]
1527
                * economic_data[ANNUITY_FACTOR][VALUE],
1528
                UNIT: dict_asset[SPECIFIC_COSTS_OM][UNIT][:-2],
1529
            }
1530
        }
1531
    )
1532

1533
    dict_asset.update(
1✔
1534
        {
1535
            SIMULATION_ANNUITY: {
1536
                VALUE: C2.simulation_annuity(
1537
                    dict_asset[ANNUITY_SPECIFIC_INVESTMENT_AND_OM][VALUE],
1538
                    settings[EVALUATED_PERIOD][VALUE],
1539
                ),
1540
                UNIT: CURR + "/" + UNIT + "/" + EVALUATED_PERIOD,
1541
            }
1542
        }
1543
    )
1544

1545

1546
# read timeseries. 2 cases are considered: Input type is related to demand or generation profiles,
1547
# so additional values like peak, total or average must be calculated. Any other type does not need this additional info.
1548
def receive_timeseries_from_csv(
1✔
1549
    settings, dict_asset, input_type, is_demand_profile=False
1550
):
1551
    """
1552

1553
    :param settings:
1554
    :param dict_asset:
1555
    :param type:
1556
    :return:
1557
    """
1558

1559
    load_from_timeseries_instead_of_file = False
1✔
1560

1561
    if input_type == "input" and "input" in dict_asset:
1✔
1562
        file_name = dict_asset[input_type][FILENAME]
×
1563
        header = dict_asset[input_type][HEADER]
×
1564
        unit = dict_asset[input_type][UNIT]
×
1565
    elif FILENAME in dict_asset:
1✔
1566
        # todo this input/file_name thing is a workaround and has to be improved in the future
1567
        # if only filename is given here, then only one column can be in the csv
1568
        file_name = dict_asset[FILENAME]
1✔
1569
        unit = dict_asset[UNIT] + "/" + UNIT_HOUR
1✔
1570
    elif FILENAME in dict_asset.get(input_type, []):
1✔
1571
        file_name = dict_asset[input_type][FILENAME]
×
1572
        header = dict_asset[input_type][HEADER]
×
1573
        unit = dict_asset[input_type][UNIT]
×
1574
    else:
1575
        load_from_timeseries_instead_of_file = True
1✔
1576
        file_name = ""
1✔
1577

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

1580
    if os.path.exists(file_path) is False or os.path.isfile(file_path) is False:
1✔
1581
        msg = (
1✔
1582
            f"Missing file! The timeseries file '{file_path}' \nof asset "
1583
            + f"{dict_asset[LABEL]} can not be found."
1584
        )
1585
        logging.warning(msg + " Looking for {TIMESERIES} entry.")
1✔
1586
        # if the file is not found
1587
        load_from_timeseries_instead_of_file = True
1✔
1588

1589
    else:
1590
        data_set = pd.read_csv(file_path, sep=",", keep_default_na=True)
1✔
1591

1592
    # If loading the data from the file does not work (file not present), the data might be
1593
    # already present in dict_values under TIMESERIES
1594
    if load_from_timeseries_instead_of_file is False:
1✔
1595
        if FILENAME in dict_asset:
1✔
1596
            header = data_set.columns[0]
1✔
1597
        series_values = data_set[header]
1✔
1598
    else:
1599
        if TIMESERIES in dict_asset:
1✔
1600
            series_values = dict_asset[TIMESERIES]
1✔
1601
        else:
1602
            raise FileNotFoundError(msg)
×
1603

1604
    if len(series_values.index) == settings[PERIODS]:
1✔
1605
        if input_type == "input":
1✔
1606
            timeseries = pd.Series(series_values.values, index=settings[TIME_INDEX])
1✔
1607
            timeseries = replace_nans_in_timeseries_with_0(
1✔
1608
                timeseries, dict_asset[LABEL]
1609
            )
1610
            dict_asset.update({TIMESERIES: timeseries})
1✔
1611
        else:
1612
            timeseries = pd.Series(series_values.values, index=settings[TIME_INDEX])
×
1613
            timeseries = replace_nans_in_timeseries_with_0(
×
1614
                timeseries, dict_asset[LABEL] + "(" + input_type + ")"
1615
            )
1616
            dict_asset[input_type][VALUE] = timeseries
×
1617

1618
        logging.debug("Added timeseries of %s (%s).", dict_asset[LABEL], file_path)
1✔
1619
    elif len(series_values.index) >= settings[PERIODS]:
1✔
1620
        if input_type == "input":
1✔
1621
            dict_asset.update(
1✔
1622
                {
1623
                    TIMESERIES: pd.Series(
1624
                        series_values[0 : len(settings[TIME_INDEX])].values,
1625
                        index=settings[TIME_INDEX],
1626
                    )
1627
                }
1628
            )
1629
        else:
1630
            dict_asset[input_type][VALUE] = pd.Series(
×
1631
                series_values[0 : len(settings[TIME_INDEX])].values,
1632
                index=settings[TIME_INDEX],
1633
            )
1634

1635
        logging.info(
1✔
1636
            "Provided timeseries of %s (%s) longer than evaluated period. "
1637
            "Excess data dropped.",
1638
            dict_asset[LABEL],
1639
            file_path,
1640
        )
1641

1642
    elif len(series_values.index) <= settings[PERIODS]:
×
1643
        logging.critical(
×
1644
            "Input error! "
1645
            "Provided timeseries of %s (%s) shorter than evaluated period. "
1646
            "Operation terminated",
1647
            dict_asset[LABEL],
1648
            file_path,
1649
        )
1650
        sys.exit()
×
1651

1652
    if input_type == "input":
1✔
1653
        compute_timeseries_properties(dict_asset)
1✔
1654

1655

1656
def replace_nans_in_timeseries_with_0(timeseries, label):
1✔
1657
    """Replaces nans in the timeseries (if any) with 0
1658

1659
    Parameters
1660
    ----------
1661

1662
    timeseries: pd.Series
1663
        demand or resource timeseries in dict_asset (having nan value(s) if any),
1664
        also of parameters that are not defined as scalars but as timeseries
1665

1666
    label: str
1667
        Contains user-defined information about the timeseries to be printed into the eventual error message
1668

1669
    Returns
1670
    -------
1671
    timeseries: pd.Series
1672
        timeseries without NaN values
1673

1674
    Notes
1675
    -----
1676
    Function tested with
1677
    - C0.test_replace_nans_in_timeseries_with_0()
1678
    """
1679
    if sum(pd.isna(timeseries)) > 0:
1✔
1680
        incidents = sum(pd.isna(timeseries))
1✔
1681
        logging.warning(
1✔
1682
            f"A number of {incidents} NaN value(s) found in the {TIMESERIES} of {label}. Changing NaN value(s) to 0."
1683
        )
1684
        timeseries = timeseries.fillna(0)
1✔
1685
    return timeseries
1✔
1686

1687

1688
def compute_timeseries_properties(dict_asset):
1✔
1689
    """Compute peak, aggregation, average and normalize timeseries
1690

1691
    Parameters
1692
    ----------
1693
    dict_asset: dict
1694
        dict of all asset parameters, must contain TIMESERIES key
1695

1696
    Returns
1697
    -------
1698
    None
1699
    Add TIMESERIES_PEAK, TIMESERIES_TOTAL, TIMESERIES_AVERAGE and TIMESERIES_NORMALIZED
1700
    to dict_asset
1701

1702
    Notes
1703
    -----
1704
    Function tested with
1705
    - C0.test_compute_timeseries_properties_TIMESERIES_in_dict_asset()
1706
    - C0.test_compute_timeseries_properties_TIMESERIES_not_in_dict_asset()
1707
    """
1708

1709
    if TIMESERIES in dict_asset:
1✔
1710
        timeseries = dict_asset[TIMESERIES]
1✔
1711
        unit = dict_asset[UNIT]
1✔
1712

1713
        dict_asset.update(
1✔
1714
            {
1715
                TIMESERIES_PEAK: {VALUE: max(timeseries), UNIT: unit,},
1716
                TIMESERIES_TOTAL: {VALUE: sum(timeseries), UNIT: unit,},
1717
                TIMESERIES_AVERAGE: {
1718
                    VALUE: sum(timeseries) / len(timeseries),
1719
                    UNIT: unit,
1720
                },
1721
            }
1722
        )
1723

1724
        logging.debug("Normalizing timeseries of %s.", dict_asset[LABEL])
1✔
1725
        dict_asset.update(
1✔
1726
            {TIMESERIES_NORMALIZED: timeseries / dict_asset[TIMESERIES_PEAK][VALUE]}
1727
        )
1728
        # just to be sure!
1729
        if any(dict_asset[TIMESERIES_NORMALIZED].values) > 1:
1✔
1730
            logging.error(
×
1731
                f"{dict_asset[LABEL]} normalized timeseries has values greater than 1."
1732
            )
1733
        if any(dict_asset[TIMESERIES_NORMALIZED].values) < 0:
1✔
1734
            logging.error(
×
1735
                f"{dict_asset[LABEL]} normalized timeseries has negative values."
1736
            )
1737

1738

1739
def treat_multiple_flows(dict_asset, dict_values, parameter):
1✔
1740
    """
1741
    This function consider the case a technical parameter on the json file has a list of values because multiple
1742
    inputs or outputs busses are considered.
1743
    Parameters
1744
    ----------
1745
    dict_values:
1746
    dictionary of current values of the asset
1747
    parameter:
1748
    usually efficiency. Different efficiencies will be given if an asset has multiple inputs or outputs busses,
1749
    so a list must be considered.
1750

1751
    Returns
1752
    -------
1753

1754
    """
1755
    updated_values = []
×
1756
    values_info = (
×
1757
        []
1758
    )  # filenames and headers will be stored to allow keeping track of the timeseries generation
1759
    for element in dict_asset[parameter][VALUE]:
×
1760
        if isinstance(element, dict):
×
1761
            updated_values.append(
×
1762
                get_timeseries_multiple_flows(
1763
                    dict_values[SIMULATION_SETTINGS],
1764
                    dict_asset,
1765
                    element[FILENAME],
1766
                    element[HEADER],
1767
                )
1768
            )
1769
            values_info.append(element)
×
1770
        else:
1771
            updated_values.append(element)
×
1772
    dict_asset[parameter][VALUE] = updated_values
×
1773
    if len(values_info) > 0:
×
1774
        dict_asset[parameter].update({"values_info": values_info})
×
1775

1776

1777
# reads timeseries specifically when the need comes from a multiple or output busses situation
1778
# returns the timeseries. Does not update any dictionary
1779
def get_timeseries_multiple_flows(settings, dict_asset, file_name, header):
1✔
1780
    """
1781

1782
    Parameters
1783
    ----------
1784
    dict_asset:
1785
    dictionary of the asset
1786
    file_name:
1787
    name of the file to read the time series
1788
    header:
1789
    name of the column where the timeseries is provided
1790

1791
    Returns
1792
    -------
1793

1794
    """
1795
    file_path = os.path.join(settings[PATH_INPUT_FOLDER], TIME_SERIES, file_name)
×
1796
    C1.lookup_file(file_path, dict_asset[LABEL])
×
1797

1798
    # TODO if FILENAME is not defined
1799

1800
    data_set = pd.read_csv(file_path, sep=",")
×
1801
    if len(data_set.index) == settings[PERIODS]:
×
1802
        return pd.Series(data_set[header].values, index=settings[TIME_INDEX])
×
1803
    elif len(data_set.index) >= settings[PERIODS]:
×
1804
        return pd.Series(
×
1805
            data_set[header][0 : len(settings[TIME_INDEX])].values,
1806
            index=settings[TIME_INDEX],
1807
        )
1808
    elif len(data_set.index) <= settings[PERIODS]:
×
1809
        logging.critical(
×
1810
            "Input error! "
1811
            "Provided timeseries of %s (%s) shorter then evaluated period. "
1812
            "Operation terminated",
1813
            dict_asset[LABEL],
1814
            file_path,
1815
        )
1816
        sys.exit()
×
1817

1818

1819
def process_maximum_cap_constraint(dict_values, group, asset, subasset=None):
1✔
1820
    # ToDo: should function be split into separate processing and validation functions?
1821
    """
1822
    Processes the maximumCap constraint depending on its value.
1823

1824
    * If MaximumCap not in asset dict: MaximumCap is None
1825
    * If MaximumCap < installedCap: invalid, MaximumCapValueInvalid raised
1826
    * If MaximumCap == 0: invalid, MaximumCap is None
1827
    * If group == energyProduction and filename not in asset_dict (dispatchable assets): pass
1828
    * If group == energyProduction and filename in asset_dict (non-dispatchable assets): MaximumCapNormalized == MaximumCap*peak(timeseries), MaximumAddCapNormalized == MaximumAddCap*peak(timeseries)
1829

1830
    Parameters
1831
    ----------
1832
    dict_values: dict
1833
        dictionary of all assets
1834

1835
    group: str
1836
        Group that the asset belongs to (str). Used to acces sub-asset data and for error messages.
1837

1838
    asset: str
1839
        asset name
1840

1841
    subasset: str or None
1842
        subasset name.
1843
        Default: None.
1844

1845
    Notes
1846
    -----
1847
    Tested with:
1848
    - test_process_maximum_cap_constraint_maximumCap_undefined()
1849
    - test_process_maximum_cap_constraint_maximumCap_is_None()
1850
    - test_process_maximum_cap_constraint_maximumCap_is_int()
1851
    - test_process_maximum_cap_constraint_maximumCap_is_float()
1852
    - test_process_maximum_cap_constraint_maximumCap_is_0()
1853
    - test_process_maximum_cap_constraint_maximumCap_is_int_smaller_than_installed_cap()
1854
    - test_process_maximum_cap_constraint_group_is_ENERGY_PRODUCTION_fuel_source()
1855
    - test_process_maximum_cap_constraint_group_is_ENERGY_PRODUCTION_non_dispatchable_asset()
1856
    - test_process_maximum_cap_constraint_subasset()
1857

1858
    Returns
1859
    -------
1860
    Updates the asset dictionary.
1861

1862
    * Unit of MaximumCap is asset unit
1863

1864
    """
1865
    if subasset is None:
1✔
1866
        asset_dict = dict_values[group][asset]
1✔
1867
    else:
1868
        asset_dict = dict_values[group][asset][subasset]
1✔
1869

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

1873
    # check if a maximumCap is defined
1874
    if MAXIMUM_CAP not in asset_dict:
1✔
1875
        asset_dict.update({MAXIMUM_CAP: {VALUE: None}})
1✔
1876
    else:
1877
        if asset_dict[MAXIMUM_CAP][VALUE] is not None:
1✔
1878
            # maximum additional capacity = maximum total capacity - installed capacity
1879
            max_add_cap = (
1✔
1880
                asset_dict[MAXIMUM_CAP][VALUE] - asset_dict[INSTALLED_CAP][VALUE]
1881
            )
1882
            # include the maximumAddCap parameter to the asset dictionary
1883
            asset_dict[MAXIMUM_ADD_CAP].update({VALUE: max_add_cap})
1✔
1884
            # raise error if maximumCap is smaller than installedCap and is not set to zero
1885
            if (
1✔
1886
                asset_dict[MAXIMUM_CAP][VALUE] < asset_dict[INSTALLED_CAP][VALUE]
1887
                and asset_dict[MAXIMUM_CAP][VALUE] != 0
1888
            ):
1889
                message = (
1✔
1890
                    f"The stated total maximumCap in {group} {asset} is smaller than the "
1891
                    f"installedCap ({asset_dict[MAXIMUM_CAP][VALUE]}/{asset_dict[INSTALLED_CAP][VALUE]}). Please enter a greater maximumCap."
1892
                )
1893
                raise MaximumCapValueInvalid(message)
1✔
1894

1895
            # set maximumCap to None if it is zero
1896
            if asset_dict[MAXIMUM_CAP][VALUE] == 0:
1✔
1897
                message = (
1✔
1898
                    f"The stated maximumCap of zero in {group} {asset} is invalid."
1899
                    "For this simulation, the maximumCap will be "
1900
                    "disregarded and not be used in the simulation."
1901
                )
1902
                warnings.warn(UserWarning(message))
1✔
1903
                logging.warning(message)
1✔
1904
                asset_dict[MAXIMUM_CAP][VALUE] = None
1✔
1905

1906
            # adapt maximumCap and maximumAddCap of non-dispatchable sources
1907
            if (
1✔
1908
                group == ENERGY_PRODUCTION
1909
                and asset_dict.get(DISPATCHABILITY, True) is False
1910
                and asset_dict[MAXIMUM_CAP][VALUE] is not None
1911
            ):
1912
                max_cap_norm = (
1✔
1913
                    asset_dict[MAXIMUM_CAP][VALUE] * asset_dict[TIMESERIES_PEAK][VALUE]
1914
                )
1915
                asset_dict.update(
1✔
1916
                    {
1917
                        MAXIMUM_CAP_NORMALIZED: {
1918
                            VALUE: max_cap_norm,
1919
                            UNIT: asset_dict[UNIT],
1920
                        }
1921
                    }
1922
                )
1923
                logging.debug(
1✔
1924
                    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."
1925
                )
1926
                max_add_cap_norm = (
1✔
1927
                    asset_dict[MAXIMUM_ADD_CAP][VALUE]
1928
                    * asset_dict[TIMESERIES_PEAK][VALUE]
1929
                )
1930
                asset_dict.update(
1✔
1931
                    {
1932
                        MAXIMUM_ADD_CAP_NORMALIZED: {
1933
                            VALUE: max_add_cap_norm,
1934
                            UNIT: asset_dict[UNIT],
1935
                        }
1936
                    }
1937
                )
1938
                logging.debug(
1✔
1939
                    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."
1940
                )
1941

1942
    asset_dict[MAXIMUM_CAP].update({UNIT: asset_dict[UNIT]})
1✔
1943

1944

1945
def process_normalized_installed_cap(dict_values, group, asset, subasset=None):
1✔
1946
    """
1947
    Processes the normalized installed capacity value based on the installed capacity value and the chosen timeseries.
1948

1949
    Parameters
1950
    ----------
1951
    dict_values: dict
1952
        dictionary of all assets
1953

1954
    group: str
1955
        Group that the asset belongs to (str). Used to acces sub-asset data and for error messages.
1956

1957
    asset: str
1958
        asset name
1959

1960
    subasset: str or None
1961
        subasset name.
1962
        Default: None.
1963

1964
    Notes
1965
    -----
1966
    Tested with:
1967
    - test_process_normalized_installed_cap()
1968

1969
    Returns
1970
    -------
1971
    Updates the asset dictionary with the normalizedInstalledCap value.
1972

1973
    """
1974
    if subasset is None:
1✔
1975
        asset_dict = dict_values[group][asset]
1✔
1976
    else:
1977
        asset_dict = dict_values[group][asset][subasset]
×
1978

1979
    if asset_dict[FILENAME] is not None:
1✔
1980
        inst_cap_norm = (
1✔
1981
            asset_dict[INSTALLED_CAP][VALUE] * asset_dict[TIMESERIES_PEAK][VALUE]
1982
        )
1983
        asset_dict.update(
1✔
1984
            {INSTALLED_CAP_NORMALIZED: {VALUE: inst_cap_norm, UNIT: asset_dict[UNIT]}}
1985
        )
1986
        logging.debug(
1✔
1987
            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]})."
1988
        )
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