Skip to content

Commit 4b26da4

Browse files
committed
improvements to LLE; workaround for bug (needs future fix)
1 parent d41af3b commit 4b26da4

6 files changed

Lines changed: 99 additions & 60 deletions

File tree

thermosteam/equilibrium/bubble_point.py

Lines changed: 60 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -39,22 +39,51 @@ def solve_y(y_phi, phi, T, P, y_guess):
3939
# %% Bubble point values container
4040

4141
class BubblePointValues:
42-
__slots__ = ('T', 'P', 'IDs', 'z', 'y')
42+
__slots__ = ('T', 'P', 'IDs', 'z', 'y', 'α')
4343

44-
def __init__(self, T, P, IDs, z, y):
44+
def __init__(self, T, P, IDs, z, y, α=None):
4545
self.T = T
4646
self.P = P
4747
self.IDs = IDs
4848
self.z = z
4949
self.y = y
50-
50+
self.α = α
51+
5152
@property
5253
def x(self): return self.z
5354

5455
@property
5556
def K(self):
5657
return self.y / self.x
5758

59+
@property
60+
def (self):
61+
return self.α / self.α.sum()
62+
63+
@property
64+
def (self):
65+
return self.β / self.β.sum()
66+
67+
@property
68+
def β(self):
69+
return self.z - self.α
70+
71+
@property
72+
def ϕ(self):
73+
return self.α.sum()
74+
75+
@property
76+
def KL(self):
77+
return self. / self.
78+
79+
@property
80+
def (self):
81+
return self.y / self.
82+
83+
@property
84+
def (self):
85+
return self.y / self.
86+
5887
def __repr__(self):
5988
return f"{type(self).__name__}(T={self.T:.2f}, P={self.P:.0f}, IDs={self.IDs}, z={self.z}, y={self.y})"
6089

@@ -157,11 +186,14 @@ def _T_error(self, T, P, z_over_P, z_norm, y):
157186
y[:] = solve_y(y_phi, self.phi, T, P, y)
158187
return 1. - y.sum()
159188

