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

rl-institut / multi-vector-simulator / 8870538658

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

push

github

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

Fix/black vulnerability

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

826 existing lines in 21 files now uncovered.

5977 of 7908 relevant lines covered (75.58%)

0.76 hits per line

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

80.72
/src/multi_vector_simulator/D1_model_components.py
1
"""
2
Module D1 - Oemof components
3
============================
4

5
Module D1 includes all functions that are required to build an oemof model with adaptable components.
6

7
- add transformer objects (fix, to be optimized)
8
- add source objects (fix, to be optimized, dispatchable, non-dispatchable)
9
- add sink objects (fix, to be optimized, dispatchable, non-dispatchable)
10
- add storage objects (fix, to be optimized)
11
- add multiple input/output busses if required for each of the assets
12
- add oemof component parameters as scalar or time series values
13

14
"""
15

16
import logging
1✔
17

18
import pandas as pd
1✔
19
from oemof import solph
1✔
20

21
from multi_vector_simulator.utils.constants_json_strings import (
1✔
22
    VALUE,
23
    UNIT,
24
    LABEL,
25
    DISPATCH_PRICE,
26
    AVAILABILITY_DISPATCH,
27
    OPTIMIZE_CAP,
28
    INSTALLED_CAP,
29
    INSTALLED_CAP_NORMALIZED,
30
    EFFICIENCY,
31
    ENERGY_VECTOR,
32
    INPUT_POWER,
33
    OUTPUT_POWER,
34
    C_RATE,
35
    SOC_INITIAL,
36
    SOC_MAX,
37
    SOC_MIN,
38
    THERM_LOSSES_REL,
39
    THERM_LOSSES_ABS,
40
    STORAGE_CAPACITY,
41
    TIMESERIES,
42
    TIMESERIES_NORMALIZED,
43
    TIMESERIES_PEAK,
44
    INFLOW_DIRECTION,
45
    OUTFLOW_DIRECTION,
46
    SIMULATION_ANNUITY,
47
    MAXIMUM_ADD_CAP,
48
    MAXIMUM_ADD_CAP_NORMALIZED,
49
    DISPATCHABILITY,
50
    TYPE_ASSET,
51
    OEMOF_ASSET_TYPE,
52
    OEMOF_GEN_STORAGE,
53
    OEMOF_SINK,
54
    OEMOF_SOURCE,
55
    OEMOF_TRANSFORMER,
56
    OEMOF_BUSSES,
57
    OEMOF_ExtractionTurbineCHP,
58
    EMISSION_FACTOR,
59
    BETA,
60
    INVESTMENT_BUS,
61
    REDUCABLE_DEMAND,
62
)
63
from multi_vector_simulator.utils.helpers import (
1✔
64
    get_item_if_list,
65
    get_length_if_list,
66
    reducable_demand_name,
67
)
68
from multi_vector_simulator.utils.exceptions import (
1✔
69
    MissingParameterError,
70
    WrongParameterFormatError,
71
)
72

73

74
def check_list_parameters_transformers_single_input_single_output(
1✔
75
    dict_asset, n_timesteps
76
):
77
    parameters_defined_as_list = []
1✔
78
    for parameter in [DISPATCH_PRICE, EFFICIENCY]:
1✔
79
        len_param = get_length_if_list(dict_asset[parameter][VALUE])
1✔
80
        if len_param != 0 and len_param != n_timesteps:
1✔
81
            parameters_defined_as_list.append(parameter)
1✔
82

83
    if parameters_defined_as_list:
1✔
84
        parameters_defined_as_list = ", ".join(parameters_defined_as_list)
1✔
85
        missing_dispatch_prices_or_efficiencies = (
1✔
86
            f"You defined multiple values for parameter(s) '{parameters_defined_as_list}'"
87
            f" although you you have one input and one output for"
88
            f" the conversion asset named '{dict_asset[LABEL]}', please provide only scalars"
89
            f" or define more input/output busses"
90
        )
91
        logging.error(missing_dispatch_prices_or_efficiencies)
1✔
92
        raise ValueError(missing_dispatch_prices_or_efficiencies)
1✔
93

94

95
def transformer(model, dict_asset, **kwargs):
1✔
96
    r"""
97
    Defines a transformer component specified in `dict_asset`.
98

99
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the transformer
100
    is defined with a fixed capacity or a capacity to be optimized.
101
    The transformer has multiple or single input or output busses depending on
102
    the types of keys 'inflow_direction' and 'outflow_direction' in `dict_asset`.
103

104
    Parameters
105
    ----------
106
    model : oemof.solph.network.EnergySystem object
107
        See the oemof documentation for more information.
108
    dict_asset : dict
109
        Contains information about the transformer like (not exhaustive):
110
        efficiency, installed capacity ('installedCap'), information on the
111
        busses the transformer is connected to ('inflow_direction',
112
        'outflow_direction').
113

114
    Other Parameters
115
    ----------------
116
    busses : dict
117
    sinks : dict, optional
118
    sources : dict, optional
119
    transformers : dict
120
    storages : dict, optional
121

122
    Notes
123
    -----
124
    The transformer has either multiple input or multiple output busses.
125

126
    The following functions are used for defining the transformer:
127
    * :py:func:`~.transformer_constant_efficiency_fix`
128
    * :py:func:`~.transformer_constant_efficiency_optimize`
129

130
    Tested with:
131
    - test_transformer_optimize_cap_single_busses()
132
    - test_transformer_optimize_cap_multiple_input_busses()
133
    - test_transformer_optimize_cap_multiple_output_busses()
134
    - test_transformer_fix_cap_single_busses()
135
    - test_transformer_fix_cap_multiple_input_busses()
136
    - test_transformer_fix_cap_multiple_output_busses()
137

138
    Returns
139
    -------
140
    Indirectly updated `model` and dict of asset in `kwargs` with transformer object.
141

142
    """
143
    check_optimize_cap(
1✔
144
        model,
145
        dict_asset,
146
        func_constant=transformer_constant_efficiency_fix,
147
        func_optimize=transformer_constant_efficiency_optimize,
148
        **kwargs,
149
    )
150

151

152
def chp(model, dict_asset, **kwargs):
1✔
153
    r"""
154
    Defines a chp component specified in `dict_asset`.
155

156
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the chp
157
    is defined with a fixed capacity or a capacity to be optimized.
158
    The chp has single input and multiple output busses.
159

160
    Parameters
161
    ----------
162
    model : oemof.solph.network.EnergySystem object
163
        See the oemof documentation for more information.
164
    dict_asset : dict
165
        Contains information about the chp like (not exhaustive):
166
        efficiency, installed capacity ('installedCap'), information on the
167
        busses the chp is connected to ('inflow_direction',
168
        'outflow_direction'), beta coefficient.
169

170
    Other Parameters
171
    ----------------
172
    busses : dict
173
    sinks : dict, optional
174
    sources : dict, optional
175
    transformers : dict
176
    storages : dict, optional
177
    extractionTurbineCHP: dict, optional
178

179
    Notes
180
    -----
181
    The transformer has either multiple input or multiple output busses.
182

183
    The following functions are used for defining the chp:
184
    * :py:func:`~.chp_fix`
185
    * :py:func:`~.chp_optimize` for investment optimization
186

187
    Tested with:
188
    - test_chp_fix_cap()
189
    - test_chp_optimize_cap()
190
    - test_chp_missing_beta()
191
    - test_chp_wrong_beta_formatting()
192
    - test_chp_wrong_efficiency_formatting()
193
    - test_chp_wrong_outflow_bus_energy_vector()
194

195
    Returns
196
    -------
197
    Indirectly updated `model` and dict of asset in `kwargs` with chp object.
198

199
    """
200
    if BETA in dict_asset:
1✔
201
        beta = dict_asset[BETA]
1✔
202
        if isinstance(beta, dict) is False:
1✔
203
            raise WrongParameterFormatError(
1✔
204
                f"For the conversion asset named '{dict_asset[LABEL]}' of type {OEMOF_ExtractionTurbineCHP}, "
205
                f"the {BETA} parameter should have the following format {{ '{VALUE}': ..., '{UNIT}': ... }}"
206
            )
207
        else:
208
            beta = beta[VALUE]
1✔
209
        if 0 <= beta <= 1:
1✔
UNCOV
210
            pass
×
211
        else:
UNCOV
212
            raise ValueError("beta should be a number between 0 and 1.")
×
213
    else:
214
        raise MissingParameterError("No beta for extraction turbine chp specified.")
1✔
215

