solph.components._offset_converter   A
last analyzed

Complexity

Total Complexity 35

Size/Duplication

Total Lines 656
Duplicated Lines 0 %

Importance

Changes 0
Metric Value
wmc 35
eloc 214
dl 0
loc 656
rs 9.6
c 0
b 0
f 0

6 Methods

Rating   Name   Duplication   Size   Complexity  
A OffsetConverter.plot_partload() 0 58 1
A OffsetConverterBlock.__init__() 0 2 1
C OffsetConverterBlock._create() 0 81 8
A OffsetConverter.constraint_group() 0 2 1
B OffsetConverter.normed_offset_and_conversion_factors_from_coefficients() 0 64 6
F OffsetConverter.__init__() 0 114 16

2 Functions

Rating   Name   Duplication   Size   Complexity  
A slope_offset_from_nonconvex_output() 0 75 1
A slope_offset_from_nonconvex_input() 0 75 1
1
# -*- coding: utf-8 -
2
3
"""
4
OffsetConverter and associated individual constraints (blocks) and groupings.
5
6
SPDX-FileCopyrightText: Uwe Krien <[email protected]>
7
SPDX-FileCopyrightText: Simon Hilpert
8
SPDX-FileCopyrightText: Cord Kaldemeyer
9
SPDX-FileCopyrightText: Patrik Schönfeldt
10
SPDX-FileCopyrightText: FranziPl
11
SPDX-FileCopyrightText: jnnr
12
SPDX-FileCopyrightText: Stephan Günther
13
SPDX-FileCopyrightText: FabianTU
14
SPDX-FileCopyrightText: Johannes Röder
15
SPDX-FileCopyrightText: Saeed Sayadi
16
SPDX-FileCopyrightText: Johannes Kochems
17
SPDX-FileCopyrightText: Francesco Witte
18
19
SPDX-License-Identifier: MIT
20
21
"""
22
from warnings import warn
23
24
from oemof.network import Node
25
from pyomo.core import BuildAction
26
from pyomo.core.base.block import ScalarBlock
27
from pyomo.environ import Constraint
28
from pyomo.environ import Set
29
30
from oemof.solph._plumbing import sequence
31
32
33
class OffsetConverter(Node):
34
    r"""An object with one input and multiple outputs and two coefficients
35
    per output to model part load behaviour.
36
    The output must contain a NonConvex object.
37
38
    Parameters
39
    ----------
40
    conversion_factors : dict, (:math:`m(t)`)
41
        Dict containing the respective bus as key and as value the parameter
42
        :math:`m(t)`. It represents the slope of a linear equation with
43
        respect to the `NonConvex` flow. The value can either be a scalar or a
44
        sequence with length of time horizon for simulation.
45
46
    normed_offsets : dict, (:math:`y_\text{0,normed}(t)`)
47
        Dict containing the respective bus as key and as value the parameter
48
        :math:`y_\text{0,normed}(t)`. It represents the y-intercept with respect
49
        to the `NonConvex` flow divided by the `nominal_capacity` of the
50
        `NonConvex` flow (this is for internal purposes). The value can either
51
        be a scalar or a sequence with length of time horizon for simulation.
52
    Notes
53
    -----
54
55
    :math:`m(t)` and :math:`y_\text{0,normed}(t)` can be calculated as follows:
56
57
    .. _OffsetConverterCoefficients-equations:
58
59
    .. math::
60
61
        m = \frac{(l_{max}/\eta_{max}-l_{min}/\eta_{min}}{l_{max}-l_{min}}
62
63
        y_\text{0,normed} = \frac{1}{\eta_{max}} - m
64
65
    Where :math:`l_{max}` and :math:`l_{min}` are the maximum and minimum
66
    partload share (e.g. 1.0 and 0.5) with reference to the `NonConvex` flow
67
    and :math:`\eta_{max}` and :math:`\eta_{min}` are the respective
68
    efficiencies/conversion factors at these partloads. Alternatively, you can
69
    use the inbuilt methods:
70
71
    - If the `NonConvex` flow is at an input of the component:
72
      :py:meth:`oemof.solph.components._offset_converter.slope_offset_from_nonconvex_input`,
73
    - If the `NonConvex` flow is at an output of the component:
74
      :py:meth:`oemof.solph.components._offset_converter.slope_offset_from_nonconvex_output`
75
76
    You can import these methods from the `oemof.solph.components` level:
77
78
    >>> from oemof.solph.components import slope_offset_from_nonconvex_input
79
    >>> from oemof.solph.components import slope_offset_from_nonconvex_output
80
81
    The sets, variables, constraints and objective parts are created
82
     * :py:class:`~oemof.solph.components._offset_converter.OffsetConverterBlock`
83
84
    Examples
85
    --------
86
    >>> from oemof import solph
87
    >>> bel = solph.buses.Bus(label='bel')
88
    >>> bth = solph.buses.Bus(label='bth')
89
    >>> l_nominal = 60
90
    >>> l_max = 1
91
    >>> l_min = 0.5
92
    >>> eta_max = 0.5
93
    >>> eta_min = 0.3
94
    >>> slope = (l_max / eta_max - l_min / eta_min) / (l_max - l_min)
95
    >>> offset = 1 / eta_max - slope
96
97
    Or use the provided method as explained in the previous section:
98
99
    >>> _slope, _offset = slope_offset_from_nonconvex_output(
100
    ...     l_max, l_min, eta_max, eta_min
101
    ... )
102
    >>> slope == _slope
103
    True
104
    >>> offset == _offset
105
    True
106
107
    >>> ostf = solph.components.OffsetConverter(
108
    ...    label='ostf',
109
    ...    inputs={bel: solph.flows.Flow()},
110
    ...    outputs={bth: solph.flows.Flow(
111
    ...         nominal_capacity=l_nominal, min=l_min, max=l_max,
112
    ...         nonconvex=solph.NonConvex())},
113
    ...    conversion_factors={bel: slope},
114
    ...    normed_offsets={bel: offset},
115
    ... )
116
    >>> type(ostf)
117
    <class 'oemof.solph.components._offset_converter.OffsetConverter'>
118
119
    The input required to operate at minimum load, can be computed from the
120
    slope and offset:
121
122
    >>> input_at_min = ostf.conversion_factors[bel][0] * l_min + ostf.normed_offsets[bel][0] * l_max
123
    >>> input_at_min * l_nominal
124
    100.0
125
126
    The same can be done for the input at nominal load:
127
128
    >>> input_at_max = l_max * (ostf.conversion_factors[bel][0] + ostf.normed_offsets[bel][0])
129
    >>> input_at_max * l_nominal
130
    120.0
131
132
    """  # noqa: E501
