t(min) | 0.00 | 5.00 | 10.00 | 15.00 | 20.00 | 25.00 | 30.00 | 35.00 | 40.00 |
C_{\text{tracer}} \ (g/dm^3) | 0.00 | 4.00 | 6.00 | 7.00 | 5.00 | 3.00 | 2.00 | 1.00 | 0.00 |
In class activity: Distribution of residence time
Lecture notes for chemical reaction engineering
Finding the RTD function by experiment
Plot the C-curve and E-curve for a response to a pulse input into a reactor given in Table 1.
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import quad
from scipy.interpolate import interp1d
= np.array([
data_p1 0, 0), (5, 4), (10, 6), (15, 7), (20, 5),
(25, 3), (30, 2), (35, 1), (40, 0)
(=[('t(min)', float), ('$C_{tracer} \\ (g/dm^3)$', float)])
], dtype
= data_p1["t(min)"]
t = data_p1["$C_{tracer} \\ (g/dm^3)$"] c
= interp1d(t, c, kind="cubic", fill_value="extrapolate")
c_interp
# Plot C(t)
= np.linspace(0, np.max(t), 500)
t_plot = c_interp(t_plot)
c_plot
='C(t) experimental')
plt.scatter(t, c, label='C(t) fitted')
plt.plot(t_plot, c_plot, label'Time (min)')
plt.xlabel('$C_{tracer} \\ (g/dm^3)$')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.0,10)
plt.ylim(
plt.legend() plt.show()
# Normalize concentration to calculate E(t)
= np.trapz(c, t)
integral_c = c / integral_c
et
= interp1d(t, et, kind="cubic", fill_value="extrapolate")
et_interp
# Plot C(t)
= et_interp(t_plot)
et_plot
='E(t) experimental')
plt.scatter(t, et, label='E(t) fitted')
plt.plot(t_plot, et_plot, label'Time (min)')
plt.xlabel('E(t)')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.0,0.06)
plt.ylim(
plt.legend() plt.show()
Constructing the C(t) and E(t) Curves
A sample of the tracer hytane at 320 K was injected as a pulse into a reactor, and the effluent concentration was measured as a function of time, resulting in the data shown in Table 2
t(min) | 0.00 | 1.00 | 2.00 | 3.00 | 4.00 | 5.00 | 6.00 | 7.00 | 8.00 | 9.00 | 10.00 | 12.00 | 14.00 |
C (g/m^3) | 0.00 | 1.00 | 5.00 | 8.00 | 10.00 | 8.00 | 6.00 | 4.00 | 3.00 | 2.20 | 1.50 | 0.60 | 0.00 |
The measurements represent the exact concentrations at the times listed and not average values between the various sampling tests.
- Construct a figure showing the tracer concentration C(t) as a function of time.
- Construct a figure showing E(t) as a function of time.
import numpy as np
import matplotlib.pyplot as plt
from scipy.integrate import quad
from scipy.interpolate import interp1d
= np.array([
data_p2 0, 0), (1, 1), (2, 5), (3, 8), (4, 10),
(5, 8), (6, 6), (7, 4), (8, 3), (9, 2.2),
(10, 1.5), (12, 0.6), (14, 0)
(=[('t(min)', float), ('C ($g/m^3$)', float)])
], dtype
= data_p2["t(min)"]
t = data_p2["C ($g/m^3$)"] c
= interp1d(t, c, kind="cubic", fill_value="extrapolate")
c_interp
# Plot C(t)
= np.linspace(0, np.max(t), 500)
t_plot = c_interp(t_plot)
c_plot
='C(t) experimental')
plt.scatter(t, c, label='C(t) fitted')
plt.plot(t_plot, c_plot, label'Time (min)')
plt.xlabel('$C_{tracer} \\ (g/dm^3)$')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.
plt.legend() plt.show()
# Normalize concentration to calculate E(t)
= np.trapz(c, t)
integral_c = c / integral_c
et
= interp1d(t, et, kind="cubic", fill_value="extrapolate")
et_interp
# Plot C(t)
= et_interp(t_plot)
et_plot
='E(t) experimental')
plt.scatter(t, et, label='E(t) fitted')
plt.plot(t_plot, et_plot, label'Time (min)')
plt.xlabel('E(t)')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.
plt.legend() plt.show()
The interpolation results in unrealistic values for C(t) and E(t) between 0 and 1. An undershoot is observed resulting in negative concentrations.
The interp1d
function with kind='cubic'
performs cubic spline interpolation, which can sometimes result in overshooting, especially when the data points have steep slopes or sharp changes. Cubic splines are designed to produce smooth curves, but this smoothness can lead to values that fall below zero in regions where the slope changes rapidly.
To avoid the cubic interpolation returning negative values, one can use the scipy.interpolate.PchipInterpolator
instead of interp1d
. The PchipInterpolator
uses piecewise cubic Hermite interpolating polynomials (PCHIP stands for Piecewise Cubic Hermite Interpolating Polynomial) which preserve the shape and monotonicity of the data. This ensures that the interpolated values are monotonic and do not overshoot, which helps in avoiding negative values for this type of data.
from scipy.interpolate import PchipInterpolator
= PchipInterpolator(t, c, extrapolate=True)
c_interp = PchipInterpolator(t, et, extrapolate=True)
et_interp
= c_interp(t_plot)
c_plot = et_interp(t_plot)
et_plot
# Plot side by side
= plt.subplots(1, 2, figsize=(12, 5))
fig, (ax1, ax2)
# Plot C(t)
='C(t) experimental')
ax1.scatter(t, c, label='C(t) fitted')
ax1.plot(t_plot, c_plot, label'Time (min)')
ax1.set_xlabel('$C_{tracer} \\ (g/dm^3)$')
ax1.set_ylabel(min(t_plot), np.max(t_plot))
ax1.set_xlim(np.
ax1.legend()
# Plot E(t)
='E(t) experimental')
ax2.scatter(t, et, label='E(t) fitted')
ax2.plot(t_plot, et_plot, label'Time (min)')
ax2.set_xlabel('E(t)')
ax2.set_ylabel(min(t_plot), np.max(t_plot))
ax2.set_xlim(np.
ax2.legend()
plt.tight_layout() plt.show()
Mean Residence Time and Variance Calculations
Using the data given in Table 2 in the previous problem
- Construct the F(t) curve.
- Calculate the mean residence time, t_m.
- Calculate the variance about the mean, \sigma^2.
- Calculate the fraction of fluid that spends between 3 and 6 minutes in the reactor.
- Calculate the fraction of fluid that spends 2 minutes or less in the reactor.
- Calculate the fraction of the material that spends 3 minutes or longer in the reactor.
# Define cumulative distribution F(t)
def f_interp(t):
return np.array([quad(et_interp,
0,
ti, =1000)[0]
limitfor ti in np.atleast_1d(t)])
# Mean residence time function
= lambda t: t * et_interp(t)
tau_func
# Variance function
= lambda t, tm: (t - tm) ** 2 * et_interp(t)
variance_func
# Skewness function
= lambda t, tm: (t - tm) ** 3 * et_interp(t)
skewness_func
# Calculate mean residence time (t_m)
= quad(tau_func, 0, np.max(t))
tau, _
# Calculate variance (σ²)
= quad(variance_func, 0, np.max(t), args=(tau,))
variance, _
# Calculate skewness (
= variance**0.5
sigma = 1.0 / (sigma**1.5)
fac = quad(skewness_func, 0, np.max(t), args=(tau,))
integral, _ = fac * integral skewness
= f_interp(t_plot)
f_plot
='F(t)')
plt.plot(t_plot, f_plot, label'Time (min)')
plt.xlabel('F(t)')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.0,1)
plt.ylim(
plt.legend() plt.show()
# Calculate specific time fractions
= quad(et_interp, 3, 6)
fraction_3_to_6, _ = quad(et_interp, 0, 2)
fraction_below_2, _ = quad(et_interp, 3, np.max(t)) fraction_above_3, _
Mean Residence Time tau: \approx 5.126 min.
Variance \sigma^2: \approx 6.096 min^2
Skewness s^3: \approx 3.178 min^3
Specific Time Fractions:
Fraction of material that spends between 3 and 6 minutes: \approx 0.501
Fraction of material that spends less than 2 minutes: \approx 0.063
Fraction of material that spends longer than 3 minutes: \approx 0.805
Step experiment
A step experiment is made on a reactor at C_{in} ( t \ge 0 ) = 0.5 \ mol/dm^3. The results are as given in Table 3.
t (min) | 0.00 | 1.00 | 2.00 | 3.00 | 4.00 | 5.00 | 6.00 | 7.00 | 8.00 |
C_{out} (mol/L) | 0.00 | 0.00 | 0.10 | 0.20 | 0.30 | 0.40 | 0.50 | 0.50 | 0.50 |
Plot the Concentration - time curve, F(t) curve and E(t) curve.
= np.array([
data_p4 0, 0), (1, 0), (2, 0.1), (3, 0.2), (4, 0.3),
(5, 0.4), (6, 0.5), (7, 0.5), (8, 0.5)
(=[('t (min)', float), ('$C_{out} (mol/L)$', float)])
], dtype
= data_p4["t (min)"]
t = data_p4["$C_{out} (mol/L)$"] c
= interp1d(t, c, kind="linear", fill_value="extrapolate")
c_interp
# Plot C(t)
= np.linspace(0, np.max(t), 500)
t_plot = c_interp(t_plot)
c_plot
='C(t) experimental')
plt.scatter(t, c, label='C(t) fitted')
plt.plot(t_plot, c_plot, label'Time (min)')
plt.xlabel('$C_{tracer} \\ (g/dm^3)$')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.
plt.legend() plt.show()
= 0.5 # mol/l
C0 = c/C0
f = interp1d(t, f, kind="linear", fill_value="extrapolate")
f_interp
= f_interp(t_plot)
f_plot
='F(t) experimental')
plt.scatter(t, f, label='F(t) fitted')
plt.plot(t_plot, f_plot, label'Time (min)')
plt.xlabel('F(t)')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.0,1)
plt.ylim(
plt.legend() plt.show()
To calculate E(t) as the derivative of F(t) with respect to time t, we can use the np.gradient
function, which provides a numerical differentiation method. Once E(t) is calculated, we can plot it against time.
# Calculate E(t) as the derivative of F(t) with respect to t
= np.gradient(f_plot, t_plot)
et_plot = interp1d(t_plot, et_plot, kind="linear", fill_value="extrapolate")
et_interp
# Plot C(t)
='E(t)')
plt.plot(t_plot, et_plot, label'Time (min)')
plt.xlabel('E(t)')
plt.ylabel(min(t_plot), np.max(t_plot))
plt.xlim(np.
plt.legend() plt.show()
Mean conversion using segragation model
Using the pulse input response in Table 1, calculate the mean conversion in the reactor for first order reaction: \ce{A ->[k] Products} with k = 0.1 [min]^{-1}.
For the first order reaction, conversion in each globule can be written as
X(t) = 1 - e^{-kt} = 1 - e^{-0.1 t}
The mean conversion can be calculated as
\bar{X} = \int_0^\infty \left( 1 - e^{-0.1 t}\right ) E(t)dt
# For data in table 1
= data_p1["t(min)"]
t = data_p1["$C_{tracer} \\ (g/dm^3)$"]
c
= 0.1
k # For data in table 2
# t = data_p2["t(min)"]
# c = data_p2["C ($g/m^3$)"]
= 1 - np.exp(-k * t)
x
= np.trapz(c, t)
integral_c = c / integral_c
et
= x * et
xet
= PchipInterpolator(t, xet, extrapolate=True)
xet_interp
= quad(xet_interp, 0, np.max(t)) mean_x,_
The mean conversion is 74.8 %
Citation
@online{utikar2024,
author = {Utikar, Ranjeet},
title = {In Class Activity: {Distribution} of Residence Time},
date = {2024-05-09},
url = {https://cre.smilelab.dev//content/notes/10-distribution-of-residence-time/in-class-activities.html},
langid = {en}
}