Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
37 commits
Select commit Hold shift + click to select a range
25d8ce7
Move some methods from the network to the respective connection classes
fwitte Jan 10, 2026
bb51297
Check in a new HumidAirConnection class with some basic functionality
fwitte Jan 10, 2026
327b753
Add relative humidity equation
fwitte Jan 10, 2026
a7fbdea
Try to improve the convergence stabilization
fwitte Jan 10, 2026
8dc7e97
Make some quick and dirty adjustments to reuse _mix_ph methods with h…
fwitte Jan 10, 2026
0f6b40c
Make a much simpler implementation
fwitte Jan 11, 2026
cd17f16
Add one sample notebook
fwitte Jan 11, 2026
15a1bd2
Make it possible to specify relative humidity as equation to the model
fwitte Jan 14, 2026
57c5f70
Make quick and dirty convergence helpers for humidair mixing_rule
fwitte Jan 14, 2026
bb82005
Remove outputs
fwitte Jan 14, 2026
85465c1
Add a first draft to consider freezing in energy balance
fwitte Jan 14, 2026
f7d7ea7
Merge branch 'dev' into feature/humid-air-connection
fwitte Jan 14, 2026
1b35835
Externalize identification of dQ to function and fix calculation of h…
fwitte Jan 15, 2026
91a1cf2
Merge branch 'dev' into feature/humid-air-connection
fwitte Jan 16, 2026
9543fa6
Move humid air context out of Connection into HAConnection
fwitte Jan 16, 2026
d4b97cd
Rename method for better clarity
fwitte Jan 16, 2026
4792362
Make it an f string
fwitte Jan 16, 2026
72c109d
Make sure water and air are in the fluid vector
fwitte Jan 16, 2026
a4bfb3a
Allow specification of fluid composition through w
fwitte Jan 16, 2026
1c8f0d8
Clean up the example
fwitte Jan 16, 2026
39af317
Implement presolving of T for the HAConnection
fwitte Jan 20, 2026
017e20d
Fix a bug in the enthalpy value limitations
fwitte Jan 20, 2026
71848e0
ONly make a guess if there is no value imposed or precalculated and i…
fwitte Jan 20, 2026
9151ff2
Relaxing and helping the UA equation
JacobCHP Jan 22, 2026
c8706ee
Allow to store design cases as strings instead of files. makes sense …
jowr Feb 1, 2026
798758c
Allow to store design cases as strings instead of files. makes sense …
jowr Feb 1, 2026
ad2c0e6
Merge remote-tracking branch 'origin/feature/humid-air-connection' in…
jowr Feb 1, 2026
903ee0e
Limit the humidity to 100% RH
jowr Feb 9, 2026
153c284
directly calculate condensate flow next to the humid air. Can be used…
jowr Feb 9, 2026
785e214
Bugfix of seeding in HAConnection init
JacobCHP Feb 11, 2026
74af82b
Fix all uses of a random number generator
jowr Feb 11, 2026
e888404
fix a typo
jowr Feb 11, 2026
805e96b
Increasing relaxation
JacobCHP Feb 12, 2026
ac0a969
helper function for the sorted residual index values
jowr Feb 12, 2026
d65af03
changed relaxation
jowr Feb 12, 2026
7b97b3e
Make a hack to allow local_offdesign to work for the heat exchanger kA
fwitte Feb 13, 2026
16c049b
Slightly adjust the convergence helpers
fwitte Feb 13, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
17 changes: 13 additions & 4 deletions src/tespy/components/heat_exchangers/base.py
Original file line number Diff line number Diff line change
Expand Up @@ -465,9 +465,9 @@ def calculate_td_log(self):
T_o2 = o2.calc_T()

if T_i1 <= T_o2:
T_i1 = T_o2 + 0.01
T_o2 = T_i1 - 0.1
if T_o1 <= T_i2:
T_o1 = T_i2 + 0.01
T_o1 = T_i2 + 0.1

