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

rl-institut / multi-vector-simulator / 4084543790

pending completion
4084543790

push

github

GitHub
Merge pull request #952 from rl-institut/fix/chp_component

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

5899 of 7665 relevant lines covered (76.96%)

0.77 hits per line

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

92.05
/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 oemof.solph as solph
1✔
19

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

65

66
def transformer(model, dict_asset, **kwargs):
1✔
67
    r"""
68
    Defines a transformer component specified in `dict_asset`.
69

70
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the transformer
71
    is defined with a fixed capacity or a capacity to be optimized.
72
    The transformer has multiple or single input or output busses depending on
73
    the types of keys 'inflow_direction' and 'outflow_direction' in `dict_asset`.
74

75
    Parameters
76
    ----------
77
    model : oemof.solph.network.EnergySystem object
78
        See the oemof documentation for more information.
79
    dict_asset : dict
80
        Contains information about the transformer like (not exhaustive):
81
        efficiency, installed capacity ('installedCap'), information on the
82
        busses the transformer is connected to ('inflow_direction',
83
        'outflow_direction').
84

85
    Other Parameters
86
    ----------------
87
    busses : dict
88
    sinks : dict, optional
89
    sources : dict, optional
90
    transformers : dict
91
    storages : dict, optional
92

93
    Notes
94
    -----
95
    The transformer has either multiple input or multiple output busses.
96

97
    The following functions are used for defining the transformer:
98
    * :py:func:`~.transformer_constant_efficiency_fix`
99
    * :py:func:`~.transformer_constant_efficiency_optimize`
100

101
    Tested with:
102
    - test_transformer_optimize_cap_single_busses()
103
    - test_transformer_optimize_cap_multiple_input_busses()
104
    - test_transformer_optimize_cap_multiple_output_busses()
105
    - test_transformer_fix_cap_single_busses()
106
    - test_transformer_fix_cap_multiple_input_busses()
107
    - test_transformer_fix_cap_multiple_output_busses()
108

109
    Returns
110
    -------
111
    Indirectly updated `model` and dict of asset in `kwargs` with transformer object.
112

113
    """
114
    check_optimize_cap(
1✔
115
        model,
116
        dict_asset,
117
        func_constant=transformer_constant_efficiency_fix,
118
        func_optimize=transformer_constant_efficiency_optimize,
119
        **kwargs,
120
    )
121

122

123
def chp(model, dict_asset, **kwargs):
1✔
124
    r"""
125
    Defines a chp component specified in `dict_asset`.
126

127
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the chp
128
    is defined with a fixed capacity or a capacity to be optimized.
129
    The chp has single input and multiple output busses.
130

131
    Parameters
132
    ----------
133
    model : oemof.solph.network.EnergySystem object
134
        See the oemof documentation for more information.
135
    dict_asset : dict
136
        Contains information about the chp like (not exhaustive):
137
        efficiency, installed capacity ('installedCap'), information on the
138
        busses the chp is connected to ('inflow_direction',
139
        'outflow_direction'), beta coefficient.
140

141
    Other Parameters
142
    ----------------
143
    busses : dict
144
    sinks : dict, optional
145
    sources : dict, optional
146
    transformers : dict
147
    storages : dict, optional
148
    extractionTurbineCHP: dict, optional
149

150
    Notes
151
    -----
152
    The transformer has either multiple input or multiple output busses.
153

154
    The following functions are used for defining the chp:
155
    * :py:func:`~.chp_fix`
156
    * :py:func:`~.chp_optimize` for investment optimization
157

158
    Tested with:
159
    - test_chp_fix_cap()
160
    - test_chp_optimize_cap()
161
    - test_chp_missing_beta()
162
    - test_chp_wrong_beta_formatting()
163
    - test_chp_wrong_efficiency_formatting()
164
    - test_chp_wrong_outflow_bus_energy_vector()
165

166
    Returns
167
    -------
168
    Indirectly updated `model` and dict of asset in `kwargs` with chp object.
169

170
    """
171
    if BETA in dict_asset:
1✔
172
        beta = dict_asset[BETA]
1✔
173
        if isinstance(beta, dict) is False:
1✔
174
            raise WrongParameterFormatError(
1✔
175
                f"For the conversion asset named '{dict_asset[LABEL]}' of type {OEMOF_ExtractionTurbineCHP}, "
176
                f"the {BETA} parameter should have the following format {{ '{VALUE}': ..., '{UNIT}': ... }}"
177
            )
178
        else:
179
            beta = beta[VALUE]
1✔
180
        if 0 <= beta <= 1:
1✔
181
            pass
×
182
        else:
183
            raise ValueError("beta should be a number between 0 and 1.")
×
184
    else:
185
        raise MissingParameterError("No beta for extraction turbine chp specified.")
1✔
186

187
    if isinstance(dict_asset[EFFICIENCY][VALUE], list) is False:
1✔
188
        missing_efficiencies = (
1✔
189
            f"For the conversion asset named '{dict_asset[LABEL]}' of type {OEMOF_ExtractionTurbineCHP} "
190
            f"you must provide exactly 2 values for the parameter '{EFFICIENCY}'."
191
        )
192
        logging.error(missing_efficiencies)
1✔
193
        raise WrongParameterFormatError(missing_efficiencies)
1✔
194

195
    busses_energy_vectors = [
1✔
196
        kwargs[OEMOF_BUSSES][b].energy_vector for b in dict_asset[OUTFLOW_DIRECTION]
197
    ]
198
    if (
1✔
199
        "Heat" not in busses_energy_vectors
200
        or "Electricity" not in busses_energy_vectors
201
    ):
202
        mapping_busses = [
1✔
203
            f"'{v}' (from '{k}')"
204
            for k, v in zip(dict_asset[OUTFLOW_DIRECTION], busses_energy_vectors)
205
        ]
206
        wrong_output_energy_vectors = (
1✔
207
            f"For the conversion asset named '{dict_asset[LABEL]}' of type {OEMOF_ExtractionTurbineCHP} "
208
            f"you must provide 1 output bus for energy vector 'Heat' and one for 'Electricity'. You provided "
209
            f"{' and '.join(mapping_busses)}"
210
        )
211
        logging.error(wrong_output_energy_vectors)
1✔
212
        raise WrongParameterFormatError(wrong_output_energy_vectors)
1✔
213

214
    check_optimize_cap(
1✔
215
        model, dict_asset, func_constant=chp_fix, func_optimize=chp_optimize, **kwargs
216
    )
217

218

