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. The attribute
min_val=0 means that this value must not be negative - a warning is
issued in postprocessing automatically if it is. The parameter will only be
implemented as a postprocessing result. For this a calc method on the
dc_cp is declared. The base class dispatches the method automatically
after convergence. The corresponding _calc_td_minimal method computes
the internal maximum temperature in the compressor and returns the temperature difference to the
cooling fluid outlet.
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",
calc=self._calc_td_minimal
)
return params
def _calc_td_minimal(self):
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(
o.p.val_SI,
h_2,
o.fluid_data,
o.mixing_rule,
T0=o.T.val_SI
)
return T_max_compressor_internal - self.outl[1].T.val_SI
Further tests are being carried out to check the additional parameter definitions.
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 pressure balance of the cooling circuit into account, the parameter
definition is extended one last time with dp_cooling. The
structure_matrix links inlet and outlet pressure linearly so they can
be mapped to a single variable during presolving. The func_params
attribute specifies the assignment for the internal calculation and
quantity indicates the physical unit. To retrieve the value in the
postprocessing _calc_dp is used, which is available from the base
component class.
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",
calc=self._calc_td_minimal
)
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",
calc=self._calc_dp,
calc_params={"inconn": 1, "outconn": 1}
)
return params
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.