Skip to content

Commit 731e02f

Browse files
author
Rob DeJaco
committed
Add Tutorial and complete algorithm
- version 0.1.0 - update docs - update tests
1 parent 5220611 commit 731e02f

23 files changed

+1172
-285
lines changed

distillation/amundson_1958/main.py

Lines changed: 115 additions & 103 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,9 @@
55

66

77
class Model:
8-
def __init__(self, components: list=None, F: float=0., P: float=101325.,
9-
z_feed: list=None, RR: float=1, D: float=0, N: int=1, feed_stage: int=0, T_feed_guess: float=300.):
8+
def __init__(self, components: list = None, F: float = 0., P: float = 101325.,
9+
z_feed: list = None, RR: float = 1, D: float = 0, N: int = 1, feed_stage: int = 0,
10+
T_feed_guess: float = 300.):
1011
"""Distillation column with partial reboiler and total condenser.
1112
Feed is saturated liquid.
1213
@@ -56,29 +57,32 @@ def __init__(self, components: list=None, F: float=0., P: float=101325.,
5657
}
5758

5859
# create matrices for variables
59-
self.L = np.zeros(self.N + 1)
60-
self.V = np.zeros(self.N + 1)
61-
self.L_old = self.L[:]
62-
self.V_old = self.V[:]
60+
self.num_stages = self.N + 1
61+
self.stages = range(self.num_stages)
62+
self.L = np.zeros(self.num_stages)
63+
self.V = np.zeros(self.num_stages)
64+
self.L_old = np.zeros(self.num_stages)
65+
self.V_old = np.zeros(self.num_stages)
6366
self.L[N] = self.B
6467
self.V[0] = 0. # total condenser
65-
self.F = np.zeros(N + 1)
68+
self.F = np.zeros(self.num_stages)
6669
self.F[self.feed_stage] = self.F_feed
6770
self.z = {
68-
key: np.zeros(N + 1) for key in components
71+
key: np.zeros(self.num_stages) for key in components
6972
}
7073
# todo: implement component flow rates throughout
7174
self.l = {
72-
key: np.zeros(N + 1) for key in components
75+
key: np.zeros(self.num_stages) for key in components
7376
}
7477
for component in self.components:
7578
self.z[component][feed_stage] = self.z_feed[component]
7679

7780
self.T_feed = self.T_feed_guess
78-
self.T = self.T_feed_guess * np.ones(self.N + 1)
79-
self.T_old = self.T[:]
81+
self.T = self.T_feed_guess * np.ones(self.num_stages)
82+
self.T_old = np.zeros(self.num_stages)
8083
self.K = {
81-
key: self.K_func[key].eval_SI(self.T_feed, self.P_feed)*np.ones(self.N + 1) for key in self.components
84+
key: self.K_func[key].eval_SI(self.T_feed, self.P_feed) * np.ones(self.num_stages) for key in
85+
self.components
8286
}
8387

8488
# solver parameters
@@ -98,15 +102,21 @@ def h_j_rule(self, stage):
98102
99103
where the asterisk indicates the pure component enthalpy
100104
101-
.. todo::
102-
convert x mole fractions to dynamic expression
103-
104105
:return: :math:`h_j` [J/kmol]
105106
"""
106107
return sum(
107-
self.x[c][stage]*self.h_pure_rule(c, self.T[stage]) for c in self.components
108+
self.x_ij_expr(c, stage) * self.h_pure_rule(c, self.T[stage]) for c in self.components
108109
)
109110

111+
def x_ij_expr(self, i, j):
112+
"""
113+
114+
:param i: component name
115+
:param j: stage number
116+
:return: mole fraction on stage
117+
"""
118+
return self.l[i][j] / self.L[j]
119+
110120
def h_feed_rule(self, stage):
111121
"""Enthalpy of liquid in feed mixture
112122
Calculated for ideal mixture
@@ -120,7 +130,7 @@ def h_feed_rule(self, stage):
120130
:return: :math:`h` [J/kmol]
121131
"""
122132
return sum(
123-
self.z[c][stage]*self.h_pure_rule(c, self.T_feed) for c in self.components
133+
self.z[c][stage] * self.h_pure_rule(c, self.T_feed) for c in self.components
124134
)
125135

