Skip to content

Commit 888ea01

Browse files
committed
implement tests for phenomenodes
1 parent afab36d commit 888ea01

File tree

3 files changed

+200
-12
lines changed

3 files changed

+200
-12
lines changed

biosteam/_system.py

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -2517,7 +2517,8 @@ def track_convergence(self, track=True):
25172517
if track:
25182518
self._track_convergence = True
25192519
for i in self.stages: i.create_equation_nodes()
2520-
for i in self.stages: i.initialize_equation_nodes()
2520+
with self.stage_configuration(aggregated=False):
2521+
for i in self.stages: i.initialize_equation_nodes()
25212522
equations = tuple(set(sum([i.equation_nodes for i in self.stages], ())))
25222523
variables = tuple(set(sum([i.variables for i in equations], ())))
25232524
self.variable_profiles = {i: [] for i in variables}
@@ -2641,7 +2642,9 @@ def _collect_subgraph_variables(self, configuration, subgraph):
26412642
f'only {all_subgraphs!r} are valid'
26422643
)
26432644
for i in configuration.nodes:
2644-
for variable, value in getattr(i, f'_collect_{subgraph}_variables')():
2645+
method = getattr(i, f'_collect_{subgraph}_variables', None)
2646+
if method is None: continue
2647+
for variable, value in method():
26452648
if variable in collected_variables: continue
26462649
variables[variable].append(value)
26472650
collected_variables.add(variable)

biosteam/units/stage.py

Lines changed: 76 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -79,7 +79,11 @@ class SinglePhaseStage(Unit):
7979
_N_ins = 2
8080
_N_outs = 1
8181
_ins_size_is_fixed = False
82-
_energy_variable = 'T'
82+
83+
@property
84+
def _energy_variable(self):
85+
if self.T is None: return 'T'
86+
else: return None
8387

8488
def _init(self, T=None, P=None, Q=None, phase=None):
8589
self.T = T
@@ -156,7 +160,75 @@ def _update_energy_variable(self, departure):
156160
for i in self.outs: i.T += departure
157161

158162
def _update_nonlinearities(self): pass
163+
164+
@property
165+
def equation_node_names(self):
166+
if self._energy_variable is None:
167+
return (
168+
'overall_material_balance_node',
169+
)
170+
else:
171+
return (
172+
'overall_material_balance_node',
173+
'energy_balance_node',
174+
)
175+
176+
def initialize_overall_material_balance_node(self):
177+
self.overall_material_balance_node.set_equations(
178+
inputs=[j for i in self.ins if (j:=i.F_node)],
179+
outputs=[i.F_node for i in self.outs],
180+
)
181+
182+
def initialize_energy_balance_node(self):
183+
self.energy_balance_node.set_equations(
184+
inputs=(
185+
self.T_node, *[i.F_node for i in self.outs],
186+
*[j for i in self.ins if (j:=i.E_node)],
187+
),
188+
outputs=[
189+
j for i in self.outs if (j:=i.E_node)
190+
],
191+
)
192+
193+
@property
194+
def T_node(self):
195+
if hasattr(self, '_T_node'):
196+
self._T_node.value = self.T
197+
return self._T_node
198+
self._T_node = var = VariableNode(f"{self.node_tag}.T",
199+
self.T,
200+
self.energy_balance_node,
201+
*[i.sink.energy_balance_node for i in self.outs if i.sink],
202+
)
203+
return var
204+
205+
@property
206+
def E_node(self):
207+
if self._energy_variable is None:
208+
return None
209+
else:
210+
return self.T_node
159211

212+
def get_connected_material_nodes(self, stream):
213+
# TODO: Make this work for inlet streams and split streams
214+
eqs = [self.overall_material_balance_node]
215+
if self._energy_variable is not None:
216+
eqs.append(self.energy_balance_node)
217+
return eqs
218+
219+
def _collect_material_balance_variables(self):
220+
nodes = [i.F_node for i in self.outs]
221+
return [
222+
(i, i.value) for i in nodes
223+
]
224+
# list[tuple[VariableNode, value]] of stage-name and material balance variable pairs
225+
226+
def _collect_energy_balance_variables(self):
227+
nodes = [j for i in self.outs if (j:=i.E_node)]
228+
return [
229+
(i, i.value) for i in nodes
230+
] # list[tuple[VariableNode, value]] of stage-name and energy balance variable pairs
231+
160232