ttd_u = T_i1 - T_o2
ttd_l = T_o1 - T_i2
Expand Down Expand Up @@ -563,8 +563,17 @@ def kA_char_func(self):
"""
p1 = self.kA_char1.param
p2 = self.kA_char2.param
f1 = self.get_char_expr(p1, **self.kA_char1.char_params)
f2 = self.get_char_expr(p2, **self.kA_char2.char_params)
if self.local_offdesign:
design_value = self._connection_offdesign[self.inl[0].label][p1]
actual_value = getattr(self.inl[0], p1).val_SI
f1 = actual_value / design_value

design_value = self._connection_offdesign[self.inl[1].label][p2]
actual_value = getattr(self.inl[1], p2).val_SI
f2 = actual_value / design_value
else:
f1 = self.get_char_expr(p1, **self.kA_char1.char_params)
f2 = self.get_char_expr(p2, **self.kA_char2.char_params)

fkA1 = self.kA_char1.char_func.evaluate(f1)
fkA2 = self.kA_char2.char_func.evaluate(f2)
Expand Down
4 changes: 2 additions & 2 deletions src/tespy/components/heat_exchangers/sectioned.py
Original file line number Diff line number Diff line change
Expand Up @@ -759,9 +759,9 @@ def UA_cecchinato_func(self):
secondary_index = 0

m_r = self.inl[refrigerant_index].m
m_ratio_r = m_r.val_SI / m_r.design
m_ratio_r = max(m_r.val_SI / m_r.design, 1e-6)
m_sf = self.inl[secondary_index].m
m_ratio_sf = m_sf.val_SI / m_sf.design
m_ratio_sf = max(m_sf.val_SI / m_sf.design, 1e-6)

fUA = (
(1 + alpha_ratio * area_ratio)
Expand Down
324 changes: 324 additions & 0 deletions src/tespy/components/nodes/humidity_control.py
Original file line number Diff line number Diff line change
@@ -0,0 +1,324 @@
# -*- coding: utf-8

"""Module of class HumidityControl.


This file is part of project TESPy (github.com/oemof/tespy). It's copyrighted
by the contributors recorded in the version control history of the file,
available from its original location tespy/components/nodes/humidity_control.py

SPDX-License-Identifier: MIT
"""

from tespy.components.component import component_registry
from tespy.components.nodes.base import NodeBase
from tespy.tools.data_containers import ComponentMandatoryConstraints as dc_cmc
from tespy.tools.data_containers import SimpleDataContainer as dc_simple
from tespy.tools.fluid_properties import dT_mix_dph
from tespy.tools.fluid_properties import dT_mix_pdh

# from tespy.tools.fluid_properties import dT_mix_ph_dfluid


@component_registry
class HumidityControl(NodeBase):
r"""
A humidity control handles the water content from a humid air flow.

**Mandatory Equations**

- :py:meth:`tespy.components.nodes.base.NodeBase.mass_flow_func`
- :py:meth:`tespy.components.nodes.base.NodeBase.pressure_structure_matrix`
- :py:meth:`tespy.components.nodes.humidity_control.HumidityControl.fluid_func`
- :py:meth:`tespy.components.nodes.humidity_control.HumidityControl.energy_balance_func`

Inlets/Outlets

- in1: inlet with humid air
- out1: outlet with humid air
- out2: outlet of liquid water or ice

Image

.. image:: /api/_images/Splitter.svg
:alt: flowsheet of the splitter
:align: center
:class: only-light

.. image:: /api/_images/Splitter_darkmode.svg
:alt: flowsheet of the splitter
:align: center
:class: only-dark

Note
----
Fluid separation requires power and cooling, equations have not been
implemented, yet!

Parameters
----------
label : str
The label of the component.

design : list
List containing design parameters (stated as String).

offdesign : list
List containing offdesign parameters (stated as String).

design_path : str
Path to the components design case.

local_offdesign : boolean
Treat this component in offdesign mode in a design calculation.

local_design : boolean
Treat this component in design mode in an offdesign calculation.

char_warnings : boolean
Ignore warnings on default characteristics usage for this component.

printout : boolean
Include this component in the network's results printout.

num_out : float, dict
Number of outlets for this component, default value: 2.