216
    if isinstance(dict_asset[EFFICIENCY][VALUE], list) is False:
1✔
217
        missing_efficiencies = (
1✔
218
            f"For the conversion asset named '{dict_asset[LABEL]}' of type {OEMOF_ExtractionTurbineCHP} "
219
            f"you must provide exactly 2 values for the parameter '{EFFICIENCY}'."
220
        )
221
        logging.error(missing_efficiencies)
1✔
222
        raise WrongParameterFormatError(missing_efficiencies)
1✔
223

224
    busses_energy_vectors = [
1✔
225
        kwargs[OEMOF_BUSSES][b].energy_vector for b in dict_asset[OUTFLOW_DIRECTION]
226
    ]
227
    if (
1✔
228
        "Heat" not in busses_energy_vectors
229
        or "Electricity" not in busses_energy_vectors
230
    ):
231
        mapping_busses = [
1✔
232
            f"'{v}' (from '{k}')"
233
            for k, v in zip(dict_asset[OUTFLOW_DIRECTION], busses_energy_vectors)
234
        ]
235
        wrong_output_energy_vectors = (
1✔
236
            f"For the conversion asset named '{dict_asset[LABEL]}' of type {OEMOF_ExtractionTurbineCHP} "
237
            f"you must provide 1 output bus for energy vector 'Heat' and one for 'Electricity'. You provided "
238
            f"{' and '.join(mapping_busses)}"
239
        )
240
        logging.error(wrong_output_energy_vectors)
1✔
241
        raise WrongParameterFormatError(wrong_output_energy_vectors)
1✔
242

243
    check_optimize_cap(
1✔
244
        model, dict_asset, func_constant=chp_fix, func_optimize=chp_optimize, **kwargs
245
    )
246

247

248
def storage(model, dict_asset, **kwargs):
1✔
249
    r"""
250
    Defines a storage component specified in `dict_asset`.
251

252
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the storage
253
    is defined with a fixed capacity or a capacity to be optimized.
254

255
    Parameters
256
    ----------
257
    model : oemof.solph.network.EnergySystem object
258
        See the oemof documentation for more information.
259
    dict_asset : dict
260
        Contains information about the storage like (not exhaustive):
261
        efficiency, installed capacity ('installedCap'), information on the
262
        busses the storage is connected to ('inflow_direction',
263
        'outflow_direction'),
264

265
    Other Parameters
266
    ----------------
267
    busses : dict
268
    sinks : dict, optional
269
    sources : dict, optional
270
    transformers : dict, optional
271
    storages : dict
272

273
    Notes
274
    -----
275
    The following functions are used for defining the storage:
276
    * :py:func:`~.storage_fix`
277
    * :py:func:`~.storage_optimize`
278

279
    Tested with:
280
    - test_storage_optimize()
281
    - test_storage_fix()
282

283
    """
284

285
    # Make sure the initial storage level is within the max and min values
286
    initial_storage_level = dict_asset[STORAGE_CAPACITY][SOC_INITIAL][VALUE]
1✔
287
    min_storage_level = dict_asset[STORAGE_CAPACITY][SOC_MIN][VALUE]
1✔
288
    max_storage_level = dict_asset[STORAGE_CAPACITY][SOC_MAX][VALUE]
1✔
289

290
    if initial_storage_level is not None:
1✔
UNCOV
291
        if pd.isna(initial_storage_level):
×
UNCOV
292
            dict_asset[STORAGE_CAPACITY][SOC_INITIAL][VALUE] = min_storage_level
×
293

UNCOV
294
        if initial_storage_level < min_storage_level:
×
UNCOV
295
            dict_asset[STORAGE_CAPACITY][SOC_INITIAL][VALUE] = min_storage_level
×
UNCOV
296
            logging.warning(
×
297
                f"The initial storage level of the battery asset {dict_asset[LABEL]} was below the minimal allowed value ({initial_storage_level} < {min_storage_level}), the initial level was ajusted to be equal to the minimum, please check your input files."
298
            )
UNCOV
299
        elif initial_storage_level > max_storage_level:
×
UNCOV
300
            dict_asset[STORAGE_CAPACITY][SOC_INITIAL][VALUE] = max_storage_level
×
UNCOV
301
            logging.warning(
×
302
                f"The initial storage level of the battery asset {dict_asset[LABEL]} was above the maximal allowed value ({initial_storage_level} > {max_storage_level}), the initial level was ajusted to be equal to the maximum, please check your input files."
303
            )
304
    else:
305
        if not isinstance(min_storage_level, float):
1✔
UNCOV
306
            raise ValueError(
×
307
                f"At the moment it is not possible to use multiple values of min_storage level"
308
            )
UNCOV
309
            min_storage_level = min_storage_level[0]
×
310
        dict_asset[STORAGE_CAPACITY][SOC_INITIAL][VALUE] = min_storage_level
1✔
311

312
    check_optimize_cap(
1✔
313
        model,
314
        dict_asset,
315
        func_constant=storage_fix,
316
        func_optimize=storage_optimize,
317
        **kwargs,
318
    )
319

320

321
def sink(model, dict_asset, **kwargs):
1✔
322
    r"""
323
    Defines a sink component specified in `dict_asset`.
324

325
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the sink
326
    is defined with a fixed capacity or a capacity to be optimized. If a time
327
    series is provided for the sink (key 'timeseries' in `dict_asset`) it is
328
    defined as a non dispatchable sink, otherwise as dispatchable sink.
329
    The sink has multiple or a single input bus depending on the type of the
330
    key 'inflow_direction' in `dict_asset`.
331

332
    Parameters
333
    ----------
334
    model : oemof.solph.network.EnergySystem object
335
        See the oemof documentation for more information.
336
    dict_asset : dict
337
        Contains information about the storage like (not exhaustive):
338
        efficiency, installed capacity ('installedCap'), information on the
339
        busses the sink is connected to ('inflow_direction'),
340

341
    Other Parameters
342
    ----------------
343
    busses : dict
344
    sinks : dict
345
    sources : dict, optional
346
    transformers : dict, optional
347
    storages : dict, optional
348

349
    Notes
350
    -----
351
    The following functions are used for defining the sink:
352
    * :py:func:`~.sink_non_dispatchable`
353
    * :py:func:`~.sink_dispatchable`
354

355
    Tested with:
356
    - test_sink_non_dispatchable_single_input_bus()
357
    - test_sink_non_dispatchable_multiple_input_busses()
358
    - test_sink_dispatchable_single_input_bus()
359
    - test_sink_dispatchable_multiple_input_busses()
360

361
    """
362
    if TIMESERIES in dict_asset:
1✔
363
        if dict_asset.get(TYPE_ASSET) == REDUCABLE_DEMAND:
1✔
UNCOV
364
            sink_demand_reduction(model, dict_asset, **kwargs)
×
365
        else:
366
            sink_non_dispatchable(model, dict_asset, **kwargs)
1✔
367

368
    else:
369
        sink_dispatchable_optimize(model, dict_asset, **kwargs)
1✔
370

371

372
def source(model, dict_asset, **kwargs):
1✔
373
    r"""
374
    Defines a source component specified in `dict_asset`.
375

376
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the source
377
    is defined with a fixed capacity or a capacity to be optimized. If a time
378
    series is provided for the source (key 'timeseries' in `dict_asset`) it is
379
    defined as a non dispatchable source, otherwise as dispatchable source.
380
    The source has multiple or a single output bus depending on the type of the
381
    key 'inflow_direction' in `dict_asset`.
382

383
    Parameters
384
    ----------
385
    model : oemof.solph.network.EnergySystem object
386
        See the oemof documentation for more information.
387
    dict_asset : dict
388
        Contains information about the storage like (not exhaustive):
389
        efficiency, installed capacity ('installedCap'), information on the
390
        busses the sink is connected to ('inflow_direction'),
391

392
    Other Parameters
393
    ----------------
394
    busses : dict
395
    sinks : dict
396
    sources : dict, optional
397
    transformers : dict, optional
398
    storages : dict, optional
399

400
    TODOS
401
    ^^^^^
402
    * We should actually not allow multiple output busses, probably - because a pv would then
403
    feed in twice as much as solar_gen_specific for example, see issue #121
404

405
    Notes
406
    -----
407
    The following functions are used for defining the source:
408
    * :py:func:`~.source_dispatchable_fix`
409
    * :py:func:`~.source_dispatchable_optimize`
410
    * :py:func:`~.source_non_dispatchable_fix`
411
    * :py:func:`~.source_non_dispatchable_optimize`
412

413
    Tested with:
414
    - test_source_non_dispatchable_optimize()
415
    - test_source_non_dispatchable_fix()
416
    - test_source_dispatchable_optimize_normalized_timeseries()
417
    - test_source_dispatchable_optimize_timeseries_not_normalized_timeseries()
418
    - test_source_dispatchable_fix_normalized_timeseries()
419
    - test_source_dispatchable_fix_timeseries_not_normalized_timeseries()
420
    """