161233
class ReactivePhaseStage(bst.Unit): # Does not include VLE
162234
_N_outs = _N_ins = 1
@@ -253,7 +325,7 @@ def _update_nonlinearities(self):
253325
old = self.dmol
254326
new = self.reaction.conversion(self.ins[0])
255327
self.dmol = f * old + (1 - f) * new
256-
328+
257329

258330
# %% Two phases
259331

@@ -479,7 +551,7 @@ def _run(self):
479551
self.partition._run()
480552
for i in self.splitters: i._run()
481553
self._update_separation_factors()
482-
554+
483555
def initialize_overall_material_balance_node(self):
484556
self.overall_material_balance_node.set_equations(
485557
inputs=[j for i in self.ins if (j:=i.F_node)],
@@ -561,7 +633,7 @@ def get_connected_material_nodes(self, stream):
561633
self.overall_material_balance_node]
562634
if stream.phase == 'l':
563635
eqs.append(self.phenomena_node)
564-
elif self._energy_variable is None and stream.phase == 'g':
636+
elif self._energy_variable is not None and stream.phase == 'g':
565637
eqs.append(self.energy_balance_node)
566638
return eqs
567639

tests/test_phenomenode_tracking.py

Lines changed: 119 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -10,20 +10,54 @@ def test_flash():
1010
import biosteam as bst
1111
import numpy as np
1212
from numpy.testing import assert_allclose
13-
bst.System.strict_phenomena_decomposition = True
14-
13+
bst.settings.set_thermo(['Water', 'Ethanol'], ideal=True)
14+
feed_liquid = bst.Stream('feed_liquid', Water=50, Ethanol=50, phase='l', T = 400)
15+
feed_gas = bst.Stream('feed_gas', Water=50, Ethanol=50, phase='g', T=400)
16+
flash_1 = bst.StageEquilibrium('flash_1', ins=[feed_liquid, feed_gas], Q=0, P=101325, phases=('g', 'l'))
17+
flashes = [flash_1]
18+
for flash in flashes:
19+
flash.T = 350
20+
flash.K = np.array([0.5, 1.5])
21+
flash.B = 1.
22+
flash.outs[0].mol[:] = [50, 50]
23+
flash.outs[1].mol[:] = [50, 50]
24+
sys = bst.System('sys', path=flashes, algorithm='phenomena-oriented')
25+
sys.track_convergence()
26+
for i in range(10):
27+
sys.run_phenomena()
28+
phenomena_graph = sys.get_phenomena_graph()
29+
shape = phenomena_graph.variable_profiles.shape
30+
assert shape == (30, 8)
31+
1532

1633
def test_2_stage_flash():
1734
import biosteam as bst
1835
import numpy as np
1936
from numpy.testing import assert_allclose
20-
bst.System.strict_phenomena_decomposition = True
37+
bst.settings.set_thermo(['Water', 'Ethanol'], ideal=True)
38+
feed_liquid = bst.Stream('feed_liquid', Water=50, Ethanol=50, phase='l', T = 400)
39+
feed_gas = bst.Stream('feed_gas', Water=50, Ethanol=50, phase='g', T=400)
40+
flash_1 = bst.StageEquilibrium('flash_1', ins=[feed_liquid, feed_gas], Q=0, P=101325, phases=('g', 'l'))
41+
flash_2 = bst.StageEquilibrium('flash_2', ins=[flash_1-0, feed_liquid], B=1, P=101325, phases=('g', 'l'))
42+
flashes = [flash_1, flash_2]
43+
for flash in flashes:
44+
flash.T = 350
45+
flash.K = np.array([0.5, 1.5])
46+
flash.B = 1.
47+
flash.outs[0].mol[:] = [50, 50]
48+
flash.outs[1].mol[:] = [50, 50]
49+
sys = bst.System('sys', path=flashes, algorithm='phenomena-oriented')
50+
sys.track_convergence()
51+
for i in range(10):
52+
sys.run_phenomena()
53+
phenomena_graph = sys.get_phenomena_graph()
54+
shape = phenomena_graph.variable_profiles.shape
55+
assert shape == (30, 15)
2156

