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
Figure: 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.