421
    if DISPATCHABILITY in dict_asset and dict_asset[DISPATCHABILITY] is True:
1✔
422
        check_optimize_cap(
1✔
423
            model,
424
            dict_asset,
425
            func_constant=source_dispatchable_fix,
426
            func_optimize=source_dispatchable_optimize,
427
            **kwargs,
428
        )
429

430
    else:
431
        check_optimize_cap(
1✔
432
            model,
433
            dict_asset,
434
            func_constant=source_non_dispatchable_fix,
435
            func_optimize=source_non_dispatchable_optimize,
436
            **kwargs,
437
        )
438

439

440
def check_optimize_cap(model, dict_asset, func_constant, func_optimize, **kwargs):
1✔
441
    r"""
442
    Defines a component specified in `dict_asset` with fixed capacity or capacity to be optimized.
443

444
    Parameters
445
    ----------
446
    model : oemof.solph.network.EnergySystem object
447
        See the oemof documentation for more information.
448
    dict_asset : dict
449
        Contains information about the asset like (not exhaustive):
450
        efficiency, installed capacity ('installedCap'), information on the
451
        busses the asset is connected to (f.e. 'inflow_direction',
452
        'outflow_direction').
453
    func_constant : func
454
        Function to be applied if optimization not intended.
455
    func_optimize : func
456
        Function to be applied if optimization not intended.
457

458
    Other Parameters
459
    ----------------
460
    Required are `busses` and a dictionary belonging to the respective oemof
461
    type of the asset.
462

463
    busses : dict, optional
464
    sinks : dict, optional
465
    sources : dict, optional
466
    transformers : dict, optional
467
    storages : dict, optional
468

469
    Returns
470
    -------
471
    Indirectly updated `model` and dict of asset in `kwargs` with the component object.
472

473
    TODOS
474
    ^^^^^
475
    Might be possible to drop non invest optimization in favour of invest optimization if max_capactiy
476
    attributes ie. are set to 0 for fix (but less beautiful, and in case of generator even blocks nonconvex opt.).
477

478
    Notes
479
    -----
480
    Tested with:
481
    - test_check_optimize_cap_raise_error()
482

483
    """
484
    if dict_asset[OPTIMIZE_CAP][VALUE] is False:
1✔
485
        func_constant(model, dict_asset, **kwargs)
1✔
486
        if dict_asset[OEMOF_ASSET_TYPE] != OEMOF_SOURCE:
1✔
487
            logging.debug(
1✔
488
                "Added: %s %s (fixed capacity)",
489
                dict_asset[OEMOF_ASSET_TYPE].capitalize(),
490
                dict_asset[LABEL],
491
            )
492

493
    elif dict_asset[OPTIMIZE_CAP][VALUE] is True:
1✔
494
        func_optimize(model, dict_asset, **kwargs)
1✔
495
        if dict_asset[OEMOF_ASSET_TYPE] != OEMOF_SOURCE:
1✔
496
            logging.debug(
1✔
497
                "Added: %s %s (capacity to be optimized)",
498
                dict_asset[OEMOF_ASSET_TYPE].capitalize(),
499
                dict_asset[LABEL],
500
            )
501
    else:
502
        raise ValueError(
1✔
503
            f"Input error! '{OPTIMIZE_CAP}' of asset {dict_asset[LABEL]}\n should be True/False but is {dict_asset[OPTIMIZE_CAP][VALUE]}."
504
        )
505

506

507
class CustomBus(solph.Bus):
1✔
508
    def __init__(self, *args, **kwargs):
1✔
509
        ev = kwargs.pop("energy_vector", None)  # change to ENERGY_VECTOR
1✔
510
        super(CustomBus, self).__init__(*args, **kwargs)
1✔
511
        self.energy_vector = ev
1✔
512

513

514
def bus(model, name, **kwargs):
1✔
515
    r"""
516
    Adds bus `name` to `model` and to 'busses' in `kwargs`.
517

518
    Notes
519
    -----
520
    Tested with:
521
    - test_bus_add_to_empty_dict()
522
    - test_bus_add_to_not_empty_dict()
523

524
    """
525
    logging.debug(f"Added: Bus {name}")
1✔
526
    energy_vector = kwargs.get("energy_vector", None)  # change to ENERGY_VECTOR
1✔
527
    bus = CustomBus(label=name, energy_vector=energy_vector)
1✔
528
    kwargs[OEMOF_BUSSES].update({name: bus})
1✔
529
    model.add(bus)
1✔
530

531

532
def transformer_constant_efficiency_fix(model, dict_asset, **kwargs):
1✔
533
    r"""
534
    Defines a transformer with constant efficiency and fixed capacity.
535

536
    See :py:func:`~.transformer` for more information, including parameters.
537

538
    Notes
539
    -----
540
    Tested with:
541
    - test_transformer_fix_cap_single_busses()
542
    - test_transformer_fix_cap_multiple_input_busses()
543
    - test_transformer_fix_cap_multiple_output_busses()
544

545
    Returns
546
    -------
547
    Indirectly updated `model` and dict of asset in `kwargs` with the transformer object.
548

549
    """
550

551
    missing_dispatch_prices_or_efficiencies = None
1✔
552

553
    # check if the transformer has multiple input or multiple output busses
554
    if isinstance(dict_asset[INFLOW_DIRECTION], list) or isinstance(
1✔
555
        dict_asset[OUTFLOW_DIRECTION], list
556
    ):
557
        if isinstance(dict_asset[INFLOW_DIRECTION], list) and isinstance(
1✔
558
            dict_asset[OUTFLOW_DIRECTION], str
559
        ):
560
            # multiple inputs and single output
561
            inputs = {}
1✔
562

563
            num_inputs = len(dict_asset[INFLOW_DIRECTION])
1✔
564
            inputs_names = ", ".join([f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]])
1✔
565
            if get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_inputs:
1✔
UNCOV
566
                missing_dispatch_prices_or_efficiencies = (
×
567
                    f"You defined multiple values for parameter '{INFLOW_DIRECTION}' "
568
                    f"({inputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
569
                    f"You must also provide exactly {num_inputs} values for the parameter '{EFFICIENCY}'."
570
                )
UNCOV
571
                logging.error(missing_dispatch_prices_or_efficiencies)
×
UNCOV
572
                raise ValueError(missing_dispatch_prices_or_efficiencies)
×
573

574
            if get_length_if_list(dict_asset[DISPATCH_PRICE][VALUE]) == 0:
1✔
575
                # only one dispatch price provided --> it will be ignored
576
                warning_msg = (
×
577
                    f"You defined multiple values for parameter '{INFLOW_DIRECTION}' "
578
                    f"({inputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
579
                    f"You can also provide exactly {num_inputs} values for the parameter '{DISPATCH_PRICE}'."
580
                    f"You did only provide one value, so we will ignore it"
581
                )
UNCOV
582
                logging.warning(warning_msg)
×
583
                for i, bus in enumerate(dict_asset[INFLOW_DIRECTION]):
×
584
                    inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow()
×
585
            else:
586
                for i, bus in enumerate(dict_asset[INFLOW_DIRECTION]):
1✔
587
                    inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
588
                        variable_costs=get_item_if_list(
589
                            dict_asset[DISPATCH_PRICE][VALUE], i
590
                        )
591
                    )
592

593
            outputs = {
1✔
594
                kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
595
                    nominal_value=dict_asset[INSTALLED_CAP][VALUE]
596
                )
597
            }
598
            efficiencies = {}
1✔
599
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
600
                efficiencies[kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION][i]]] = (
1✔
601
                    efficiency
602
                )
603

604
        elif isinstance(dict_asset[INFLOW_DIRECTION], str) and isinstance(
1✔
605
            dict_asset[OUTFLOW_DIRECTION], list
606
        ):
607
            # single input and multiple outputs
608
            num_outputs = len(dict_asset[OUTFLOW_DIRECTION])
1✔
609
            if get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_outputs:
1✔
610
                outputs_names = ", ".join(
1✔
611
                    [f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]]
612
                )