219
def storage(model, dict_asset, **kwargs):
1✔
220
    r"""
221
    Defines a storage component specified in `dict_asset`.
222

223
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the storage
224
    is defined with a fixed capacity or a capacity to be optimized.
225

226
    Parameters
227
    ----------
228
    model : oemof.solph.network.EnergySystem object
229
        See the oemof documentation for more information.
230
    dict_asset : dict
231
        Contains information about the storage like (not exhaustive):
232
        efficiency, installed capacity ('installedCap'), information on the
233
        busses the storage is connected to ('inflow_direction',
234
        'outflow_direction'),
235

236
    Other Parameters
237
    ----------------
238
    busses : dict
239
    sinks : dict, optional
240
    sources : dict, optional
241
    transformers : dict, optional
242
    storages : dict
243

244
    Notes
245
    -----
246
    The following functions are used for defining the storage:
247
    * :py:func:`~.storage_fix`
248
    * :py:func:`~.storage_optimize`
249

250
    Tested with:
251
    - test_storage_optimize()
252
    - test_storage_fix()
253

254
    """
255
    check_optimize_cap(
1✔
256
        model,
257
        dict_asset,
258
        func_constant=storage_fix,
259
        func_optimize=storage_optimize,
260
        **kwargs,
261
    )
262

263

264
def sink(model, dict_asset, **kwargs):
1✔
265
    r"""
266
    Defines a sink component specified in `dict_asset`.
267

268
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the sink
269
    is defined with a fixed capacity or a capacity to be optimized. If a time
270
    series is provided for the sink (key 'timeseries' in `dict_asset`) it is
271
    defined as a non dispatchable sink, otherwise as dispatchable sink.
272
    The sink has multiple or a single input bus depending on the type of the
273
    key 'inflow_direction' in `dict_asset`.
274

275
    Parameters
276
    ----------
277
    model : oemof.solph.network.EnergySystem object
278
        See the oemof documentation for more information.
279
    dict_asset : dict
280
        Contains information about the storage like (not exhaustive):
281
        efficiency, installed capacity ('installedCap'), information on the
282
        busses the sink is connected to ('inflow_direction'),
283

284
    Other Parameters
285
    ----------------
286
    busses : dict
287
    sinks : dict
288
    sources : dict, optional
289
    transformers : dict, optional
290
    storages : dict, optional
291

292
    Notes
293
    -----
294
    The following functions are used for defining the sink:
295
    * :py:func:`~.sink_non_dispatchable`
296
    * :py:func:`~.sink_dispatchable`
297

298
    Tested with:
299
    - test_sink_non_dispatchable_single_input_bus()
300
    - test_sink_non_dispatchable_multiple_input_busses()
301
    - test_sink_dispatchable_single_input_bus()
302
    - test_sink_dispatchable_multiple_input_busses()
303

304
    """
305
    if TIMESERIES in dict_asset:
1✔
306
        sink_non_dispatchable(model, dict_asset, **kwargs)
1✔
307
    else:
308
        sink_dispatchable_optimize(model, dict_asset, **kwargs)
1✔
309

310

311
def source(model, dict_asset, **kwargs):
1✔
312
    r"""
313
    Defines a source component specified in `dict_asset`.
314

315
    Depending on the 'value' of 'optimizeCap' in `dict_asset` the source
316
    is defined with a fixed capacity or a capacity to be optimized. If a time
317
    series is provided for the source (key 'timeseries' in `dict_asset`) it is
318
    defined as a non dispatchable source, otherwise as dispatchable source.
319
    The source has multiple or a single output bus depending on the type of the
320
    key 'inflow_direction' in `dict_asset`.
321

322
    Parameters
323
    ----------
324
    model : oemof.solph.network.EnergySystem object
325
        See the oemof documentation for more information.
326
    dict_asset : dict
327
        Contains information about the storage like (not exhaustive):
328
        efficiency, installed capacity ('installedCap'), information on the
329
        busses the sink is connected to ('inflow_direction'),
330

331
    Other Parameters
332
    ----------------
333
    busses : dict
334
    sinks : dict
335
    sources : dict, optional
336
    transformers : dict, optional
337
    storages : dict, optional
338

339
    TODOS
340
    ^^^^^
341
    * We should actually not allow multiple output busses, probably - because a pv would then
342
    feed in twice as much as solar_gen_specific for example, see issue #121
343

344
    Notes
345
    -----
346
    The following functions are used for defining the source:
347
    * :py:func:`~.source_dispatchable_fix`
348
    * :py:func:`~.source_dispatchable_optimize`
349
    * :py:func:`~.source_non_dispatchable_fix`
350
    * :py:func:`~.source_non_dispatchable_optimize`
351

352
    Tested with:
353
    - test_source_non_dispatchable_optimize()
354
    - test_source_non_dispatchable_fix()
355
    - test_source_dispatchable_optimize_normalized_timeseries()
356
    - test_source_dispatchable_optimize_timeseries_not_normalized_timeseries()
357
    - test_source_dispatchable_fix_normalized_timeseries()
358
    - test_source_dispatchable_fix_timeseries_not_normalized_timeseries()
359
    """
360
    if DISPATCHABILITY in dict_asset and dict_asset[DISPATCHABILITY] is True:
1✔
361
        check_optimize_cap(
1✔
362
            model,
363
            dict_asset,
364
            func_constant=source_dispatchable_fix,
365
            func_optimize=source_dispatchable_optimize,
366
            **kwargs,
367
        )
368

369
    else:
370
        check_optimize_cap(
1✔
371
            model,
372
            dict_asset,
373
            func_constant=source_non_dispatchable_fix,
374
            func_optimize=source_non_dispatchable_optimize,
375
            **kwargs,
376
        )
377

378

379
def check_optimize_cap(model, dict_asset, func_constant, func_optimize, **kwargs):
1✔
380
    r"""
381
    Defines a component specified in `dict_asset` with fixed capacity or capacity to be optimized.
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 asset like (not exhaustive):
389
        efficiency, installed capacity ('installedCap'), information on the
390
        busses the asset is connected to (f.e. 'inflow_direction',
391
        'outflow_direction').
392
    func_constant : func
393
        Function to be applied if optimization not intended.
394
    func_optimize : func
395
        Function to be applied if optimization not intended.
396

397
    Other Parameters
398
    ----------------
399
    Required are `busses` and a dictionary belonging to the respective oemof
400
    type of the asset.
401

402
    busses : dict, optional
403
    sinks : dict, optional
404
    sources : dict, optional
405
    transformers : dict, optional
406
    storages : dict, optional
407

408
    Returns
409
    -------
410
    Indirectly updated `model` and dict of asset in `kwargs` with the component object.
411

412
    TODOS
413
    ^^^^^
414
    Might be possible to drop non invest optimization in favour of invest optimization if max_capactiy
415
    attributes ie. are set to 0 for fix (but less beautiful, and in case of generator even blocks nonconvex opt.).
416

417
    Notes
418
    -----
419
    Tested with:
420
    - test_check_optimize_cap_raise_error()
421

422
    """