160-
def _T_error_lle(self, T, P, z, y, x):
189+
def _T_error_lle(self, T, P, z, y, x, α):
161190
if T <= 0: raise InfeasibleRegion('negative temperature')
162191
Psats = np.array([i(T) for i in self.Psats], dtype=float)
163-
mol = solve_lle_mol(self.gamma, z, T, P, sample=x)
164-
x[:] = mol / mol.sum() if mol.any() else z
192+
α[:] = solve_lle_mol(self.gamma, z, T, P, sample=x)
193+
if α.any():
194+
x[:] = α / α.sum()
195+
else:
196+
x = z
165197
y_phi = (x / P
166198
* Psats
167199
* self.gamma(x, T, P)
@@ -181,10 +213,13 @@ def _P_error_dep(self, P, T, z, Psats, z_Psats, y):
181213
y[:] = solve_y(y_phi, self.phi, T, P, y)
182214
return 1. - y.sum()
183215

184-
def _P_error_lle(self, P, T, z, Psats, y, x):
216+
def _P_error_lle(self, P, T, z, Psats, y, x, α):
185217
if P <= 0: raise InfeasibleRegion('negative pressure')
186-
mol = solve_lle_mol(self.gamma, z, T, P, sample=x)
187-
x[:] = mol / mol.sum() if mol.any() else z
218+
α[:] = solve_lle_mol(self.gamma, z, T, P, sample=x)
219+
if α.any():
220+
x[:] = α / α.sum()
221+
else:
222+
x = z
188223
y_phi = Psats * x * self.gamma(x, T, P) * self.pcf(T, P, Psats) / P
189224
y[:] = solve_y(y_phi, self.phi, T, P, y)
190225
return 1. - y.sum()
@@ -242,17 +277,17 @@ def __call__(self, z, *, T=None, P=None, liquid_conversion=None, lle=False):
242277
z = np.asarray(z, float)
243278
if T:
244279
if P: raise ValueError("may specify either T or P, not both")
245-
P, *args = self.solve_Py(z, T, liquid_conversion, lle)
280+
P, *args = self.solve_Py(z, T, liquid_conversion, lle, full=True)
246281
elif P:
247-
T, *args = self.solve_Ty(z, P, liquid_conversion, lle)
282+
T, *args = self.solve_Ty(z, P, liquid_conversion, lle, full=True)
248283
else:
249284
raise ValueError("must specify either T or P")
250285
if liquid_conversion:
251286
return ReactiveBubblePointValues(T, P, self.IDs, z, *args)
252287
else:
253288
return BubblePointValues(T, P, self.IDs, z, *args)
254289

255-
def solve_Ty(self, z, P, liquid_conversion=None, lle=False):
290+
def solve_Ty(self, z, P, liquid_conversion=None, lle=False, full=False):
256291
"""
257292
Bubble point at given composition and pressure.
258293
@@ -295,10 +330,13 @@ def solve_Ty(self, z, P, liquid_conversion=None, lle=False):
295330
z_over_P = z / P
296331
T_guess, y = self._Ty_ideal(z_over_P)
297332
if lle:
333+
x = y.copy()
334+
α = x.copy()
298335
f = self._T_error_lle
299-
args = (P, z, y, y.copy())
336+
args = (P, z, y, x, α)
300337
else:
301338
f = self._T_error
339+
α= None
302340
args = (P, z_over_P, z, y)
303341
try:
304342
T = flx.aitken_secant(f, T_guess, T_guess + 1e-3,
@@ -313,7 +351,8 @@ def solve_Ty(self, z, P, liquid_conversion=None, lle=False):
313351
T_guess, self.T_tol, 5e-12, args,
314352
checkiter=False, checkbounds=False,
315353
maxiter=self.maxiter)
316-
return T, fn.normalize(y)
354+
y = fn.normalize(y)
355+
return (T, y, α) if full else (T, y)
317356
else:
318357
f = self._T_error_reactive
319358
z_norm = z / z.sum()
@@ -336,7 +375,7 @@ def solve_Ty(self, z, P, liquid_conversion=None, lle=False):
336375
maxiter=self.maxiter)
337376
return T, dz, fn.normalize(y), x
338377

339-
def solve_Py(self, z, T, liquid_conversion=None, lle=False):
378+
def solve_Py(self, z, T, liquid_conversion=None, lle=False, full=False):
340379
"""
341380
Bubble point at given composition and temperature.
342381
@@ -379,6 +418,7 @@ def solve_Py(self, z, T, liquid_conversion=None, lle=False):
379418
elif T < self.Tmin: T = self.Tmin
380419
Psats = np.array([i(T) for i in self.Psats])
381420
z = z / z.sum()
421+
α = None
382422
if self.gamma.P_dependent:
383423
f = self._P_error_dep
384424
z_Psats = z * Psats
@@ -389,7 +429,9 @@ def solve_Py(self, z, T, liquid_conversion=None, lle=False):
389429
z_Psat_gamma = z * Psats * self.gamma(z, T, 101325)
390430
P_guess, y = self._Py_ideal(z_Psat_gamma)
391431
f = self._P_error_lle
392-
args = (P, T, z, Psats, y, y.copy())
432+
x = y.copy()
433+
α = x.copy()
434+
args = (P, T, z, Psats, y, x, α)
393435
else:
394436
z_Psat_gamma = z * Psats * self.gamma(z, T, 101325)
395437
P_guess, y = self._Py_ideal(z_Psat_gamma)
@@ -406,7 +448,8 @@ def solve_Py(self, z, T, liquid_conversion=None, lle=False):
406448
P_guess, self.P_tol, 5e-12, args,
407449
checkiter=False, checkbounds=False,
408450
maxiter=self.maxiter)
409-
return P, fn.normalize(y)
451+
y = fn.normalize(y)
452+
return (P, y, α) if full else P, y
410453
else:
411454
f = self._P_error_reactive
412455
z_norm = z / z.sum()

thermosteam/equilibrium/lle.py