613
                missing_dispatch_prices_or_efficiencies = (
1✔
614
                    f"You defined multiple values for parameter '{OUTFLOW_DIRECTION}' "
615
                    f"({outputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
616
                    f"You must also provide exactly {num_outputs} values for the parameters "
617
                    f"'{EFFICIENCY}' and you can do so for the parameter '{DISPATCH_PRICE}'."
618
                )
619
                logging.error(missing_dispatch_prices_or_efficiencies)
1✔
620
                raise ValueError(missing_dispatch_prices_or_efficiencies)
1✔
621

622
            inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
623
            outputs = {}
1✔
624
            for i, bus in enumerate(dict_asset[OUTFLOW_DIRECTION]):
1✔
625
                outputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
626
                    nominal_value=get_item_if_list(dict_asset[INSTALLED_CAP][VALUE], i),
627
                    variable_costs=get_item_if_list(
628
                        dict_asset[DISPATCH_PRICE][VALUE], i
629
                    ),
630
                )
631

632
            efficiencies = {}
1✔
633
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
634
                efficiencies[kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][i]]] = (
1✔
635
                    efficiency
636
                )
637

638
        else:
639
            # multiple inputs and multiple outputs
UNCOV
640
            inputs_names = ", ".join([f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]])
×
UNCOV
641
            outputs_names = ", ".join([f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]])
×
UNCOV
642
            missing_dispatch_prices_or_efficiencies = (
×
643
                f"You defined multiple values for parameter '{INFLOW_DIRECTION}'"
644
                f" ({inputs_names}) as well as for parameter '{OUTFLOW_DIRECTION}' ({outputs_names})"
645
                f" of the conversion asset named '{dict_asset[LABEL]}', this is not supported"
646
                f" at the moment."
647
            )
UNCOV
648
            logging.error(missing_dispatch_prices_or_efficiencies)
×
649
            raise ValueError(missing_dispatch_prices_or_efficiencies)
×
650
    else:
651
        # single input and single output
652

653
        min_load_opts = {"min": 0, "max": 1}
1✔
654
        min_load = dict_asset.get(SOC_MIN, None)
1✔
655
        if min_load is not None:
1✔
656
            if min_load[VALUE] != 0:
×
UNCOV
657
                logging.warning(
×
658
                    f"Minimal load of {min_load[VALUE]} was set to asset {dict_asset[LABEL]}"
659
                )
UNCOV
660
            min_load_opts["min"] = min_load[VALUE]
×
661
        max_load = dict_asset.get(SOC_MAX, None)
1✔
662
        if max_load is not None:
1✔
UNCOV
663
            if max_load[VALUE] != 1:
×
UNCOV
664
                logging.warning(
×
665
                    f"Maximal load of {max_load[VALUE]} was set to asset {dict_asset[LABEL]}"
666
                )
667

UNCOV
668
            min_load_opts["max"] = max_load[VALUE]
×
669

670
        check_list_parameters_transformers_single_input_single_output(
1✔
671
            dict_asset, model.timeindex.size
672
        )
673

674
        inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
675
        outputs = {
1✔
676
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
677
                nominal_value=dict_asset[INSTALLED_CAP][VALUE],
678
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
679
                **min_load_opts,
680
            )
681
        }
682
        efficiencies = {
1✔
683
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: dict_asset[EFFICIENCY][
684
                VALUE
685
            ]
686
        }
687

688
    if missing_dispatch_prices_or_efficiencies is None:
1✔
689
        t = solph.components.Converter(
1✔
690
            label=dict_asset[LABEL],
691
            inputs=inputs,
692
            outputs=outputs,
693
            conversion_factors=efficiencies,
694
        )
695

696
        model.add(t)
1✔
697
        kwargs[OEMOF_TRANSFORMER].update({dict_asset[LABEL]: t})
1✔
698

699

700
def transformer_constant_efficiency_optimize(model, dict_asset, **kwargs):
1✔
701
    r"""
702
    Defines a transformer with constant efficiency and a capacity to be optimized.
703

704
    See :py:func:`~.transformer` for more information, including parameters.
705

706
    Notes
707
    -----
708
    Tested with:
709
    - test_transformer_optimize_cap_single_busses()
710
    - test_transformer_optimize_cap_multiple_input_busses()
711
    - test_transformer_optimize_cap_multiple_output_busses()
712

713
    Returns
714
    -------
715
    Indirectly updated `model` and dict of asset in `kwargs` with the transformer object.
716

717
    """
718
    missing_dispatch_prices_or_efficiencies = None
1✔
719

720
    investment_bus = dict_asset.get(INVESTMENT_BUS)
1✔
721
    invest_opts = {}
1✔
722
    if dict_asset[MAXIMUM_ADD_CAP][VALUE] is not None:
1✔
723
        invest_opts["maximum"] = dict_asset[MAXIMUM_ADD_CAP][VALUE]
1✔
724

725
    investment = solph.Investment(
1✔
726
        ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
727
        existing=dict_asset[INSTALLED_CAP][VALUE],
728
        **invest_opts,
729
    )
730

731
    # check if the transformer has multiple input or multiple output busses
732
    # the investment object is always in the output bus
733
    if isinstance(dict_asset[INFLOW_DIRECTION], list) or isinstance(
1✔
734
        dict_asset[OUTFLOW_DIRECTION], list
735
    ):
736
        if isinstance(dict_asset[INFLOW_DIRECTION], list) and isinstance(
1✔
737
            dict_asset[OUTFLOW_DIRECTION], str
738
        ):
739
            # multiple inputs and single output
740
            num_inputs = len(dict_asset[INFLOW_DIRECTION])
1✔
741
            if get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_inputs:
1✔
UNCOV
742
                inputs_names = ", ".join(
×
743
                    [f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]]
744
                )
UNCOV
745
                missing_dispatch_prices_or_efficiencies = (
×
746
                    f"You defined multiple values for parameter '{INFLOW_DIRECTION}' "
747
                    f"({inputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
748
                    f"You must also provide exactly {num_inputs} values for the parameter "
749
                    f"'{EFFICIENCY}' and you can do so for the parameter '{DISPATCH_PRICE}'."
750
                )
UNCOV
751
                logging.error(missing_dispatch_prices_or_efficiencies)
×
UNCOV
752
                raise ValueError(missing_dispatch_prices_or_efficiencies)
×
753

754
            if investment_bus is None:
1✔
755
                investment_bus = dict_asset[OUTFLOW_DIRECTION]
1✔
756

757
            inputs = {}
1✔
758
            for i, bus in enumerate(dict_asset[INFLOW_DIRECTION]):
1✔
759
                inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
760
                    variable_costs=get_item_if_list(
761
                        dict_asset[DISPATCH_PRICE][VALUE], i
762
                    ),
763
                    investment=investment if bus == investment_bus else None,
764
                )
765

766
            bus = dict_asset[OUTFLOW_DIRECTION]
1✔
767
            outputs = {
1✔
768
                kwargs[OEMOF_BUSSES][bus]: solph.Flow(
769
                    investment=investment if bus == investment_bus else None
770
                )
771
            }
772

773
            efficiencies = {}
1✔
774
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
775
                efficiencies[kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION][i]]] = (
1✔
776
                    efficiency
777
                )
778

779
        elif isinstance(dict_asset[INFLOW_DIRECTION], str) and isinstance(
1✔
780
            dict_asset[OUTFLOW_DIRECTION], list
781
        ):
782
            # single input and multiple outputs
783
            num_outputs = len(dict_asset[OUTFLOW_DIRECTION])
1✔
784
            if get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_outputs:
1✔
785
                outputs_names = ", ".join(
1✔
786
                    [f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]]
787
                )
788
                missing_dispatch_prices_or_efficiencies = (
1✔
789
                    f"You defined multiple values for parameter '{OUTFLOW_DIRECTION}' "
790
                    f"({outputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
791
                    f"You must also provide exactly {num_outputs} values for the parameter "
792
                    f"'{EFFICIENCY}' and you can do so for the parameter '{DISPATCH_PRICE}'."
793
                )
794
                logging.error(missing_dispatch_prices_or_efficiencies)
1✔
795
                raise ValueError(missing_dispatch_prices_or_efficiencies)
1✔
796

797
            if investment_bus is None:
1✔
798
                investment_bus = dict_asset[INFLOW_DIRECTION]
1✔
799
            bus = dict_asset[INFLOW_DIRECTION]
1✔
800
            inputs = {
1✔
801
                kwargs[OEMOF_BUSSES][bus]: solph.Flow(
802
                    investment=investment if bus == investment_bus else None
803
                )
804
            }