423
    if dict_asset[OPTIMIZE_CAP][VALUE] is False:
1✔
424
        func_constant(model, dict_asset, **kwargs)
1✔
425
        if dict_asset[OEMOF_ASSET_TYPE] != OEMOF_SOURCE:
1✔
426
            logging.debug(
1✔
427
                "Added: %s %s (fixed capacity)",
428
                dict_asset[OEMOF_ASSET_TYPE].capitalize(),
429
                dict_asset[LABEL],
430
            )
431

432
    elif dict_asset[OPTIMIZE_CAP][VALUE] is True:
1✔
433
        func_optimize(model, dict_asset, **kwargs)
1✔
434
        if dict_asset[OEMOF_ASSET_TYPE] != OEMOF_SOURCE:
1✔
435
            logging.debug(
1✔
436
                "Added: %s %s (capacity to be optimized)",
437
                dict_asset[OEMOF_ASSET_TYPE].capitalize(),
438
                dict_asset[LABEL],
439
            )
440
    else:
441
        raise ValueError(
1✔
442
            f"Input error! '{OPTIMIZE_CAP}' of asset {dict_asset[LABEL]}\n should be True/False but is {dict_asset[OPTIMIZE_CAP][VALUE]}."
443
        )
444

445

446
class CustomBus(solph.Bus):
1✔
447
    def __init__(self, *args, **kwargs):
1✔
448
        ev = kwargs.pop("energy_vector", None)  # change to ENERGY_VECTOR
1✔
449
        super(CustomBus, self).__init__(*args, **kwargs)
1✔
450
        self.energy_vector = ev
1✔
451

452

453
def bus(model, name, **kwargs):
1✔
454
    r"""
455
    Adds bus `name` to `model` and to 'busses' in `kwargs`.
456

457
    Notes
458
    -----
459
    Tested with:
460
    - test_bus_add_to_empty_dict()
461
    - test_bus_add_to_not_empty_dict()
462

463
    """
464
    logging.debug(f"Added: Bus {name}")
1✔
465
    energy_vector = kwargs.get("energy_vector", None)  # change to ENERGY_VECTOR
1✔
466
    bus = CustomBus(label=name, energy_vector=energy_vector)
1✔
467
    kwargs[OEMOF_BUSSES].update({name: bus})
1✔
468
    model.add(bus)
1✔
469

470

471
def transformer_constant_efficiency_fix(model, dict_asset, **kwargs):
1✔
472
    r"""
473
    Defines a transformer with constant efficiency and fixed capacity.
474

475
    See :py:func:`~.transformer` for more information, including parameters.
476

477
    Notes
478
    -----
479
    Tested with:
480
    - test_transformer_fix_cap_single_busses()
481
    - test_transformer_fix_cap_multiple_input_busses()
482
    - test_transformer_fix_cap_multiple_output_busses()
483

484
    Returns
485
    -------
486
    Indirectly updated `model` and dict of asset in `kwargs` with the transformer object.
487

488
    """
489

490
    missing_dispatch_prices_or_efficiencies = None
1✔
491

492
    # check if the transformer has multiple input or multiple output busses
493
    if isinstance(dict_asset[INFLOW_DIRECTION], list) or isinstance(
1✔
494
        dict_asset[OUTFLOW_DIRECTION], list
495
    ):
496
        if isinstance(dict_asset[INFLOW_DIRECTION], list) and isinstance(
1✔
497
            dict_asset[OUTFLOW_DIRECTION], str
498
        ):
499
            # multiple inputs and single output
500
            num_inputs = len(dict_asset[INFLOW_DIRECTION])
1✔
501
            if (
1✔
502
                get_length_if_list(dict_asset[DISPATCH_PRICE][VALUE]) != num_inputs
503
                or get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_inputs
504
            ):
505
                inputs_names = ", ".join(
×
506
                    [f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]]
507
                )
508
                missing_dispatch_prices_or_efficiencies = (
×
509
                    f"You defined multiple values for parameter '{INFLOW_DIRECTION}' "
510
                    f"({inputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
511
                    f"You must also provide exactly {num_inputs} values for the parameters "
512
                    f"'{DISPATCH_PRICE}' and '{EFFICIENCY}'."
513
                )
514
                logging.error(missing_dispatch_prices_or_efficiencies)
×
515
                raise ValueError(missing_dispatch_prices_or_efficiencies)
×
516

517
            inputs = {}
1✔
518
            for i, bus in enumerate(dict_asset[INFLOW_DIRECTION]):
1✔
519
                inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
520
                    variable_costs=get_item_if_list(
521
                        dict_asset[DISPATCH_PRICE][VALUE], i
522
                    )
523
                )
524

525
            outputs = {
1✔
526
                kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
527
                    nominal_value=dict_asset[INSTALLED_CAP][VALUE]
528
                )
529
            }
530
            efficiencies = {}
1✔
531
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
532
                efficiencies[
1✔
533
                    kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION][i]]
534
                ] = efficiency
535

536
        elif isinstance(dict_asset[INFLOW_DIRECTION], str) and isinstance(
1✔
537
            dict_asset[OUTFLOW_DIRECTION], list
538
        ):
539
            # single input and multiple outputs
540
            num_outputs = len(dict_asset[OUTFLOW_DIRECTION])
1✔
541
            if (
1✔
542
                get_length_if_list(dict_asset[DISPATCH_PRICE][VALUE]) != num_outputs
543
                or get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_outputs
544
            ):
545
                outputs_names = ", ".join(
1✔
546
                    [f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]]
547
                )
548
                missing_dispatch_prices_or_efficiencies = (
1✔
549
                    f"You defined multiple values for parameter '{OUTFLOW_DIRECTION}' "
550
                    f"({outputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
551
                    f"You must also provide exactly {num_outputs} values for the parameters "
552
                    f"'{DISPATCH_PRICE}' and '{EFFICIENCY}'."
553
                )
554
                logging.error(missing_dispatch_prices_or_efficiencies)
1✔
555
                raise ValueError(missing_dispatch_prices_or_efficiencies)
1✔
556

557
            inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
558
            outputs = {}
1✔
559
            for i, bus in enumerate(dict_asset[OUTFLOW_DIRECTION]):
1✔
560
                outputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
561
                    nominal_value=get_item_if_list(dict_asset[INSTALLED_CAP][VALUE], i),
562
                    variable_costs=get_item_if_list(
563
                        dict_asset[DISPATCH_PRICE][VALUE], i
564
                    ),
565
                )
566

567
            efficiencies = {}
1✔
568
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
569
                efficiencies[
1✔
570
                    kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][i]]
571
                ] = efficiency
572

573
        else:
574
            # multiple inputs and multiple outputs
575
            inputs_names = ", ".join([f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]])
