OffsetConverterBlock.__init__()   A
last analyzed

Complexity

Conditions 1

Size

Total Lines 2
Code Lines 2

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 2
dl 0
loc 2
rs 10
c 0
b 0
f 0
cc 1
nop 3
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
        parent_node=None,
139
        label=None,
140
        conversion_factors=None,
141
        normed_offsets=None,
142
        coefficients=None,
143
        custom_properties=None,
144
    ):
145
        if custom_properties is None:
146
            custom_properties = {}
147
148
        super().__init__(
149
            inputs=inputs,
150
            outputs=outputs,
151
            parent_node=parent_node,
152
            label=label,
153
            custom_properties=custom_properties,
154
        )
155
156
        # --- BEGIN: To be removed for versions >= v0.7 ---
157
        # this part is used for the transition phase from the old
158
        # OffsetConverter API to the new one. It calcualtes the
159
        # conversion_factors and normed_offsets from the coefficients and the
160
        # outputs information on min and max.
161
        if coefficients is not None:
162
            if conversion_factors is not None or normed_offsets is not None:
163
                msg = (
164
                    "The deprecated argument `coefficients` cannot be used "
165
                    "in combination with its replacements "
166
                    "(`conversion_factors` and `normed_offsets`)."
167
                )
168
                raise TypeError(msg)
169
170
            (
171
                normed_offsets,
172
                conversion_factors,
173
            ) = self.normed_offset_and_conversion_factors_from_coefficients(
174
                coefficients
175
            )
176
        # --- END ---
177
178
        _reference_flow = [v for v in self.inputs.values() if v.nonconvex]
179
        _reference_flow += [v for v in self.outputs.values() if v.nonconvex]
180
        if len(_reference_flow) != 1:
181
            raise ValueError(
182
                "Exactly one flow of the `OffsetConverter` must have the "
183
                "`NonConvex` attribute."
184
            )
185
186
        if _reference_flow[0] in self.inputs.values():
187
            self._reference_node_at_input = True
188
            self._reference_node = _reference_flow[0].input
189
        else:
190
            self._reference_node_at_input = False
191
            self._reference_node = _reference_flow[0].output
192
193
        _investment_node = [
194
            v.input for v in self.inputs.values() if v.investment
195
        ]
196
        _investment_node += [
197
            v.output for v in self.outputs.values() if v.investment
198
        ]
199
200
        if len(_investment_node) > 0:
201
            if (
202
                len(_investment_node) > 1
203
                or self._reference_node != _investment_node[0]
204
            ):
205
                raise TypeError(
206
                    "`Investment` attribute must be defined only for the "
207
                    "NonConvex flow!"
208
                )
209
210
        self._reference_flow = _reference_flow[0]
211
212
        if conversion_factors is None:
213
            conversion_factors = {}
214
215
        if self._reference_node in conversion_factors:
216
            raise ValueError(
217
                "Conversion factors cannot be specified for the `NonConvex` "
218
                "flow."
219
            )
220
221
        self.conversion_factors = {
222
            k: sequence(v) for k, v in conversion_factors.items()
223
        }
224
225
        missing_conversion_factor_keys = (
226
            set(self.outputs) | set(self.inputs)
227
        ) - set(self.conversion_factors)
228
229
        for cf in missing_conversion_factor_keys:
230
            self.conversion_factors[cf] = sequence(1)
231
232
        if normed_offsets is None:
233
            normed_offsets = {}
234
235
        if self._reference_node in normed_offsets:
236
            raise ValueError(
237
                "Normed offsets cannot be specified for the `NonConvex` flow."
238
            )
239
240
        self.normed_offsets = {
241
            k: sequence(v) for k, v in normed_offsets.items()
242
        }
243
244
        missing_normed_offsets_keys = (
245
            set(self.outputs) | set(self.inputs)
246
        ) - set(self.normed_offsets)
247
248
        for cf in missing_normed_offsets_keys:
249
            self.normed_offsets[cf] = sequence(0)
250
251
    def constraint_group(self):
252
        return OffsetConverterBlock
253
254
    # --- BEGIN: To be removed for versions >= v0.7 ---
255
    def normed_offset_and_conversion_factors_from_coefficients(
256
        self, coefficients
257
    ):
258
        """
259
        Calculate slope and offset for new API from the old API coefficients.
260
261
        Parameters
262
        ----------
263
        coefficients : tuple
264
            tuple holding the coefficients (offset, slope) for the old style
265
            OffsetConverter.
266
267
        Returns
268
        -------
269
        tuple
270
            A tuple holding the slope and the offset for the new
271
            OffsetConverter API.
272
        """
273
        coefficients = tuple([sequence(i) for i in coefficients])
274
        if len(coefficients) != 2:
