Workflow integration using model classes

ModelTemplate is a base class for building reusable, self-contained TESPy models. Subclassing it gives you a consistent interface for parameter access, solving, sensitivity sweeps, optimization and diagram plotting - without having to rewrite that plumbing for every model.

Note

This is a new feature. ModelTemplate is actively developed and will receive additional capabilities and refinements in future releases. If you have ideas, encounter unexpected behavior, or have built something useful on top of ModelTemplate that others could benefit from, we warmly invite you to open an issue or start a discussion in the TESPy issue tracker. Contributions and pull requests are equally welcome. See the contribution guide for details.

Background

Subclassing ModelTemplate

Every concrete model overrides two methods:

  1. _parameter_lookup - returns a dict mapping human-readable parameter names to their location in the network.

  2. _create_network - assembles the TESPy network and produces a stable initial solution. Always call super()._create_network() first.

For use with the optimization API, also implement solve_model to delegate to solve_model_design or solve_model_offdesign. Alternatively, you can wrap solve model around your custom implementation logic for solving a model including arbitrary pre- or postprocessings.

Parameter lookup

_parameter_lookup maps each name to one of four entry forms:

Entry

Meaning

["Connections", "label", "attr"]

Read and write a connection attribute

["Components", "label", "attr"]

Read and write a component attribute

{"get": callable}

Read-only derived quantity; callable takes no arguments

{"set": callable} or {"get": ..., "set": ...}

Write-only or read/write custom quantity

Solving

  • solve_model_design(**kwargs) - sets parameters and runs a design-mode solve, recovering from corruption via the last stable solution stored in self._stable_solution.

  • solve_model_offdesign(**kwargs) - off-design mode; requires self._design_path to be set.

  • solve_model(**kwargs) - not implemented in the base class; implement it in the subclass to make the model callable from OptimizationProblem.

Reading and writing parameters

get_parameter(name) and set_parameters(**kwargs) use the names defined in _parameter_lookup. Multiple parameters targeting the same network object are batched into a single set_attr call.

Sensitivity analysis

sensitivity_analysis(param_dict, result_param_list) sweeps a table of input combinations and returns a DataFrame. All lists in param_dict must have the same length - each row is one simulation. The evaluation order is minimized automatically with a greedy nearest-neighbor heuristic to reduce step sizes between consecutive solves.

Optimization

optimize(algorithm, termination, variables, ...) wraps OptimizationProblem and a pymoo algorithm into a single call, returning a DataFrame of all evaluated individuals. For a detailed walkthrough of the optimization API see Thermal Power Plant Efficiency Optimization.

Plotting

Three diagram methods are available on every instance:

  • plot_Ts_diagram_matplotlib(connection_label) - T-s diagram for a cycle

  • plot_logph_diagram_matplotlib(connection_label) - log(p,h) diagram for a cycle

  • plot_QT_diagram_matplotlib(component_label) - Q-T diagram for a heat exchanger

The connection label specified for the cycle diagrams can be any connection within the respective (sub)cycles.

All three accept an optional ax to draw into an existing matplotlib axes and save_dir to write the figure to a file.

Examples

ORC example

This example builds an Organic Rankine Cycle with an internal recuperator. The _parameter_lookup uses the ["Connections", ...] and ["Components", ...] forms. solve_model delegates directly to solve_model_design.

