Skip to content

Commit 753951b

Browse files
committed
working reactive distillation algorithm with homotopy continuation
1 parent 8e795d3 commit 753951b

2 files changed

Lines changed: 65 additions & 17 deletions

File tree

biosteam/units/stage.py

Lines changed: 54 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -921,7 +921,7 @@ def _run(self):
921921

922922
def _update_separation_factors(self, f=None):
923923
if self.B == inf:
924-
self.S = np.ones(len(self.partition.IDs)) * np.inf
924+
self.S = np.ones(len(self.partition.IDs)) * inf
925925
elif self.B == 0:
926926
self.S = np.zeros(len(self.partition.IDs))
927927
else:
@@ -1481,7 +1481,7 @@ def initialize_energy_balance_node(self):
14811481
def K_node(self):
14821482
if hasattr(self, '_K_node'): return self._K_node
14831483
partition_data = self.partition.partition_data
1484-
if (self.specified_variable == 'B') and (self.B == 0 or self.B == np.inf):
1484+
if (self.specified_variable == 'B') and (self.B == 0 or self.B == inf):
14851485
var = None
14861486
elif (partition_data and 'K' in partition_data):
14871487
var = None
@@ -1525,7 +1525,7 @@ class PhasePartition(Unit):
15251525
_N_ins = 1
15261526
_N_outs = 2
15271527
strict_infeasibility_check = False
1528-
conversion_relaxation_factor = 0
1528+
conversion_relaxation_factor = 0.5
15291529
S_relaxation_factor = 0
15301530
B_relaxation_factor = 0
15311531
K_relaxation_factor = 0
@@ -1805,7 +1805,7 @@ def _run_lle(self, P=None, update=True, top_chemical=None, single_loop=False):
18051805
self.strict_infeasibility_check, 1
18061806
)
18071807
if phi == 1:
1808-
self.B = np.inf
1808+
self.B = inf
18091809
else:
18101810
self.B = phi / (1 - phi)
18111811
self.K = K
@@ -1818,7 +1818,7 @@ def _run_lle(self, P=None, update=True, top_chemical=None, single_loop=False):
18181818
else:
18191819
lle_chemicals, K_new, gamma_y, phi = eq(T=ms.T, P=P or self.P, top_chemical=top_chemical, update=update, use_cache=self.use_cache)
18201820
if phi == 1 or phi is None:
1821-
self.B = np.inf
1821+
self.B = inf
18221822
self.T = ms.T
18231823
return
18241824
else:
@@ -2088,8 +2088,9 @@ class MultiStageEquilibrium(Unit):
20882088
iteration_memory = 10 # Length of recorded iterations.
20892089
preconditioning_tolerance = 1e-3
20902090
preconditioning_relative_tolerance = 1e-3
2091+
homotopy_continuation_steps = 5
20912092
inside_maxiter = 100
2092-
default_max_attempts = 5
2093+
default_max_attempts = 2
20932094
default_maxiter = 100
20942095
default_optimize_result = False
20952096
default_tolerance = 1e-6
@@ -2404,6 +2405,7 @@ def _init(self,
24042405
else:
24052406
self.methods = methods
24062407

2408+
self.homotopy_continuation = bool(stage_reactions)
24072409
self.vle_decomposition = vle_decomposition
24082410

24092411

@@ -2438,7 +2440,7 @@ def _residuals(self, x):
24382440
H_model = self._eq_thermo.mixture.H
24392441
if self.stage_reactions:
24402442
feed_flows = self.feed_flows + self.conversion
2441-
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies + (self.conversion * self._Hf_eq).sum(axis=1)
2443+
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies - (self.conversion * self._Hf_eq).sum(axis=1)
24422444
else:
24432445
feed_flows = self.feed_flows
24442446
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies
@@ -2534,6 +2536,15 @@ def material_errors(self):
25342536
)
25352537
return pd.DataFrame(errors, columns=IDs)
25362538

2539+
# %% Homotopy for reactive stages
2540+
2541+
@property
2542+
def conversion(self):
2543+
if self.homotopy_continuation:
2544+
return self.conversion_homotopy * self._conversion
2545+
else:
2546+
return self._conversion
2547+
25372548
# %% Simulation
25382549