805
            outputs = {}
1✔
806
            efficiencies = {}
1✔
807

808
            for i, (bus, efficiency) in enumerate(
1✔
809
                zip(dict_asset[OUTFLOW_DIRECTION], dict_asset[EFFICIENCY][VALUE])
810
            ):
811

812
                outputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
813
                    investment=investment if bus == investment_bus else None
814
                )
815
                efficiencies[kwargs[OEMOF_BUSSES][bus]] = efficiency
1✔
816
        else:
817
            # multiple inputs and multiple outputs
UNCOV
818
            inputs_names = ", ".join([f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]])
×
UNCOV
819
            outputs_names = ", ".join([f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]])
×
UNCOV
820
            missing_dispatch_prices_or_efficiencies = (
×
821
                f"You defined multiple values for parameter '{INFLOW_DIRECTION}'"
822
                f" ({inputs_names}) as well as for parameter '{OUTFLOW_DIRECTION}' ({outputs_names})"
823
                f" of the conversion asset named '{dict_asset[LABEL]}', this is not supported"
824
                f" at the moment."
825
            )
UNCOV
826
            logging.error(missing_dispatch_prices_or_efficiencies)
×
UNCOV
827
            raise ValueError(missing_dispatch_prices_or_efficiencies)
×
828
    else:
829
        check_list_parameters_transformers_single_input_single_output(
1✔
830
            dict_asset, model.timeindex.size
831
        )
832

833
        # single input and single output
834

835
        min_load_opts = {"min": 0, "max": 1}
1✔
836
        min_load = dict_asset.get(SOC_MIN, None)
1✔
837
        if min_load is not None:
1✔
UNCOV
838
            if min_load[VALUE] != 0:
×
UNCOV
839
                logging.warning(
×
840
                    f"Minimal load of {min_load[VALUE]} was set to asset {dict_asset[LABEL]}"
841
                )
UNCOV
842
                min_load_opts["nonconvex"] = solph.NonConvex()
×
UNCOV
843
            min_load_opts["min"] = min_load[VALUE]
×
844

845
        max_load = dict_asset.get(SOC_MAX, None)
1✔
846
        if max_load is not None:
1✔
UNCOV
847
            if max_load[VALUE] != 1:
×
UNCOV
848
                logging.warning(
×
849
                    f"Maximal load of {max_load[VALUE]} was set to asset {dict_asset[LABEL]}"
850
                )
UNCOV
851
                min_load_opts["nonconvex"] = solph.NonConvex()
×
852

UNCOV
853
            min_load_opts["max"] = max_load[VALUE]
×
854

855
        if "nonconvex" in min_load_opts:
1✔
UNCOV
856
            if invest_opts.get("maximum", None) is None:
×
UNCOV
857
                raise ValueError(
×
858
                    f"You need to provide a maximum_capacity to the asset {dict_asset[LABEL]}, if you set a minimal/maximal load different from 0/1"
859
                )
860

861
        if investment_bus is None:
1✔
862
            investment_bus = dict_asset[OUTFLOW_DIRECTION]
1✔
863

864
        bus = dict_asset[INFLOW_DIRECTION]
1✔
865
        inputs = {
1✔
866
            kwargs[OEMOF_BUSSES][bus]: solph.Flow(
867
                investment=investment if bus == investment_bus else None
868
            )
869
        }
870

871
        bus = dict_asset[OUTFLOW_DIRECTION]
1✔
872
        if AVAILABILITY_DISPATCH in dict_asset.keys():
1✔
873
            # This key is only present in DSO peak demand pricing transformers.
874
            outputs = {
1✔
875
                kwargs[OEMOF_BUSSES][bus]: solph.Flow(
876
                    investment=investment if bus == investment_bus else None,
877
                    variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
878
                    max=dict_asset[AVAILABILITY_DISPATCH].values,
879
                )
880
            }
881
        else:
882
            outputs = {
1✔
883
                kwargs[OEMOF_BUSSES][bus]: solph.Flow(
884
                    investment=investment if bus == investment_bus else None,
885
                    variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
886
                    **min_load_opts,
887
                )
888
            }
889

890
        efficiencies = {
1✔
891
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: dict_asset[EFFICIENCY][
892
                VALUE
893
            ]
894
        }
895

896
    if missing_dispatch_prices_or_efficiencies is None:
1✔
897
        t = solph.components.Converter(
1✔
898
            label=dict_asset[LABEL],
899
            inputs=inputs,
900
            outputs=outputs,
901
            conversion_factors=efficiencies,
902
        )
903
        model.add(t)
1✔
904
        kwargs[OEMOF_TRANSFORMER].update({dict_asset[LABEL]: t})
1✔
905

906

907
def storage_fix(model, dict_asset, **kwargs):
1✔
908
    r"""
909
    Defines a storage with a fixed capacity.
910

911
    See :py:func:`~.storage` for more information, including parameters.
912

913
    Notes
914
    -----
915
    Tested with:
916
    - test_storage_fix()
917

918
    Returns
919
    -------
920
    Indirectly updated `model` and dict of asset in `kwargs` with the storage object.
921

922
    """
923
    storage = solph.components.GenericStorage(
1✔
924
        label=dict_asset[LABEL],
925
        nominal_storage_capacity=dict_asset[STORAGE_CAPACITY][INSTALLED_CAP][
926
            VALUE
927
        ],  # THERMAL --> yes
928
        inputs={
929
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
930
                nominal_value=dict_asset[INPUT_POWER][INSTALLED_CAP][
931
                    VALUE
932
                ],  # limited through installed capacity, NOT c-rate
933
                # might be too much
934
                variable_costs=dict_asset[INPUT_POWER][DISPATCH_PRICE][VALUE],
935
            )
936
        },  # maximum charge possible in one timestep
937
        outputs={
938
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
939
                nominal_value=dict_asset[OUTPUT_POWER][INSTALLED_CAP][
940
                    VALUE
941
                ],  # limited through installed capacity, NOT c-rate #todo actually, if we only have a lithium battery... crate should suffice? i mean, with crate fixed AND fixed power, this is defined two times
942
                variable_costs=dict_asset[OUTPUT_POWER][DISPATCH_PRICE][VALUE],
943
            )
944
        },  # maximum discharge possible in one timestep
945
        loss_rate=1
946
        - dict_asset[STORAGE_CAPACITY][EFFICIENCY][
947
            VALUE
948
        ],  # from timestep to timestep #THERMAL
949
        fixed_losses_absolute=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_ABS][
950
            VALUE
951
        ],  # THERMAL
952
        fixed_losses_relative=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_REL][
953
            VALUE
954
        ],  # THERMAL
955
        min_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MIN][VALUE],
956
        max_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MAX][VALUE],
957
        initial_storage_level=dict_asset[STORAGE_CAPACITY][SOC_INITIAL][
958
            VALUE
959
        ],  # in terms of SOC
960
        inflow_conversion_factor=dict_asset[INPUT_POWER][EFFICIENCY][
961
            VALUE
962
        ],  # storing efficiency
963
        outflow_conversion_factor=dict_asset[OUTPUT_POWER][EFFICIENCY][VALUE],
964
    )  # efficiency of discharge
965
    model.add(storage)
1✔
966
    kwargs[OEMOF_GEN_STORAGE].update({dict_asset[LABEL]: storage})
1✔
967

968

969
def storage_optimize(model, dict_asset, **kwargs):
1✔
970
    r"""
971
    Defines a storage with a capacity to be optimized.
972

973
    See :py:func:`~.storage` for more information, including parameters.
974

975
    Notes
976
    -----
977
    Tested with:
978
    - test_storage_optimize()
979
    - test_storage_optimize_investment_minimum_0_float()
980
    - test_storage_optimize_investment_minimum_0_time_series()
981
    - test_storage_optimize_investment_minimum_1_rel_float()
982
    - test_storage_optimize_investment_minimum_1_abs_float()
983
    - test_storage_optimize_investment_minimum_1_rel_times_series()
984
    - test_storage_optimize_investment_minimum_1_abs_times_series()
985

986
    Returns
987
    -------
988
    Indirectly updated `model` and dict of asset in `kwargs` with the storage object.
989

990
    """
991
    # investment.minimum for an InvestmentStorage is 0 as default
992
    minimum = 0
1✔
993

994
    # Set investment.minimum to 1 if
995
    # non-zero fixed_thermal_losses_relative or fixed_thermal_losses_absolute exist as
