Skip to content

Commit 0642c4c

Browse files
committed
Add StreamSplitter block for ratio-based stream splitting
1 parent 178155c commit 0642c4c

2 files changed

Lines changed: 162 additions & 0 deletions

File tree

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
#########################################################################################
2+
##
3+
## Stream Splitter Block
4+
##
5+
#########################################################################################
6+
7+
# IMPORTS ===============================================================================
8+
9+
from pathsim.blocks.function import Function
10+
11+
# BLOCKS ================================================================================
12+
13+
class StreamSplitter(Function):
14+
"""Algebraic stream splitter that divides one stream into two by a fixed ratio.
15+
16+
Named ``StreamSplitter`` to avoid collision with ``tritium.Splitter``.
17+
18+
Mathematical Formulation
19+
-------------------------
20+
.. math::
21+
22+
F_1 = \\mathrm{split} \\cdot F_{in}, \\quad F_2 = (1 - \\mathrm{split}) \\cdot F_{in}
23+
24+
.. math::
25+
26+
T_1 = T_2 = T_{in}
27+
28+
Parameters
29+
----------
30+
split : float
31+
Fraction of feed sent to first outlet (0 to 1). Default 0.5.
32+
"""
33+
34+
input_port_labels = {
35+
"F_in": 0,
36+
"T_in": 1,
37+
}
38+
39+
output_port_labels = {
40+
"F_1": 0,
41+
"T_1": 1,
42+
"F_2": 2,
43+
"T_2": 3,
44+
}
45+
46+
def __init__(self, split=0.5):
47+
if not 0.0 <= split <= 1.0:
48+
raise ValueError(f"'split' must be in [0, 1] but is {split}")
49+
50+
self.split = split
51+
super().__init__(func=self._eval)
52+
53+
def _eval(self, F_in, T_in):
54+
F_1 = self.split * F_in
55+
F_2 = (1.0 - self.split) * F_in
56+
return (F_1, T_in, F_2, T_in)
Lines changed: 106 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,106 @@
1+
########################################################################################
2+
##
3+
## TESTS FOR
4+
## 'process.stream_splitter.py'
5+
##
6+
########################################################################################
7+
8+
# IMPORTS ==============================================================================
9+
10+
import unittest
11+
12+
from pathsim_chem.process import StreamSplitter
13+
14+
15+
# TESTS ================================================================================
16+
17+
class TestStreamSplitter(unittest.TestCase):
18+
"""Test the stream splitter block."""
19+
20+
def test_init_default(self):
21+
"""Test default split ratio."""
22+
S = StreamSplitter()
23+
self.assertEqual(S.split, 0.5)
24+
25+
def test_init_custom(self):
26+
"""Test custom split ratio."""
27+
S = StreamSplitter(split=0.3)
28+
self.assertEqual(S.split, 0.3)
29+
30+
def test_init_validation(self):
31+
"""Test input validation."""
32+
with self.assertRaises(ValueError):
33+
StreamSplitter(split=-0.1)
34+
with self.assertRaises(ValueError):
35+
StreamSplitter(split=1.5)
36+
37+
def test_port_labels(self):
38+
"""Test port label definitions."""
39+
self.assertEqual(StreamSplitter.input_port_labels["F_in"], 0)
40+
self.assertEqual(StreamSplitter.input_port_labels["T_in"], 1)
41+
self.assertEqual(StreamSplitter.output_port_labels["F_1"], 0)
42+
self.assertEqual(StreamSplitter.output_port_labels["T_1"], 1)
43+
self.assertEqual(StreamSplitter.output_port_labels["F_2"], 2)
44+
self.assertEqual(StreamSplitter.output_port_labels["T_2"], 3)
45+
46+
def test_mass_balance(self):
47+
"""F_1 + F_2 should equal F_in."""
48+
S = StreamSplitter(split=0.7)
49+
S.inputs[0] = 10.0 # F_in
50+
S.inputs[1] = 350.0 # T_in
51+
52+
S.update(None)
53+
54+
F_1 = S.outputs[0]
55+
F_2 = S.outputs[2]
56+
self.assertAlmostEqual(F_1 + F_2, 10.0, places=8)
57+
58+
def test_split_ratio(self):
59+
"""Check split produces correct flow fractions."""
60+
S = StreamSplitter(split=0.3)
61+
S.inputs[0] = 10.0
62+
S.inputs[1] = 350.0
63+
64+
S.update(None)
65+
66+
self.assertAlmostEqual(S.outputs[0], 3.0) # F_1 = 0.3 * 10
67+
self.assertAlmostEqual(S.outputs[2], 7.0) # F_2 = 0.7 * 10
68+
69+
def test_temperature_unchanged(self):
70+
"""Both outlet temperatures should equal inlet."""
71+
S = StreamSplitter(split=0.4)
72+
S.inputs[0] = 10.0
73+
S.inputs[1] = 375.0
74+
75+
S.update(None)
76+
77+
self.assertAlmostEqual(S.outputs[1], 375.0) # T_1
78+
self.assertAlmostEqual(S.outputs[3], 375.0) # T_2
79+
80+
def test_split_zero(self):
81+
"""With split=0, all flow goes to second outlet."""
82+
S = StreamSplitter(split=0.0)
83+
S.inputs[0] = 10.0
84+
S.inputs[1] = 350.0
85+
86+
S.update(None)
87+
88+
self.assertAlmostEqual(S.outputs[0], 0.0) # F_1
89+
self.assertAlmostEqual(S.outputs[2], 10.0) # F_2
90+
91+
def test_split_one(self):
92+
"""With split=1, all flow goes to first outlet."""
93+
S = StreamSplitter(split=1.0)
94+
S.inputs[0] = 10.0
95+
S.inputs[1] = 350.0
96+
97+
S.update(None)
98+
99+
self.assertAlmostEqual(S.outputs[0], 10.0) # F_1
100+
self.assertAlmostEqual(S.outputs[2], 0.0) # F_2
101+
102+
103+
# RUN TESTS LOCALLY ====================================================================
104+
105+
if __name__ == '__main__':
106+
unittest.main(verbosity=2)

0 commit comments

Comments
 (0)