133
134
    def __init__(
135
        self,
136
        inputs,
137
        outputs,
138
        label=None,
139
        conversion_factors=None,
140
        normed_offsets=None,
141
        coefficients=None,
142
        custom_properties=None,
143
    ):
144
        if custom_properties is None:
145
            custom_properties = {}
146
147
        super().__init__(
148
            inputs=inputs,
149
            outputs=outputs,
150
            label=label,
151
            custom_properties=custom_properties,
152
        )
153
154
        # --- BEGIN: To be removed for versions >= v0.7 ---
155
        # this part is used for the transition phase from the old
156
        # OffsetConverter API to the new one. It calcualtes the
157
        # conversion_factors and normed_offsets from the coefficients and the
158
        # outputs information on min and max.
159
        if coefficients is not None:
160
            if conversion_factors is not None or normed_offsets is not None:
161
                msg = (
162
                    "The deprecated argument `coefficients` cannot be used "
163
                    "in combination with its replacements "
164
                    "(`conversion_factors` and `normed_offsets`)."
165
                )
166
                raise TypeError(msg)
167
168
            (
169
                normed_offsets,
170
                conversion_factors,
171
            ) = self.normed_offset_and_conversion_factors_from_coefficients(
172
                coefficients
173
            )
174
        # --- END ---
175
176
        _reference_flow = [v for v in self.inputs.values() if v.nonconvex]
177
        _reference_flow += [v for v in self.outputs.values() if v.nonconvex]
178
        if len(_reference_flow) != 1:
179
            raise ValueError(
180
                "Exactly one flow of the `OffsetConverter` must have the "
181
                "`NonConvex` attribute."
182
            )
183
184
        if _reference_flow[0] in self.inputs.values():
185
            self._reference_node_at_input = True
186
            self._reference_node = _reference_flow[0].input
187
        else:
188
            self._reference_node_at_input = False
189
            self._reference_node = _reference_flow[0].output
190
191
        _investment_node = [
192
            v.input for v in self.inputs.values() if v.investment
193
        ]
194
        _investment_node += [
195
            v.output for v in self.outputs.values() if v.investment
196
        ]
