5
5
6
6
7
7
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. ):
10
11
"""Distillation column with partial reboiler and total condenser.
11
12
Feed is saturated liquid.
12
13
@@ -56,29 +57,32 @@ def __init__(self, components: list=None, F: float=0., P: float=101325.,
56
57
}
57
58
58
59
# 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 )
63
66
self .L [N ] = self .B
64
67
self .V [0 ] = 0. # total condenser
65
- self .F = np .zeros (N + 1 )
68
+ self .F = np .zeros (self . num_stages )
66
69
self .F [self .feed_stage ] = self .F_feed
67
70
self .z = {
68
- key : np .zeros (N + 1 ) for key in components
71
+ key : np .zeros (self . num_stages ) for key in components
69
72
}
70
73
# todo: implement component flow rates throughout
71
74
self .l = {
72
- key : np .zeros (N + 1 ) for key in components
75
+ key : np .zeros (self . num_stages ) for key in components
73
76
}
74
77
for component in self .components :
75
78
self .z [component ][feed_stage ] = self .z_feed [component ]
76
79
77
80
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 )
80
83
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
82
86
}
83
87
84
88
# solver parameters
@@ -98,15 +102,21 @@ def h_j_rule(self, stage):
98
102
99
103
where the asterisk indicates the pure component enthalpy
100
104
101
- .. todo::
102
- convert x mole fractions to dynamic expression
103
-
104
105
:return: :math:`h_j` [J/kmol]
105
106
"""
106
107
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
108
109
)
109
110
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
+
110
120
def h_feed_rule (self , stage ):
111
121
"""Enthalpy of liquid in feed mixture
112
122
Calculated for ideal mixture
@@ -120,7 +130,7 @@ def h_feed_rule(self, stage):
120
130
:return: :math:`h` [J/kmol]
121
131
"""
122
132
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
124
134
)
125
135
126
136
def H_pure_rule (self , c , T ):
@@ -142,42 +152,49 @@ def H_j_rule(self, stage):
142
152
:return: :math:`H_j` [J/kmol]
143
153
"""
144
154
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
146
156
)
147
157
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
+
148
167
def Q_condenser_rule (self ):
149
168
"""Condenser requirement can be determined from balances around total condenser"""
150
169
return self .D * (1. + self .RR ) * (self .h_j_rule (0 ) - self .H_j_rule (1 ))
151
170
152
171
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 ):
165
186
self .generate_initial_guess ()
166
- self .step_3_start ()
187
+ self .step_3_to_step_6 ()
167
188
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 )
181
198
182
199
def update_K_values (self ):
183
200
"""
@@ -187,7 +204,7 @@ def update_K_values(self):
187
204
for c in self .components :
188
205
self .K [c ][:] = self .K_func [c ].eval_SI (self .T [:], self .P_feed )
189
206
190
- self .T_old = self .T [:]
207
+ self .T_old [:] = self .T [:]
191
208
192
209
def update_T_values (self ):
193
210
"""Update temperatures in all stages
@@ -196,10 +213,16 @@ def update_T_values(self):
196
213
.. todo::
197
214
vectorize with matrix multiplication
198
215
199
- .. todo::
200
- convert x mole fractions to dynamic expressions
201
216
"""
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
+ )
203
226
204
227
def generate_initial_guess (self ):
205
228
"""
@@ -219,87 +242,75 @@ def generate_initial_guess(self):
219
242
self .V [1 :] = self .RR * self .D + self .D
220
243
221
244
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
228
247
229
- The temperature tolerance :math:`\epsilon`
230
- is attribute :attr:`distillation.amundson_1958.main.Model.temperature_tol`
231
248
232
249
:return: True if T is converged, else False
233
-
234
250
"""
235
251
eps = np .abs (self .T - self .T_old )
236
252
return eps .max () < self .temperature_tol
237
253
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
243
256
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
245
259
246
260
"""
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 )
247
265
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 )
262
269
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 ))
269
274
270
275
def solve_energy_balances (self ):
271
276
"""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 )
275
284
276
285
# total condenser
277
286
BE [0 ] = 0.
278
287
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 )
280
297
281
298
# 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 )
286
303
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 ))
294
304
A = diags (
295
- diagonals = [BE [1 :], CE [1 :]],
305
+ diagonals = [BE [1 :], CE [1 :- 1 ]],
296
306
offsets = [0 , 1 ],
297
307
shape = (self .N , self .N ),
298
308
format = 'csr'
299
309
)
300
310
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 ))
303
314
self .L [self .N ] = self .B
304
315
305
316
def flow_rates_converged (self ):
@@ -308,7 +319,8 @@ def flow_rates_converged(self):
308
319
Use the mathematical criterion in :meth:`Model.is_below_relative_error`
309
320
310
321
"""
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 :])
312
324
313
325
def is_below_relative_error (self , new , old ):
314
326
"""Determine relative error between two vectors
@@ -325,7 +337,7 @@ def is_below_relative_error(self, new, old):
325
337
:rtype: bool
326
338
327
339
"""
328
- return np .abs ((new - old )/ new ).max () < self .flow_rate_tol
340
+ return np .abs ((new - old ) / new ).max () < self .flow_rate_tol
329
341
330
342
331
343
def make_ABC (V : np .array , L : np .array , K : np .array , F : np .array , z : np .array ,
0 commit comments