How to develop a new component

What should I do if I need a component for a specific simulation that is not available in TESPy? One solution would be to request support on GitHub or to raise the issue at the next online or community meeting. But what good is that if I need the new component right now?

I will just implement it myself! The question becomes how to do this, since I don’t know the code structure. These instructions on how to develop a new component are intended to simplify the transition from user to an awesome developer.

Implementation of a polynomial compressor with cooling

The following basic principles should be in your mind when implementing a component:

  • Visualize your task with a flowsheet, cycle diagram and/or the equations you want to implement

  • Adding the new equations and variables

  • Expand the parameter definitions and derived calculations

  • Perform a lot of tests

  • Write docstrings for the new component

  • Implement tests to verify correctness

Blackboard drawing of compressor with cooling

Figure: Blackboard drawing of compressor with cooling

Blackboard drawing of compressor with cooling

Figure: Blackboard drawing of compressor with cooling

1. Implement the inputs and outputs

First, we create a new class named PolynomialCompressorWithCooling. Then, we have to implement the inputs and outputs. Based on our blackboard drawing we need two inputs as well as two outputs.

Note

The base component class has pairwise mass flow and fluid composition balance:

  • in1 matches with out1

  • in2 matches with out2

This is automatically expanded with every new pair of ports.

Attention

If you want to add ports with other names or non-paired ports, this may break.

class PolynomialCompressorWithCooling(PolynomialCompressor):

    @staticmethod
    def inlets():
        return ['in1', 'in2']

    @staticmethod
    def outlets():
        return ['out1', 'out2']

After creating the inputs and outputs, a simple model should be built up to test the code. As usual, we will first create a network with the correct unit definitions. Then we will compile the components, including the new polynomial compressor with cooling and its connections. Once these have been added to the network, we will parameterise the system and solve it in the design mode. The correctness of the process can be confirmed by checking the mass flow and fluid composition in results. Since there is no connection between the working fluid ports of the compressor and the cooling fluid yet, we have to provide pressure, temperature and a mass flow.

Display source code for testing the model
from tespy.components import PolynomialCompressor
from tespy.components import Sink
from tespy.components import Source
from tespy.connections import Connection
from tespy.networks import Network
nw = Network()
nw.units.set_defaults(
    temperature="°C",
    pressure="bar"
)

gas_inlet = Source("gas inlet")
gas_outlet = Sink("gas outlet")

water_inlet = Source("water cold")
water_outlet = Sink("water hot")

compressor = PolynomialCompressorWithCooling("compressor")

c1 = Connection(gas_inlet, "out1", compressor, "in1", label="c1")
c2 = Connection(compressor, "out1", gas_outlet, "in1", label="c2")

b1 = Connection(water_inlet, "out1", compressor, "in2", label="b1")
b2 = Connection(compressor, "out2", water_outlet, "in1", label="b2")

nw.add_conns(c1, c2, b1, b2)
c1.set_attr(fluid={"R290": 1}, m=1, T_dew=10, td_dew=10)
c2.set_attr(T_dew=60, td_dew=50)

b1.set_attr(fluid={"water": 1}, m=0.5, T=15, p=1)
b2.set_attr(T=25, p=1)

compressor.set_attr(dissipation_ratio=0.1)

nw.solve("design")
nw.print_results()

b1.fluid.val, b2.fluid.val

2. Add mandatory constraints

The next step is to add the mandatory constraints. To do this, a method is created that adds an additional constraint to the dictionary of mandatory constraints.

from tespy.tools.data_containers import ComponentMandatoryConstraints as dc_cmc
    def get_mandatory_constraints(self) -> dict:
        constraints = super().get_mandatory_constraints()
        # this is a dictionary
        constraints["cooling_energy_balance_constraints"] = dc_cmc(
            func=self.cooling_energy_balance_func,
            dependents=self.cooling_energy_balance_dependents,
            num_eq_sets=1
        )
        return constraints

In this case, it retrieves the basic constraints of the upper class. A new constraint is added to ensure that the energy balance of the cooling system is met. Finally, the complete set of constraints is returned.

3. Define new equations