Lines changed: 8 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -135,11 +135,14 @@ def solve_lle_mol(gamma, z, T, P, sample=None, phi=1):
135135
else:
136136
return z
137137
K[K <= 0] = 1e-9
138-
mol = pseudo_equilibrium(
139-
K, phi, z, T, n, gamma.f, gamma.args,
140-
LLE.pseudo_equilibrium_inner_loop_options,
141-
LLE.pseudo_equilibrium_outer_loop_options,
142-
)
138+
try:
139+
mol = pseudo_equilibrium(
140+
K, phi, z, T, n, gamma.f, gamma.args,
141+
LLE.pseudo_equilibrium_inner_loop_options,
142+
LLE.pseudo_equilibrium_outer_loop_options,
143+
)
144+
except:
145+
breakpoint()
143146
if mol.any():
144147
return mol / mol.sum()
145148
else:

thermosteam/equilibrium/plot_equilibrium.py

Lines changed: 5 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -60,7 +60,7 @@ def as_thermo(thermo, chemicals): # pragma: no cover
6060
# %% Plot functions
6161

6262
def plot_vle_binary_phase_envelope(chemicals, T=None, P=None, vc=None, lc=None, thermo=None,
63-
yticks=None, line_styles=None, N=None): # pragma: no cover
63+
yticks=None, line_styles=None, N=None, lle=False): # pragma: no cover
6464
"""
6565
Plot the binary phase envelope of two chemicals at a given temperature or pressure.
6666
@@ -100,15 +100,15 @@ def plot_vle_binary_phase_envelope(chemicals, T=None, P=None, vc=None, lc=None,
100100
Xs = P
101101
X_units = 'Pa'
102102
assert not T, "must pass either T or P, but not both"
103-
bpss = [[BP(z, P=i) for z in zs] for i in P]
103+
bpss = [[BP(z, P=i, lle=lle) for z in zs] for i in P]
104104
mss = [[bp.T for bp in bps] for bps in bpss]
105105
ylabel = 'Temperature [K]'
106106
elif T:
107107
if not isinstance(T, Iterable): T = [T]
108108
assert not P, "must pass either T or P, but not both"
109109
Xs = T
110110
X_units = 'K'
111-
bpss = [[BP(z, T=i) for z in zs] for i in T]
111+
bpss = [[BP(z, T=i, lle=lle) for z in zs] for i in T]
112112
mss = [[bp.P for bp in bps] for bps in bpss]
113113
ylabel = 'Pressure [Pa]'
114114
else:
@@ -118,7 +118,7 @@ def plot_vle_binary_phase_envelope(chemicals, T=None, P=None, vc=None, lc=None,
118118
for X, ms, bps, ls in zip(Xs, mss, bpss, line_styles):
119119
ys_a = np.array([bp.y[0] for bp in bps])
120120
ms_liq = ms.copy()
121-
ms_gas = interpolate.interp1d(ys_a, ms, bounds_error=False, kind='slinear')(zs_a)
121+
# ms_gas = interpolate.interp1d(ys_a, ms, bounds_error=False, kind='slinear')(zs_a)
122122
# if P:
123123
# azeotrope = ms_liq > ms_gas
124124
# elif T:
@@ -134,7 +134,7 @@ def plot_vle_binary_phase_envelope(chemicals, T=None, P=None, vc=None, lc=None,
134134
# azeotrope[mid] = False
135135
# ms_gas[mid] = ms_liq[mid]
136136
# ms_gas = interpolate.interp1d(zs_a[~azeotrope], ms_gas[~azeotrope], bounds_error=False, kind='slinear')(zs_a)
137-
plt.plot(zs_a, ms_gas, c=vc if vc is not None else colors.red.RGBn, label=f'vapor [{int(X)} {X_units}]', ls=ls)
137+
plt.plot(ys_a, ms, c=vc if vc is not None else colors.red.RGBn, label=f'vapor [{int(X)} {X_units}]', ls=ls)
138138
plt.plot(zs_a, ms_liq, c=lc if lc is not None else colors.blue.RGBn, label=f'liquid [{int(X)} {X_units}]', ls=ls)
139139
plt.xlim([0, 1])
140140
plt.ylim([mss.min(), mss.max()])

thermosteam/equilibrium/tangential_plane_stability.py

Lines changed: 3 additions & 19 deletions
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class StabilityReport(NamedTuple):
2222
sample_unstable: bool = False
2323

2424
def edge_points_simplex_masked(z: np.ndarray,
25-
points_per_edge: int = 8,
25+
points_per_edge: int = 12,
2626
epsilon: float = 1e-3,
2727
min_active: int = 2) -> np.ndarray:
2828
"""
@@ -121,14 +121,6 @@ def objective(w, T, P, logfz, softmax=False):
121121
w /= w.sum()
122122
best_result = w
123123
best_val = value
124-
unstable = best_val < 0
125-
if unstable:
126-
return StabilityReport(
127-
unstable=unstable,
128-
candidate=sample,
129-
tpd=best_val,
130-
sample_unstable=True,
131-
)
132124
w = z * MW
133125
w /= w.sum()
134126
samples = edge_points_simplex_masked(w)
@@ -154,7 +146,7 @@ def objective(w, T, P, logfz, softmax=False):
154146
best_result = w
155147
best_val = value
156148
return StabilityReport(
157-
unstable=(best_val < 0),
149+
unstable=best_val < 0 and np.abs(best_result - z).sum() > 1e-9,
158150
candidate=best_result,
159151
tpd=best_val
160152
)
@@ -202,14 +194,6 @@ def __call__(self, z, T, P, reference_phase='l', potential_phase='L', sample=Non
202194
w /= w.sum()
203195
best_result = w
204196
best_val = value
205-
unstable = best_val < 0
206-
if unstable:
207-
return StabilityReport(
208-
unstable=unstable,
209-
candidate=sample,
210-
tpd=best_val,
211-
sample_unstable=True,
212-
)
213197
MW = self.MW
214198
w = z * MW
215199
w /= w.sum()
@@ -236,7 +220,7 @@ def __call__(self, z, T, P, reference_phase='l', potential_phase='L', sample=Non
236220
best_result = w
237221
best_val = value
238222
return StabilityReport(
239-
unstable=(best_val < 0),
223+
unstable=best_val < 0 and np.abs(best_result - z).sum() > 1e-9,
240224
candidate=best_result,
241225
tpd=best_val
242226
)

thermosteam/indexer.py

Lines changed: 20 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1423,20 +1423,26 @@ def by_mass(self):
14231423
"""Return a ChemicalMassFlowIndexer that references this object's molar data."""
14241424
try:
14251425
mass = self._data_cache['mass']
1426-
except:
1427-
chemicals = self.chemicals
1428-
dct = MassFlowDict(self.data.dct, chemicals.MW)
1429-
data_cache = self._data_cache
1430-
data_cache['mass'] = mass = \
1431-
ChemicalMassFlowIndexer.from_data(
1432-
SparseVector.from_dict(
1433-
dct,
1434-
chemicals.size
1435-
),
1436-
self,
1437-
chemicals,
1438-
False,
1439-
)
1426+
# TODO: understand where is this error coming from.
1427+
# Create test case trying to break it.
1428+
if mass.data.dct.dct is self.data.dct:
1429+
return mass
1430+
else:
1431+
self._data_cache.clear()
1432+
except: pass
1433+
chemicals = self.chemicals
1434+
dct = MassFlowDict(self.data.dct, chemicals.MW)
1435+
data_cache = self._data_cache
1436+
data_cache['mass'] = mass = \
1437+
ChemicalMassFlowIndexer.from_data(
1438+
SparseVector.from_dict(
1439+
dct,
1440+
chemicals.size
1441+
),
1442+
self,
1443+
chemicals,
1444+
False,
1445+
)
14401446
return mass
14411447
ChemicalMolarFlowIndexer.by_mass = by_mass
14421448

thermosteam/units_of_measure.py

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -114,6 +114,9 @@ def format_units(units, ends='$', mathrm=True):
114114
all_numerators = []
115115
all_denominators = []
116116
unprocessed_numerators, *unprocessed_denominators = units.split("/")
117+
unprocessed_numerators = unprocessed_numerators.strip(' ')
118+
unprocessed_numerators = unprocessed_numerators.replace(' \cdot ', '*')
119+
unprocessed_numerators = unprocessed_numerators.replace(' * ', '*')
117120
unprocessed_numerators = unprocessed_numerators.replace(' ', '*')
118121
unprocessed_numerators = unprocessed_numerators.replace('*%', '\ %')
119122
unprocessed_numerators = unprocessed_numerators.replace('%*', '%\ ')

0 commit comments

Comments
 (0)