197
198
        if len(_investment_node) > 0:
199
            if (
200
                len(_investment_node) > 1
201
                or self._reference_node != _investment_node[0]
202
            ):
203
                raise TypeError(
204
                    "`Investment` attribute must be defined only for the "
205
                    "NonConvex flow!"
206
                )
207
208
        self._reference_flow = _reference_flow[0]
209
210
        if conversion_factors is None:
211
            conversion_factors = {}
212
213
        if self._reference_node in conversion_factors:
214
            raise ValueError(
215
                "Conversion factors cannot be specified for the `NonConvex` "
216
                "flow."
217
            )
218
219
        self.conversion_factors = {
220
            k: sequence(v) for k, v in conversion_factors.items()
221
        }
222
223
        missing_conversion_factor_keys = (
224
            set(self.outputs) | set(self.inputs)
225
        ) - set(self.conversion_factors)
226
227
        for cf in missing_conversion_factor_keys:
228
            self.conversion_factors[cf] = sequence(1)
229
230
        if normed_offsets is None:
231
            normed_offsets = {}
232
233
        if self._reference_node in normed_offsets:
234
            raise ValueError(
235
                "Normed offsets cannot be specified for the `NonConvex` flow."
236
            )
237
238
        self.normed_offsets = {
239
            k: sequence(v) for k, v in normed_offsets.items()
240
        }
241
242
        missing_normed_offsets_keys = (
243
            set(self.outputs) | set(self.inputs)
244
        ) - set(self.normed_offsets)
245
246
        for cf in missing_normed_offsets_keys:
247
            self.normed_offsets[cf] = sequence(0)
248
249
    def constraint_group(self):
250
        return OffsetConverterBlock
251
252
    # --- BEGIN: To be removed for versions >= v0.7 ---
253
    def normed_offset_and_conversion_factors_from_coefficients(
254
        self, coefficients
255
    ):
256
        """
257
        Calculate slope and offset for new API from the old API coefficients.
258
259
        Parameters
260
        ----------
261
        coefficients : tuple
262
            tuple holding the coefficients (offset, slope) for the old style
263
            OffsetConverter.
264
265
        Returns
266
        -------
267
        tuple
268
            A tuple holding the slope and the offset for the new
269
            OffsetConverter API.
270
        """
271
        coefficients = tuple([sequence(i) for i in coefficients])
272
        if len(coefficients) != 2:
273
            raise ValueError(
274
                "Two coefficients or coefficient series have to be given."
275
            )
276
277
        input_bus = list(self.inputs.values())[0].input
278
        for flow in self.outputs.values():
279
            if flow.max.size is not None:
280
                target_len = flow.max.size
281
            else:
282
                target_len = 1
283
284
            slope = []
285
            offset = []
286
            for i in range(target_len):
287
                eta_at_max = (
288
                    flow.max[i]
289
                    * coefficients[1][i]
290
                    / (flow.max[i] - coefficients[0][i])
291
                )
292
                eta_at_min = (
293
                    flow.min[i]
294
                    * coefficients[1][i]
295
                    / (flow.min[i] - coefficients[0][i])
296
                )
297
298
                c0, c1 = slope_offset_from_nonconvex_output(
299
                    flow.max[i], flow.min[i], eta_at_max, eta_at_min
300
                )
301
                slope.append(c0)
302
                offset.append(c1)
303
304
            if target_len == 1:
305
                slope = slope[0]
306
                offset = offset[0]
307
308
            conversion_factors = {input_bus: slope}
309
            normed_offsets = {input_bus: offset}
310
            msg = (
311
                "The usage of coefficients is depricated, use "
312
                "conversion_factors and normed_offsets instead."
313
            )
314
            warn(msg, DeprecationWarning)
315
316
        return normed_offsets, conversion_factors
0 ignored issues
show
introduced by
The variable conversion_factors does not seem to be defined in case the for loop on line 278 is not entered. Are you sure this can never be the case?
Loading history...
introduced by
The variable normed_offsets does not seem to be defined in case the for loop on line 278 is not entered. Are you sure this can never be the case?
Loading history...
317
318
    # --- END ---
319
320
    def plot_partload(self, bus, tstep):
321
        """Create a matplotlib figure of the flow to nonconvex flow relation.
322
323
        Parameters
324
        ----------
325
        bus : oemof.solph.Bus
326
            Bus, to which the NOT-nonconvex input or output is connected to.
327
        tstep : int
328
            Timestep to generate the figure for.
329
330
        Returns
331
        -------
332
        tuple
333
            A tuple with the matplotlib figure and axes objects.
334
        """