996
    for losses in [THERM_LOSSES_REL, THERM_LOSSES_ABS]:
1✔
997
        # 1. float or
998
        try:
1✔
999
            float(dict_asset[STORAGE_CAPACITY][losses][VALUE])
1✔
1000
            if dict_asset[STORAGE_CAPACITY][losses][VALUE] != 0:
1✔
1001
                minimum = 1
1✔
1002
        # 2. time series
1003
        except TypeError:
1✔
1004
            if sum(dict_asset[STORAGE_CAPACITY][losses][VALUE]) != 0:
1✔
1005
                minimum = 1
1✔
1006

1007
    storage = solph.components.GenericStorage(
1✔
1008
        label=dict_asset[LABEL],
1009
        investment=solph.Investment(
1010
            ep_costs=dict_asset[STORAGE_CAPACITY][SIMULATION_ANNUITY][VALUE],
1011
            minimum=minimum,
1012
            maximum=dict_asset[STORAGE_CAPACITY][MAXIMUM_ADD_CAP][VALUE],
1013
            existing=dict_asset[STORAGE_CAPACITY][INSTALLED_CAP][VALUE],
1014
        ),
1015
        inputs={
1016
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1017
                investment=solph.Investment(
1018
                    ep_costs=dict_asset[INPUT_POWER][SIMULATION_ANNUITY][VALUE],
1019
                    maximum=dict_asset[INPUT_POWER][MAXIMUM_ADD_CAP][VALUE],
1020
                    existing=dict_asset[INPUT_POWER][INSTALLED_CAP][
1021
                        VALUE
1022
                    ],  # todo: `existing needed here?`
1023
                ),
1024
                variable_costs=dict_asset[INPUT_POWER][DISPATCH_PRICE][VALUE],
1025
            )
1026
        },  # maximum charge power
1027
        outputs={
1028
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1029
                investment=solph.Investment(
1030
                    ep_costs=dict_asset[OUTPUT_POWER][SIMULATION_ANNUITY][VALUE],
1031
                    maximum=dict_asset[OUTPUT_POWER][MAXIMUM_ADD_CAP][VALUE],
1032
                    existing=dict_asset[OUTPUT_POWER][INSTALLED_CAP][
1033
                        VALUE
1034
                    ],  # todo: `existing needed here?`
1035
                ),
1036
                variable_costs=dict_asset[OUTPUT_POWER][DISPATCH_PRICE][VALUE],
1037
            )
1038
        },  # maximum discharge power
1039
        loss_rate=1
1040
        - dict_asset[STORAGE_CAPACITY][EFFICIENCY][VALUE],  # from timestep to timestep
1041
        fixed_losses_absolute=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_ABS][VALUE],
1042
        fixed_losses_relative=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_REL][VALUE],
1043
        min_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MIN][VALUE],
1044
        max_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MAX][VALUE],
1045
        initial_storage_level=dict_asset[STORAGE_CAPACITY][SOC_INITIAL][
1046
            VALUE
1047
        ],  # in terms of SOC #implication: balanced = True, ie. start=end
1048
        inflow_conversion_factor=dict_asset[INPUT_POWER][EFFICIENCY][
1049
            VALUE
1050
        ],  # storing efficiency
1051
        outflow_conversion_factor=dict_asset[OUTPUT_POWER][EFFICIENCY][
1052
            VALUE
1053
        ],  # efficiency of discharge
1054
        invest_relation_input_capacity=dict_asset[INPUT_POWER][C_RATE][VALUE],
1055
        # storage can be charged with invest_relation_output_capacity*capacity in one timeperiod
1056
        invest_relation_output_capacity=dict_asset[OUTPUT_POWER][C_RATE][VALUE],
1057
        # storage can be emptied with invest_relation_output_capacity*capacity in one timeperiod
1058
    )
1059
    model.add(storage)
1✔
1060
    kwargs[OEMOF_GEN_STORAGE].update({dict_asset[LABEL]: storage})
1✔
1061

1062

1063
def source_non_dispatchable_fix(model, dict_asset, **kwargs):
1✔
1064
    r"""
1065
    Defines a non dispatchable source with a fixed capacity.
1066

1067
    See :py:func:`~.source` for more information, including parameters.
1068

1069
    Notes
1070
    -----
1071
    Tested with:
1072
    - test_source_non_dispatchable_fix()
1073

1074
    Returns
1075
    -------
1076
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
1077

1078
    """
1079
    outputs = {
1✔
1080
        kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1081
            fix=dict_asset[TIMESERIES],
1082
            nominal_value=dict_asset[INSTALLED_CAP][VALUE],
1083
            variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1084
            custom_attributes=dict(emission_factor=dict_asset[EMISSION_FACTOR][VALUE]),
1085
        )
1086
    }
1087

1088
    source_non_dispatchable = solph.components.Source(
1✔
1089
        label=dict_asset[LABEL], outputs=outputs
1090
    )
1091

1092
    model.add(source_non_dispatchable)
1✔
1093
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_non_dispatchable})
1✔
1094
    logging.debug(
1✔
1095
        f"Added: Non-dispatchable source {dict_asset[LABEL]} (fixed capacity) to bus {dict_asset[OUTFLOW_DIRECTION]}.",
1096
    )
1097

1098

1099
def source_non_dispatchable_optimize(model, dict_asset, **kwargs):
1✔
1100
    r"""
1101
    Defines a non dispatchable source with a capacity to be optimized.
1102

1103
    See :py:func:`~.source` for more information, including parameters.
1104

1105
    Notes
1106
    -----
1107
    Tested with:
1108
    - test_source_non_dispatchable_optimize()
1109

1110
    Returns
1111
    -------
1112
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
1113

1114
    """
1115
    if MAXIMUM_ADD_CAP_NORMALIZED in dict_asset:
1✔
1116
        maximum = dict_asset[MAXIMUM_ADD_CAP_NORMALIZED][VALUE]
1✔
1117
    else:
UNCOV
1118
        maximum = dict_asset[MAXIMUM_ADD_CAP][VALUE]
×
1119
    if INSTALLED_CAP_NORMALIZED in dict_asset:
1✔
1120
        existing = dict_asset[INSTALLED_CAP_NORMALIZED][VALUE]
1✔
1121
    else:
1122
        existing = dict_asset[INSTALLED_CAP][VALUE]
1✔
1123
    outputs = {
1✔
1124
        kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1125
            fix=dict_asset[TIMESERIES_NORMALIZED],
1126
            investment=solph.Investment(
1127
                ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE]
1128
                / dict_asset[TIMESERIES_PEAK][VALUE],
1129
                maximum=maximum,
1130
                existing=existing,
1131
            ),
1132
            # variable_costs are devided by time series peak as normalized time series are used as actual_value
1133
            variable_costs=dict_asset[DISPATCH_PRICE][VALUE]
1134
            / dict_asset[TIMESERIES_PEAK][VALUE],
1135
            # add emission_factor for emission contraint
1136
            custom_attributes=dict(emission_factor=dict_asset[EMISSION_FACTOR][VALUE]),
1137
        )
1138
    }
1139

1140
    source_non_dispatchable = solph.components.Source(
1✔
1141
        label=dict_asset[LABEL], outputs=outputs
1142
    )
1143

1144
    model.add(source_non_dispatchable)
1✔
1145
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_non_dispatchable})
1✔
1146
    logging.debug(
1✔
1147
        f"Added: Non-dispatchable source {dict_asset[LABEL]} (capacity to be optimized) to bus {dict_asset[OUTFLOW_DIRECTION]}."
1148
    )
1149

1150

1151
def source_dispatchable_optimize(model, dict_asset, **kwargs):
1✔
1152
    r"""
1153
    Defines a dispatchable source with a fixed capacity.
1154

1155
    See :py:func:`~.source` for more information, including parameters.
1156

1157
    Notes
1158
    -----
1159
    Tested with:
1160
    - test_source_dispatchable_optimize_normalized_timeseries()
1161
    - test_source_dispatchable_optimize_timeseries_not_normalized_timeseries()
1162

1163
     Returns
1164
    -------
1165
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
1166

1167
    """
1168
    if TIMESERIES_NORMALIZED in dict_asset:
1✔
1169
        outputs = {
1✔
1170
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1171
                max=dict_asset[TIMESERIES_NORMALIZED],
1172
                investment=solph.Investment(
1173
                    ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE]
1174
                    / dict_asset[TIMESERIES_PEAK][VALUE],
1175
                    maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
1176
                    existing=dict_asset[INSTALLED_CAP][VALUE],
1177
                ),