275
            raise ValueError(
276
                "Two coefficients or coefficient series have to be given."
277
            )
278
279
        input_bus = list(self.inputs.values())[0].input
280
        for flow in self.outputs.values():
281
            if flow.max.size is not None:
282
                target_len = flow.max.size
283
            else:
284
                target_len = 1
285
286
            slope = []
287
            offset = []
288
            for i in range(target_len):
289
                eta_at_max = (
290
                    flow.max[i]
291
                    * coefficients[1][i]
292
                    / (flow.max[i] - coefficients[0][i])
293
                )
294
                eta_at_min = (
295
                    flow.min[i]
296
                    * coefficients[1][i]
297
                    / (flow.min[i] - coefficients[0][i])
298
                )
299
300
                c0, c1 = slope_offset_from_nonconvex_output(
301
                    flow.max[i], flow.min[i], eta_at_max, eta_at_min
302
                )
303
                slope.append(c0)
304
                offset.append(c1)
305
306
            if target_len == 1:
307
                slope = slope[0]
308
                offset = offset[0]
309
310
            conversion_factors = {input_bus: slope}
311
            normed_offsets = {input_bus: offset}
312
            msg = (
313
                "The usage of coefficients is depricated, use "
314
                "conversion_factors and normed_offsets instead."
315
            )
316
            warn(msg, DeprecationWarning)
317
318
        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 280 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 280 is not entered. Are you sure this can never be the case?
Loading history...
319
320
    # --- END ---
321
322
    def plot_partload(self, bus, tstep):
323
        """Create a matplotlib figure of the flow to nonconvex flow relation.
324
325
        Parameters
326
        ----------
327
        bus : oemof.solph.Bus
328
            Bus, to which the NOT-nonconvex input or output is connected to.
329
        tstep : int
330
            Timestep to generate the figure for.
331
332
        Returns
333
        -------
334
        tuple
335
            A tuple with the matplotlib figure and axes objects.
336
        """
337
        import matplotlib.pyplot as plt
338
        import numpy as np
339
340
        fig, ax = plt.subplots(2, sharex=True)
341
342
        slope = self.conversion_factors[bus][tstep]
343
        offset = self.normed_offsets[bus][tstep]
344
345
        min_load = self._reference_flow.min[tstep]
346
        max_load = self._reference_flow.max[tstep]
347
348
        infeasible_load = np.linspace(0, min_load)
349
        feasible_load = np.linspace(min_load, max_load)
350
351
        y_feasible = feasible_load * slope + offset
352
        y_infeasible = infeasible_load * slope + offset
353
354
        _ = ax[0].plot(feasible_load, y_feasible, label="operational range")
355
        color = _[0].get_color()
356
        ax[0].plot(infeasible_load, y_infeasible, "--", color=color)
357
        ax[0].scatter(
358
            [0, feasible_load[0], feasible_load[-1]],
359
            [y_infeasible[0], y_feasible[0], y_feasible[-1]],
360
            color=color,
361
        )
362
        ax[0].legend()
363
364
        ratio = y_feasible / feasible_load
365
        ax[1].plot(feasible_load, ratio)
366
        ax[1].scatter(
367
            [feasible_load[0], feasible_load[-1]],
368
            [ratio[0], ratio[-1]],
369
            color=color,
370
        )
371
372
        ax[0].set_ylabel(f"flow from/to bus '{bus.label}'")
373
        ax[1].set_ylabel("efficiency $\\frac{y}{x}$")
374
        ax[1].set_xlabel("nonconvex flow")
375
376
        _ = [(_.set_axisbelow(True), _.grid()) for _ in ax]
377
        plt.tight_layout()
378
379
        return fig, ax
380
381
382
class OffsetConverterBlock(ScalarBlock):
383
    r"""Block for the relation of nodes with type
384
    :class:`~oemof.solph.components._offset_converter.OffsetConverter`
385
386
    **The following constraints are created:**
387
388
    .. _OffsetConverter-equations:
389
390
    .. math::
391
        &
392
        P(p, t) = P_\text{ref}(p, t) \cdot m(t)
393
        + P_\text{nom,ref}(p) \cdot Y_\text{ref}(t) \cdot y_\text{0,normed}(t) \\
394
395
396
    The symbols used are defined as follows (with Variables (V) and Parameters (P)):
397
398
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
399
    | symbol                       | attribute                                                    | type | explanation                                                                 |
400
    +==============================+==============================================================+======+=============================================================================+
401
    | :math:`P(t)`                 | `flow[i,n,p,t]` or `flow[n,o,p,t]`                           | V    | **Non**-nonconvex flows at input or output                                  |
402
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
403
    | :math:`P_{in}(t)`            | `flow[i,n,p,t]` or `flow[n,o,p,t]`                           | V    | nonconvex flow of converter                                                 |
404
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
405
    | :math:`Y(t)`                 |                                                              | V    | Binary status variable of nonconvex flow                                    |
406
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
407
    | :math:`P_{nom}(t)`           |                                                              | V    | Nominal value (max. capacity) of the nonconvex flow                         |
408
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
409
    | :math:`m(t)`                 | `conversion_factors[i][n,t]` or `conversion_factors[o][n,t]` | P    | Linear coefficient 1 (slope) of a **Non**-nonconvex flows                   |
410
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
411
    | :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 |
412
    +------------------------------+--------------------------------------------------------------+------+-----------------------------------------------------------------------------+
413
414
    Note that :math:`P_{nom}(t) \cdot Y(t)` is merged into one variable,
415
    called `status_nominal[n, o, p, t]`.
416
    """  # noqa: E501