from tespy.components import (
    CycleCloser, Generator, Motor, MovingBoundaryHeatExchanger,
    PowerBus, PowerSink, Pump, Sink, Source, Turbine,
)
from tespy.connections import Connection, PowerConnection
from tespy.models import ModelTemplate
class ORCModel(ModelTemplate):

    def _parameter_lookup(self) -> dict:
        return {
            "evaporator_pinch": ["Components", "evaporator", "td_pinch"],
            "condenser_pinch": ["Components", "condenser", "td_pinch"],
            "turbine__efficiency": ["Components", "turbine", "eta_s"],
            "net_power": ["Connections", "e5", "E"],
            "T_source": ["Connections", "a1", "T"],
            "T_outflow": ["Connections", "a3", "T"],
            "m_source": ["Connections", "a1", "m"]
        }

    def solve_model(self, **kwargs):
        self.solve_model_design(**kwargs)

    def _create_network(self) -> None:
        super()._create_network()
        self.nw.units.set_defaults(
            temperature="degC",
            pressure="bar",
            pressure_difference="bar",
            power="kW",
            heat="kW"
        )

        turbine = Turbine("turbine")
        recuperator = MovingBoundaryHeatExchanger("recuperator")
        condenser = MovingBoundaryHeatExchanger("condenser")
        pump = Pump("pump")
        preheater = MovingBoundaryHeatExchanger("preheater")
        evaporator = MovingBoundaryHeatExchanger("evaporator")
        cc = CycleCloser("cc")

        heat_source = Source("heat source")
        heat_outflow = Sink("heat outflow")

        air_source = Source("air source")
        air_sink = Sink("air sink")

        a1 = Connection(heat_source, "out1", evaporator, "in1", label="a1")
        a2 = Connection(evaporator, "out1", preheater, "in1", label="a2")
        a3 = Connection(preheater, "out1", heat_outflow, "in1", label="a3")

        b1 = Connection(cc, "out1", turbine, "in1", label="b1")
        b2 = Connection(turbine, "out1", recuperator, "in1", label="b2")
        b3 = Connection(recuperator, "out1", condenser, "in1", label="b3")
        b4 = Connection(condenser, "out1", pump, "in1", label="b4")
        b5 = Connection(pump, "out1", recuperator, "in2", label="b5")
        b6 = Connection(recuperator, "out2", preheater, "in2", label="b6")
        b7 = Connection(preheater, "out2", evaporator, "in2", label="b7")
        b8 = Connection(evaporator, "out2", cc, "in1", label="b8")

        c1 = Connection(air_source, "out1", condenser, "in2", label="c1")
        c2 = Connection(condenser, "out2", air_sink, "in1", label="c2")

        self.nw.add_conns(a1, a2, a3, b1, b2, b3, b4, b5, b6, b7, b8, c1, c2)

        generator = Generator("generator")
        motor = Motor("motor")
        power_bus = PowerBus("bus", num_in=1, num_out=2)
        grid = PowerSink("grid")

        e1 = PowerConnection(turbine, "power", generator, "power_in", label="e1")
        e2 = PowerConnection(generator, "power_out", power_bus, "power_in1", label="e2")
        e3 = PowerConnection(power_bus, "power_out1", motor, "power_in", label="e3")
        e4 = PowerConnection(motor, "power_out", pump, "power", label="e4")
        e5 = PowerConnection(power_bus, "power_out2", grid, "power", label="e5")

        self.nw.add_conns(e1, e2, e3, e4, e5)

        generator.set_attr(eta=0.98)
        motor.set_attr(eta=0.98)

        a1.set_attr(fluid={"air": 1}, T=200, p=1, m=10)
        a2.set_attr(T=155)

        b1.set_attr(fluid={"Isopentane": 1}, x=1, T=150)
        b3.set_attr(td_dew=10, T_dew=30)
        b4.set_attr(td_bubble=5)
        b7.set_attr(td_bubble=5)

        c1.set_attr(fluid={"air": 1}, T=10, p=1)
        c2.set_attr(T=20)

        recuperator.set_attr(dp1=0, dp2=0)
        condenser.set_attr(dp1=0, dp2=0)
        preheater.set_attr(dp1=0, dp2=0)
        evaporator.set_attr(dp1=0, dp2=0)

        turbine.set_attr(eta_s=0.8)
        pump.set_attr(eta_s=0.7)

        self.nw.solve("design")

        b3.set_attr(T_dew=None)
        condenser.set_attr(td_pinch=5)

        a2.set_attr(T=None)
        evaporator.set_attr(td_pinch=10)

        self.nw.solve("design")
        self._stable_solution = "stable_solution.json"
        self.nw.save(self._stable_solution)

Create an instance

Instantiating the model triggers _create_network and runs the initial solve.

model = ORCModel()
 iter  | residual   | progress   | massflow   | pressure   | enthalpy   | fluid      | component  
-------+------------+------------+------------+------------+------------+------------+------------
 1     | 3.19e+06   | 0 %        | 7.81e+01   | 0.00e+00   | 3.13e+05   | 0.00e+00   | 2.81e+05   
 2     | 5.21e+04   | 14 %       | 1.24e-09   | 0.00e+00   | 2.06e+04   | 0.00e+00   | 3.98e+04   
 3     | 1.14e-03   | 99 %       | 1.01e-14   | 0.00e+00   | 1.21e-08   | 0.00e+00   | 1.03e-03   
 4     | 5.29e-10   | 100 %      | 1.01e-14   | 0.00e+00   | 4.43e-11   | 0.00e+00   | 2.42e-11   
 5     | 4.12e-10   | 100 %      | 1.01e-14   | 0.00e+00   | 4.73e-11   | 0.00e+00   | 3.13e-11   