1178
                # variable_costs are devided by time series peak as normalized time series are used as actual_value
1179
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE]
1180
                / dict_asset[TIMESERIES_PEAK][VALUE],
1181
                # add emission_factor for emission contraint
1182
                custom_attributes=dict(
1183
                    emission_factor=dict_asset[EMISSION_FACTOR][VALUE]
1184
                ),
1185
            )
1186
        }
1187
        source_dispatchable = solph.components.Source(
1✔
1188
            label=dict_asset[LABEL],
1189
            outputs=outputs,
1190
        )
1191
    else:
1192
        if TIMESERIES in dict_asset:
1✔
1193
            logging.info(
1✔
1194
                f"Asset {dict_asset[LABEL]} is introduced as a dispatchable source with an availability schedule."
1195
            )
1196
            logging.debug(
1✔
1197
                f"The availability schedule is solely introduced because the key {TIMESERIES_NORMALIZED} was not in the asset´s dictionary. \n"
1198
                f"It should only be applied to DSO sources. "
1199
                f"If the asset should not have this behaviour, please create an issue.",
1200
            )
1201
        outputs = {
1✔
1202
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1203
                investment=solph.Investment(
1204
                    ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
1205
                    existing=dict_asset[INSTALLED_CAP][VALUE],
1206
                    maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
1207
                ),
1208
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1209
                # add emission_factor for emission contraint
1210
                custom_attributes=dict(
1211
                    emission_factor=dict_asset[EMISSION_FACTOR][VALUE],
1212
                ),
1213
            )
1214
        }
1215
        print(dict_asset[LABEL])
1✔
1216
        source_dispatchable = solph.components.Source(
1✔
1217
            label=dict_asset[LABEL], outputs=outputs
1218
        )
1219
    model.add(source_dispatchable)
1✔
1220
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_dispatchable})
1✔
1221
    logging.debug(
1✔
1222
        f"Added: Dispatchable source {dict_asset[LABEL]} (capacity to be optimized) to bus {dict_asset[OUTFLOW_DIRECTION]}."
1223
    )
1224

1225

1226
def source_dispatchable_fix(model, dict_asset, **kwargs):
1✔
1227
    r"""
1228
    Defines a dispatchable source with a fixed capacity.
1229

1230
    See :py:func:`~.source` for more information, including parameters.
1231

1232
    Notes
1233
    -----
1234
    Tested with:
1235
    - test_source_dispatchable_fix_normalized_timeseries()
1236
    - test_source_dispatchable_fix_timeseries_not_normalized_timeseries()
1237

1238
    Returns
1239
    -------
1240
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
1241

1242
    """
1243
    if TIMESERIES_NORMALIZED in dict_asset:
1✔
1244
        outputs = {
1✔
1245
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1246
                max=dict_asset[TIMESERIES_NORMALIZED],
1247
                nominal_value=dict_asset[INSTALLED_CAP][VALUE],
1248
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1249
                # add emission_factor for emission contraint
1250
                custom_attributes=dict(
1251
                    emission_factor=dict_asset[EMISSION_FACTOR][VALUE]
1252
                ),
1253
            )
1254
        }
1255
        source_dispatchable = solph.components.Source(
1✔
1256
            label=dict_asset[LABEL],
1257
            outputs=outputs,
1258
        )
1259
    else:
1260
        if TIMESERIES in dict_asset:
1✔
1261
            logging.info(
1✔
1262
                f"Asset {dict_asset[LABEL]} is introduced as a dispatchable source with an availability schedule."
1263
            )
1264
            logging.debug(
1✔
1265
                f"The availability schedule is solely introduced because the key {TIMESERIES_NORMALIZED} was not in the asset´s dictionary. \n"
1266
                f"It should only be applied to DSO sources. "
1267
                f"If the asset should not have this behaviour, please create an issue.",
1268
            )
1269
        outputs = {
1✔
1270
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1271
                nominal_value=dict_asset[INSTALLED_CAP][VALUE],
1272
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1273
            )
1274
        }
1275
        source_dispatchable = solph.components.Source(
1✔
1276
            label=dict_asset[LABEL],
1277
            outputs=outputs,
1278
        )
1279
    model.add(source_dispatchable)
1✔
1280
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_dispatchable})
1✔
1281
    logging.debug(
1✔
1282
        f"Added: Dispatchable source {dict_asset[LABEL]} (fixed capacity) to bus {dict_asset[OUTFLOW_DIRECTION]}."
1283
    )
1284

1285

1286
def sink_dispatchable_optimize(model, dict_asset, **kwargs):
1✔
1287
    r"""
1288
    Define a dispatchable sink.
1289

1290
    The dispatchable sink is capacity-optimized, without any costs connected to the capacity of the asset.
1291
    Applications of this asset type are: Feed-in sink, excess sink.
1292

1293
    See :py:func:`~.sink` for more information, including parameters.
1294

1295
    Notes
1296
    -----
1297
    Tested with:
1298
    - test_sink_dispatchable_single_input_bus()
1299
    - test_sink_dispatchable_multiple_input_busses()
1300

1301
    Returns
1302
    -------
1303
    Indirectly updated `model` and dict of asset in `kwargs` with the sink object.
1304

1305
    """
1306
    # check if the sink has multiple input busses
1307
    if isinstance(dict_asset[INFLOW_DIRECTION], list):
1✔
1308
        inputs = {}
1✔
1309
        index = 0
1✔
1310
        for bus in dict_asset[INFLOW_DIRECTION]:
1✔
1311
            inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
1312
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE][index],
1313
                investment=solph.Investment(),
1314
            )
1315
            index += 1
1✔
1316
    else:
1317
        inputs = {
1✔
1318
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1319
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1320
                investment=solph.Investment(),
1321
            )
1322
        }
1323

1324
    # create and add excess electricity sink to micro_grid_system - variable
1325
    sink_dispatchable = solph.components.Sink(
1✔
1326
        label=dict_asset[LABEL],
1327
        inputs=inputs,
1328
    )
1329
    model.add(sink_dispatchable)
1✔
1330
    kwargs[OEMOF_SINK].update({dict_asset[LABEL]: sink_dispatchable})
1✔
1331
    logging.debug(
1✔
1332
        f"Added: Dispatchable sink {dict_asset[LABEL]} (to be capacity optimized) to bus {dict_asset[INFLOW_DIRECTION]}.",
1333
    )
1334

1335

1336
def sink_non_dispatchable(model, dict_asset, **kwargs):
1✔
1337
    r"""
1338
    Defines a non dispatchable sink.
1339

1340
    See :py:func:`~.sink` for more information, including parameters.
1341

1342
    Notes
1343
    -----
1344
    Tested with:
1345
    - test_sink_non_dispatchable_single_input_bus()
1346
    - test_sink_non_dispatchable_multiple_input_busses()
1347

1348
    Returns
1349
    -------
1350
    Indirectly updated `model` and dict of asset in `kwargs` with the sink object.
1351

1352
    """
1353
    # check if the sink has multiple input busses
1354
    if isinstance(dict_asset[INFLOW_DIRECTION], list):
1✔
1355
        inputs = {}
1✔
1356
        index = 0
1✔
1357
        for bus in dict_asset[INFLOW_DIRECTION]:
1✔
1358
            inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
1359
                fix=dict_asset[TIMESERIES], nominal_value=1
1360
            )
1361
            index += 1
1✔
1362
    else:
1363
        inputs = {
1✔
1364
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1365
                fix=dict_asset[TIMESERIES], nominal_value=1
1366
            )
1367
        }
1368

1369
    # create and add demand sink to micro_grid_system - fixed
1370
    sink_demand = solph.components.Sink(
1✔
1371
        label=dict_asset[LABEL],
1372
        inputs=inputs,
1373
    )
1374
    model.add(sink_demand)
1✔
1375
    kwargs[OEMOF_SINK].update({dict_asset[LABEL]: sink_demand})
1✔
1376
    logging.debug(
1✔
1377
        f"Added: Non-dispatchable sink {dict_asset[LABEL]} to bus {dict_asset[INFLOW_DIRECTION]}"
1378
    )
1379

1380

1381
def sink_demand_reduction(model, dict_asset, **kwargs):
1✔
1382
    r"""
1383
    Defines a non dispatchable sink to serve critical and non-critical demand.
1384

1385
    See :py:func:`~.sink` for more information, including parameters.
1386

1387
    Notes
1388
    -----
1389
    Tested with:
1390
    - test_sink_non_dispatchable_single_input_bus()
1391
    - test_sink_non_dispatchable_multiple_input_busses()
1392

1393
    Returns
1394
    -------
1395
    Indirectly updated `model` and dict of asset in `kwargs` with the sink object.
1396

1397
    """