Now we define the methods that are connected to the constraint: The function returning the residual value of the equation and the list of variables the equation depends on. The cooling_energy_balance_func() method describes the actual energy balance equation. It calculates the heat dissipated by the working fluid side in the compressor through the value of the dissipation_ratio. On the cold side this heat should be added to the cooling fluid, but not in its entirety, only an usable share of it. For now we can hardcode that usable share in the equation with the variable eta_recovery.

    def cooling_energy_balance_func(self):
        eta_recovery = 0.8
        residual = (
            self.inl[1].m.val_SI * (self.outl[1].h.val_SI - self.inl[1].h.val_SI)
            + self.inl[0].m.val_SI * (
                self.outl[0].h.val_SI
                - self.outl[0].h.val_SI / (1 - self.dissipation_ratio.val_SI)
                + self.inl[0].h.val_SI * (
                    self.dissipation_ratio.val_SI / (1 - self.dissipation_ratio.val_SI)
                )
            ) * eta_recovery
        )
        return residual

    def cooling_energy_balance_dependents(self):
        return [
            self.inl[0].m, self.inl[1].m,
            self.inl[0].h, self.inl[1].h,
            self.outl[0].h, self.outl[1].h
        ]

As in the first step, it is recommended to test the new code in between.

Display source code for testing the model
from tespy.components import PolynomialCompressor
from tespy.components import Sink
from tespy.components import Source
from tespy.connections import Connection
from tespy.networks import Network
nw = Network()
nw.units.set_defaults(
    temperature="°C",
    pressure="bar"
)

gas_inlet = Source("gas inlet")
gas_outlet = Sink("gas outlet")

water_inlet = Source("water cold")
water_outlet = Sink("water hot")

compressor = PolynomialCompressorWithCooling("compressor")

c1 = Connection(gas_inlet, "out1", compressor, "in1", label="c1")
c2 = Connection(compressor, "out1", gas_outlet, "in1", label="c2")

b1 = Connection(water_inlet, "out1", compressor, "in2", label="b1")
b2 = Connection(compressor, "out2", water_outlet, "in1", label="b2")

nw.add_conns(c1, c2, b1, b2)
c1.set_attr(fluid={"R290": 1}, m=1, T_dew=10, td_dew=10)
c2.set_attr(T_dew=60, td_dew=50)

b1.set_attr(fluid={"water": 1}, m=1, T=15, p=1)
b2.set_attr(T=25, p=1)
compressor.set_attr(dissipation_ratio=0.1)

nw.solve("design")

Error

You have provided too many parameters: 0 required, 1 supplied. Aborting calculation!

TESPyNetworkError                                 Traceback (most recent call last):

Cell In[6], line 8
 5 b2.set_attr(T=25, p=1)
 6 compressor.set_attr(dissipation_ratio=0.1)
 8 nw.solve("design")

File ~/gitprojects/tespy/src/tespy/networks/network.py:2486, in Network.solve(self, mode, init_path,
design_path, max_iter, min_iter, init_only, init_previous, use_cuda, print_results, robust_relax)
2483 msg = 'Starting solver.'
2484 logger.info(msg)
2486 self.solve_determination()
2488 try:
2489     self.solve_loop(print_results=print_results)

File ~/gitprojects/tespy/src/tespy/networks/network.py:2603, in Network.solve_determination(self)
2601     logger.error(msg)
2602     self.status = 12
2603     raise hlp.TESPyNetworkError(msg)
2604 elif n  self.variable_counter:
2605     msg = (
2606         f"You have not provided enough parameters: {self.variable_counter} "
2607         f"required, {n} supplied. Aborting calculation!"
2608     )

TESPyNetworkError: You have provided too many parameters: 0
required, 1 supplied. Aborting calculation!

With no changes in our original specifications, the model results in an error. Since we now have an additional equation, this means that we have to specify one parameter less than before, e.g., the cooling mass flow.

b1.set_attr(m=None)
nw.solve("design")
b1.m.val_SI

4. Expand definitions of the parameters and add derived calculations

Next, we can define parameters for the PolynomialCompressorWithCooling. We do this similar to the mandatory constraints by calling the get_parameters method, updating the dictionary and returning it. We can start with the definition for the efficiency of the heat recovery eta_recovery.

For this, we use a data container dc_cp(), which creates an object of the ComponentProperties class. These objects describe how TESPy should handle a specific physical parameter, e.g., temperature, pressure loss, efficiency, etc. Accordingly, TESPy uses these objects to automate unit conversion, validation, equation integration and documentation.

from tespy.tools.data_containers import ComponentProperties as dc_cp
from tespy.tools.helpers import TESPyComponentError


    def get_parameters(self):
        params = super().get_parameters()
        params["eta_recovery"] = dc_cp()
        return params

Along with the introduction of this parameter, we also update the equation.