Total iterations: 5, Calculation time: 0.01 s, Iterations per second: 724.38

 iter  | residual   | progress   | massflow   | pressure   | enthalpy   | fluid      | component  
-------+------------+------------+------------+------------+------------+------------+------------
 1     | 1.86e+05   | 8 %        | 2.03e+00   | 2.03e+04   | 2.26e+04   | 0.00e+00   | 2.62e+05   
 2     | 2.67e+03   | 28 %       | 3.86e-02   | 1.41e+03   | 1.23e+03   | 0.00e+00   | 3.29e+02   
 3     | 1.61e+01   | 53 %       | 1.38e-05   | 8.73e+00   | 7.54e+00   | 0.00e+00   | 1.67e+00   
 4     | 5.80e-04   | 100 %      | 2.86e-09   | 3.20e-04   | 2.77e-04   | 0.00e+00   | 7.08e-05   
 5     | 1.52e-09   | 100 %      | 2.14e-13   | 2.81e-11   | 1.52e-09   | 0.00e+00   | 5.84e-09   
Total iterations: 5, Calculation time: 0.03 s, Iterations per second: 166.04

Resolve with updated parameters

Use solve_model_design to re-run the model with new input values.

model.solve_model(
    **{"T_source": 225}
)
 iter  | residual   | progress   | massflow   | pressure   | enthalpy   | fluid      | component  
-------+------------+------------+------------+------------+------------+------------+------------
 1     | 3.20e+05   | 5 %        | 4.77e+01   | 8.03e-12   | 3.36e+04   | 0.00e+00   | 4.24e+05   
 2     | 1.47e-02   | 87 %       | 3.24e-04   | 4.99e-13   | 4.44e-01   | 0.00e+00   | 1.08e+00   
 3     | 4.13e-09   | 100 %      | 3.97e-12   | 1.82e-10   | 5.24e-09   | 0.00e+00   | 1.22e-08   
 4     | 5.30e-09   | 100 %      | 4.83e-14   | 8.53e-12   | 6.43e-10   | 0.00e+00   | 6.29e-10   
Total iterations: 4, Calculation time: 0.02 s, Iterations per second: 181.58

Read results

Parameters are read back by name via get_parameter. Diagrams can be produced directly from the model instance.

model.get_parameter("T_outflow")
72.41164019435348
fig, ax = model.plot_Ts_diagram_matplotlib("b1")
../_images/c2abdca096877f2a9ea043e45af625717362e5498f1de2ab97d093d4424bdc41.png

Heat pump with two refrigerant cycles

This example has a low-temperature and a high-temperature refrigerant cycle coupled through an internal heat exchanger. It showcases all four _parameter_lookup entry forms:

  • ["Components", ...] and ["Connections", ...] for standard network attributes,

  • {"get": callable} for a read-only derived quantity - here the computed COP,

  • {"get": ..., "set": ...} for a read/write custom quantity - here a COP target that reads back the current target value and, when written, activates a UserDefinedEquation that enforces it. Passing None deactivates the constraint and restores the degree of freedom,

  • {"set": callable} for a write-only parameter with arbitrary setter logic - here used to assign a pressure drop to all heat exchanger working-fluid sides in one call.

See the API docs of ModelTemplate for more information.