×
576
            outputs_names = ", ".join([f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]])
×
577
            missing_dispatch_prices_or_efficiencies = (
×
578
                f"You defined multiple values for parameter '{INFLOW_DIRECTION}'"
579
                f" ({inputs_names}) as well as for parameter '{OUTFLOW_DIRECTION}' ({outputs_names})"
580
                f" of the conversion asset named '{dict_asset[LABEL]}', this is not supported"
581
                f" at the moment."
582
            )
583
            logging.error(missing_dispatch_prices_or_efficiencies)
×
584
            raise ValueError(missing_dispatch_prices_or_efficiencies)
×
585
    else:
586
        # single input and single output
587
        inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
588
        outputs = {
1✔
589
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
590
                nominal_value=dict_asset[INSTALLED_CAP][VALUE],
591
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
592
            )
593
        }
594
        efficiencies = {
1✔
595
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: dict_asset[EFFICIENCY][
596
                VALUE
597
            ]
598
        }
599

600
    if missing_dispatch_prices_or_efficiencies is None:
1✔
601
        t = solph.Transformer(
1✔
602
            label=dict_asset[LABEL],
603
            inputs=inputs,
604
            outputs=outputs,
605
            conversion_factors=efficiencies,
606
        )
607

608
        model.add(t)
1✔
609
        kwargs[OEMOF_TRANSFORMER].update({dict_asset[LABEL]: t})
1✔
610

611

612
def transformer_constant_efficiency_optimize(model, dict_asset, **kwargs):
1✔
613
    r"""
614
    Defines a transformer with constant efficiency and a capacity to be optimized.
615

616
    See :py:func:`~.transformer` for more information, including parameters.
617

618
    Notes
619
    -----
620
    Tested with:
621
    - test_transformer_optimize_cap_single_busses()
622
    - test_transformer_optimize_cap_multiple_input_busses()
623
    - test_transformer_optimize_cap_multiple_output_busses()
624

625
    Returns
626
    -------
627
    Indirectly updated `model` and dict of asset in `kwargs` with the transformer object.
628

629
    """
630
    missing_dispatch_prices_or_efficiencies = None
1✔
631

632
    # check if the transformer has multiple input or multiple output busses
633
    # the investment object is always in the output bus
634
    if isinstance(dict_asset[INFLOW_DIRECTION], list) or isinstance(
1✔
635
        dict_asset[OUTFLOW_DIRECTION], list
636
    ):
637
        if isinstance(dict_asset[INFLOW_DIRECTION], list) and isinstance(
1✔
638
            dict_asset[OUTFLOW_DIRECTION], str
639
        ):
640
            # multiple inputs and single output
641
            num_inputs = len(dict_asset[INFLOW_DIRECTION])
1✔
642
            if (
1✔
643
                get_length_if_list(dict_asset[DISPATCH_PRICE][VALUE]) != num_inputs
644
                or get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_inputs
645
            ):
646
                inputs_names = ", ".join(
×
647
                    [f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]]
648
                )
649
                missing_dispatch_prices_or_efficiencies = (
×
650
                    f"You defined multiple values for parameter '{INFLOW_DIRECTION}' "
651
                    f"({inputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
652
                    f"You must also provide exactly {num_inputs} values for the parameters "
653
                    f"'{DISPATCH_PRICE}' and '{EFFICIENCY}'."
654
                )
655
                logging.error(missing_dispatch_prices_or_efficiencies)
×
656
                raise ValueError(missing_dispatch_prices_or_efficiencies)
×
657

658
            inputs = {}
1✔
659
            for i, bus in enumerate(dict_asset[INFLOW_DIRECTION]):
1✔
660
                inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
661
                    variable_costs=dict_asset[DISPATCH_PRICE][VALUE][i]
662
                )
663

664
            outputs = {
1✔
665
                kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
666
                    investment=solph.Investment(
667
                        ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
668
                        maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
669
                        existing=dict_asset[INSTALLED_CAP][VALUE],
670
                    )
671
                )
672
            }
673

674
            efficiencies = {}
1✔
675
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
676
                efficiencies[
1✔
677
                    kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION][i]]
678
                ] = efficiency
679

680
        elif isinstance(dict_asset[INFLOW_DIRECTION], str) and isinstance(
1✔
681
            dict_asset[OUTFLOW_DIRECTION], list
682
        ):
683
            # single input and multiple outputs
684
            num_outputs = len(dict_asset[OUTFLOW_DIRECTION])
1✔
685
            if (
1✔
686
                get_length_if_list(dict_asset[DISPATCH_PRICE][VALUE]) != num_outputs
687
                or get_length_if_list(dict_asset[EFFICIENCY][VALUE]) != num_outputs
688
            ):
689
                outputs_names = ", ".join(
1✔
690
                    [f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]]
691
                )
692
                missing_dispatch_prices_or_efficiencies = (
1✔
693
                    f"You defined multiple values for parameter '{OUTFLOW_DIRECTION}' "
694
                    f"({outputs_names}) of the conversion asset named '{dict_asset[LABEL]}'. "
695
                    f"You must also provide exactly {num_outputs} values for the parameters "
696
                    f"'{DISPATCH_PRICE}' and '{EFFICIENCY}'."
697
                )
698
                logging.error(missing_dispatch_prices_or_efficiencies)
1✔
699
                raise ValueError(missing_dispatch_prices_or_efficiencies)
1✔
700

701
            # TODO move the investment in the input bus???
702
            inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
703
            outputs = {}
1✔
704

705
            for i, bus in enumerate(dict_asset[OUTFLOW_DIRECTION]):
1✔
706
                outputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
707
                    investment=solph.Investment(
708
                        ep_costs=get_item_if_list(
709
                            dict_asset[SIMULATION_ANNUITY][VALUE], i
710
                        ),
711
                        maximum=get_item_if_list(dict_asset[MAXIMUM_ADD_CAP][VALUE], i),
712
                        existing=get_item_if_list(dict_asset[INSTALLED_CAP][VALUE], i),
713
                    ),
714
                    variable_costs=dict_asset[DISPATCH_PRICE][VALUE][i],
715
                )
716

717
            efficiencies = {}
1✔
718
            for i, efficiency in enumerate(dict_asset[EFFICIENCY][VALUE]):
1✔
719
                efficiencies[
1✔
720
                    kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][i]]
721
                ] = efficiency
722

723
        else:
724
            # multiple inputs and multiple outputs
725
            inputs_names = ", ".join([f"'{n}'" for n in dict_asset[INFLOW_DIRECTION]])
×
726
            outputs_names = ", ".join([f"'{n}'" for n in dict_asset[OUTFLOW_DIRECTION]])