25392550
def default_vle_decomposition(self):
@@ -2575,14 +2586,39 @@ def _run(self):
25752586
solver = flx.wegstein
25762587
else:
25772588
raise ValueError(f'invalid method {method!r}')
2578-
if self._has_lle and algorithm == 'inside out': continue
2589+
if self._has_lle and algorithm == 'inside out':
2590+
raise RuntimeError('inside out method does not support liquid extraction yet')
25792591
if analysis_mode:
25802592
self._tracked_algorithms.append(
25812593
(self.iter + 1, algorithm)
25822594
)
2583-
try: x = solver(f, x, maxiter=maxiter, xtol=xtol, rtol=rtol, args=(algorithm,))
2595+
try:
2596+
if self.homotopy_continuation:
2597+
if self.conversion_homotopy == 1:
2598+
try:
2599+
x = solver(f, x, maxiter=maxiter, xtol=xtol, rtol=rtol, args=(algorithm,))
2600+
except:
2601+
x = self._best_result.x
2602+
self._mean_residual = inf
2603+
self._iteration_record[0] = IterationResult(None, inf)
2604+
self._best_result = IterationResult(x, inf)
2605+
else:
2606+
break
2607+
steps = self.homotopy_continuation_steps
2608+
for t in np.linspace(0, 1, steps):
2609+
self.conversion_homotopy = t
2610+
try:
2611+
x = solver(f, x, maxiter=maxiter, xtol=xtol, rtol=rtol, args=(algorithm,))
2612+
except:
2613+
x = self._best_result.x
2614+
self._mean_residual = inf
2615+
self._iteration_record[0] = IterationResult(None, inf)
2616+
self._best_result = IterationResult(x, inf)
2617+
else:
2618+
x = solver(f, x, maxiter=maxiter, xtol=xtol, rtol=rtol, args=(algorithm,))
25842619
except:
2585-
self._mean_residual = np.inf
2620+
self._mean_residual = inf
2621+
self._iteration_record[0] = IterationResult(None, inf)
25862622
if self._best_result.x is not None: x = self._best_result.x
25872623
maxiter = self.maxiter - self.iter
25882624
if maxiter <= 0: break
@@ -2696,7 +2732,7 @@ def _run_inside_out(self):
26962732
feed_flows = self.feed_flows + self.conversion
26972733
total_feed_flows = feed_flows.sum(axis=1)
26982734
bulk_feed = total_feed_flows.sum()
2699-
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies + (self.conversion * self._Hf_eq).sum(axis=1)
2735+
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies - (self.conversion * self._Hf_eq).sum(axis=1)
27002736
else:
27012737
total_feed_flows = self._total_feed_flows
27022738
bulk_feed = self._bulk_feed
@@ -2835,7 +2871,7 @@ def f(x):
28352871
self._result = result = least_squares(f,
28362872
x0=x.flatten(),
28372873
jac=lambda x: MESH.create_block_tridiagonal_matrix(*self._jacobian(x.reshape(shape))),
2838-
bounds=(0, np.inf),
2874+
bounds=(0, inf),
28392875
method='trf',
28402876
max_nfev=self.maxiter,
28412877
xtol=self.tolerance,
@@ -2973,7 +3009,7 @@ def hot_start(self):
29733009
i._N_chemicals = N_chemicals
29743010
self.feed_flows = feed_flows = np.zeros([N_stages, N_chemicals])
29753011
if self.stage_reactions:
2976-
self.conversion = conversion = feed_flows.copy()
3012+
self._conversion = conversion = feed_flows.copy()
29773013
self._Hf_eq = self.chemicals.Hf[index]
29783014
for i, j in zip(partitions, conversion): i.conversion = j
29793015
self.feed_enthalpies = feed_enthalpies = np.zeros(N_stages)
@@ -3136,6 +3172,7 @@ def hot_start(self):
31363172
for s in partition.outs: s.T = T
31373173
xs = np.array([i.x for i in partitions])
31383174
ys = np.array([i.y for i in partitions])
3175+
self.conversion_homotopy = 0
31393176
Vs, Ls = self.estimate_bulk_vapor_and_liquid_flow_rates(xs, ys, Ts)
31403177
phase_ratios = Vs / Ls
31413178
for partition, B in zip(partitions, phase_ratios):
@@ -3172,8 +3209,8 @@ def hot_start(self):
31723209
self._phi = self._eq_thermo.Phi(self._eq_thermo.chemicals)
31733210
self._H_magnitude = 100 * sum([i.mixture.Cn('l', i.mol, i.T, i.P) for i in self.ins])
31743211
self.attempt = 0
3175-
self._mean_residual = np.inf
3176-
self._best_result = empty = IterationResult(None, np.inf)
3212+
self._mean_residual = inf
3213+
self._best_result = empty = IterationResult(None, inf)
31773214
self._point_shape = (N_stages, 2 * N_chemicals + 1)
31783215
record = self.iteration_memory * [empty]
31793216
x = self._get_point()
@@ -3328,7 +3365,7 @@ def estimate_bulk_vapor_and_liquid_flow_rates(self, xs, ys, Ts):
33283365
feed_flows = self.feed_flows + self.conversion
33293366
total_feed_flows = feed_flows.sum(axis=1)
33303367
bulk_feed = total_feed_flows.sum()
3331-
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies + (self.conversion * self._Hf_eq).sum(axis=1)
3368+
feed_and_invariable_enthalpies = self._feed_and_invariable_enthalpies - (self.conversion * self._Hf_eq).sum(axis=1)
33323369
else:
33333370
total_feed_flows = self._total_feed_flows
33343371
bulk_feed = self._bulk_feed
@@ -3669,7 +3706,7 @@ def f(x):
36693706
least_squares(f,
36703707
x0=x0.flatten(),
36713708
jac=lambda x: MESH.create_block_tridiagonal_matrix(*self._jacobian(x.reshape(shape))),
3672-
bounds=(0, np.inf),
3709+
bounds=(0, inf),
36733710
method='trf',
36743711
max_nfev=iterations,
36753712
x_scale='jac',

biosteam/yogurt_acid_whey.py

Lines changed: 11 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
# -*- coding: utf-8 -*-
2+
"""
3+
Created on Wed Dec 31 10:54:34 2025
4+
5+
@author: yoelr
6+
"""
7+
from pint import UnitRegistry
8+
u = UnitRegistry()
9+
yogurt_wt = 4.9 * u.lb
10+
yogurt_density = 1.03 * u.g / u.mL
11+
print((yogurt_wt / yogurt_density).to('gal'))

0 commit comments

Comments
 (0)