from tespy.components import Compressor
from tespy.components import CycleCloser
from tespy.components import Motor
from tespy.components import PowerBus
from tespy.components import PowerSource
from tespy.components import SectionedHeatExchanger
from tespy.components import SimpleHeatExchanger
from tespy.components import Sink
from tespy.components import Source
from tespy.components import Valve
from tespy.connections import Connection
from tespy.connections import PowerConnection
from tespy.models import ModelTemplate
from tespy.tools import UserDefinedEquation
class HeatPumpModel(ModelTemplate):

    def _parameter_lookup(self):
        return {
            "ihx pinch": ["Components", "internal heat exchanger", "td_pinch"],
            "heat": ["Components", "condenser high", "Q"],
            "power": ["Connections", "e1", "E"],
            "higher cycle compressor efficiency": ["Components", "compressor high", "eta_s"],
            "lower cycle mass flow": ["Connections", "a1", "m"],
            "lower cycle condensation temperature": ["Connections", "a3", "T_bubble"],
            "cop": {"get": self.calc_cop, "set": self.set_cop},
            "heat_exchangers_all_dp": {"set": self.set_all_hx_dp},
        }

    def calc_cop(self):
        return (
            abs(self.get_parameter("heat"))
            / self.get_parameter("power")
        )

    def set_cop(self, value):
        def cop_ude(ude):
            e1, b2, b3 = ude.conns
            return (
                ude.params["cop"] * e1.E.val_SI
                - b2.m.val_SI * (b2.h.val_SI - b3.h.val_SI)
            )

        def cop_ude_dependents(ude):
            e1, b2, b3 = ude.conns
            return [e1.E, b2.m, b2.h, b3.h]

        if value is None:
            ude = self.nw.user_defined_eq.get("cop_ude")
            if ude is not None:
                ude.is_set = False
        else:
            ude = self.nw.user_defined_eq.get("cop_ude")
            if ude is None:
                ude = UserDefinedEquation(
                    "cop_ude",
                    cop_ude,
                    cop_ude_dependents,
                    conns=self.nw.get_conn(["e1", "b2", "b3"]),
                    params={"cop": value}
                )
                self.nw.add_ude(ude)
            ude.is_set = True
            ude.params["cop"] = value

    def set_all_hx_dp(self, value):
        self.nw.get_comp("internal heat exchanger").set_attr(dp1=value, dp2=value)
        self.nw.get_comp("evaporator low").set_attr(dp2=value)
        self.nw.get_comp("condenser high").set_attr(dp=value)

    def _create_network(self):
        super()._create_network()

        self.nw.units.set_defaults(
            temperature="degC",
            pressure="bar",
            pressure_difference="bar",
            power="kW",
            heat="kW"
        )

        cc_low = CycleCloser("cc low")
        compressor_low = Compressor("compressor low")
        condenser_low = SectionedHeatExchanger("internal heat exchanger")
        valve_low = Valve("valve low")
        evaporator_low = SectionedHeatExchanger("evaporator low")
        he_in = Source("source")
        he_out = Sink("sink")

        cc_high = CycleCloser("cc high")
        compressor_high = Compressor("compressor high")
        condenser_high = SimpleHeatExchanger("condenser high")
        valve_high = Valve("valve high")

        a1 = Connection(cc_low, "out1", compressor_low, "in1", label="a1")
        a2 = Connection(compressor_low, "out1", condenser_low, "in1", label="a2")
        a3 = Connection(condenser_low, "out1", valve_low, "in1", label="a3")
        a4 = Connection(valve_low, "out1", evaporator_low, "in2", label="a4")
        a5 = Connection(evaporator_low, "out2", cc_low, "in1", label="a5")

        b1 = Connection(cc_high, "out1", compressor_high, "in1", label="b1")
        b2 = Connection(compressor_high, "out1", condenser_high, "in1", label="b2")
        b3 = Connection(condenser_high, "out1", valve_high, "in1", label="b3")
        b4 = Connection(valve_high, "out1", condenser_low, "in2", label="b4")
        b5 = Connection(condenser_low, "out2", cc_high, "in1", label="b5")

        c1 = Connection(he_in, "out1", evaporator_low, "in1", label="c1")
        c2 = Connection(evaporator_low, "out1", he_out, "in1", label="c2")

        self.nw.add_conns(a1, a2, a3, a4, a5, b1, b2, b3, b4, b5, c1, c2)

        grid = PowerSource("grid")
        distribution = PowerBus("distribution", num_in=1, num_out=2)
        motor1 = Motor("motor1")
        motor2 = Motor("motor2")

        e1 = PowerConnection(grid, "power", distribution, "power_in1", label="e1")
        e2 = PowerConnection(distribution, "power_out1", motor1, "power_in", label="e2")
        e3 = PowerConnection(motor1, "power_out", compressor_low, "power", label="e3")

        e4 = PowerConnection(distribution, "power_out2", motor2, "power_in", label="e4")
        e5 = PowerConnection(motor2, "power_out", compressor_high, "power", label="e5")

        self.nw.add_conns(e1, e2, e3, e4, e5)

        a1.set_attr(fluid={"R600a": 1}, T_dew=0, td_dew=10, m=1)
        a3.set_attr(T_bubble=55, td_bubble=5)
        b1.set_attr(fluid={"R600a": 1}, td_dew=10)
        b3.set_attr(T_bubble=100, td_bubble=2)

        condenser_high.set_attr(dp=0)
        condenser_low.set_attr(dp1=0, dp2=0, td_pinch=5)
        evaporator_low.set_attr(dp1=0, dp2=0)
        c1.set_attr(p=1, T=20, fluid={"air": 1})
        c2.set_attr(T=10)

        compressor_low.set_attr(eta_s=0.8)
        compressor_high.set_attr(eta_s=0.8)
        motor1.set_attr(eta=0.98)
        motor2.set_attr(eta=0.98)

        self.nw.solve("design")
        self.nw.print_results()
        self.nw.save(self._stable_solution)

    def solve_model(self, **kwargs):
        self.solve_model_design(**kwargs)