×
727
            missing_dispatch_prices_or_efficiencies = (
×
728
                f"You defined multiple values for parameter '{INFLOW_DIRECTION}'"
729
                f" ({inputs_names}) as well as for parameter '{OUTFLOW_DIRECTION}' ({outputs_names})"
730
                f" of the conversion asset named '{dict_asset[LABEL]}', this is not supported"
731
                f" at the moment."
732
            )
733
            logging.error(missing_dispatch_prices_or_efficiencies)
×
734
            raise ValueError(missing_dispatch_prices_or_efficiencies)
×
735
    else:
736
        # single input and single output
737
        inputs = {kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow()}
1✔
738
        if AVAILABILITY_DISPATCH in dict_asset.keys():
1✔
739
            # This key is only present in DSO peak demand pricing transformers.
740
            outputs = {
1✔
741
                kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
742
                    investment=solph.Investment(
743
                        ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
744
                        maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
745
                        existing=dict_asset[INSTALLED_CAP][VALUE],
746
                    ),
747
                    variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
748
                    max=dict_asset[AVAILABILITY_DISPATCH].values,
749
                )
750
            }
751
        else:
752
            outputs = {
1✔
753
                kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
754
                    investment=solph.Investment(
755
                        ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
756
                        maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
757
                        existing=dict_asset[INSTALLED_CAP][VALUE],
758
                    ),
759
                    variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
760
                )
761
            }
762

763
        efficiencies = {
1✔
764
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: dict_asset[EFFICIENCY][
765
                VALUE
766
            ]
767
        }
768

769
    if missing_dispatch_prices_or_efficiencies is None:
1✔
770
        t = solph.Transformer(
1✔
771
            label=dict_asset[LABEL],
772
            inputs=inputs,
773
            outputs=outputs,
774
            conversion_factors=efficiencies,
775
        )
776

777
        model.add(t)
1✔
778
        kwargs[OEMOF_TRANSFORMER].update({dict_asset[LABEL]: t})
1✔
779

780

781
def storage_fix(model, dict_asset, **kwargs):
1✔
782
    r"""
783
    Defines a storage with a fixed capacity.
784

785
    See :py:func:`~.storage` for more information, including parameters.
786

787
    Notes
788
    -----
789
    Tested with:
790
    - test_storage_fix()
791

792
    Returns
793
    -------
794
    Indirectly updated `model` and dict of asset in `kwargs` with the storage object.
795

796
    """
797
    storage = solph.components.GenericStorage(
1✔
798
        label=dict_asset[LABEL],
799
        nominal_storage_capacity=dict_asset[STORAGE_CAPACITY][INSTALLED_CAP][VALUE],
800
        inputs={
801
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
802
                nominal_value=dict_asset[INPUT_POWER][INSTALLED_CAP][
803
                    VALUE
804
                ],  # limited through installed capacity, NOT c-rate
805
                variable_costs=dict_asset[INPUT_POWER][DISPATCH_PRICE][VALUE],
806
            )
807
        },  # maximum charge possible in one timestep
808
        outputs={
809
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
810
                nominal_value=dict_asset[OUTPUT_POWER][INSTALLED_CAP][
811
                    VALUE
812
                ],  # 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
813
                variable_costs=dict_asset[OUTPUT_POWER][DISPATCH_PRICE][VALUE],
814
            )
815
        },  # maximum discharge possible in one timestep
816
        loss_rate=1
817
        - dict_asset[STORAGE_CAPACITY][EFFICIENCY][VALUE],  # from timestep to timestep
818
        fixed_losses_absolute=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_ABS][VALUE],
819
        fixed_losses_relative=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_REL][VALUE],
820
        min_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MIN][VALUE],
821
        max_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MAX][VALUE],
822
        initial_storage_level=dict_asset[STORAGE_CAPACITY][SOC_INITIAL][
823
            VALUE
824
        ],  # in terms of SOC
825
        inflow_conversion_factor=dict_asset[INPUT_POWER][EFFICIENCY][
826
            VALUE
827
        ],  # storing efficiency
828
        outflow_conversion_factor=dict_asset[OUTPUT_POWER][EFFICIENCY][VALUE],
829
    )  # efficiency of discharge
830
    model.add(storage)
1✔
831
    kwargs[OEMOF_GEN_STORAGE].update({dict_asset[LABEL]: storage})
1✔
832

833

834
def storage_optimize(model, dict_asset, **kwargs):
1✔
835
    r"""
836
    Defines a storage with a capacity to be optimized.
837

838
    See :py:func:`~.storage` for more information, including parameters.
839

840
    Notes
841
    -----
842
    Tested with:
843
    - test_storage_optimize()
844
    - test_storage_optimize_investment_minimum_0_float()
845
    - test_storage_optimize_investment_minimum_0_time_series()
846
    - test_storage_optimize_investment_minimum_1_rel_float()
847
    - test_storage_optimize_investment_minimum_1_abs_float()
848
    - test_storage_optimize_investment_minimum_1_rel_times_series()
849
    - test_storage_optimize_investment_minimum_1_abs_times_series()
850

851
    Returns
852
    -------
853
    Indirectly updated `model` and dict of asset in `kwargs` with the storage object.
854

855
    """
856
    # investment.minimum for an InvestmentStorage is 0 as default
857
    minimum = 0
1✔
858

859
    # Set investment.minimum to 1 if
860
    # non-zero fixed_thermal_losses_relative or fixed_thermal_losses_absolute exist as
861
    for losses in [THERM_LOSSES_REL, THERM_LOSSES_ABS]:
1✔
862
        # 1. float or
863
        try:
1✔
864
            float(dict_asset[STORAGE_CAPACITY][losses][VALUE])
1✔
865
            if dict_asset[STORAGE_CAPACITY][losses][VALUE] != 0:
1✔
866
                minimum = 1
1✔
867
        # 2. time series
868
        except TypeError:
1✔
869
            if sum(dict_asset[STORAGE_CAPACITY][losses][VALUE]) != 0:
1✔
870
                minimum = 1
1✔
871

872
    storage = solph.components.GenericStorage(
1✔
873
        label=dict_asset[LABEL],
874
        investment=solph.Investment(
875
            ep_costs=dict_asset[STORAGE_CAPACITY][SIMULATION_ANNUITY][VALUE],
876
            minimum=minimum,
877
            maximum=dict_asset[STORAGE_CAPACITY][MAXIMUM_ADD_CAP][VALUE],
878
            existing=dict_asset[STORAGE_CAPACITY][INSTALLED_CAP][VALUE],
879
        ),
880
        inputs={
881
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
882
                investment=solph.Investment(
883
                    ep_costs=dict_asset[INPUT_POWER][SIMULATION_ANNUITY][VALUE],
884
                    maximum=dict_asset[INPUT_POWER][MAXIMUM_ADD_CAP][VALUE],
885
                    existing=dict_asset[INPUT_POWER][INSTALLED_CAP][
886
                        VALUE
887
                    ],  # todo: `existing needed here?`
888
                ),
889
                variable_costs=dict_asset[INPUT_POWER][DISPATCH_PRICE][VALUE],
890
            )
891
        },  # maximum charge power