335
        import matplotlib.pyplot as plt
336
        import numpy as np
337
338
        fig, ax = plt.subplots(2, sharex=True)
339
340
        slope = self.conversion_factors[bus][tstep]
341
        offset = self.normed_offsets[bus][tstep]
342
343
        min_load = self._reference_flow.min[tstep]
344
        max_load = self._reference_flow.max[tstep]
345
346
        infeasible_load = np.linspace(0, min_load)
347
        feasible_load = np.linspace(min_load, max_load)
348
349
        y_feasible = feasible_load * slope + offset
350
        y_infeasible = infeasible_load * slope + offset
351
352
        _ = ax[0].plot(feasible_load, y_feasible, label="operational range")
353
        color = _[0].get_color()
354
        ax[0].plot(infeasible_load, y_infeasible, "--", color=color)
355
        ax[0].scatter(
356
            [0, feasible_load[0], feasible_load[-1]],
357
            [y_infeasible[0], y_feasible[0], y_feasible[-1]],
358
            color=color,
359
        )
360
        ax[0].legend()
361
362
        ratio = y_feasible / feasible_load
363
        ax[1].plot(feasible_load, ratio)
364
        ax[1].scatter(
365
            [feasible_load[0], feasible_load[-1]],
366
            [ratio[0], ratio[-1]],
367
            color=color,
368
        )
369
370
        ax[0].set_ylabel(f"flow from/to bus '{bus.label}'")
371
        ax[1].set_ylabel("efficiency $\\frac{y}{x}$")
372
        ax[1].set_xlabel("nonconvex flow")
373
374
        _ = [(_.set_axisbelow(True), _.grid()) for _ in ax]
375
        plt.tight_layout()
376
377
        return fig, ax
378
379
380
class OffsetConverterBlock(ScalarBlock):
381
    r"""Block for the relation of nodes with type
382
    :class:`~oemof.solph.components._offset_converter.OffsetConverter`
383
384
    **The following constraints are created:**
385
386
    .. _OffsetConverter-equations:
387
388
    .. math::
389
        &
390
        P(p, t) = P_\text{ref}(p, t) \cdot m(t)
391
        + P_\text{nom,ref}(p) \cdot Y_\text{ref}(t) \cdot y_\text{0,normed}(t) \\
392
393
394
    The symbols used are defined as follows (with Variables (V) and Parameters (P)):
395
396
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
397
    | symbol                       | attribute                                                    | type | explanation                                                                 |
398
    +==============================+==============================================================+======+=============================================================================+
399
    | :math:`P(t)`                 | `flow[i,n,p,t]` or `flow[n,o,p,t]`                           | V    | **Non**-nonconvex flows at input or output                                  |
400
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
401
    | :math:`P_{in}(t)`            | `flow[i,n,p,t]` or `flow[n,o,p,t]`                           | V    | nonconvex flow of converter                                                 |
402
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
403
    | :math:`Y(t)`                 |                                                              | V    | Binary status variable of nonconvex flow                                    |
404
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
405
    | :math:`P_{nom}(t)`           |                                                              | V    | Nominal value (max. capacity) of the nonconvex flow                         |
406
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
407
    | :math:`m(t)`                 | `conversion_factors[i][n,t]` or `conversion_factors[o][n,t]` | P    | Linear coefficient 1 (slope) of a **Non**-nonconvex flows                   |
408
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
409
    | :math:`y_\text{0,normed}(t)` | `normed_offsets[i][n,t]` or `normed_offsets[o][n,t]`         | P    | Linear coefficient 0 (y-intersection)/P_{nom}(t) of **Non**-nonconvex flows |
410
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
411
412
    Note that :math:`P_{nom}(t) \cdot Y(t)` is merged into one variable,
413
    called `status_nominal[n, o, p, t]`.
414
    """  # noqa: E501
415
416
    CONSTRAINT_GROUP = True
417
418
    def __init__(self, *args, **kwargs):
419
        super().__init__(*args, **kwargs)
420
421
    def _create(self, group=None):
422
        """Creates the relation for the class:`OffsetConverter`.
423
424
        Parameters
425
        ----------
426
        group : list
427
            List of oemof.solph.experimental.OffsetConverter objects for
428
            which the relation of inputs and outputs is created
429
            e.g. group = [ostf1, ostf2, ostf3, ...]. The components inside
430
            the list need to hold an attribute `coefficients` of type dict
431
            containing the conversion factors for all inputs to outputs.
432
        """