def cooling_energy_balance_func(self):
    residual = (
        self.inl[1].m.val_SI * (self.outl[1].h.val_SI - self.inl[1].h.val_SI)
        + self.inl[0].m.val_SI * (
            self.outl[0].h.val_SI
            - self.outl[0].h.val_SI / (1 - self.dissipation_ratio.val_SI)
            + self.inl[0].h.val_SI * (
                self.dissipation_ratio.val_SI / (1 - self.dissipation_ratio.val_SI)
            )
        ) * self.eta_recovery.val_SI
    )
    return residual

And, we can make the specification of eta_recovery mandatory if we want. This can be done by overriding the default _preprocess method like this:

def _preprocess(self, row_idx):
    if not self.eta_recovery.is_set:
        msg = (
            f"The component {self.label} of type {self.__class__.__name__}"
            "requires you to specify the share of heat recovery "
            "eta_recovery."
        )
        raise TESPyComponentError(msg)

    return super()._preprocess(row_idx)

Once a again, it is recommended to test the code.

Display source code for testing the model
from tespy.components import PolynomialCompressor
from tespy.components import Sink
from tespy.components import Source
from tespy.connections import Connection
from tespy.networks import Network
nw = Network()
nw.units.set_defaults(
    temperature="°C",
    pressure="bar"
)

gas_inlet = Source("gas inlet")
gas_outlet = Sink("gas outlet")

water_inlet = Source("water cold")
water_outlet = Sink("water hot")

compressor = PolynomialCompressorWithCooling("compressor")

c1 = Connection(gas_inlet, "out1", compressor, "in1", label="c1")
c2 = Connection(compressor, "out1", gas_outlet, "in1", label="c2")

b1 = Connection(water_inlet, "out1", compressor, "in2", label="b1")
b2 = Connection(compressor, "out2", water_outlet, "in1", label="b2")

nw.add_conns(c1, c2, b1, b2)
c1.set_attr(fluid={"R290": 1}, m=1, T_dew=10, td_dew=10)
c2.set_attr(T_dew=60, td_dew=25)

b1.set_attr(fluid={"water": 1}, T=15, p=1)
b2.set_attr(T=25, p=1)
compressor.set_attr(dissipation_ratio=0.1, eta_recovery=0.9)

nw.solve("design")
compressor.Q_diss.val

b1.m.val_SI * (b2.h.val_SI - b1.h.val_SI)

We can also check if changing boundary conditions works and if the results seem reasonable:

b1.set_attr(m=0.005)
b2.set_attr(T=None)
nw.solve("design")

b2.T.val, c2.T.val
h_2 = c1.h.val_SI + (c2.h.val_SI - c1.h.val_SI) / (1 - compressor.dissipation_ratio.val_SI)
c2.p.val_SI
from tespy.tools.fluid_properties import T_mix_ph
T_mix_ph(c2.p.val_SI, h_2, c2.fluid_data) - 273.15
compressor.eta_s.val

The tests are going well. But with very small mass flows temperature at cooling output can be higher than the temperature on the gas side, as there is no limit implemented.

Further expansion of parameter definition

The next step is to define the parameter for the minimum temperature difference td_minimal between the compressor and the cooling medium. In this case, the attribute min_val=0 means that this value must not be negative. If it is, a warning is put out in the postprocessing automatically.

from tespy.tools.data_containers import ComponentProperties as dc_cp
from tespy.tools.helpers import TESPyComponentError

    def get_parameters(self):
        params = super().get_parameters()
        params["eta_recovery"] = dc_cp()
        params["td_minimal"] = dc_cp(
            min_val=0
        )
        return params

In addition, calc_parameters() is used to calculate the derived properties after the simulation. In this case, the internal maximum temperature in the compressor (T_max_compressor_internal) and the minimum temperature difference between the compressor and the cooling fluid (td_minimal) are calculated using the outlet enthalpy of the compressor.

    def calc_parameters(self):
        super().calc_parameters()

        i = self.inl[0]
        o = self.outl[0]
        h_2 = (
            (o.h.val_SI - i.h.val_SI * self.dissipation_ratio.val_SI)
            / (1 - self.dissipation_ratio.val_SI)
        )
        T_max_compressor_internal = T_mix_ph(
            self.outl[0].p.val_SI,
            h_2,
            self.outl[0].fluid_data,
            self.outl[0].mixing_rule,
            T0=self.outl[0].T.val_SI
        )
        self.td_minimal.val_SI = (
            T_max_compressor_internal
            - self.outl[1].T.val_SI
        )

Further tests are being carried out at this point to examine the further expansion of the parameter definition.