892
        outputs={
893
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
894
                investment=solph.Investment(
895
                    ep_costs=dict_asset[OUTPUT_POWER][SIMULATION_ANNUITY][VALUE],
896
                    maximum=dict_asset[OUTPUT_POWER][MAXIMUM_ADD_CAP][VALUE],
897
                    existing=dict_asset[OUTPUT_POWER][INSTALLED_CAP][
898
                        VALUE
899
                    ],  # todo: `existing needed here?`
900
                ),
901
                variable_costs=dict_asset[OUTPUT_POWER][DISPATCH_PRICE][VALUE],
902
            )
903
        },  # maximum discharge power
904
        loss_rate=1
905
        - dict_asset[STORAGE_CAPACITY][EFFICIENCY][VALUE],  # from timestep to timestep
906
        fixed_losses_absolute=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_ABS][VALUE],
907
        fixed_losses_relative=dict_asset[STORAGE_CAPACITY][THERM_LOSSES_REL][VALUE],
908
        min_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MIN][VALUE],
909
        max_storage_level=dict_asset[STORAGE_CAPACITY][SOC_MAX][VALUE],
910
        initial_storage_level=dict_asset[STORAGE_CAPACITY][SOC_INITIAL][
911
            VALUE
912
        ],  # in terms of SOC #implication: balanced = True, ie. start=end
913
        inflow_conversion_factor=dict_asset[INPUT_POWER][EFFICIENCY][
914
            VALUE
915
        ],  # storing efficiency
916
        outflow_conversion_factor=dict_asset[OUTPUT_POWER][EFFICIENCY][
917
            VALUE
918
        ],  # efficiency of discharge
919
        invest_relation_input_capacity=dict_asset[INPUT_POWER][C_RATE][VALUE],
920
        # storage can be charged with invest_relation_output_capacity*capacity in one timeperiod
921
        invest_relation_output_capacity=dict_asset[OUTPUT_POWER][C_RATE][VALUE]
922
        # storage can be emptied with invest_relation_output_capacity*capacity in one timeperiod
923
    )
924
    model.add(storage)
1✔
925
    kwargs[OEMOF_GEN_STORAGE].update({dict_asset[LABEL]: storage})
1✔
926

927

928
def source_non_dispatchable_fix(model, dict_asset, **kwargs):
1✔
929
    r"""
930
    Defines a non dispatchable source with a fixed capacity.
931

932
    See :py:func:`~.source` for more information, including parameters.
933

934
    Notes
935
    -----
936
    Tested with:
937
    - test_source_non_dispatchable_fix()
938

939
    Returns
940
    -------
941
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
942

943
    """
944
    outputs = {
1✔
945
        kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
946
            label=dict_asset[LABEL],
947
            fix=dict_asset[TIMESERIES],
948
            nominal_value=dict_asset[INSTALLED_CAP][VALUE],
949
            variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
950
            emission_factor=dict_asset[EMISSION_FACTOR][VALUE],
951
        )
952
    }
953

954
    source_non_dispatchable = solph.Source(label=dict_asset[LABEL], outputs=outputs)
1✔
955

956
    model.add(source_non_dispatchable)
1✔
957
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_non_dispatchable})
1✔
958
    logging.debug(
1✔
959
        f"Added: Non-dispatchable source {dict_asset[LABEL]} (fixed capacity) to bus {dict_asset[OUTFLOW_DIRECTION]}.",
960
    )
961

962

963
def source_non_dispatchable_optimize(model, dict_asset, **kwargs):
1✔
964
    r"""
965
    Defines a non dispatchable source with a capacity to be optimized.
966

967
    See :py:func:`~.source` for more information, including parameters.
968

969
    Notes
970
    -----
971
    Tested with:
972
    - test_source_non_dispatchable_optimize()
973

974
    Returns
975
    -------
976
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
977

978
    """
979
    if MAXIMUM_ADD_CAP_NORMALIZED in dict_asset:
1✔
980
        maximum = dict_asset[MAXIMUM_ADD_CAP_NORMALIZED][VALUE]
1✔
981
    else:
982
        maximum = dict_asset[MAXIMUM_ADD_CAP][VALUE]
×
983
    if INSTALLED_CAP_NORMALIZED in dict_asset:
1✔
984
        existing = dict_asset[INSTALLED_CAP_NORMALIZED][VALUE]
1✔
985
    else:
986
        existing = dict_asset[INSTALLED_CAP][VALUE]
1✔
987
    outputs = {
1✔
988
        kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
989
            label=dict_asset[LABEL],
990
            fix=dict_asset[TIMESERIES_NORMALIZED],
991
            investment=solph.Investment(
992
                ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE]
993
                / dict_asset[TIMESERIES_PEAK][VALUE],
994
                maximum=maximum,
995
                existing=existing,
996
            ),
997
            # variable_costs are devided by time series peak as normalized time series are used as actual_value
998
            variable_costs=dict_asset[DISPATCH_PRICE][VALUE]
999
            / dict_asset[TIMESERIES_PEAK][VALUE],
1000
            # add emission_factor for emission contraint
1001
            emission_factor=dict_asset[EMISSION_FACTOR][VALUE],
1002
        )
1003
    }
1004

1005
    source_non_dispatchable = solph.Source(label=dict_asset[LABEL], outputs=outputs)
1✔
1006

1007
    model.add(source_non_dispatchable)
1✔
1008
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_non_dispatchable})
1✔
1009
    logging.debug(
1✔
1010
        f"Added: Non-dispatchable source {dict_asset[LABEL]} (capacity to be optimized) to bus {dict_asset[OUTFLOW_DIRECTION]}."
1011
    )
1012

1013

1014
def source_dispatchable_optimize(model, dict_asset, **kwargs):
1✔
1015
    r"""
1016
    Defines a dispatchable source with a fixed capacity.
1017

1018
    See :py:func:`~.source` for more information, including parameters.
1019

1020
    Notes
1021
    -----
1022
    Tested with:
1023
    - test_source_dispatchable_optimize_normalized_timeseries()
1024
    - test_source_dispatchable_optimize_timeseries_not_normalized_timeseries()
1025

1026
     Returns
1027
    -------
1028
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
1029

1030
    """
1031
    if TIMESERIES_NORMALIZED in dict_asset:
1✔
1032
        outputs = {
1✔
1033
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1034
                label=dict_asset[LABEL],
1035
                max=dict_asset[TIMESERIES_NORMALIZED],
1036
                investment=solph.Investment(
1037
                    ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE]
1038
                    / dict_asset[TIMESERIES_PEAK][VALUE],
1039
                    maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
1040
                    existing=dict_asset[INSTALLED_CAP][VALUE],
1041
                ),