126136
def H_pure_rule(self, c, T):
@@ -142,42 +152,49 @@ def H_j_rule(self, stage):
142152
:return: :math:`H_j` [J/kmol]
143153
"""
144154
return sum(
145-
self.y[c][stage] * self.H_pure_rule(c, self.T[stage]) for c in self.components
155+
self.y_ij_expr(c, stage) * self.H_pure_rule(c, self.T[stage]) for c in self.components
146156
)
147157

158+
def y_ij_expr(self, i, j):
159+
"""
160+
161+
:param i: component name
162+
:param j: stage number
163+
:return: gas-phase mole fraction on stage
164+
"""
165+
return self.K_func[i].eval_SI(self.T[j], self.P_feed) * self.x_ij_expr(i, j)
166+
148167
def Q_condenser_rule(self):
149168
"""Condenser requirement can be determined from balances around total condenser"""
150169
return self.D * (1. + self.RR) * (self.h_j_rule(0) - self.H_j_rule(1))
151170

152171
def Q_reboiler_rule(self):
153-
"""Condenser requirement can be determined from balances around total condenser"""
154-
return self.D*self.h_j_rule(0) + self.B*self.h_j_rule(self.N) \
155-
- self.F_feed*self.h_feed_rule(self.feed_stage) - self.Q_condenser_rule()
156-
157-
def step_3_start(self):
158-
"""Beginning of step 3.
159-
Come back here whenever not converged at step 6.
160-
"""
161-
self.update_K_values()
162-
self.solve_component_mass_balances()
163-
164-
def run(self, num_iter=1):
172+
return self.D * self.h_j_rule(0) + self.B * self.h_j_rule(self.N) \
173+
- self.F_feed * self.h_feed_rule(self.feed_stage) - self.Q_condenser_rule()
174+
175+
def step_3_to_step_6(self):
176+
num_iter = 0
177+
while not self.T_is_converged():
178+
self.update_K_values()
179+
for i in self.components:
180+
self.solve_component_mass_bal(i)
181+
self.update_T_values()
182+
num_iter += 1
183+
print('while loop exits with %i iterations' % num_iter)
184+
185+
def run(self):
165186
self.generate_initial_guess()
166-
self.step_3_start()
187+
self.step_3_to_step_6()
167188
self.solve_energy_balances()
168-
for i in range(1, num_iter + 1):
169-
self.solve_component_mass_balances()
170-
if self.T_is_converged():
171-
print('temperature converged in %i iterations' % i)
172-
for j in range(1, num_iter + 1):
173-
converged = self.solve_energy_balances()
174-
if converged:
175-
print('flow rates converged in %i iterations' % i)
176-
177-
print('flow ratesdid not converge in %i iterations' % num_iter)
178-
return
179-
180-
print('temperature did not converge in %i iterations' % num_iter)
189+
main_loop = 0
190+
while not self.flow_rates_converged():
191+
for i in self.components:
192+
self.solve_component_mass_bal(i)
193+
self.update_T_values()
194+
self.step_3_to_step_6()
195+
self.solve_energy_balances()
196+
main_loop += 1
197+
print(main_loop)
181198

182199
def update_K_values(self):
183200
"""
@@ -187,7 +204,7 @@ def update_K_values(self):
187204
for c in self.components:
188205
self.K[c][:] = self.K_func[c].eval_SI(self.T[:], self.P_feed)
189206

190-
self.T_old = self.T[:]
207+
self.T_old[:] = self.T[:]
191208

192209
def update_T_values(self):
193210
"""Update temperatures in all stages
@@ -196,10 +213,16 @@ def update_T_values(self):
196213
.. todo::
197214
vectorize with matrix multiplication
198215
199-
.. todo::
200-
convert x mole fractions to dynamic expressions
201216
"""
202-
pass
217+
# update from old calculations
218+
for i in range(self.num_stages):
219+
# calculate stage temperature now that all liquid-phase mole fractions are known
220+
K_vals = [self.K_func[c].eval_SI for c in self.components]
221+
l_total = sum(self.l[c][i] for c in self.components)
222+
x_vals = [self.l[c][i] / l_total for c in self.components]
223+
self.T[i] = self.T_old[i] + self.df * (
224+
bubble_point(x_vals, K_vals, self.P_feed, self.T_old[i]) - self.T_old[i]
225+
)
203226