433
        if group is None:
434
            return None
435
436
        m = self.parent_block()
437
438
        self.OFFSETCONVERTERS = Set(initialize=[n for n in group])
439
440
        reference_node = {n: n._reference_node for n in group}
441
        reference_node_at_input = {
442
            n: n._reference_node_at_input for n in group
443
        }
444
        in_flows = {
445
            n: [i for i in n.inputs.keys() if i != n._reference_node]
446
            for n in group
447
        }
448
        out_flows = {
449
            n: [o for o in n.outputs.keys() if o != n._reference_node]
450
            for n in group
451
        }
452
453
        self.relation = Constraint(
454
            [
455
                (n, reference_node[n], f, t)
456
                for t in m.TIMESTEPS
457
                for n in group
458
                for f in in_flows[n] + out_flows[n]
459
            ],
460
            noruleinit=True,
461
        )
462
463
        def _relation_rule(block):
464
            """Link binary input and output flow to component outflow."""
465
            for t in m.TIMESTEPS:
0 ignored issues
show
introduced by
The variable m does not seem to be defined for all execution paths.
Loading history...
466
                for n in group:
467
                    if reference_node_at_input[n]:
0 ignored issues
show
introduced by
The variable reference_node_at_input does not seem to be defined for all execution paths.
Loading history...
468
                        ref_flow = m.flow[reference_node[n], n, t]
0 ignored issues
show
introduced by
The variable reference_node does not seem to be defined for all execution paths.
Loading history...
469
                        status_nominal_idx = reference_node[n], n, t
470
                    else:
471
                        ref_flow = m.flow[n, reference_node[n], t]
472
                        status_nominal_idx = n, reference_node[n], t
473
474
                    try:
475
                        ref_status_nominal = (
476
                            m.InvestNonConvexFlowBlock.status_nominal[
477
                                status_nominal_idx
478
                            ]
479
                        )
480
                    except (AttributeError, KeyError):
481
                        ref_status_nominal = (
482
                            m.NonConvexFlowBlock.status_nominal[
483
                                status_nominal_idx
484
                            ]
485
                        )
486
487
                    for f in in_flows[n] + out_flows[n]:
0 ignored issues
show
introduced by
The variable in_flows does not seem to be defined for all execution paths.
Loading history...
introduced by
The variable out_flows does not seem to be defined for all execution paths.
Loading history...
488
                        rhs = 0
489
                        if f in in_flows[n]:
490
                            rhs += m.flow[f, n, t]
491
                        else:
492
                            rhs += m.flow[n, f, t]
493
494
                        lhs = 0
495
                        lhs += ref_flow * n.conversion_factors[f][t]
496
                        lhs += ref_status_nominal * n.normed_offsets[f][t]
497
                        block.relation.add(
498
                            (n, reference_node[n], f, t), (lhs == rhs)
499
                        )
500
501
        self.relation_build = BuildAction(rule=_relation_rule)
502
503
504
def slope_offset_from_nonconvex_input(
505
    max_load, min_load, eta_at_max, eta_at_min
506
):
507
    r"""Calculate the slope and the offset with max and min given for input
508
509
    The reference is the input flow here. That means, the `NonConvex` flow
510
    is specified at one of the input flows. The `max_load` and the `min_load`
511
    are the `max` and the `min` specifications for the `NonConvex` flow.
512
    `eta_at_max` and `eta_at_min` are the efficiency values of a different
513
    flow, e.g. an output, with respect to the `max_load` and `min_load`
514
    operation points.
515
516
    .. math::
517
518
        \text{slope} =
519
        \frac{
520
            \text{max} \cdot \eta_\text{at max}
521
            - \text{min} \cdot \eta_\text{at min}
522
        }{\text{max} - \text{min}}\\
523
524
        \text{offset} = \eta_\text{at,max} - \text{slope}
525
526
    Parameters
527
    ----------
528
    max_load : float
529
        Maximum load value, e.g. 1
530
    min_load : float
531
        Minimum load value, e.g. 0.5
532
    eta_at_max : float
533
        Efficiency at maximum load.
534
    eta_at_min : float
535
        Efficiency at minimum load.
536
537
    Returns
538
    -------
539
    tuple
540
        slope and offset
541
542
    Example
543
    -------
544
    >>> from oemof import solph
545
    >>> max_load = 1
546
    >>> min_load = 0.5
547
    >>> eta_at_min = 0.4
548
    >>> eta_at_max = 0.3
549
550
    With the input load being at 100 %, in this example, the efficiency should
551
    be 30 %. With the input load being at 50 %, it should be 40 %. We can
552
    calcualte slope and the offset which is normed to the nominal capacity of
553
    the referenced flow (in this case the input flow) always.
554
555
    >>> slope, offset = solph.components.slope_offset_from_nonconvex_input(
556
    ...     max_load, min_load, eta_at_max, eta_at_min
557
    ... )
558
    >>> input_flow = 10
559
    >>> input_flow_nominal = 10
560
    >>> output_flow = slope * input_flow + offset * input_flow_nominal
561
562
    We can then calculate with the `OffsetConverter` input output relation,
563
    what the resulting efficiency is. At max operating conditions it should be
564
    identical to the efficiency we put in initially. Analogously, we apply this
565
    to the minimal load point.
566
567
    >>> round(output_flow / input_flow, 3) == eta_at_max
568
    True
569
    >>> input_flow = 5
570
    >>> output_flow = slope * input_flow + offset * input_flow_nominal
571
    >>> round(output_flow / input_flow, 3) == eta_at_min
572
    True
573
    """
