#!/usr/bin/env python
# coding: utf-8
# # Tutorial 3 - Part 2
# _applying our knowledge to solve Chem Reactor problems_
#
# We now know ODEs
#
# It is time everything together with other cool python modules to solve engineering problems.
#
# If you don't know how to solve these problems it's ok, start small and make your way up.
#
# The easiest example we could do would be to analyze the chemical reaction kinetics in the simplest case:
#
# # Problem 1:
#
# *A -> B *
#
# We assume it is a first order reaction, and we use the design equation from the Batch reactor:
#
# So that means that $-r_A = k C_A$.
#
# Let's assume $k = 10^{-3} 1/s$ and $C_A(t=0) = 10$ moles
#
# We can expect the half-time of A to be around 10000 seconds, which is ~3 hours.
#
# Ok, let's see that through Python.
#
# We want to track the concentration, which we will call $y_A$.
#
# $dy_A/dt = - k C_A = -k y_A$
#
# An important tool we need to use here is the ODEINT module:
#
# `from scipy.integrate import odeint`
#
# In this module if you have an equation $\frac{dy}{dt} = f(y,t)$ you need:
#
# - To define at least the Python function f which implements the mathematical function $f$. It must take a vector y and a time t, and return the new vector f(y,t).
#
#
# Some additional commands that you may need in your script:
#
# ```python
# #lets import the modules # plotting everything inline
# # %matplotlib inline
# import matplotlib.pyplot as plt # plotting modules
# plt.style.use('presentation') # just have in your script for prettier plotting
# import numpy as np # our matlab-like module
# from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
# ```
#
# To plot your data use these commands:
#
# ```python
# plt.plot(times, y_A_exact,'k--', label='analytical')
# plt.xlabel('$t [s]$')
# plt.ylabel('$C_A [moles]$')
# plt.legend()
# plt.savefig('odeint.pdf')
# plt.show()
#
# ```
#
# In[ ]:
#let's import the modules # plotting everything inline
get_ipython().run_line_magic('matplotlib', 'inline')
import matplotlib.pyplot as plt # plotting modules
#plt.style.use('presentation') # just have in your script for prettier plotting
import numpy as np # our matlab-like module
from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
#reaction order
def f(y, t):
return -k*y
k = 0.001 # L/(mol*sec)
time_start = 0 #s
time_finish = 10000 #s
N_points = 50
times = np.linspace(time_start,time_finish,N_points)
y_A0 = 10. # moles
y_A_calc = odeint(f, y_A0, times)
plt.plot(times, y_A_calc, 'ro-', label='integrated')
y_A_exact = y_A0*np.exp(-k*times)
plt.plot(times, y_A_exact,'k--', label='analytical')
plt.xlabel('$t [s]$')
plt.ylabel('$C_A [moles]$')
plt.legend()
plt.savefig('odeint.pdf')
plt.show()
# Now let's do the same calculation as case number 1 (of this problem), but this time we will use the conversion factor $X_A$ as our variable:
#
# $X_A = \dfrac{N_{A0} - N_A}{N_{A0}} = \dfrac{C_{A0} - C_A}{C_{A0}}$ if the volume $V=const$
#
# So the design equation looks like:
#
# $\dfrac{d X_A}{dt} = (-r_A/N_{A0}) V $
#
# In[ ]:
get_ipython().run_line_magic('reset', '')
get_ipython().run_line_magic('matplotlib', 'inline')
import matplotlib.pyplot as plt # plotting modules
plt.style.use('ggplot') # just have in your script for prettier plotting
import numpy as np # our matlab-like module
from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
plt.clf()
k=0.001
y_A0 = 10.
time_start = 0 #s
time_finish = 10000 #s
N_points = 50
times = np.linspace(time_start,time_finish,N_points)
y_A_exact = y_A0*np.exp(-k*times)
x_A = ((y_A0 - y_A_exact)/y_A0)
# print(y_A_exact)
# print(x_A)
plt.plot(times, x_A, 'ro-')
plt.xlabel('$t [s]$')
plt.ylabel('$X_A$')
# plt.savefig('conversion factor.pdf')
plt.show()
# ## Problem 2:
#
# *2A ->B*
#
# We assume that it is a first order reaction and we use the design equation from the Batch reactor:
#
# So that means that $-r_A = k [C_A]^2$.
#
# Let's assume $k = 10^{-3} 1/s$ and $C_A(t=0) = 10$ moles
#
# So we can expect the half-time of A to be around 10000 seconds which is ~3 hours.
#
# Ok let's see that through Python.
#
# We want to track the concentration which we will call $y_A$.
#
# $dy_A/dt = - k C_A = -k y^2_A$
#
# The exact solution here would be
#
# $ 1/y_A = kt + C$
# $y_A(0) = 10$ => $C = 1/10$
#
#
# $1/y_A = kt + 1/y_A_0$
#
# So now it is easier to track $1/y_A$ rather than the function itself
#
# In[ ]:
# %matplotlib inline
get_ipython().run_line_magic('reset', '')
import matplotlib.pyplot as plt # plotting modules
#plt.style.use('presentation') # just have in your script for prettier plotting
import numpy as np # our matlab-like module
from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
#reaction order
def f(y, t):
return -k*y**2
k = 0.001 # 1/sec
time_start = 0 #s
time_finish = 10 #s
N_points = 50
times = np.linspace(time_start,time_finish,N_points)
y_A0 = 10. # moles
y_A_calc = odeint(f, y_A0, times)
plt.plot(times, 1./y_A_calc, 'ro-', label='integrated')
inverse_y_A_exact = (1/y_A0 + k*times)
plt.plot(times, inverse_y_A_exact,'k--', label='analytical')
plt.xlabel('$t [s]$')
plt.ylabel('$1/C_A [moles]$')
plt.legend()
# plt.savefig('odeint.pdf')
plt.show()
# ## Problem 3: CSTR
#
# Having had solved ODEs, now we are going to solve "Simple" algebraic equations.
#
# Given a continuously stirred tank reactor with a volume of $66 m^3$ where the reaction $A \to B$ occurs, at a rate of $−r_A= k C^2_A$ ($k=3 L/mol/h$), with an entering molar flow of $F_{A0} = 5 mol/h$ and a volumetric flowrate of $\upsilon = 10 L/h$, what is the exit concentration of A?
#
# From a mole balance we know that at steady state $0=F_{A0}−F_A+ V \cdot r_A$. That equation simply states the sum of the molar flow of A in in minus the molar flow of A out plus the molar rate A is generated is equal to zero at steady state. This is directly the equation we need to solve. We need the following relationship:
# In[ ]:
#Copyright (C) 2013 by John Kitchin
from scipy.optimize import fsolve
Fa0 = 5.0
v0 = 10.
V = 66000.0 # reactor volume L^3
k = 3.0 # rate constant L/mol/h
def func(Ca):
"Mole balance for a CSTR. Solve this equation for func(Ca)=0"
Fa = v0 * Ca # exit molar flow of A
ra = -k * Ca**2 # rate of reaction of A L/mol/h
return Fa0 - Fa + V * ra
# CA guess that 90 % is reacted away
CA_guess = 0.1 * Fa0 / v0
CA_sol, = fsolve(func, CA_guess)
print('The exit concentration is {0} mol/L'.format(CA_sol))
# In[ ]:
# Now let's solve the same problem using Python's
# built-in functionality through a Python package odeint
# Step 1:
# Import modules into Python
import numpy as np
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
plt.style.use('fivethirtyeight')
# integration of ODEs so you don't have
# to write your finite difference yourself
from scipy.integrate import odeint
# Step 2:
# define the required parameters and functions
def f(y, t, k):
dydt = -k*t
return dydt
# define initial values, arrays, intervals
t_0 = 0.
t_f = 10.
npoints = 10
t_array = np.linspace(t_0, t_f, npoints + 1)
y_0 = 1.
k = 2.
h = t_array[1] - t_array[0]
y_num = np.zeros_like(t_array)
# let's also define a numerical solution
t_array_analyt = np.linspace(t_0, t_f, 1000 + 1)
y_analyt = -k*t_array_analyt**2/2. + y_0
# here the syntax is
# module odeint(right hand side function,
# initial value of y,
# array for the time values
# additional parameters like k in a tuple (k,))
# if we needed two parameters like k1, k2
# then args=(k1, k2)
y_num = odeint(f, y_0, t_array, args=(k,))
plt.xlabel('$t, s$')
plt.ylabel('$y$')
plt.plot(t_array_analyt, y_analyt, 'k--', lw=3,label='analytical')
plt.plot(t_array, y_num, 'go',alpha=0.6, label='numerical')
plt.legend()
# # Convergence
#
# Let's compare a set number of intervals vs. 1001 intervals from the analytical, at a specific time.
# For the sake of simplicity, I chose the last point. And because typically, error increases as we increase the number of steps, since every step is dependant on the last.
#
#
# When we have 5 intervals the difference in $y$ is ~4.13922407e-08
#
# When we have 10 intervals the difference in $y$ is ~2.98024077e-08
#
# When we have 20 intervals the difference in $y$ is ~7.4505806e-09
#
# When we have 30 intervals the difference in $y$ is ~3.31137073e-09
#
# When we have 100 intervals the difference in $y$ is ~2.98030045e-10
#
#
# In these cases, we have an overestimate, which gets more accurate as we increase the number of intervals.
#
# Although it is harder to see visually, most approximations are fairly accurate.
# In[ ]:
#https://en.wikipedia.org/wiki/Euler_method
#https://rosettacode.org/wiki/Euler_method
import numpy as np
import matplotlib.pyplot as plt
get_ipython().run_line_magic('matplotlib', 'inline')
plt.style.use('fivethirtyeight')
from scipy.integrate import odeint
def f(y, t, k):
dydt = -k*t
return dydt
t_0 = 0.
t_f = 10
npoints0 = 5
t_array0 = np.linspace(t_0, t_f, npoints0 + 1)
npoints1 = 10
t_array1 = np.linspace(t_0, t_f, npoints1 + 1)
npoints2 = 20
t_array2 = np.linspace(t_0, t_f, npoints2 + 1)
npoints3 = 30
t_array3 = np.linspace(t_0, t_f, npoints3 + 1)
npoints4 = 100
t_array4 = np.linspace(t_0, t_f, npoints4 + 1)
y_0 = 1.
k = 2.
h = t_array1[1] - t_array1[0]
t_array_analyt = np.linspace(t_0, t_f, 1000 + 1)
y_analyt = -k*t_array_analyt**2/2. + y_0
y_num0 = np.zeros_like(t_array0)
y_num0 = odeint(f, y_0, t_array0, args=(k,))
y_num1 = np.zeros_like(t_array1)
y_num1 = odeint(f, y_0, t_array1, args=(k,))
y_num2 = np.zeros_like(t_array2)
y_num2 = odeint(f, y_0, t_array2, args=(k,))
y_num3 = np.zeros_like(t_array3)
y_num3 = odeint(f, y_0, t_array3, args=(k,))
y_num4 = np.zeros_like(t_array4)
y_num4 = odeint(f, y_0, t_array4, args=(k,))
plt.xlabel('$t, s$')
plt.ylabel('$y$')
plt.plot(t_array_analyt, y_analyt, 'k--', lw=3,label='analytical')
plt.plot(t_array0, y_num0, 'go',alpha=0.4, label='numerical0')
plt.plot(t_array1, y_num1, 'bo',alpha=0.4, label='numerical1')
plt.plot(t_array2, y_num2, 'co',alpha=0.4, label='numerical2')
plt.plot(t_array3, y_num3, 'mo',alpha=0.4, label='numerical3')
plt.plot(t_array4, y_num4, 'yo',alpha=0.4, label='numerical3')
plt.legend()
print(y_num0[5] - y_analyt[1000])
print(y_num1[10] - y_analyt[1000])
print(y_num2[20] - y_analyt[1000])
print(y_num3[30] - y_analyt[1000])
print(y_num4[100] - y_analyt[1000])
# # Problem 4
#
# This problem should be familiar, but we will throw in a twist.
#
# When $A -> B$ as simple reaction with the rate $-r_A = k C_A$ and the solution for A could be found in this way:
# $\dfrac{d C_A}{dt} = -k C_A(t)$
# $C_{A0} = 10$ mole/L
# In[ ]:
import matplotlib.pyplot as plt # plotting modules
get_ipython().run_line_magic('matplotlib', 'inline')
plt.style.use('ggplot') # just have in your script for prettier plotting
# instead of 'ggplot' you can use 'presentation'
import numpy as np # our matlab-like module
from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
#step 1 definition of the function:
def f(C_A, t):
dcadt = - k*C_A
return dcadt
k = 1
C_A0 = 10.
time_start = 0.
time_finish = 10000.
N_points = 100
time_array = np.linspace(0, 10., N_points)
C_num = odeint(f,C_A0, time_array)
plt.plot(time_array, C_num, 'r-')
plt.plot(time_array, C_num, 'go', alpha=0.8, fillstyle='none') # alpha - 80% transparency
# fillstyle - none - means no filling of the circles
plt.xlabel('t, s')
plt.ylabel('$C_A(t)$, mole/L')
plt.show()
#
# Now the same thing for B
# $ -(r_B) = -(-r_A) = - k C_A$
# so the design equation will have an additional equation:
#
# $\dfrac{d C_B}{dt} = k C_A(t)$
# $C_{B0} = 0$ mol/L
#
# In[ ]:
import matplotlib.pyplot as plt # plotting modules
get_ipython().run_line_magic('matplotlib', 'inline')
plt.style.use('ggplot') # just have in your script for prettier plotting
# instead of 'ggplot' you can use 'presentation'
import numpy as np # our matlab-like module
from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
#step 1 definition of the function:
def f(C_list, t):
C_A = C_list[0]
C_B = C_list[1]
dcadt = - k*C_A
dcbdt = k*C_A
return [dcadt, dcbdt]
k = 2
C_A0 = 10.
C_B0 = 0.
time_start = 0.
time_finish = 10.
N_points = 100
time_array = np.linspace(0, 10., N_points)
C0_list = [1, 0.]
C_num_list = odeint(f, C0_list, time_array)
print(C_num_list.shape)
C_A_num = C_num_list[:,0]
C_B_num = C_num_list[:,1]
plt.plot(time_array, C_A_num, 'r--', fillstyle='none', label='$A$')
plt.plot(time_array, C_B_num, 'g--', fillstyle='none', label='$B$')
plt.xlabel('t, s')
plt.ylabel('$C(t)$, mole/L')
plt.legend()
plt.show()
# To see precisely what the values were from the integration, we can use
#
# ```python
# print(C_num_list[:,0])
# ```
# In[ ]:
C_num_list[:,0]
# # Problem 5
# This one is more complicated:
#
#
# A system of ODES
#
# $ A + B \to C$, $k_1 = 1$ L/(mol s)
#
# $ B + C \to D$, $k_1 = 1.5$ L/(mol s)
#
# with initial concentrations
# $C_{A0} = 1$ mol/L
# $C_{B0} = 1$ mol/L
# $C_{C0} = 0$ mol/L
# $C_{D0} = 0$ mol/L
#
# 1. Calculate concentration of each component
# 2. Calculate the selectivity parameters $S = \dfrac{C}{(C+D)}$, S(t=0) = 1.0
#
# r1 = k1*C_A*C_B
# r2 = k2*C_B*C_C
#
# dcAdt = -r1
# dcBdt = -r1 - r2
# dcCdt = r1 - r2
# dcDdt = r2
#
#
# In[ ]:
import matplotlib.pyplot as plt # plotting modules
get_ipython().run_line_magic('matplotlib', 'inline')
plt.style.use('ggplot') # just have in your script for prettier plotting
# instead of 'ggplot' you can use 'presentation'
import numpy as np # our matlab-like module
from scipy.integrate import odeint # integration of ODEs so you don't have to write your finite difference yourself
#step 1 definition of the function:
def f(C_list, t):
k1 = 1.
k2 = 1.5
C_A = C_list[0]
C_B = C_list[1]
C_C = C_list[2]
r1 = k1*C_A*C_B
r2 = k2*C_B*C_C
dcAdt = -r1
dcBdt = -r1 - r2
dcCdt = r1 - r2
dcDdt = r2
return [dcAdt,dcBdt,dcCdt,dcDdt]
k1 = 1.
k2 = 1.5
C0_list = [1.0, 1.0, 0.0, 0.0]
time_start = 0.
time_finish = 3.
N_points = 100
time_array = np.linspace(0, 3., N_points)
C_num_list = odeint(f, C0_list, time_array)
print(C_num_list.shape)
C_A_num = C_num_list[:,0]
C_B_num = C_num_list[:,1]
C_C_num = C_num_list[:,2]
C_D_num = C_num_list[:,3]
plt.plot(time_array, C_A_num, 'r-', label='$A$')
plt.plot(time_array, C_B_num, 'g-', label='$B$')
plt.plot(time_array, C_C_num, 'm--', label='$C$')
plt.plot(time_array, C_D_num, 'k--', label='$D$')
plt.xlabel('t, s')
plt.ylabel('$C(t)$, mole/L')
plt.legend()
plt.show()
#
# # Additional problem :
#
# $$\require{mhchem}$$
# Normal butane, C4H10, is to be isomerized to isobutane in a plug-flow reactor, because isobutane is worth more. This elementary reversible reaction is to be carried out adiabatically in the liquid phase under high pressure using trace amounts of a liquid catalyst which gives a specific reaction rate of 31.1/hr at 360K. The feed enters at 330K.
#
# Additional information:
# * $\Delta H^\circ_{Rx} = -6900$ J/mol $n$-butane
# * Activation energy $E_a = 65.7$ kJ/mol
# * $K_C = 3.03$ at $60^\circ$ C
# * $C_{P,n\text{-Butane}} = 141$ J/mol/K
# * $C_{P,i\text{-Butane}} = 141$ J/mol/K
# * $C_{P,n\text{-Pentane}} = 161$ J/mol/K
# * $C_{A0} = 9.3$ mol/dm3 = 9.3 kmol/m3 feed concentration of n-butane
#
# a) Calculate the PFR volume necessary to process 100,000 gal/day (163 kmol/h) at 70% conversion of a mixture of 90 mol% n-butane and 10 mol% i-pentane, which is considered an inert.
#
# b) Plot and analyze $X, X_e, T$, and $-r_A$ down the length of the reactor.
#
#
# You may use this code in your program.
# You just need to understand and plot it.
#
# ```python
# DHrx = -6900. # J/mol
# Ea = 65700. # J/mol
# R = 8.314 # J/mol/K
# T0 = 330 # K
# FA0 = 0.9 * 163e3 # mol/hr
# CA0 = 9.3e3 # mol/m3
#
# def Kc(T):
# "Equilibrium constant, as a function of temperature"
# return 3.03 * np.exp(( -1* DHrx / R) * (1./T - 1./(273.15+60)) )
# assert Kc(273.15+60) == 3.03
# assert Kc(273.15+65) < 3.03 # le Chatellier's principle
#
# def k(T):
# "Specific reaction rate in 1 hour, as a function of temperature"
# return 31.1 * np.exp((-1*Ea/R) * (1./T - 1./360.))
# assert k(360) == 31.1
# assert k(365) > 31.1
#
# def Teb(X):
# "Temperature from energy balance, as a function of conversion."
# Cps = np.array([141, 141, 161])
# Thetas = np.array([1., 0., 0.1/0.9])
# return T0 - DHrx * X / sum(Cps*Thetas)
# assert Teb(0) == T0
# assert Teb(0.5) > T0 # Exothermic
#
# def dXdV(X,V):
# """
# dX/dV in a plug flow reactor is -rA/ FA0
# """
# T = Teb(X)
# CA = CA0*(1.-X)
# CB = CA0*X
# rA = -k(T)*(CA - CB/Kc(T))
# return -rA/FA0
# ```
#
# ```
# Xeq = Kc(Teb(X))/ (1+Kc(Teb(X)))
# rate = k(Teb(X))*(CA0*(1.-X) - CA0*X/Kc(Teb(X)))
#
# #For Levenspiel plot you may use this:
#
# plt.plot(X, FA0/rate)
# plt.ylim(0,10)
# plt.title("Levenspiel Plot")
# plt.xlabel("X")
# plt.ylabel("$\\frac{F_{A0}}{-r_A}$")
# plt.plot((Xeq[-1],Xeq[-1]),(0,FA0/rate[-1]), ':')
# plt.show()
# ```
# In[ ]:
import matplotlib.pyplot as plt # plotting modules
get_ipython().run_line_magic('matplotlib', 'inline')
plt.style.use('ggplot') # just have in your script for prettier plotting
# instead of 'ggplot' you can use 'presentation'
import numpy as np # our matlab-like module
import scipy
from scipy.integrate import odeint
DHrx = -6900. # J/mol
Ea = 65700. # J/mol
R = 8.314 # J/mol/K
T0 = 330 # K
FA0 = 0.9 * 163e3 # mol/hr
CA0 = 9.3e3 # mol/m3
def Kc(T):
"Equilibrium constant, as a function of temperature"
return 3.03 * np.exp(( -1* DHrx / R) * (1./T - 1./(273.15+60)) )
assert Kc(273.15+60) == 3.03
assert Kc(273.15+65) < 3.03 # le Chatellier's principle
def k(T):
"Specific reaction rate in 1/hour, as a function of temperature"
return 31.1 * np.exp((-1*Ea/R) * (1./T - 1./360.))
assert k(360) == 31.1
assert k(365) > 31.1
def Teb(X):
"Temperature from energy balance, as a function of conversion."
Cps = np.array([141, 141, 161])
Thetas = np.array([1., 0., 0.1/0.9])
return T0 - DHrx * X / sum(Cps*Thetas)
assert Teb(0) == T0
assert Teb(0.5) > T0 # Exothermic
def dXdV(X,V):
"""
dX/dV in a plug flow reactor is -rA/ FA0
"""
T = Teb(X)
CA = CA0*(1.-X)
CB = CA0*X
rA = -k(T)*(CA - CB/Kc(T))
return -rA/FA0
volumes = np.linspace(0,5,501)
X = scipy.integrate.odeint(dXdV, 0., volumes)
plt.plot(volumes,X,label="Conversion")
Xeq = Kc(Teb(X))/ (1+Kc(Teb(X)))
plt.plot(volumes, Xeq,label="Equilibrium conversion")
plt.legend(loc="best")
plt.xlabel("Volume")
plt.ylabel("Conversion X")
plt.show()
rate = k(Teb(X))*(CA0*(1.-X) - CA0*X/Kc(Teb(X)))
plt.plot(volumes, rate, label="Rate")
plt.xlabel("Volume")
plt.ylabel("Rate")
plt.show()
for volume, conversion in zip(volumes, X):
if conversion>0.7:
break
print(volume)
print(conversion)
plt.plot(X, FA0/rate)
plt.ylim(0,10)
plt.title("Levenspiel Plot")
plt.xlabel("X")
plt.ylabel("$\\frac{F_{A0}}{-r_A}$")
plt.plot((Xeq[-1],Xeq[-1]),(0,FA0/rate[-1]), ':')
plt.show()