204227
def generate_initial_guess(self):
205228
"""
@@ -219,87 +242,75 @@ def generate_initial_guess(self):
219242
self.V[1:] = self.RR * self.D + self.D
220243

221244
def T_is_converged(self):
222-
"""Determine whether stage temperatures are converged.
223-
Mathematically, we require the following
224-
225-
.. math::
226-
227-
\\sqrt{\\left(T_{j,\\mathrm{new}} - T_{j,\\mathrm{old}}\\right)^2} < \\epsilon
245+
"""
246+
.. include:: temp-converge.rst
228247
229-
The temperature tolerance :math:`\epsilon`
230-
is attribute :attr:`distillation.amundson_1958.main.Model.temperature_tol`
231248
232249
:return: True if T is converged, else False
233-
234250
"""
235251
eps = np.abs(self.T - self.T_old)
236252
return eps.max() < self.temperature_tol
237253

238-
def solve_component_mass_balances(self):
239-
"""Calculate component liquid flow rates.
240-
241-
242-
.. todo: dont need to calculate D and A here as they are constant
254+
def solve_component_mass_bal(self, component):
255+
"""Solve component mass balances
243256
244-
.. todo: move second part to update_T_values method
257+
.. todo:
258+
dont need to calculate D and A here as they are constant
245259
246260
"""
261+
A, B, C, D = make_ABC(
262+
self.V, self.L, self.K[component], self.F, self.z[component], self.D, self.B, self.N
263+
)
264+
self.l[component][:] = solve_diagonal(A, B, C, D)
247265

248-
# save old L, V
249-
self.L_old[:] = self.L[:]
250-
self.V_old[:] = self.V[:]
251-
252-
l = {}
253-
for i, component in enumerate(self.components):
254-
A, B, C, D = make_ABC(
255-
self.V, self.L, self.K[component], self.F, self.z[component], self.D, self.B, self.N
256-
)
257-
l[component] = solve_diagonal(A, B, C, D)
258-
259-
# update from old calculations
260-
for i in range(self.N + 1):
261-
266+
def update_flow_rates(self):
267+
for i in self.stages:
268+
self.L[i] = sum(self.l[c][i] for c in self.components)
262269

263-
# calculate stage temperature now that all liquid-phase mole fractions are known
264-
K_vals = [self.K_func[c].eval_SI for c in self.components]
265-
x_vals = [self.x[c][i] for c in self.components]
266-
self.T[i] = self.T_old[i] + self.df * (
267-
bubble_point(x_vals, K_vals, self.P_feed, self.T_old[i]) - self.T_old[i]
268-
)
270+
self.V[0] = 0. # total condenser
271+
self.V[1] = (self.RR + 1.) * self.D
272+
for i in range(2, self.num_stages):
273+
self.V[i] = self.L[i - 1] + self.D - sum(self.F[k] for k in range(i))
269274

270275
def solve_energy_balances(self):
271276
"""Solve energy balances"""
272-
BE = np.zeros(self.N + 1)
273-
CE = np.zeros(self.N)
274-
DE = np.zeros(self.N + 1)
277+
278+
self.L_old[:] = self.L[:]
279+
self.V_old[:] = self.V[:]
280+
281+
BE = np.zeros(self.num_stages)
282+
CE = np.zeros(self.num_stages)
283+
DE = np.zeros(self.num_stages)
275284

276285
# total condenser
277286
BE[0] = 0.
278287
CE[0] = self.h_j_rule(0) - self.H_j_rule(1)
279-
DE[0] = self.F[0]*self.h_feed_rule(0) + self.Q_condenser_rule()
288+
DE[0] = self.F[0] * self.h_feed_rule(0) + self.Q_condenser_rule()
289+
290+
# stages 1 to N-1
291+
for j in range(1, self.N):
292+
BE[j] = self.H_j_rule(j) - self.h_j_rule(j - 1)
293+
CE[j] = self.h_j_rule(j) - self.H_j_rule(j + 1)
294+
DE[j] = self.F[j] * self.h_feed_rule(j) - self.D * (self.h_j_rule(j - 1) - self.h_j_rule(j)) \
295+
- sum(self.F[k] for k in range(j + 1)) * self.h_j_rule(j) \
296+
+ sum(self.F[k] for k in range(j)) * self.h_j_rule(j - 1)
280297

281298
# partial reboiler
282-
BE[self.N] = self.H_j_rule(self.N) - self.H_j_rule(self.N-1)
283-
DE[self.N] = self.F[self.N]*self.h_feed_rule(self.N) + self.Q_reboiler_rule() \
284-
+ self.B*(self.h_j_rule(self.N-1)-self.h_j_rule(self.N)) \
285-
- self.F[self.N-1]*self.h_j_rule(self.N-1)
299+
BE[self.N] = self.H_j_rule(self.N) - self.h_j_rule(self.N - 1)
300+
DE[self.N] = self.F[self.N] * self.h_feed_rule(self.N) + self.Q_reboiler_rule() \
301+
- self.B * (self.h_j_rule(self.N - 1) - self.h_j_rule(self.N)) \
302+
- self.F[self.N - 1] * self.h_j_rule(self.N - 1)
286303

287-
# stages 1 to N-1
288-
BE[1:self.N] = list(self.H_j_rule(j) - self.h_j_rule(j-1) for j in range(1, self.N))
289-
CE[1:self.N] = list(self.h_j_rule(j) - self.H_j_rule(j+1) for j in range(1, self.N))
290-
DE[1:self.N] = list(self.F[j]*self.h_feed_rule(j) + self.D*(self.h_j_rule(j-1) - self.h_j_rule(j))
291-
- sum(self.F[k] for k in range(j+1))*self.h_j_rule(j)
292-
+ sum(self.F[k] for k in range(j))*self.h_j_rule(j-1)
293-
for j in range(1, self.N))
294304
A = diags(
295-
diagonals=[BE[1:], CE[1:]],
305+
diagonals=[BE[1:], CE[1:-1]],
296306
offsets=[0, 1],
297307
shape=(self.N, self.N),
298308
format='csr'
299309
)
300310
self.V[1:] = linalg.spsolve(A, DE[1:])
301-
for i in range(self.N):
302-
self.L[i] = self.V[i+1] - self.D + sum(self.F[k] for k in range(i + 1))
311+
self.L[0] = self.RR * self.D
312+
for i in range(1, self.N):
313+
self.L[i] = self.V[i + 1] - self.D + sum(self.F[k] for k in range(i + 1))
303314
self.L[self.N] = self.B
304315

305316
def flow_rates_converged(self):
@@ -308,7 +319,8 @@ def flow_rates_converged(self):
308319
Use the mathematical criterion in :meth:`Model.is_below_relative_error`
309320
310321
"""
311-
return self.is_below_relative_error(self.L, self.L_old) and self.is_below_relative_error(self.V, self.V_old)
322+
return self.is_below_relative_error(self.L, self.L_old) and self.is_below_relative_error(self.V[1:],
323+
self.V_old[1:])
312324