Create an instance

Instantiating HeatPumpModel builds the network, runs the initial design-point solve, and stores a stable solution for automatic recovery. After this the model is ready for all operations without any further setup.

model = HeatPumpModel()
FutureWarning: Calling Network.save() without a file path returns a JSON string, which is deprecated and will be removed in a future release. Use Network.save(as_dict=True) to get a dict that can be passed directly as design_path or init_path.
 iter  | residual   | progress   | massflow   | pressure   | enthalpy   | fluid      | component  
-------+------------+------------+------------+------------+------------+------------+------------
 1     | 9.63e+05   | 0 %        | 2.36e+01   | 1.73e+06   | 2.68e+05   | 0.00e+00   | 1.33e+06   
 2     | 1.02e+06   | 0 %        | 2.94e+00   | 3.49e+05   | 6.93e+04   | 0.00e+00   | 1.68e+06   
 3     | 1.40e+05   | 9 %        | 3.97e-01   | 4.52e+04   | 4.40e+03   | 0.00e+00   | 1.44e+05   
 4     | 1.96e+03   | 30 %       | 9.85e-03   | 1.37e+03   | 1.29e+02   | 0.00e+00   | 2.92e+03   
 5     | 1.84e+00   | 63 %       | 7.11e-06   | 1.16e+00   | 1.08e-01   | 0.00e+00   | 2.02e+00   
 6     | 1.10e-06   | 100 %      | 4.22e-12   | 8.26e-07   | 6.39e-08   | 0.00e+00   | 1.14e-06   
 7     | 1.71e-09   | 100 %      | 1.04e-15   | 8.73e-10   | 7.14e-10   | 0.00e+00   | 1.07e-09   
Total iterations: 7, Calculation time: 0.08 s, Iterations per second: 91.18