Example
-------
The separator is used to split up a single mass flow into a specified
number of different parts at identical pressure and temperature but
different fluid composition. Fluids can be separated from each other.

>>> from tespy.components import Sink, Source, Separator
>>> from tespy.connections import Connection
>>> from tespy.networks import Network
>>> nw = Network(iterinfo=False)
>>> nw.units.set_defaults(**{
... "pressure": "bar", "temperature": "degC"
... })
>>> so = Source('source')
>>> si1 = Sink('sink1')
>>> si2 = Sink('sink2')
>>> s = Separator('separator', num_out=2)
>>> inc = Connection(so, 'out1', s, 'in1')
>>> outg1 = Connection(s, 'out1', si1, 'in1')
>>> outg2 = Connection(s, 'out2', si2, 'in1')
>>> nw.add_conns(inc, outg1, outg2)

An Air (simplified) mass flow of 5 kg/s is split up into two mass flows.
One mass flow of 1 kg/s containing 10 % oxygen and 90 % nitrogen leaves the
separator. It is possible to calculate the fluid composition of the second
mass flow. Specify starting values for the second mass flow fluid
composition for calculation stability.

>>> inc.set_attr(fluid={'O2': 0.23, 'N2': 0.77}, p=1, T=20, m=5)
>>> outg1.set_attr(fluid={'O2': 0.1, 'N2': 0.9}, m=1)
>>> outg2.set_attr(fluid0={'O2': 0.5, 'N2': 0.5})
>>> nw.solve('design')
>>> outg2.fluid.val['O2']
0.2625

In the same way, it is possible to specify one of the fluid components in
the second mass flow instead of the first mass flow. The solver will find
the mass flows matching the desired composition. 65 % of the mass flow
will leave the separator at the second outlet the case of 30 % oxygen
mass fraction for this outlet.

>>> outg1.set_attr(m=None)
>>> outg2.set_attr(fluid={'O2': 0.3})
>>> nw.solve('design')
>>> outg2.fluid.val['O2']
0.3
>>> round(outg2.m.val_SI / inc.m.val_SI, 2)
0.65
"""

@staticmethod
def get_parameters():
return {'num_out': dc_simple(description="number of outlets")}

def _update_num_eq(self):
self.variable_fluids = set(
[fluid for c in self.inl + self.outl for fluid in c.fluid.is_var]
)
num_fluid_eq = len(self.variable_fluids)
if num_fluid_eq == 0:
num_fluid_eq = 1
self.variable_fluids = [list(self.inl[0].fluid.is_set)[0]]

self.constraints["fluid_constraints"].num_eq = num_fluid_eq

def get_mandatory_constraints(self):
return {
'mass_flow_constraints': dc_cmc(**{
'num_eq_sets': 1,
'func': self.mass_flow_func,
'dependents': self.mass_flow_dependents,
'description': 'mass balance constraint'
}),
'fluid_constraints': dc_cmc(**{
'num_eq_sets': self.num_o,
'func': self.fluid_func,
'deriv': self.fluid_deriv,
'dependents': self.fluid_dependents,
'description': 'fluid mass fraction balance constraints'
}),
'energy_balance_constraints': dc_cmc(**{
'num_eq_sets': self.num_o,
'func': self.energy_balance_func,
'deriv': self.energy_balance_deriv,
'dependents': self.energy_balance_dependents,
'description': 'equal temperature at all outlets constraints'
}),
'pressure_constraints': dc_cmc(**{
'num_eq_sets': self.num_o,
'structure_matrix': self.pressure_structure_matrix,
'description': 'pressure equality constraints'
})
}

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

def outlets(self):
if self.num_out.is_set:
return [f'out{i + 1}' for i in range(self.num_out.val)]
else:
self.set_attr(num_out=2)
return self.outlets()

def propagate_wrapper_to_target(self, branch):
branch["components"] += [self]
for outconn in self.outl:
branch["connections"] += [outconn]
outconn.target.propagate_wrapper_to_target(branch)

def fluid_func(self):
r"""
Calculate the vector of residual values for fluid balance equations.

