Skip to content

Commit 017c2da

Browse files
committed
Add Heater block for duty-specified heating/cooling
1 parent f7d077d commit 017c2da

File tree

3 files changed

+184
-0
lines changed

3 files changed

+184
-0
lines changed

src/pathsim_chem/process/__init__.py

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -10,3 +10,7 @@
1010
from .distillation import *
1111
from .multicomponent_flash import *
1212
from .pfr import *
13+
from .mixer import *
14+
from .stream_splitter import *
15+
from .valve import *
16+
from .heater import *

src/pathsim_chem/process/heater.py

Lines changed: 63 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,63 @@
1+
#########################################################################################
2+
##
3+
## Duty-Specified Heater/Cooler Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
from pathsim.blocks.function import Function
10+
11+
# BLOCKS ================================================================================
12+
13+
class Heater(Function):
14+
"""Algebraic duty-specified heater/cooler with no thermal mass.
15+
16+
Raises or lowers the stream temperature by a specified heat duty.
17+
Flow passes through unchanged.
18+
19+
Mathematical Formulation
20+
-------------------------
21+
.. math::
22+
23+
T_{out} = T_{in} + \\frac{Q}{F \\, \\rho \\, C_p}
24+
25+
.. math::
26+
27+
F_{out} = F_{in}
28+
29+
Parameters
30+
----------
31+
rho : float
32+
Fluid density [kg/m³].
33+
Cp : float
34+
Fluid heat capacity [J/(kg·K)].
35+
"""
36+
37+
input_port_labels = {
38+
"F": 0,
39+
"T_in": 1,
40+
"Q": 2,
41+
}
42+
43+
output_port_labels = {
44+
"F_out": 0,
45+
"T_out": 1,
46+
}
47+
48+
def __init__(self, rho=1000.0, Cp=4184.0):
49+
if rho <= 0:
50+
raise ValueError(f"'rho' must be positive but is {rho}")
51+
if Cp <= 0:
52+
raise ValueError(f"'Cp' must be positive but is {Cp}")
53+
54+
self.rho = rho
55+
self.Cp = Cp
56+
super().__init__(func=self._eval)
57+
58+
def _eval(self, F, T_in, Q):
59+
if F > 0:
60+
T_out = T_in + Q / (F * self.rho * self.Cp)
61+
else:
62+
T_out = T_in
63+
return (F, T_out)

tests/process/test_heater.py

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
########################################################################################
2+
##
3+
## TESTS FOR
4+
## 'process.heater.py'
5+
##
6+
########################################################################################
7+
8+
# IMPORTS ==============================================================================
9+
10+
import unittest
11+
12+
from pathsim_chem.process import Heater
13+
14+
15+
# TESTS ================================================================================
16+
17+
class TestHeater(unittest.TestCase):
18+
"""Test the duty-specified heater/cooler block."""
19+
20+
def test_init_default(self):
21+
"""Test default parameters."""
22+
H = Heater()
23+
self.assertEqual(H.rho, 1000.0)
24+
self.assertEqual(H.Cp, 4184.0)
25+
26+
def test_init_custom(self):
27+
"""Test custom parameters."""
28+
H = Heater(rho=800.0, Cp=3000.0)
29+
self.assertEqual(H.rho, 800.0)
30+
self.assertEqual(H.Cp, 3000.0)
31+
32+
def test_init_validation(self):
33+
"""Test input validation."""
34+
with self.assertRaises(ValueError):
35+
Heater(rho=0)
36+
with self.assertRaises(ValueError):
37+
Heater(rho=-1)
38+
with self.assertRaises(ValueError):
39+
Heater(Cp=0)
40+
with self.assertRaises(ValueError):
41+
Heater(Cp=-1)
42+
43+
def test_port_labels(self):
44+
"""Test port label definitions."""
45+
self.assertEqual(Heater.input_port_labels["F"], 0)
46+
self.assertEqual(Heater.input_port_labels["T_in"], 1)
47+
self.assertEqual(Heater.input_port_labels["Q"], 2)
48+
self.assertEqual(Heater.output_port_labels["F_out"], 0)
49+
self.assertEqual(Heater.output_port_labels["T_out"], 1)
50+
51+
def test_heating(self):
52+
"""Positive Q should increase temperature."""
53+
H = Heater(rho=1000.0, Cp=4184.0)
54+
F = 0.1 # m³/s
55+
T_in = 300.0
56+
Q = 41840.0 # W
57+
58+
H.inputs[0] = F
59+
H.inputs[1] = T_in
60+
H.inputs[2] = Q
61+
62+
H.update(None)
63+
64+
# dT = Q / (F * rho * Cp) = 41840 / (0.1 * 1000 * 4184) = 0.1 K
65+
expected_T = T_in + Q / (F * 1000.0 * 4184.0)
66+
self.assertAlmostEqual(H.outputs[1], expected_T, places=6)
67+
68+
def test_cooling(self):
69+
"""Negative Q should decrease temperature."""
70+
H = Heater(rho=1000.0, Cp=4184.0)
71+
H.inputs[0] = 0.1
72+
H.inputs[1] = 350.0
73+
H.inputs[2] = -41840.0
74+
75+
H.update(None)
76+
77+
self.assertLess(H.outputs[1], 350.0)
78+
79+
def test_zero_duty(self):
80+
"""Q=0 should pass temperature through unchanged."""
81+
H = Heater()
82+
H.inputs[0] = 0.1
83+
H.inputs[1] = 350.0
84+
H.inputs[2] = 0.0
85+
86+
H.update(None)
87+
88+
self.assertAlmostEqual(H.outputs[1], 350.0)
89+
90+
def test_flow_passthrough(self):
91+
"""F_out should equal F_in."""
92+
H = Heater()
93+
H.inputs[0] = 0.5
94+
H.inputs[1] = 300.0
95+
H.inputs[2] = 10000.0
96+
97+
H.update(None)
98+
99+
self.assertAlmostEqual(H.outputs[0], 0.5)
100+
101+
def test_zero_flow(self):
102+
"""With zero flow, temperature should remain unchanged."""
103+
H = Heater()
104+
H.inputs[0] = 0.0
105+
H.inputs[1] = 350.0
106+
H.inputs[2] = 10000.0
107+
108+
H.update(None)
109+
110+
self.assertAlmostEqual(H.outputs[0], 0.0)
111+
self.assertAlmostEqual(H.outputs[1], 350.0)
112+
113+
114+
# RUN TESTS LOCALLY ====================================================================
115+
116+
if __name__ == '__main__':
117+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)