Display source code for testing the model
from tespy.components import PolynomialCompressor
from tespy.components import Sink
from tespy.components import Source
from tespy.connections import Connection
from tespy.networks import Network
nw = Network()
nw.units.set_defaults(
    temperature="°C",
    pressure="bar"
)

gas_inlet = Source("gas inlet")
gas_outlet = Sink("gas outlet")

water_inlet = Source("water cold")
water_outlet = Sink("water hot")

compressor = PolynomialCompressorWithCooling("compressor")

c1 = Connection(gas_inlet, "out1", compressor, "in1", label="c1")
c2 = Connection(compressor, "out1", gas_outlet, "in1", label="c2")

b1 = Connection(water_inlet, "out1", compressor, "in2", label="b1")
b2 = Connection(compressor, "out2", water_outlet, "in1", label="b2")

nw.add_conns(c1, c2, b1, b2)
c1.set_attr(fluid={"R290": 1}, m=1, T_dew=10, td_dew=10)
c2.set_attr(T_dew=60, td_dew=25)

b1.set_attr(fluid={"water": 1}, T=15, m=0.05, p=1)
b2.set_attr(p=1)
compressor.set_attr(dissipation_ratio=0.1, eta_recovery=0.9)

nw.solve("design")

Final expansion of parameter definition

To take the energy and pressure balance system of the component into account, the parameter definition is extended one last time. For this purpose, the pressure loss in the cooling circuit dp_cooling is described. As in the previous step, the value has not to be negative due to the min_val=0 attribute. Instead of a func we can use the structure matrix for this parameter. The structure_matrix is used in the preprocessing to make a linear connection between inlet and outlet pressure in a way, that these two variables can then be mapped into a single one in the numerical solution finding. The func_params attribute specifies the assignment for the internal calculation. Finally, the quantity specified indicates the physical unit.

    def _preprocess(self, row_idx):
        if not self.eta_recovery.is_set:
            msg = (
                f"The component {self.label} of type {self.__class__.__name__}"
                "requires you to specify the share of heat recovery "
                "eta_recovery."
            )
            raise TESPyComponentError(msg)

        return super()._preprocess(row_idx)

    def get_parameters(self):
        params = super().get_parameters()
        params["eta_recovery"] = dc_cp(
            quantity="efficiency"
        )
        params["td_minimal"] = dc_cp(
            min_val=0,
            quantity="temperature_difference"
        )
        params["dp_cooling"] = dc_cp(
            min_val=0,
            structure_matrix=self.dp_structure_matrix,
            func_params={"inconn": 1, "outconn": 1, "dp": "dp_cooling"},
            quantity="pressure"
        )
        return params

Subsequent to the simulation, the calculation of the derived thermodynamic variables is also required for the pressure loss dp_cooling in the cooling flow.

        self.dp_cooling.val_SI = self.inl[1].p.val_SI - self.outl[1].p.val_SI

Our extension is also being tested here.

Display source code for testing the model
from tespy.components import PolynomialCompressor
from tespy.components import Sink
from tespy.components import Source
from tespy.connections import Connection
from tespy.networks import Network
nw = Network()
nw.units.set_defaults(
    temperature="°C",
    pressure="bar"
)

gas_inlet = Source("gas inlet")
gas_outlet = Sink("gas outlet")

water_inlet = Source("water cold")
water_outlet = Sink("water hot")

compressor = PolynomialCompressorWithCooling("compressor")

c1 = Connection(gas_inlet, "out1", compressor, "in1", label="c1")
c2 = Connection(compressor, "out1", gas_outlet, "in1", label="c2")

b1 = Connection(water_inlet, "out1", compressor, "in2", label="b1")
b2 = Connection(compressor, "out2", water_outlet, "in1", label="b2")

nw.add_conns(c1, c2, b1, b2)
c1.set_attr(fluid={"R290": 1}, m=1, T_dew=10, td_dew=10)
c2.set_attr(T_dew=60, td_dew=25)

b1.set_attr(fluid={"water": 1}, T=15, m=0.05, p=1)
b2.set_attr(p=0.9)
compressor.set_attr(dissipation_ratio=0.1, eta_recovery=0.9)

nw.solve("design")
nw.print_results()

After checking that everything is correct, it’s time to pat ourselves on the back, because we have implemented a PolynomialCompressorWithCooling in TESPy.

5. Further tasks

Once implementation is complete, the hard work begins. Docstrings make your code understandable, while tests ensure its reliability. If you want to contribute your new component to tespy, then you can have a look at the developer guide. More information on component customization and implementation can also be found in this section.