Completed
Push — dev ( 794b9b...9be114 )
by Patrik
20s queued 16s
created

OffsetTransformer.__init__()   A

Complexity

Conditions 1

Size

Total Lines 20
Code Lines 16

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
eloc 16
dl 0
loc 20
rs 9.6
c 0
b 0
f 0
cc 1
nop 6
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
            normed_offsets, conversion_factors = (
169
                self.normed_offset_and_conversion_factors_from_coefficients(
170
                    coefficients
171
                )
172
            )
173
        # --- END ---
174
175
        _reference_flow = [v for v in self.inputs.values() if v.nonconvex]
176
        _reference_flow += [v for v in self.outputs.values() if v.nonconvex]
177
        if len(_reference_flow) != 1:
178
            raise ValueError(
179
                "Exactly one flow of the `OffsetConverter` must have the "
180
                "`NonConvex` attribute."
181
            )
182
183
        if _reference_flow[0] in self.inputs.values():
184
            self._reference_node_at_input = True
185
            self._reference_node = _reference_flow[0].input
186
        else:
187
            self._reference_node_at_input = False
188
            self._reference_node = _reference_flow[0].output
189
190
        _investment_node = [
191
            v.input for v in self.inputs.values() if v.investment
192
        ]
193
        _investment_node += [
194
            v.output for v in self.outputs.values() if v.investment
195
        ]
196
197
        if len(_investment_node) > 0:
198
            if (
199
                len(_investment_node) > 1
200
                or self._reference_node != _investment_node[0]
201
            ):
202
                raise TypeError(
203
                    "`Investment` attribute must be defined only for the "
204
                    "NonConvex flow!"
205
                )
206
207
        self._reference_flow = _reference_flow[0]
208
209
        if conversion_factors is None:
210
            conversion_factors = {}
211
212
        if self._reference_node in conversion_factors:
213
            raise ValueError(
214
                "Conversion factors cannot be specified for the `NonConvex` "
215
                "flow."
216
            )
217
218
        self.conversion_factors = {
219
            k: sequence(v) for k, v in conversion_factors.items()
220
        }
221
222
        missing_conversion_factor_keys = (
223
            set(self.outputs) | set(self.inputs)
224
        ) - set(self.conversion_factors)
225
226
        for cf in missing_conversion_factor_keys:
227
            self.conversion_factors[cf] = sequence(1)
228
229
        if normed_offsets is None:
230
            normed_offsets = {}
231
232
        if self._reference_node in normed_offsets:
233
            raise ValueError(
234
                "Normed offsets cannot be specified for the `NonConvex` flow."
235
            )
236
237
        self.normed_offsets = {
238
            k: sequence(v) for k, v in normed_offsets.items()
239
        }
240
241
        missing_normed_offsets_keys = (
242
            set(self.outputs) | set(self.inputs)
243
        ) - set(self.normed_offsets)
244
245
        for cf in missing_normed_offsets_keys:
246
            self.normed_offsets[cf] = sequence(0)
247
248
    def constraint_group(self):
249
        return OffsetConverterBlock
250
251
    # --- BEGIN: To be removed for versions >= v0.7 ---
252
    def normed_offset_and_conversion_factors_from_coefficients(
253
        self, coefficients
254
    ):
255
        """
256
        Calculate slope and offset for new API from the old API coefficients.
257
258
        Parameters
259
        ----------
260
        coefficients : tuple
261
            tuple holding the coefficients (offset, slope) for the old style
262
            OffsetConverter.
263
264
        Returns
265
        -------
266
        tuple
267
            A tuple holding the slope and the offset for the new
268
            OffsetConverter API.
269
        """
270
        coefficients = tuple([sequence(i) for i in coefficients])
271
        if len(coefficients) != 2:
272
            raise ValueError(
273
                "Two coefficients or coefficient series have to be given."
274
            )
275
276
        input_bus = list(self.inputs.values())[0].input
277
        for flow in self.outputs.values():
278
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 277 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 277 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
468
                    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...
469
                        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...
470
                        status_nominal_idx = reference_node[n], n, t
471
                    else:
472
                        ref_flow = m.flow[n, reference_node[n], t]
473
                        status_nominal_idx = n, reference_node[n], t
474
475
                    try:
476
                        ref_status_nominal = (
477
                            m.InvestNonConvexFlowBlock.status_nominal[
478
                                status_nominal_idx
479
                            ]
480
                        )
481
                    except (AttributeError, KeyError):
482
                        ref_status_nominal = (
483
                            m.NonConvexFlowBlock.status_nominal[
484
                                status_nominal_idx
485
                            ]
486
                        )
487
488
                    for f in in_flows[n] + out_flows[n]:
0 ignored issues
show
introduced by
The variable out_flows does not seem to be defined for all execution paths.
Loading history...
introduced by
The variable in_flows does not seem to be defined for all execution paths.
Loading history...
489
                        rhs = 0
490
                        if f in in_flows[n]:
491
                            rhs += m.flow[f, n, t]
492
                        else:
493
                            rhs += m.flow[n, f, t]
494
495
                        lhs = 0
496
                        lhs += ref_flow * n.conversion_factors[f][t]
497
                        lhs += ref_status_nominal * n.normed_offsets[f][t]
498
                        block.relation.add(
499
                            (n, reference_node[n], f, t), (lhs == rhs)
500
                        )
501
502
        self.relation_build = BuildAction(rule=_relation_rule)