574
    slope = (max_load * eta_at_max - min_load * eta_at_min) / (
575
        max_load - min_load
576
    )
577
    offset = eta_at_max - slope
578
    return slope, offset
579
580
581
def slope_offset_from_nonconvex_output(
582
    max_load, min_load, eta_at_max, eta_at_min
583
):
584
    r"""Calculate the slope and the offset with max and min given for output.
585
586
    The reference is the output flow here. That means, the `NonConvex` flow
587
    is specified at one of the output flows. The `max_load` and the `min_load`
588
    are the `max` and the `min` specifications for the `NonConvex` flow.
589
    `eta_at_max` and `eta_at_min` are the efficiency values of a different
590
    flow, e.g. an input, with respect to the `max_load` and `min_load`
591
    operation points.
592
593
    .. math::
594
595
        \text{slope} =
596
        \frac{
597
            \frac{\text{max}}{\eta_\text{at max}}
598
            - \frac{\text{min}}{\eta_\text{at min}}
599
        }{\text{max} - \text{min}}\\
600
601
        \text{offset} = \frac{1}{\eta_\text{at,max}} - \text{slope}
602
603
    Parameters
604
    ----------
605
    max_load : float
606
        Maximum load value, e.g. 1
607
    min_load : float
608
        Minimum load value, e.g. 0.5
609
    eta_at_max : float
610
        Efficiency at maximum load.
611
    eta_at_min : float
612
        Efficiency at minimum load.
613
614
    Returns
615
    -------
616
    tuple
617
        slope and offset
618
619
    Example
620
    -------
621
    >>> from oemof import solph
622
    >>> max_load = 1
623
    >>> min_load = 0.5
624
    >>> eta_at_min = 0.7
625
    >>> eta_at_max = 0.8
626
627
    With the output load being at 100 %, in this example, the efficiency should
628
    be 80 %. With the input load being at 50 %, it should be 70 %. We can
629
    calcualte slope and the offset, which is normed to the nominal capacity of
630
    the referenced flow (in this case the output flow) always.
631
632
    >>> slope, offset = solph.components.slope_offset_from_nonconvex_output(
633
    ...     max_load, min_load, eta_at_max, eta_at_min
634
    ... )
635
    >>> output_flow = 10
636
    >>> output_flow_nominal = 10
637
    >>> input_flow = slope * output_flow + offset * output_flow_nominal
638
639
    We can then calculate with the `OffsetConverter` input output relation,
640
    what the resulting efficiency is. At max operating conditions it should be
641
    identical to the efficiency we put in initially. Analogously, we apply this
642
    to the minimal load point.
643
644
    >>> round(output_flow / input_flow, 3) == eta_at_max
645
    True
646
    >>> output_flow = 5
647
    >>> input_flow = slope * output_flow + offset * output_flow_nominal
648
    >>> round(output_flow / input_flow, 3) == eta_at_min
649
    True
650
    """
651
    slope = (max_load / eta_at_max - min_load / eta_at_min) / (
652
        max_load - min_load
653
    )
654
    offset = 1 / eta_at_max - slope
655
    return slope, offset
656