2257
def test_column():
2358
import biosteam as bst
2459
import numpy as np
2560
from numpy.testing import assert_allclose
26-
bst.System.strict_phenomena_decomposition = True
2761
bst.settings.set_thermo(['Water', 'Ethanol'], cache=True)
2862
feed = bst.Stream('feed', Ethanol=80, Water=100, T=80.215 + 273.15)
2963
D1 = bst.MESHDistillation('D1', N_stages=5, ins=[feed], feed_stages=[2],
@@ -33,9 +67,88 @@ def test_column():
3367
D1.set_flow_rates(D1.hot_start())
3468
sys = bst.System('sys', path=[D1])
3569
sys.track_convergence(True)
36-
for i in range(80):
70+
for i in range(10):
71+
sys.run_phenomena()
72+
phenomena_graph = sys.get_phenomena_graph()
73+
shape = phenomena_graph.variable_profiles.shape
74+
assert shape == (30, 38)
75+
76+
def test_system():
77+
import biosteam as bst
78+
import numpy as np
79+
from numpy.testing import assert_allclose
80+
bst.settings.set_thermo(['Water', 'AceticAcid', 'EthylAcetate'], cache=True)
81+
solvent_feed_ratio = 1
82+
@bst.SystemFactory
83+
def system(ins, outs):
84+
feed = bst.Stream('feed', AceticAcid=6660, Water=43600)
85+
solvent = bst.Stream('solvent', EthylAcetate=65000)
86+
recycle = bst.Stream('recycle')
87+
LE = bst.MultiStageEquilibrium(
88+
N_stages=6, ins=[feed, solvent, recycle],
89+
feed_stages=(0, -1, -1),
90+
phases=('L', 'l'),
91+
maxiter=200,
92+
use_cache=True,
93+
)
94+
# DAA = bst.MultiStageEquilibrium(N_stages=6, ins=[LE-0], feed_stages=[3],
95+
# outs=['vapor', 'liquid'],
96+
# stage_specifications={0: ('Reflux', 0.673), -1: ('Boilup', 2.57)},
97+
# maxiter=200,
98+
# phases=('g', 'l'),
99+
# use_cache=True,
100+
# )
101+
DEA = bst.MultiStageEquilibrium(N_stages=6, ins=[LE-1], feed_stages=[3],
102+
outs=['vapor', 'liquid'],
103+
stage_specifications={0: ('Reflux', 0.673), -1: ('Boilup', 2.57)},
104+
phases=('g', 'l'),
105+
maxiter=200,
106+
use_cache=True,
107+
)
108+
HX = bst.SinglePhaseStage(ins=DEA-0, outs=recycle, T=320, phase='l')
109+
110+
chemicals = bst.settings.chemicals
111+
112+
@LE.add_specification(run=True)
113+
def fresh_solvent_flow_rate():
114+
broth = feed.F_mass
115+
EtAc_recycle = recycle.imass['EthylAcetate']
116+
EtAc_required = broth * solvent_feed_ratio
117+
if EtAc_required < EtAc_recycle:
118+
recycle.F_mass *= EtAc_required / EtAc_recycle
119+
EtAc_recycle = recycle.imass['EthylAcetate']
120+
EtAc_fresh = EtAc_required - EtAc_recycle
121+
solvent.imass['EthylAcetate'] = max(
122+
0, EtAc_fresh
123+
)
124+
125+
@solvent.material_balance
126+
def fresh_solvent_flow_rate():
127+
s = np.ones(chemicals.size)
128+
r = np.zeros(chemicals.size)
129+
v = r.copy()
130+
index = chemicals.index('EthylAcetate')
131+
r[index] = 1
132+
v[index] = solvent_feed_ratio * feed.F_mass / chemicals.EthylAcetate.MW
133+
return (
134+
{solvent: s,
135+
recycle: r},
136+
v
137+
)
138+
sys = system(algorithm='phenomena oriented',
139+
molar_tolerance=1e-9,
140+
relative_molar_tolerance=1e-9,
141+
maxiter=2000,
142+
method='fixed-point')
143+
sys.track_convergence(True)
144+
for i in range(10):
37145
sys.run_phenomena()
38146
phenomena_graph = sys.get_phenomena_graph()
147+
shape = phenomena_graph.variable_profiles.shape
148+
assert shape == (37, 133)
39149

40150
if __name__ == '__main__':
41-
pass
151+
test_flash()
152+
test_2_stage_flash()
153+
test_column()
154+
test_system()

0 commit comments

Comments
 (0)