417
418
    CONSTRAINT_GROUP = True
419
420
    def __init__(self, *args, **kwargs):
421
        super().__init__(*args, **kwargs)
422
423
    def _create(self, group=None):
424
        """Creates the relation for the class:`OffsetConverter`.
425
426
        Parameters
427
        ----------
428
        group : list
429
            List of oemof.solph.experimental.OffsetConverter objects for
430
            which the relation of inputs and outputs is created
431
            e.g. group = [ostf1, ostf2, ostf3, ...]. The components inside
432
            the list need to hold an attribute `coefficients` of type dict
433
            containing the conversion factors for all inputs to outputs.
434
        """
435
        if group is None:
436
            return None
437
438
        m = self.parent_block()
439
440
        self.OFFSETCONVERTERS = Set(initialize=[n for n in group])
441
442
        reference_node = {n: n._reference_node for n in group}
443
        reference_node_at_input = {
444
            n: n._reference_node_at_input for n in group
445
        }
446
        in_flows = {
447
            n: [i for i in n.inputs.keys() if i != n._reference_node]
448
            for n in group
449
        }
450
        out_flows = {
451
            n: [o for o in n.outputs.keys() if o != n._reference_node]
452
            for n in group
453
        }
454
455
        self.relation = Constraint(
456
            [
457
                (n, reference_node[n], f, t)
458
                for t in m.TIMESTEPS
459
                for n in group
460
                for f in in_flows[n] + out_flows[n]
461
            ],
462
            noruleinit=True,
463
        )
464
465
        def _relation_rule(block):
466
            """Link binary input and output flow to component outflow."""
467
            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...
468
                for n in group:
469
                    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...
470
                        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...
471
                        status_nominal_idx = reference_node[n], n, t
472
                    else:
473
                        ref_flow = m.flow[n, reference_node[n], t]
474
                        status_nominal_idx = n, reference_node[n], t
475
476
                    try:
477
                        ref_status_nominal = (
478
                            m.InvestNonConvexFlowBlock.status_nominal[
479
                                status_nominal_idx
480
                            ]
481
                        )
482
                    except (AttributeError, KeyError):
483
                        ref_status_nominal = (
484
                            m.NonConvexFlowBlock.status_nominal[
485
                                status_nominal_idx
486
                            ]
487
                        )
488
489
                    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...
490
                        rhs = 0
491
                        if f in in_flows[n]:
492
                            rhs += m.flow[f, n, t]
493
                        else:
494
                            rhs += m.flow[n, f, t]
495
496
                        lhs = 0
497
                        lhs += ref_flow * n.conversion_factors[f][t]
498
                        lhs += ref_status_nominal * n.normed_offsets[f][t]
499
                        block.relation.add(
500
                            (n, reference_node[n], f, t), (lhs == rhs)
501
                        )
502
503
        self.relation_build = BuildAction(rule=_relation_rule)