1042
                # variable_costs are devided by time series peak as normalized time series are used as actual_value
1043
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE]
1044
                / dict_asset[TIMESERIES_PEAK][VALUE],
1045
                # add emission_factor for emission contraint
1046
                emission_factor=dict_asset[EMISSION_FACTOR][VALUE],
1047
            )
1048
        }
1049
        source_dispatchable = solph.Source(label=dict_asset[LABEL], outputs=outputs,)
1✔
1050
    else:
1051
        if TIMESERIES in dict_asset:
1✔
1052
            logging.info(
1✔
1053
                f"Asset {dict_asset[LABEL]} is introduced as a dispatchable source with an availability schedule."
1054
            )
1055
            logging.debug(
1✔
1056
                f"The availability schedule is solely introduced because the key {TIMESERIES_NORMALIZED} was not in the asset´s dictionary. \n"
1057
                f"It should only be applied to DSO sources. "
1058
                f"If the asset should not have this behaviour, please create an issue.",
1059
            )
1060
        outputs = {
1✔
1061
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1062
                label=dict_asset[LABEL],
1063
                investment=solph.Investment(
1064
                    ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
1065
                    existing=dict_asset[INSTALLED_CAP][VALUE],
1066
                    maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
1067
                ),
1068
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1069
                # add emission_factor for emission contraint
1070
                emission_factor=dict_asset[EMISSION_FACTOR][VALUE],
1071
            )
1072
        }
1073
        source_dispatchable = solph.Source(label=dict_asset[LABEL], outputs=outputs,)
1✔
1074
    model.add(source_dispatchable)
1✔
1075
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_dispatchable})
1✔
1076
    logging.debug(
1✔
1077
        f"Added: Dispatchable source {dict_asset[LABEL]} (capacity to be optimized) to bus {dict_asset[OUTFLOW_DIRECTION]}."
1078
    )
1079

1080

1081
def source_dispatchable_fix(model, dict_asset, **kwargs):
1✔
1082
    r"""
1083
    Defines a dispatchable source with a fixed capacity.
1084

1085
    See :py:func:`~.source` for more information, including parameters.
1086

1087
    Notes
1088
    -----
1089
    Tested with:
1090
    - test_source_dispatchable_fix_normalized_timeseries()
1091
    - test_source_dispatchable_fix_timeseries_not_normalized_timeseries()
1092

1093
    Returns
1094
    -------
1095
    Indirectly updated `model` and dict of asset in `kwargs` with the source object.
1096

1097
    """
1098
    if TIMESERIES_NORMALIZED in dict_asset:
1✔
1099
        outputs = {
1✔
1100
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1101
                label=dict_asset[LABEL],
1102
                max=dict_asset[TIMESERIES_NORMALIZED],
1103
                existing=dict_asset[INSTALLED_CAP][VALUE],
1104
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1105
                # add emission_factor for emission contraint
1106
                emission_factor=dict_asset[EMISSION_FACTOR][VALUE],
1107
            )
1108
        }
1109
        source_dispatchable = solph.Source(label=dict_asset[LABEL], outputs=outputs,)
1✔
1110
    else:
1111
        if TIMESERIES in dict_asset:
1✔
1112
            logging.info(
1✔
1113
                f"Asset {dict_asset[LABEL]} is introduced as a dispatchable source with an availability schedule."
1114
            )
1115
            logging.debug(
1✔
1116
                f"The availability schedule is solely introduced because the key {TIMESERIES_NORMALIZED} was not in the asset´s dictionary. \n"
1117
                f"It should only be applied to DSO sources. "
1118
                f"If the asset should not have this behaviour, please create an issue.",
1119
            )
1120
        outputs = {
1✔
1121
            kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION]]: solph.Flow(
1122
                label=dict_asset[LABEL],
1123
                existing=dict_asset[INSTALLED_CAP][VALUE],
1124
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1125
            )
1126
        }
1127
        source_dispatchable = solph.Source(label=dict_asset[LABEL], outputs=outputs,)
1✔
1128
    model.add(source_dispatchable)
1✔
1129
    kwargs[OEMOF_SOURCE].update({dict_asset[LABEL]: source_dispatchable})
1✔
1130
    logging.debug(
1✔
1131
        f"Added: Dispatchable source {dict_asset[LABEL]} (fixed capacity) to bus {dict_asset[OUTFLOW_DIRECTION]}."
1132
    )
1133

1134

1135
def sink_dispatchable_optimize(model, dict_asset, **kwargs):
1✔
1136
    r"""
1137
    Define a dispatchable sink.
1138

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

1142
    See :py:func:`~.sink` for more information, including parameters.
1143

1144
    Notes
1145
    -----
1146
    Tested with:
1147
    - test_sink_dispatchable_single_input_bus()
1148
    - test_sink_dispatchable_multiple_input_busses()
1149

1150
    Returns
1151
    -------
1152
    Indirectly updated `model` and dict of asset in `kwargs` with the sink object.
1153

1154
    """
1155
    # check if the sink has multiple input busses
1156
    if isinstance(dict_asset[INFLOW_DIRECTION], list):
1✔
1157
        inputs = {}
1✔
1158
        index = 0
1✔
1159
        for bus in dict_asset[INFLOW_DIRECTION]:
1✔
1160
            inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
1161
                label=dict_asset[LABEL],
1162
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE][index],
1163
                investment=solph.Investment(),
1164
            )
1165
            index += 1
1✔
1166
    else:
1167
        inputs = {
1✔
1168
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1169
                label=dict_asset[LABEL],
1170
                variable_costs=dict_asset[DISPATCH_PRICE][VALUE],
1171
                investment=solph.Investment(),
1172
            )
1173
        }
1174

1175
    # create and add excess electricity sink to micro_grid_system - variable
1176
    sink_dispatchable = solph.Sink(label=dict_asset[LABEL], inputs=inputs,)
1✔
1177
    model.add(sink_dispatchable)
1✔
1178
    kwargs[OEMOF_SINK].update({dict_asset[LABEL]: sink_dispatchable})
1✔
1179
    logging.debug(
1✔
1180
        f"Added: Dispatchable sink {dict_asset[LABEL]} (to be capacity optimized) to bus {dict_asset[INFLOW_DIRECTION]}.",
1181
    )
1182

1183

1184
def sink_non_dispatchable(model, dict_asset, **kwargs):
1✔
1185
    r"""
1186
    Defines a non dispatchable sink.
1187

1188
    See :py:func:`~.sink` for more information, including parameters.
1189

1190
    Notes
1191
    -----
1192
    Tested with:
1193
    - test_sink_non_dispatchable_single_input_bus()
1194
    - test_sink_non_dispatchable_multiple_input_busses()
1195

1196
    Returns
1197
    -------
1198
    Indirectly updated `model` and dict of asset in `kwargs` with the sink object.
1199

1200
    """