##### RESULTS (CycleCloser) #####
+---------+------------------+-------------------+
|         |   mass_deviation |   fluid_deviation |
|---------+------------------+-------------------|
| cc high |         0.00e+00 |          0.00e+00 |
| cc low  |         0.00e+00 |          0.00e+00 |
+---------+------------------+-------------------+
##### RESULTS (Compressor) #####
+-----------------+----------+----------+-----------+----------+
|                 |        P |       pr |        dp |    eta_s |
|-----------------+----------+----------+-----------+----------|
| compressor high | 1.09e+02 | 3.29e+00 | -1.38e+01 | 8.00e-01 |
| compressor low  | 7.89e+01 | 4.92e+00 | -6.16e+00 | 8.00e-01 |
+-----------------+----------+----------+-----------+----------+
##### RESULTS (SimpleHeatExchanger) #####
+----------------+-----------+----------+----------+----------+
|                |         Q |       pr |       dp |     zeta |
|----------------+-----------+----------+----------+----------|
| condenser high | -4.37e+02 | 1.00e+00 | 0.00e+00 | 0.00e+00 |
+----------------+-----------+----------+----------+----------+
##### RESULTS (SectionedHeatExchanger) #####
+-------------------------+-----------+----------+----------+----------+----------+-----------+----------+----------+----------+----------+----------+----------+-----------+------------+-----------+----------+------------+
|                         |         Q |       kA |   td_log |    ttd_u |    ttd_l |   ttd_min |      pr1 |      pr2 |      dp1 |      dp2 |    zeta1 |    zeta2 |   eff_hot |   eff_cold |   eff_max |       UA |   td_pinch |
|-------------------------+-----------+----------+----------+----------+----------+-----------+----------+----------+----------+----------+----------+----------+-----------+------------+-----------+----------+------------|
| evaporator low          | -2.49e+02 | 2.49e+04 | 1.00e+01 | 1.00e+01 | 1.00e+01 |  1.00e+01 | 1.00e+00 | 1.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 |  5.00e-01 |   9.37e-01 |  9.37e-01 | 1.76e+04 |   1.00e+01 |
| internal heat exchanger | -3.28e+02 | 4.35e+04 | 7.53e+00 | 1.08e+01 | 5.00e+00 |  5.00e+00 | 1.00e+00 | 1.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 | 0.00e+00 |  9.62e-01 |   8.90e-01 |  9.62e-01 | 3.42e+04 |   5.00e+00 |
+-------------------------+-----------+----------+----------+----------+----------+-----------+----------+----------+----------+----------+----------+----------+-----------+------------+-----------+----------+------------+
##### RESULTS (Motor) #####
+--------+----------+---------------+
|        |      eta |   delta_power |
|--------+----------+---------------|
| motor1 | 9.80e-01 |      1.61e+00 |
| motor2 | 9.80e-01 |      2.23e+00 |
+--------+----------+---------------+
##### RESULTS (Valve) #####
+------------+----------+----------+----------+----------+
|            |       pr |       dp |     zeta |       Kv |
|------------+----------+----------+----------+----------|
| valve high | 3.04e-01 | 1.38e+01 | 2.78e+07 | 2.75e+00 |
| valve low  | 2.03e-01 | 6.16e+00 | 1.81e+07 | 2.02e+00 |
+------------+----------+----------+----------+----------+
##### RESULTS (Connection) #####
+----+-----------+-----------+-----------+------------+------------+---------+
|    |         m |         p |         h |          T |          x | phase   |
|----+-----------+-----------+-----------+------------+------------+---------|
| a1 | 1.000e+00 | 1.570e+00 | 5.707e+05 |  1.000e+01 |  1.000e+00 | g       |
| a2 | 1.000e+00 | 7.730e+00 | 6.496e+05 |  6.581e+01 |  1.000e+00 | g       |
| a3 | 1.000e+00 | 7.730e+00 | 3.221e+05 |  5.000e+01 |  0.000e+00 | l       |
| a4 | 1.000e+00 | 1.570e+00 | 3.221e+05 | -2.274e-13 |  3.445e-01 | tp      |
| a5 | 1.000e+00 | 1.570e+00 | 5.707e+05 |  1.000e+01 |  1.000e+00 | g       |
| b1 | 1.874e+00 | 6.044e+00 | 6.341e+05 |  5.500e+01 |  1.000e+00 | g       |
| b2 | 1.874e+00 | 1.987e+01 | 6.923e+05 |  1.054e+02 |  1.000e+00 | g       |
| b3 | 1.874e+00 | 1.987e+01 | 4.593e+05 |  9.800e+01 |  0.000e+00 | l       |
| b4 | 1.874e+00 | 6.044e+00 | 4.593e+05 |  4.500e+01 |  4.921e-01 | tp      |
| b5 | 1.874e+00 | 6.044e+00 | 6.341e+05 |  5.500e+01 |  1.000e+00 | g       |
| c1 | 2.471e+01 | 1.000e+00 | 4.194e+05 |  2.000e+01 | -1.000e+00 | g       |
| c2 | 2.471e+01 | 1.000e+00 | 4.093e+05 |  1.000e+01 | -1.000e+00 | g       |
+----+-----------+-----------+-----------+------------+------------+---------+
##### RESULTS (PowerConnection) #####
+----+-----------+
|    |         E |
|----+-----------|
| e1 | 1.919e+02 |
| e2 | 8.049e+01 |
| e3 | 7.888e+01 |
| e4 | 1.114e+02 |
| e5 | 1.092e+02 |
+----+-----------+

Plotting

ModelTemplate provides built-in T-s, log(p,h) and Q-T diagram methods. Pass the label of the connection at the start of the cycle, or the component label for Q-T diagrams.