504
505
506
def slope_offset_from_nonconvex_input(
507
    max_load, min_load, eta_at_max, eta_at_min
508
):
509
    r"""Calculate the slope and the offset with max and min given for input
510
511
    The reference is the input flow here. That means, the `NonConvex` flow
512
    is specified at one of the input flows. The `max_load` and the `min_load`
513
    are the `max` and the `min` specifications for the `NonConvex` flow.
514
    `eta_at_max` and `eta_at_min` are the efficiency values of a different
515
    flow, e.g. an output, with respect to the `max_load` and `min_load`
516
    operation points.
517
518
    .. math::
519
520
        \text{slope} =
521
        \frac{
522
            \text{max} \cdot \eta_\text{at max}
523
            - \text{min} \cdot \eta_\text{at min}
524
        }{\text{max} - \text{min}}\\
525
526
        \text{offset} = \eta_\text{at,max} - \text{slope}
527
528
    Parameters
529
    ----------
530
    max_load : float
531
        Maximum load value, e.g. 1
532
    min_load : float
533
        Minimum load value, e.g. 0.5
534
    eta_at_max : float
535
        Efficiency at maximum load.
536
    eta_at_min : float
537
        Efficiency at minimum load.
538
539
    Returns
540
    -------
541
    tuple
542
        slope and offset
543
544
    Example
545
    -------
546
    >>> from oemof import solph
547
    >>> max_load = 1
548
    >>> min_load = 0.5
549
    >>> eta_at_min = 0.4
550
    >>> eta_at_max = 0.3
551
552
    With the input load being at 100 %, in this example, the efficiency should
553
    be 30 %. With the input load being at 50 %, it should be 40 %. We can
554
    calcualte slope and the offset which is normed to the nominal capacity of
555
    the referenced flow (in this case the input flow) always.
556
557
    >>> slope, offset = solph.components.slope_offset_from_nonconvex_input(
558
    ...     max_load, min_load, eta_at_max, eta_at_min
559
    ... )
560
    >>> input_flow = 10
561
    >>> input_flow_nominal = 10
562
    >>> output_flow = slope * input_flow + offset * input_flow_nominal
563
564
    We can then calculate with the `OffsetConverter` input output relation,
565
    what the resulting efficiency is. At max operating conditions it should be
566
    identical to the efficiency we put in initially. Analogously, we apply this
567
    to the minimal load point.
568
569
    >>> round(output_flow / input_flow, 3) == eta_at_max
570
    True
571
    >>> input_flow = 5
572
    >>> output_flow = slope * input_flow + offset * input_flow_nominal
573
    >>> round(output_flow / input_flow, 3) == eta_at_min
574
    True
575
    """
576
    slope = (max_load * eta_at_max - min_load * eta_at_min) / (
577
        max_load - min_load
578
    )
579
    offset = eta_at_max - slope
580
    return slope, offset
581
582
583
def slope_offset_from_nonconvex_output(
584
    max_load, min_load, eta_at_max, eta_at_min
585
):
586
    r"""Calculate the slope and the offset with max and min given for output.
587
588
    The reference is the output flow here. That means, the `NonConvex` flow
589
    is specified at one of the output flows. The `max_load` and the `min_load`
590
    are the `max` and the `min` specifications for the `NonConvex` flow.
591
    `eta_at_max` and `eta_at_min` are the efficiency values of a different
592
    flow, e.g. an input, with respect to the `max_load` and `min_load`
593
    operation points.
594
595
    .. math::
596
597
        \text{slope} =
598
        \frac{
599
            \frac{\text{max}}{\eta_\text{at max}}
600
            - \frac{\text{min}}{\eta_\text{at min}}
601
        }{\text{max} - \text{min}}\\
602
603
        \text{offset} = \frac{1}{\eta_\text{at,max}} - \text{slope}
604
605
    Parameters
606
    ----------
607
    max_load : float
608
        Maximum load value, e.g. 1
609
    min_load : float
610
        Minimum load value, e.g. 0.5
611
    eta_at_max : float
612
        Efficiency at maximum load.
613
    eta_at_min : float
614
        Efficiency at minimum load.
615
616
    Returns
617
    -------
618
    tuple
619
        slope and offset
620
621
    Example
622
    -------
623
    >>> from oemof import solph
624
    >>> max_load = 1
625
    >>> min_load = 0.5
626
    >>> eta_at_min = 0.7
627
    >>> eta_at_max = 0.8
628
629
    With the output load being at 100 %, in this example, the efficiency should
630
    be 80 %. With the input load being at 50 %, it should be 70 %. We can
631
    calcualte slope and the offset, which is normed to the nominal capacity of
632
    the referenced flow (in this case the output flow) always.
633
634
    >>> slope, offset = solph.components.slope_offset_from_nonconvex_output(
635
    ...     max_load, min_load, eta_at_max, eta_at_min
636
    ... )
637
    >>> output_flow = 10
638
    >>> output_flow_nominal = 10
639
    >>> input_flow = slope * output_flow + offset * output_flow_nominal
640
641
    We can then calculate with the `OffsetConverter` input output relation,
642
    what the resulting efficiency is. At max operating conditions it should be
643
    identical to the efficiency we put in initially. Analogously, we apply this
644
    to the minimal load point.
645
646
    >>> round(output_flow / input_flow, 3) == eta_at_max
647
    True
648
    >>> output_flow = 5
649
    >>> input_flow = slope * output_flow + offset * output_flow_nominal
650
    >>> round(output_flow / input_flow, 3) == eta_at_min
651
    True
652
    """
653
    slope = (max_load / eta_at_max - min_load / eta_at_min) / (
654
        max_load - min_load
655
    )
656
    offset = 1 / eta_at_max - slope
657
    return slope, offset
658