503
504
505
def slope_offset_from_nonconvex_input(
506
    max_load, min_load, eta_at_max, eta_at_min
507
):
508
    r"""Calculate the slope and the offset with max and min given for input
509
510
    The reference is the input flow here. That means, the `NonConvex` flow
511
    is specified at one of the input flows. The `max_load` and the `min_load`
512
    are the `max` and the `min` specifications for the `NonConvex` flow.
513
    `eta_at_max` and `eta_at_min` are the efficiency values of a different
514
    flow, e.g. an output, with respect to the `max_load` and `min_load`
515
    operation points.
516
517
    .. math::
518
519
        \text{slope} =
520
        \frac{
521
            \text{max} \cdot \eta_\text{at max}
522
            - \text{min} \cdot \eta_\text{at min}
523
        }{\text{max} - \text{min}}\\
524
525
        \text{offset} = \eta_\text{at,max} - \text{slope}
526
527
    Parameters
528
    ----------
529
    max_load : float
530
        Maximum load value, e.g. 1
531
    min_load : float
532
        Minimum load value, e.g. 0.5
533
    eta_at_max : float
534
        Efficiency at maximum load.
535
    eta_at_min : float
536
        Efficiency at minimum load.
537
538
    Returns
539
    -------
540
    tuple
541
        slope and offset
542
543
    Example
544
    -------
545
    >>> from oemof import solph
546
    >>> max_load = 1
547
    >>> min_load = 0.5
548
    >>> eta_at_min = 0.4
549
    >>> eta_at_max = 0.3
550
551
    With the input load being at 100 %, in this example, the efficiency should
552
    be 30 %. With the input load being at 50 %, it should be 40 %. We can
553
    calcualte slope and the offset which is normed to the nominal capacity of
554
    the referenced flow (in this case the input flow) always.
555
556
    >>> slope, offset = solph.components.slope_offset_from_nonconvex_input(
557
    ...     max_load, min_load, eta_at_max, eta_at_min
558
    ... )
559
    >>> input_flow = 10
560
    >>> input_flow_nominal = 10
561
    >>> output_flow = slope * input_flow + offset * input_flow_nominal
562
563
    We can then calculate with the `OffsetConverter` input output relation,
564
    what the resulting efficiency is. At max operating conditions it should be
565
    identical to the efficiency we put in initially. Analogously, we apply this
566
    to the minimal load point.
567
568
    >>> round(output_flow / input_flow, 3) == eta_at_max
569
    True
570
    >>> input_flow = 5
571
    >>> output_flow = slope * input_flow + offset * input_flow_nominal
572
    >>> round(output_flow / input_flow, 3) == eta_at_min
573
    True
574
    """
575
    slope = (max_load * eta_at_max - min_load * eta_at_min) / (
576
        max_load - min_load
577
    )
578
    offset = eta_at_max - slope
579
    return slope, offset
580
581
582
def slope_offset_from_nonconvex_output(
583
    max_load, min_load, eta_at_max, eta_at_min
584
):
585
    r"""Calculate the slope and the offset with max and min given for output.
586
587
    The reference is the output flow here. That means, the `NonConvex` flow
588
    is specified at one of the output flows. The `max_load` and the `min_load`
589
    are the `max` and the `min` specifications for the `NonConvex` flow.
590
    `eta_at_max` and `eta_at_min` are the efficiency values of a different
591
    flow, e.g. an input, with respect to the `max_load` and `min_load`
592
    operation points.
593
594
    .. math::
595
596
        \text{slope} =
597
        \frac{
598
            \frac{\text{max}}{\eta_\text{at max}}
599
            - \frac{\text{min}}{\eta_\text{at min}}
600
        }{\text{max} - \text{min}}\\
601
602
        \text{offset} = \frac{1}{\eta_\text{at,max}} - \text{slope}
603
604
    Parameters
605
    ----------
606
    max_load : float
607
        Maximum load value, e.g. 1
608
    min_load : float
609
        Minimum load value, e.g. 0.5
610
    eta_at_max : float
611
        Efficiency at maximum load.
612
    eta_at_min : float
613
        Efficiency at minimum load.
614
615
    Returns
616
    -------
617
    tuple
618
        slope and offset
619
620
    Example
621
    -------
622
    >>> from oemof import solph
623
    >>> max_load = 1
624
    >>> min_load = 0.5
625
    >>> eta_at_min = 0.7
626
    >>> eta_at_max = 0.8
627
628
    With the output load being at 100 %, in this example, the efficiency should
629
    be 80 %. With the input load being at 50 %, it should be 70 %. We can
630
    calcualte slope and the offset, which is normed to the nominal capacity of
631
    the referenced flow (in this case the output flow) always.
632
633
    >>> slope, offset = solph.components.slope_offset_from_nonconvex_output(
634
    ...     max_load, min_load, eta_at_max, eta_at_min
635
    ... )
636
    >>> output_flow = 10
637
    >>> output_flow_nominal = 10
638
    >>> input_flow = slope * output_flow + offset * output_flow_nominal
639
640
    We can then calculate with the `OffsetConverter` input output relation,
641
    what the resulting efficiency is. At max operating conditions it should be
642
    identical to the efficiency we put in initially. Analogously, we apply this
643
    to the minimal load point.
644
645
    >>> round(output_flow / input_flow, 3) == eta_at_max
646
    True
647
    >>> output_flow = 5
648
    >>> input_flow = slope * output_flow + offset * output_flow_nominal
649
    >>> round(output_flow / input_flow, 3) == eta_at_min
650
    True
651
    """
652
    slope = (max_load / eta_at_max - min_load / eta_at_min) / (
653
        max_load - min_load
654
    )
655
    offset = 1 / eta_at_max - slope
656
    return slope, offset
657