1201
    # check if the sink has multiple input busses
1202
    if isinstance(dict_asset[INFLOW_DIRECTION], list):
1✔
1203
        inputs = {}
1✔
1204
        index = 0
1✔
1205
        for bus in dict_asset[INFLOW_DIRECTION]:
1✔
1206
            inputs[kwargs[OEMOF_BUSSES][bus]] = solph.Flow(
1✔
1207
                fix=dict_asset[TIMESERIES], nominal_value=1
1208
            )
1209
            index += 1
1✔
1210
    else:
1211
        inputs = {
1✔
1212
            kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1213
                fix=dict_asset[TIMESERIES], nominal_value=1
1214
            )
1215
        }
1216

1217
    # create and add demand sink to micro_grid_system - fixed
1218
    sink_demand = solph.Sink(label=dict_asset[LABEL], inputs=inputs,)
1✔
1219
    model.add(sink_demand)
1✔
1220
    kwargs[OEMOF_SINK].update({dict_asset[LABEL]: sink_demand})
1✔
1221
    logging.debug(
1✔
1222
        f"Added: Non-dispatchable sink {dict_asset[LABEL]} to bus {dict_asset[INFLOW_DIRECTION]}"
1223
    )
1224

1225

1226
def chp_fix(model, dict_asset, **kwargs):
1✔
1227
    r"""
1228
    Extraction turbine chp from Oemof solph. Extraction turbine must have one input and two outputs
1229
    Notes
1230
    -----
1231
    Tested with:
1232
    - test_to_be_written()
1233

1234
    Returns
1235
    -------
1236
    Indirectly updated `model` and dict of asset in `kwargs` with the extraction turbine component.
1237

1238
    """
1239

1240
    inputs = {
1✔
1241
        kwargs[OEMOF_BUSSES][dict_asset[INFLOW_DIRECTION]]: solph.Flow(
1242
            nominal_value=dict_asset[INSTALLED_CAP][VALUE]
1243
        )
1244
    }
1245

1246
    busses_energy_vectors = [
1✔
1247
        kwargs[OEMOF_BUSSES][b].energy_vector for b in dict_asset[OUTFLOW_DIRECTION]
1248
    ]
1249
    idx_el = busses_energy_vectors.index("Electricity")
1✔
1250
    idx_th = busses_energy_vectors.index("Heat")
1✔
1251
    el_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_el]]
1✔
1252
    th_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_th]]
1✔
1253

1254
    outputs = {
1✔
1255
        el_bus: solph.Flow(),
1256
        th_bus: solph.Flow(),
1257
    }  # if kW for heat and kW for elect then insert it under nominal_value
1258

1259
    beta = dict_asset[BETA][VALUE]
1✔
1260

1261
    efficiency_el_wo_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_th]
1✔
1262
    efficiency_th_max_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_el]
1✔
1263
    efficiency_el_max_heat_extraction = (
1✔
1264
        efficiency_el_wo_heat_extraction - beta * efficiency_th_max_heat_extraction
1265
    )
1266
    efficiency_full_condensation = {el_bus: efficiency_el_wo_heat_extraction}
1✔
1267

1268
    efficiencies = {
1✔
1269
        el_bus: efficiency_el_max_heat_extraction,
1270
        th_bus: efficiency_th_max_heat_extraction,
1271
    }
1272

1273
    ext_turb_chp = solph.components.ExtractionTurbineCHP(
1✔
1274
        label=dict_asset[LABEL],
1275
        inputs=inputs,
1276
        outputs=outputs,
1277
        conversion_factors=efficiencies,
1278
        conversion_factor_full_condensation=efficiency_full_condensation,
1279
    )
1280

1281
    model.add(ext_turb_chp)
1✔
1282
    kwargs[OEMOF_ExtractionTurbineCHP].update({dict_asset[LABEL]: ext_turb_chp})
1✔
1283

1284

1285
def chp_optimize(model, dict_asset, **kwargs):
1✔
1286
    r"""
1287
    Extraction turbine chp from Oemof solph. Extraction turbine must have one input and two outputs
1288
    Notes
1289
    -----
1290
    Tested with:
1291
    - test_to_be_written()
1292
    
1293
    Returns
1294
    -------
1295
    Indirectly updated `model` and dict of asset in `kwargs` with the extraction turbine component.
1296

1297
    """
1298

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

1301
    busses_energy_vectors = [
1✔
1302
        kwargs[OEMOF_BUSSES][b].energy_vector for b in dict_asset[OUTFLOW_DIRECTION]
1303
    ]
1304

1305
    idx_el = busses_energy_vectors.index("Electricity")
1✔
1306
    idx_th = busses_energy_vectors.index("Heat")
1✔
1307
    el_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_el]]
1✔
1308
    th_bus = kwargs[OEMOF_BUSSES][dict_asset[OUTFLOW_DIRECTION][idx_th]]
1✔
1309

1310
    outputs = {
1✔
1311
        el_bus: solph.Flow(
1312
            investment=solph.Investment(
1313
                ep_costs=dict_asset[SIMULATION_ANNUITY][VALUE],
1314
                maximum=dict_asset[MAXIMUM_ADD_CAP][VALUE],
1315
                existing=dict_asset[INSTALLED_CAP][VALUE],
1316
            )
1317
        ),
1318
        th_bus: solph.Flow(),
1319
    }
1320

1321
    beta = dict_asset[BETA][VALUE]
1✔
1322

1323
    efficiency_el_wo_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_el]
1✔
1324
    efficiency_th_max_heat_extraction = dict_asset[EFFICIENCY][VALUE][idx_th]
1✔
1325
    efficiency_el_max_heat_extraction = (
1✔
1326
        efficiency_el_wo_heat_extraction - beta * efficiency_th_max_heat_extraction
1327
    )
1328
    efficiency_full_condensation = {el_bus: efficiency_el_wo_heat_extraction}
1✔
1329

1330
    efficiencies = {
1✔
1331
        el_bus: efficiency_el_max_heat_extraction,
1332
        th_bus: efficiency_th_max_heat_extraction,
1333
    }
1334

1335
    ext_turb_chp = solph.components.ExtractionTurbineCHP(
1✔
1336
        label=dict_asset[LABEL],
1337
        inputs=inputs,
1338
        outputs=outputs,
1339
        conversion_factors=efficiencies,
1340
        conversion_factor_full_condensation=efficiency_full_condensation,
1341
    )
1342

1343
    model.add(ext_turb_chp)
1✔
1344
    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