Returns
-------
residual : list
Vector of residual values for component's fluid balance.

.. math::

0 = \dot{m}_{in} \cdot x_{fl,in} - \dot {m}_{out,j}
\cdot x_{fl,out,j}\\
\forall fl \in \text{network fluids,}
\; \forall j \in \text{outlets}
"""
i = self.inl[0]
residual = []
for fluid in self.variable_fluids:
res = i.fluid.val[fluid] * i.m.val_SI
for o in self.outl:
res -= o.fluid.val[fluid] * o.m.val_SI
residual += [res]
return residual

def fluid_deriv(self, increment_filter, k, dependents=None):
r"""
Calculate partial derivatives of fluid balance.

Parameters
----------
increment_filter : ndarray
Matrix for filtering non-changing variables.

k : int
Position of derivatives in Jacobian matrix (k-th equation).
"""
i = self.inl[0]
for fluid in self.variable_fluids:
for o in self.outl:
self._partial_derivative(o.m, k, -o.fluid.val[fluid], increment_filter)
if fluid in o.fluid.is_var:
self.jacobian[k, o.fluid.J_col[fluid]] = -o.m.val_SI

self._partial_derivative(i.m, k, i.fluid.val[fluid], increment_filter)
if fluid in i.fluid.is_var:
self.jacobian[k, i.fluid.J_col[fluid]] = i.m.val_SI

k += 1

def fluid_dependents(self):
return {
"scalars": [
[c.m for c in self.inl + self.outl]
for f in self.variable_fluids
],
"vectors": [{
c.fluid: set(f) & c.fluid.is_var for c in self.inl + self.outl
} for f in self.variable_fluids]
}

def energy_balance_func(self):
r"""
Calculate energy balance.

Returns
-------
residual : list
Residual value of energy balance.

.. math::

0 = T_{in} - T_{out,j}\\
\forall j \in \text{outlets}
"""
residual = []
T_in = self.inl[0].calc_T()
for o in self.outl:
residual += [T_in - o.calc_T()]
return residual

def energy_balance_deriv(self, increment_filter, k, dependents=None):
r"""
Calculate partial derivatives of energy balance.

Parameters
----------
increment_filter : ndarray
Matrix for filtering non-changing variables.

k : int
Position of derivatives in Jacobian matrix (k-th equation).
"""
i = self.inl[0]
dT_dp_in = 0
dT_dh_in = 0
if i.p.is_var:
# outlet pressure must be variable as well in this case!
dT_dp_in = dT_mix_dph(i.p.val_SI, i.h.val_SI, i.fluid_data, i.mixing_rule)
if i.h.is_var:
dT_dh_in = dT_mix_pdh(i.p.val_SI, i.h.val_SI, i.fluid_data, i.mixing_rule)

for o in self.outl:
args = (o.p.val_SI, o.h.val_SI, o.fluid_data, o.mixing_rule)

dT_dp_out = 0
if o.p.is_var:
dT_dp_out = -dT_mix_dph(*args)
# pressure is always coupled
self._partial_derivative(i.p, k, dT_dp_in - dT_dp_out)

if o.h.is_var:
dT_dh_out = -dT_mix_pdh(*args)

# enthalpy is not necessarily coupled
if i.h._reference_container == o.h._reference_container:
self._partial_derivative(i.h, k, dT_dh_in - dT_dh_out)
else:
self._partial_derivative(i.h, k, dT_dh_in)
self._partial_derivative(o.h, k, dT_dh_out)

k += 1

def energy_balance_dependents(self):
return [
[self.inl[0].p, self.inl[0].h, o.p, o.h] for o in self.outl
]
1 change: 1 addition & 0 deletions src/tespy/connections/__init__.py
Original file line number Diff line number Diff line change
Expand Up @@ -3,4 +3,5 @@
from .bus import Bus # noqa: F401
from .connection import Connection # noqa: F401
from .connection import Ref # noqa: F401
from .humidairconnection import HAConnection # noqa: F401
from .powerconnection import PowerConnection # noqa: F401
Loading
Loading