313325
def is_below_relative_error(self, new, old):
314326
"""Determine relative error between two vectors
@@ -325,7 +337,7 @@ def is_below_relative_error(self, new, old):
325337
:rtype: bool
326338
327339
"""
328-
return np.abs((new - old)/new).max() < self.flow_rate_tol
340+
return np.abs((new - old) / new).max() < self.flow_rate_tol
329341

330342

331343
def make_ABC(V: np.array, L: np.array, K: np.array, F: np.array, z: np.array,

distillation/equilibrium_data/depriester_charts.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -33,10 +33,10 @@ def eval(self, T, p):
3333
:param p: pressure in psia
3434
:return: K-value for component at specific *T* and *p*
3535
"""
36-
import math
37-
return math.exp(
36+
import numpy as np
37+
return np.exp(
3838
self.a_T1 / T / T + self.a_T2 / T + self.a_T6
39-
+ self.a_p1 * math.log(p) + self.a_p2 / p / p + self.a_p3 / p
39+
+ self.a_p1 * np.log(p) + self.a_p2 / p / p + self.a_p3 / p
4040
)
4141

4242
def eval_SI(self, T, p):

docs/build/doctrees/about.doctree

0 Bytes
Binary file not shown.

docs/build/doctrees/amundson.doctree

37.9 KB
Binary file not shown.
47.2 KB
Binary file not shown.

docs/build/doctrees/install.doctree

-154 Bytes
Binary file not shown.

docs/build/html/_sources/about.rst.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,7 @@ Perform a calculation for distillation of mixtures
55
of chemical compounds.
66

77
.. image:: ../../distillation/column_diagram.png
8-
:width: 400
8+
:width: 300
99
:align: center
1010

1111
Currently only supports configurations with total condensers and partial reboilers.

0 commit comments

Comments
 (0)