UNCOV
1398
    demand_reduction_factor = 1 - dict_asset[EFFICIENCY][VALUE]
×
UNCOV
1399
    tot_demand = dict_asset[TIMESERIES]
×
UNCOV
1400
    non_critical_demand_ts = tot_demand * demand_reduction_factor
×
UNCOV
1401
    non_critical_demand_peak = non_critical_demand_ts.max()
×
UNCOV
1402
    if non_critical_demand_peak == 0:
×
UNCOV
1403
        max_non_critical = 1
×
1404
    else:
UNCOV
1405
        max_non_critical = non_critical_demand_ts / non_critical_demand_peak
×
UNCOV
1406
    critical_demand_ts = tot_demand * dict_asset[EFFICIENCY][VALUE]
×
1407

1408
    # check if the sink has multiple input busses
UNCOV
1409
    if isinstance(dict_asset[INFLOW_DIRECTION], list):
×
UNCOV
1410
        raise (
×
1411
            ValueError(
1412
                f"The reducable demand {dict_asset[LABEL]} does not support multiple input busses"
1413
            )
1414
        )
1415
        # inputs_noncritical = {}
1416
        # inputs_critical = {}
1417
        # index = 0
1418
        # for bus in dict_asset[INFLOW_DIRECTION]:
1419
        #     inputs_critical[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1420
        #         fix=dict_asset[TIMESERIES], nominal_value=1
1421
        #     )
1422
        #     index += 1
1423
    else:
UNCOV
1424
        inputs_noncritical = {
×
1425
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1426
                min=0,
1427
                max=max_non_critical,
1428
                nominal_value=non_critical_demand_peak,
1429
                variable_costs=-1e-15,
1430
            )
1431
        }
UNCOV
1432
        inputs_critical = {
×
1433
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1434
                fix=critical_demand_ts, nominal_value=1
1435
            )
1436
        }
1437

UNCOV
1438
    non_critical_demand = solph.components.Sink(
×
1439
        label=reducable_demand_name(dict_asset[LABEL]),
1440
        inputs=inputs_noncritical,
1441
    )
UNCOV
1442
    critical_demand = solph.components.Sink(
×
1443
        label=reducable_demand_name(dict_asset[LABEL], critical=True),
1444
        inputs=inputs_critical,
1445
    )
1446

1447
    # create and add demand sink and critical demand sink
1448

UNCOV
1449
    model.add(critical_demand)
×
UNCOV
1450
    model.add(non_critical_demand)
×
UNCOV
1451
    kwargs[OEMOF_SINK].update(
×
1452
        {reducable_demand_name(dict_asset[LABEL]): non_critical_demand}
1453
    )
UNCOV
1454
    kwargs[OEMOF_SINK].update(
×
1455
        {reducable_demand_name(dict_asset[LABEL], critical=True): critical_demand}
1456
    )
UNCOV
1457
    logging.debug(
×
1458
        f"Added: Reducable Non-dispatchable sink {dict_asset[LABEL]} to bus {dict_asset[INFLOW_DIRECTION]}"
1459
    )
1460

1461

1462
def chp_fix(model, dict_asset, **kwargs):
1✔
1463
    r"""
1464
    Extraction turbine chp from Oemof solph. Extraction turbine must have one input and two outputs
1465
    Notes
1466
    -----
1467
    Tested with:
1468
    - test_to_be_written()
1469

1470
    Returns
1471
    -------
1472
    Indirectly updated `model` and dict of asset in `kwargs` with the extraction turbine component.
1473

1474
    """
1475

1476
    inputs = {
1✔
1477
        kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1478
            nominal_value=dict_asset[INSTALLED_CAP][VALUE]
1479
        )
1480
    }
1481

1482
    busses_energy_vectors = [
1✔
1483
        kwargs[OEMOF_BUSSES][b].energy_vector for b in dict_asset[OUTFLOW_DIRECTION]
1484
    ]
1485
    idx_el = busses_energy_vectors.index("Electricity")
1✔
1486
    idx_th = busses_energy_vectors.index("Heat")
1✔
1487
    el_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_el]]
1✔
1488
    th_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_th]]
1✔
1489

1490
    outputs = {
1✔
1491
        el_bus: solph.Flow(),
1492
        th_bus: solph.Flow(),
1493
    }  # if kW for heat and kW for elect then insert it under nominal_value
1494

1495
    beta = dict_asset[BETA][VALUE]
1✔
1496

1497
    efficiency_el_wo_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_th]
1✔
1498
    efficiency_th_max_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_el]
1✔
1499
    efficiency_el_max_heat_extraction = (
1✔
1500
        efficiency_el_wo_heat_extraction - beta * efficiency_th_max_heat_extraction
1501
    )
1502
    efficiency_full_condensation = {el_bus: efficiency_el_wo_heat_extraction}
1✔
1503

1504
    efficiencies = {
1✔
1505
        el_bus: efficiency_el_max_heat_extraction,
1506
        th_bus: efficiency_th_max_heat_extraction,
1507
    }
1508

1509
    ext_turb_chp = solph.components.ExtractionTurbineCHP(
1✔
1510
        label=dict_asset[LABEL],
1511
        inputs=inputs,
1512
        outputs=outputs,
1513
        conversion_factors=efficiencies,
1514
        conversion_factor_full_condensation=efficiency_full_condensation,
1515
    )
1516

1517
    model.add(ext_turb_chp)
1✔
1518
    kwargs[OEMOF_ExtractionTurbineCHP].update({dict_asset[LABEL]: ext_turb_chp})
1✔
1519

1520

1521
def chp_optimize(model, dict_asset, **kwargs):
1✔
1522
    r"""
1523
    Extraction turbine chp from Oemof solph. Extraction turbine must have one input and two outputs
1524
    Notes
1525
    -----
1526
    Tested with:
1527
    - test_to_be_written()
1528

1529
    Returns
1530
    -------
1531
    Indirectly updated `model` and dict of asset in `kwargs` with the extraction turbine component.
1532

1533
    """
1534

1535
    inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
1536

1537
    busses_energy_vectors = [
1✔
1538
        kwargs[OEMOF_BUSSES][b].energy_vector for b in dict_asset[OUTFLOW_DIRECTION]
1539
    ]
1540

1541
    idx_el = busses_energy_vectors.index("Electricity")
1✔
1542
    idx_th = busses_energy_vectors.index("Heat")
1✔
1543
    el_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_el]]
1✔
1544
    th_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_th]]
1✔
1545

1546
    outputs = {
1✔
1547
        el_bus: solph.Flow(
1548
            investment=solph.Investment(
1549
                ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
1550
                maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
1551
                existing=dict_asset[INSTALLED_CAP][VALUE],
1552
            )
1553
        ),
1554
        th_bus: solph.Flow(),
1555
    }
1556

1557
    beta = dict_asset[BETA][VALUE]
1✔
1558

1559
    efficiency_el_wo_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_el]
1✔
1560
    efficiency_th_max_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_th]
1✔
1561
    efficiency_el_max_heat_extraction = (
1✔
1562
        efficiency_el_wo_heat_extraction - beta * efficiency_th_max_heat_extraction
1563
    )
1564
    efficiency_full_condensation = {el_bus: efficiency_el_wo_heat_extraction}
1✔
1565

1566
    efficiencies = {
1✔
1567
        el_bus: efficiency_el_max_heat_extraction,
1568
        th_bus: efficiency_th_max_heat_extraction,
1569
    }
1570

1571
    ext_turb_chp = solph.components.ExtractionTurbineCHP(
1✔
1572
        label=dict_asset[LABEL],
1573
        inputs=inputs,
1574
        outputs=outputs,
1575
        conversion_factors=efficiencies,
1576
        conversion_factor_full_condensation=efficiency_full_condensation,
1577
    )
1578

1579
    model.add(ext_turb_chp)
1✔
1580
    kwargs[OEMOF_ExtractionTurbineCHP].update({dict_asset[LABEL]: ext_turb_chp})
1✔
STATUS · Troubleshooting · Open an Issue · Sales · Support · CAREERS · ENTERPRISE · START FREE · SCHEDULE DEMO
ANNOUNCEMENTS · TWITTER · TOS & SLA · Supported CI Services · What's a CI service? · Automated Testing

© 2025 Coveralls, Inc