model.plot_logph_diagram_matplotlib("a1", save_dir=".")
model.plot_Ts_diagram_matplotlib("b1", save_dir=".")
model.plot_QT_diagram_matplotlib("internal heat exchanger", save_dir=".")
model.plot_QT_diagram_matplotlib("evaporator low", save_dir=".")
(<Figure size 1000x600 with 1 Axes>, <Axes: >)
../_images/2a67118144ce1ceb19a010cf14ebaaade9bfed3d5a7c0ce78463ac44e4746a7b.png ../_images/b48fc84774bcfc2164a7a84818401cb10c93986b1c4b287e9b9b855f8ab0f9f0.png ../_images/103cf003f77985f0090c7402671b30cda8568ad8a60db8bb0ddb6ecc4aeccca0.png ../_images/9090b0f9201655c90b68f2b81809b333db4af2bcf7fa6f8962da1682b24e7788.png

Sensitivity analysis

sensitivity_analysis sweeps a table of input combinations and returns the results as a DataFrame. The evaluation order is automatically minimised to reduce step sizes between consecutive solves.

model.nw.iterinfo = False
param_dict = {
    "ihx pinch": [3, 3, 20, 5, 0.2, 10],
    "heat": [-100, -200, -100, -800, -100, -200],
    "higher cycle compressor efficiency": [0.7, 0.7, 0.4, 0.9, 0.9, 0.7]
}
model.set_parameters(**{"lower cycle mass flow": None})
result = model.sensitivity_analysis(
    param_dict=param_dict,
    result_param_list=["cop", "lower cycle condensation temperature"]
)
result.style
  ihx pinch heat higher cycle compressor efficiency cop lower cycle condensation temperature
0 3.000000 -100.000000 0.700000 2.219955 55.000000
1 3.000000 -200.000000 0.700000 2.219955 55.000000
2 20.000000 -100.000000 0.400000 1.574370 55.000000
3 5.000000 -800.000000 0.900000 2.365062 55.000000
4 0.200000 -100.000000 0.900000 2.475170 55.000000
5 10.000000 -200.000000 0.700000 2.073391 55.000000

Setting a COP target

The "cop" entry uses the combined {"get": ..., "set": ...} form. Writing it activates a UserDefinedEquation that constrains the solution to the requested COP. For that the lower cycle condensation temperature is set to None as it is now to be calculated by the imposed COP value.

model.solve_model_design(
    **{"cop": 2.25, "lower cycle condensation temperature": None}
)
model.get_parameter("lower cycle condensation temperature")
36.770431380246805

Bulk pressure-drop assignment

"heat_exchangers_all_dp" uses the write-only {"set": callable} form. The callable receives the requested value and distributes it to every working-fluid side of every heat exchanger: dp1 and dp2 on the internal heat exchanger (both sides carry refrigerant), dp2 on the evaporator (the air side dp1 is left untouched), and dp on the high-side condenser.

model.set_parameters(**{"heat_exchangers_all_dp": 0.05})
model.solve_model_design()
model.get_parameter("lower cycle condensation temperature")
38.46665624138626

Optimization

model.optimize wraps OptimizationProblem and a pymoo algorithm into a single call. It returns a DataFrame of all evaluated individuals so results can be inspected directly.

For another walkthrough of the optimization API including constraints see Thermal Power Plant Efficiency Optimization.

from pymoo.algorithms.soo.nonconvex.pso import PSO

model.set_parameters(**{"heat": -1000, "ihx pinch": 7.5, "cop": None})

variables = {
    "lower cycle condensation temperature": {"min": 35, "max": 75},
}
result_param_list = ["ihx pinch"]  # just to show an example

algorithm = PSO(pop_size=10)

result = model.optimize(
    algorithm=algorithm,
    termination=("n_gen", 5),
    variables=variables,
    objective=["cop"],
    minimize_flags=[False],
    kpi=result_param_list
)
result.tail(10).style
  lower cycle condensation temperature cop ihx pinch
40 62.104316 2.303964 7.500000
41 58.765915 2.304766 7.500000
42 64.190151 2.300256 7.500000
43 60.301360 2.305174 7.500000
44 58.948999 2.304884 7.500000
45 60.323793 2.305170 7.500000
46 59.280271 2.305049 7.500000
47 57.553818 2.303524 7.500000
48 62.413098 2.303572 7.500000
49 61.469969 2.304600 7.500000
result.loc[result["cop"].idxmax()]
lower cycle condensation temperature    59.841641
cop                                      2.305190
ihx pinch                                7.500000
Name: 23, dtype: float64