You are on page 1of 97

Chemical and Reactive Systems

John Kitchin August 26, 2013

1
1.1

Introduction
What is Chemical Reaction Engineering?

The design of processes that transform lower value feedstocks to higher value products through chemical reactions. We answer questions such as: 1. Can we make a product at an economical rate? 2. How big should a reactor be to make a product at some rate? 3. How much heat should I remove from a reactor to maintain a safe temperature? 1

4. What kind of reactor gives the highest yield? 5. etc. . .

1.2

We are engineers

We get paid to answer those questions quantitatively with uncertainty and risk analysis even when the problems are very hard

1.3

Role of computational tools

Most problems are too hard to solve by hand and must be solved numerically. We will extensively use Python to numerically solve problems in this course. Why? Python is free You can use this anywhere you go Python does everything we need Almost every class will use and show examples of python 2

These notes will be available to you You should make sure you can run the examples, and that you get the same results Ask questions when you do not understand

1.4
1.4.1
1 2 3

Quick python examples


Super simple stu

a = 4 # define a variable print(4*a) print a**2

16 16 1.4.2 A simple plot

Here we import functionality from python modules.


1 2 3 4 5 6 7 8 9

import numpy as np import matplotlib.pyplot as plt x = np.linspace(0, np.pi) plt.plot(x, np.sin(x)) plt.xlabel(x) plt.ylabel(sin(x)) plt.savefig(images/sin.png) plt.show()

1.5

Python setup

Get and install Canopy at https://www.enthought. com/products/canopy/ You should request an academic license It has most of what we will need this semester We will install new packages as needed

1.6

Additional python packages to install

1. Open Canopy. 2. In the Ipython console type: !easy_install pip !pip install pycse 1. Restart Canopy 2. in the canopy IPython console type: pycse_test You should see: "Your installation of pycse looks ok." Later to update pycse type this: !pip install --upgrade pycse 4

1.7

A L TEX

Get and install MikTeX at http://miktex.org/ download You will need this to convert your homework assignments to pdf The rst homework (hello-world.py) is due at the end of the week.

1.8

Assignments

All of your assignments must be completed in python Each assignment will be completed by writing a python script You may use any editor you want. I suggest the Canopy editor if you do not have a preference. The script will be converted to a pdf using a command in pycse You will upload that pdf to your folder in Box.com for grading

1.9

More assignment information

Each assignment will have a label Each script you submit must have this information at the top: #+COURSE: 06-625 #+ASSIGNMENT: <uniquelabel> #+ANDREWID: <yourid> #+NAME: <your name here> The publish.py script from pycse will generate a pdf for you to upload to Box.com. The pdf will be automatically named. Do not rename it. If your information is not correct, your homework will not be graded. ;(

1.10

Example assignment

Suppose assignment 1a is to write a python script to print "Hello world" and plot the function y = x2 for x = [1,2,3,5]. This is what your script should look like:
1 2

#+COURSE: 06-625 #+ASSIGNMENT: hello-world

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18

#+ANDREWID: jkitchin #+NAME: John Kitchin print Hello world import matplotlib.pyplot as plt import numpy as np x = np.array([1,2,3,5]) y = x**2 plt.plot(x, y) plt.xlabel(x) plt.ylabel(y) plt.show()

Hello world To publish your script, make sure the IPython prompt is in the same directory as your script and type
1

publish script.py

A new le is created and opened. If you are satised, upload it to your folder on Box.com. We will not grade les that are renamed or that are incorrectly named. It is not that we do not want to grade them, but the assignments will be collected by a computer, which will not recognize renamed les After we grade the assignments you can see your grade on the assignment in your Box.com folder. 7

1.11

Getting help

I am expecting you will need help. Python is probably new for you. You may nd these resources helpful: Class Come to class everyday. Watch me use Python Ask questions about things you do not understand Learning python http://learnpythonthehardway.org/book/ http://interactivepython.org/courselib/ static/thinkcspy/index.html Python documentation Builtin modules - http://docs.python.org/ 2/library/index.html Python and scientic computing http://jkitchin.github.io/pycse/ (there is also a pdf version) 8

Numerical python - http://docs.scipy.org/ doc/numpy/reference/ Scientic python - http://docs.scipy.org/ doc/scipy/reference/ I will be posting the notes I make in class on the course website.

1.12

First assigment: due next class

Install Canopy, pycse and Latex We will use these next class First assignment to turn will be due [2013-08-30 Fri] hello-world.py (see Box.com/assignments)

The Basics - isothermal reactor design with single reactions


Chemical reactions

2.1

Chemical reaction transform reactants to products. Consider a reaction aA + bB + qQ + sS + 9

symbols on the left are reactants symbols on the right are products The lower case letters are stoichiometric coecients Stoichiometric coecients relate the amounts of each reactant that react to the amounts of products produced The upper case letters are symbols for reactant and product species For specicity let us consider aA + bB cC + dD We will express this reaction as: 0 = 3 A3 + 4 A4 1 A1 2 A2 where we have substituted A = A1 and a = 1 , C = A3 and c = 3 , etc. . . In the most compact form we might write this as a sum over all N species for a reaction: =0 where i is the stoichiometric coecient (negative for reactants, positive for products), or more preferrably in matrix equation form: 10
N i=0 i Ai

A=0 where is the vector of stoichiometric coecients, and A is the vector of chemical species. It is conventional that the stoichiometric coecients of reactants are negative and for products the stoichiometric coecients are positive. Atoms cannot be destroyed in non-nuclear chemical reactions, hence it follows that the same number of atoms entering a reactor must also leave the reactor. The atoms may leave the reactor in a different molecular conguration due to the reaction, but the total mass leaving the reactor must be the same. We consider the water gas shift reaction: CO + H2 O H2 + CO2 .

The total mass will be MCO +MH2 O +MH2 +MCO2 . These are related to the number of moles of each species through the species molecular weights. Let N be a vector that is the number of moles of each species. Then, the total mass is: N M W . Stoichiometry constrains the relationship between the moles of each species during reaction. 11

Suppose we start with this initial number of moles of each species: [N 0, N 0, 0, 0]. Now, if n moles of A1 reacts, we know that n moles of A2 react, and n moles of A3 and A4 are produced. In otherwords, the new number of moles of each species is: N 0 + n. And the new mass is correspondingly: (N 0 + n) M W or: M0 + n M W . In a properly balanced chemical reaction, there are the same number of each type of atom on each side of the reaction, hence the sum of molecular weights of reactants must be the same as products, hence M = 0. Therefore, the total mass does not change for any n! We can illustrate the conservation of mass with the following equation: M W = 0. Where is the stoichiometric coecient vector and M W is a column vector of molecular weights. For simplicity, we use pure isotope molecular weights, and not the isotope-weighted molecular weights. This equation simply examines the mass on the right side of the equation and the mass on left side of the equation. 12

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

import numpy as np alpha = [-1, -1, 1, 1]; MW = [28, 18, 2, 44] print np.dot(alpha, MW) # stoichiometric vector for CO + H2O -> H2 + CO2 # Molecular weights gm/mol

# Here is some old-fashioned code. do not do this. even though it works: total = 0 for i in range(4): total = total + alpha[i] * MW[i] print total # Kudos if you thought of this: import operator print sum(map(operator.mul, alpha, MW)) # just kidding, I would never do that! This is called functional programming

0 0 0 Stoichiometry also determines if the total number of moles in a reaction change. Even though the total mass is constant, the total number of moles may change. Here are three examples showing how this is possible. 1. CO + H2 O H2 + CO2 (no total mole change) 2. H2 O H2 + 1/2 O2 (Total moles increase by 0.5 mol per mol water reacted) 3. N2 + 3H2 2 NH3 (Total moles decrease by two moles for every mole of N2 reacted) 13

The change in number of moles is given by = N . i=0 i or =


1 2 3 4 5 6 7 8 9

# WGS alpha = [-1, -1, 1, 1]; # stoichiometric vector for CO + H2O -> H2 + CO2 print Change in moles for the WGS = {0} moles.format(sum(alpha)) alpha = [-1, 1, 0.5] # H2O -> H2 + 1/2 O2 print Change in moles for water splitting = {0} moles.format(sum(alpha)) alpha = [-1, -3, 2] # N2 + 3H2 -> 2 NH3 print Change in moles for the ammonia synthesis = {0} moles.format(sum(alpha))

Change in moles for the WGS = 0 moles Change in moles for water splitting = 0.5 moles Change in moles for the ammonia synthesis = -2 moles Changing the total number of moles in a reaction will have a big eect in gas phase reactions because it results in changing volumetric ow rates. We will come back to this later.

2.2

Reaction extent

We now consider formalizing the change in moles of each species when reactions occur. Consider: 2H2 + O2 2H2 O which we remember is: 0 = 2A3 2A1 A2 14

If we start with NA1 ,0 moles at some time, and later have NA1 moles later, then stoichiometry dictates that:
NA1 NA1 ,0 2

NA2 NA2 ,0 1

NA3 NA3 ,0 1

=X

We call X the extent of reaction, and it has units of moles. We can show generally that: NJ = NJ,0 + J X or for a ow system: FJ = FJ,0 + J X Note that the extent of reaction as written is extensive, and depends on how the reaction is written through the stoichiometric coecients. It does not, however, depend on a particular species. If we have a constant volume reactor and a constant volumetric ow, we can use an intensive reaction extent: CJ = CJ,0 + J . is now an intensive reaction extent X/V , with units of mol / L. Note that there are limits on the maximum value of because we cannot have negative concentrations. If we set CJ to zero, we derive 15

J,0 max = C J

If there are multiple reactants present, then you must pick the smallest positive (non-zero) max to avoid getting negative concentrations of one species. Consider this reaction: H2 + 0.5 O2 H2 O If you start with 0.55 mole of H2 , and 0.2 mol of O2 . What is max ?
1 2 3 4 5 6 7

import numpy as np M0 = np.array([0.55, 0.2]) alpha = np.array([-1.0, -0.5]) print - M0 / alpha print The maximum extent is {0} moles..format(min(- M0 / alpha))

[ 0.55 0.4 ] The maximum extent is 0.4 moles. Now for that extent, what is the reaction compostion?1
1 2

import numpy as np
1

Note that if you put the product in here, this will tell you that the maximum extent is zero. You should of course take the smallest positive number.

16

3 4 5 6 7 8 9

M0 = np.array([0.55, 0.2, 0.0]) alpha = np.array([-1.0, -0.5, 1.0]) xi = 0.4 M = M0 + alpha * xi print M

[ 0.15

0.

0.4 ]

You can see that at that extent we have consumed all of the oxygen. We would call that the limiting reagent, because the reaction cannot proceed further since one of the reactants is gone. Rather than work in terms of reaction extents, you may choose to dene a fractional extent: = /max which leads upon substitution to: CJ = CJ,0 (1 ) This new quantity is sometimes referred to as conversion. Conversion has the nice property of being dimensionless, and bounded between 0 and 1. While convenient for a single reaction, it is impractical for multiple reactions and we will not consider it further.

17

2.3
2.3.1

Reaction rates and rate laws


The rate of a reaction

We are now in a position to dene the rate of a reaction as: R =


dX dt

which is the rate of the change of the extensive reaction extent. Note that this is: Independent of any particular species Dependent on how the reaction is written The second point is a result of how we dened X . For the reaction 2 H2 + O2 2 H2 O we dened:
NA1 NA1 ,0 2

NA2 NA2 ,0 1

NA3 NA3 ,0 2

=X

For the reaction H2 + 1/2 O2 H2 O we dene:


NA1 NA1 ,0 1

NA2 NA2 ,0 0.5

NA3 NA3 ,0 1

=X

You can see that X depends on the stoichiometric coecients, so we have to know the reaction and how it was written when we discuss reaction rates.

18

2.3.2

The rate of disappearance of a reactant


NAj NAj ,0 , j

Now, recalling that X =


dX dt

we arrive at:

1 dNAJ j dt

or:
dNAJ dt

= j R

Thus, the change in composition of species J due to the reaction is just the stoichiometric coecient for that species times the reaction rate. If the volume is constant, we have: r = R /V = and dCAJ dt = j r
d dt

We dene the species rate of production as: rj = j r This is NOT the rate law! One of our goals is to nd the function that describes the rate of the reaction and its dependence on concentration and temperature

19

2.3.3

Rates of disappearance and appearance of other species

The stoichiometric coecients dene the rates of appearance and disappearance of other species. r=
rA1 1

rA2 2

rA3 3

Remember that stoichiometric coecients of reactants are negative, and products are positive We call r the rate of the reaction with units (typically) mol / vol / time The rate of disappearance of species A1 is rA1 = 1 r The rate generally depends on the concentration of reactants (and sometimes products), as well as the temperature of the reactor. 2.3.4 Rate laws

The rate law is an algebraic equation that relates the rate of reaction to the concentrations of reactants, products and temperature. Law of mass action for elementary steps: Reaction rate is proportional to the concentration of each reactant raised to its stoichiometric coecient 20

For example: A + B C r = kCA CB 2A B


2 r = kCA

Many other more complex rate laws exist for nonelementary reactions
k1 CA r = 1+ k2 CA e.g. for surface reactions or enzyme reactions

r = kCA for complex mechanisms Rate laws are ultimately determined from experiments We use these rate laws in conjunction with stoichiometry and mole balances to design reactors.

3/2

2.4
2.4.1

Simple mole balances


Review of the mole balance

Mole balances are performed for a species in a control volume

21

Accumulation = In Out + Generation (1) dNJ = FJ 0 FJ + V rJ (2) dt Here we use the convention that Nj refers to the total number of moles of species J in the volume, FJ is a molar ow of J , and rJ is the intensive rate of production of J , and it has a negative magnitude if species J is in fact being consumed. 2.4.2 A continuously stirred tank reactor

We assume the tank is well-mixed because it is well-stirred The concentration at the exit is the same as everywhere in the tank
A The mole balance at steady state ( dN dt = 0) is:

0 = FA0 FA + V rA 22

2.4.3

A continuously stirred tank reactor problem

We have a 10L stirred tank reactor A ows in at a molar ow rate of 1 mol/hr and volumetric owrate of 2.5 L/hr rA = kCA , k = 0.23 1/hr What is the steady-state exit concentration of A? The equations

dNA =0= dt 0= 0= CA,exit =


FA0 v0 +V k

FA0 FA + V rA FA0 FA V kCA,exit FA0 v0 CA,exit V kCA,exit

(3) (4) (5)

Only for constant volume Assumes well-mixed, i.e. uniform concentration 2.4.4 Solving the problem with algebra (CSTR)

Simple algebra

23

1 2 3 4 5 6 7

k = 0.23 Fa0 = 1.0 v0 = 2.5 V = 10

# # # #

1/hr mol /hr L /hr L

Ca_exit = (Fa0 / (v0 + V * k)) print Ca_exit = {0:1.3f} mol / L.format(Ca_exit)

Ca_exit = 0.208 mol / L This was an easy problem, but the algebraic manipulations are all possible places where errors can be made. 2.4.5 Solving the problem numerically using a solver (CSTR)

We have to create a function that is equal to zero at the solution. We have that from the mole balance: 0 = FA0 FA V rA We just have to make sure to use the correct variables. We use a nonlinear solver, so we also have to provide an initial guess.
1 2 3

from scipy.optimize import fsolve k = 0.23 # 1/hr

24

4 5 6 7 8 9 10 11 12 13

Fa0 = 1.0 v0 = 2.5 V = 10

# mol /hr # L /hr # L

def func(Ca): return Fa0 - v0 * Ca - V * k * Ca guess = 1.0 # mol / L ans, = fsolve(func, guess) print Ca_exit = {0:1.3f} mol/L.format(ans)

Ca_exit = 0.208 mol/L This had less manipulation, and fewer opportunities for mistakes On the other hand, we ended up using a solver that required an initial guess to solve a linear problem. This was a simple problem, but other problems will not be linear, and will be much more dicult. Remember what the units are? Were they consistent? 2.4.6 Solving the problem with units (CSTR)

Units are not built-in to python We have to install the quantities package Then import the package so we can use it 25

1 2 3 4 5 6 7 8

import quantities as u k = 0.23 * 1/u.hr Fa0 = 1.0 * u.mol / u.hr v0 = 2.5 * u.L / u.hr V = 10 * u.L Cae = Fa0 / (v0 + V * k) print(Ca,exit = {0}.format(Cae))

Ca,exit = 0.208333333333 mol/L This has printed with too many signicant gures quantities is great for simple problems There are limitations we will see later 2.4.7 Solving the problem with uncertainty (CSTR)

Uncertainty analysis is not built in to python We have to install the uncertainties package and import it Let us assume there is some uncertainty in the rate constant, say it is k = 0.23 0.1 1/hr. We can use the uncertainties package to propagate that error automatically.
1 2

import uncertainties as u

26

3 4 5 6 7 8

k = u.ufloat(0.23, 0.1) # rate constant 1/hr Fa0 = 1.0 # inlet molar flow mol/hr v0 = 2.5 # volumetric flow L/hr V = 10 # reactor volume L Cae = Fa0 / (v0 + V * k) print(Ca,exit = {0}.format(Cae))

Ca,exit = 0.21+/-0.04 uncertainties is also great for simple problems We have to do some work to make it work in other situations 2.4.8 Mole balance on a batch reactor

The next more complex (mathematically) mole balance is the batch reactor. The batch reactor does not operate at steady state, and therefore we have an ordinary dierential equation that describes the number of moles in the reactor as a function of time.

27

Constant volume No ow in or out


dNA dt

= V rA Simple application of a mole balance to a constant volume batch reactor

2.4.9

At t = 0 we have an initial concentration of 2 mol/L rA = kCA with k = 0.23 1/hr How much A is left after 1 hour? 28

Equations

NA = dNA = dt dCA = dt CA (t = 0) = Only for constant volume

CA V dCA V dt rA = kCA CA0

(6) (7) (8) (9)

Assumes well-mixed, i.e. uniform concentration Initial condition, ordinary dierential equation 2.4.10 Solving the problem (constant volume batch reactor)

1 2 3 4 5 6 7 8 9 10 11 12 13

import numpy as np from scipy.integrate import odeint k = 0.23 Ca0 = 2.0 # 1/hr # mol / L

def ode(Ca, t): dCadt = -k * Ca return dCadt tspan = np.linspace(0, 1) # hours sol = odeint(ode, Ca0, tspan) print C_A at t = 1 hour = {0} mol/L.format(sol[-1][0])

29

C_A at t = 1 hour = 1.58906722836 mol/L Remember what the units are? 2.4.11 Plotting CA vs. time in a batch reactor

Now let us solve the ODE at many times, and plot the solution.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

import numpy as np from scipy.integrate import odeint import matplotlib.pyplot as plt k = 0.23 Ca0 = 2.0 # 1 / hr # mol / L

def ode(Ca, t): dCadt = -k * Ca return dCadt tspan = np.linspace(0, 1) sol = odeint(ode, Ca0, tspan) plt.plot(tspan, sol) plt.xlabel(Time (hours)) plt.ylabel($C_A$ (mol / L)) plt.savefig(images/batch-time.png)

30

CA decreases with time (it is consumed) It is not apparent from this graph because of the short time, but the concentration decreases exponentially with time 2.4.12 Mole balance in a plug ow reactor

In the plug ow reactor, reactants enter the front of the reactor and disappear as they ow through the reactor

31

We assume our dierential element is well-mixed The mole balance on the dierential element leads to
dNA dt

= FA |V FA |V +V + V rA

At steady state, in the limit of V 0 we get:


dFA dV

= rA

This is an ordinary dierential equation, and to solve it we need an initial condition on the molar ow at V = 0. 2.4.13 A worked PFR example

Given a 100 L reactor with A owing in at a concentration of 3 mol/L and a rate of 10 L/min The reaction A B occurs at a rate law of r = kCA with k = 0.23 1/min 32

What is the exit concentration of A? We have


dFA dV

= rA

We have rA = r (stoichiometry) FA (V = 0) = CA0 v0


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

from scipy.integrate import odeint Ca0 = 3.0 v0 = 10.0 k = 0.23 # mol / L # volumetric flowrate L/min # rate constant 1/min

def ode(Fa, V): Ca = Fa / v0 return -k * Ca Vspan = [0, 100] # reactor volume sol = odeint(ode, Ca0 * v0, Vspan) Fa_exit = sol[-1, 0] print(Exit concentration = {0:1.4f} mol/L.format(Fa_exit / v0))

Exit concentration = 0.3008 mol/L Our solution only has two points in it: 0 and 100L We cannot visualize the concentration prole 2.4.14 A harder PFR example

The reaction A B occurs at a rate law of r = kCA with k = 0.23 1/min 33

If A is owing in at a concentration of 3 mol/L and a rate of 10 L/min How large should the reactor be to reduce the concentration of A to 0.3 mol/L? There are many ways to approach this.
A You could integrate dF dV = rA and graphically determine where the solution is.

You could setup a numerical approach to solving the equation First we graph the solution. the code is almost the same as before, but we integrate over more points and a a larger range.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

import numpy as np from scipy.integrate import odeint import matplotlib.pyplot as plt Ca0 = 3.0 # mol / L v0 = 10.0 # L / min k = 0.23 # 1/min def ode(Fa, V): Ca = Fa / v0 return -k * Ca Vspan = np.linspace(0, 200) # volumes to integrate over sol = odeint(ode, Ca0 * v0, Vspan) plt.plot(Vspan, sol)

34

18 19 20

plt.xlabel(Volume (L)) plt.ylabel($F_A$ (mol/L)) plt.savefig(images/pfr-volume.png)

at CA = 0.3 mol/L, FA = 3 mol/min. We know the answer before: It is about 100 L. It is hard to be very accurate this way, although interactive graphics help 2.4.15 Numerical solution

To numerically solve this we must solve a function f (V ) = 0. Here is one approach 35

Starting from

dFA dV

= kFA / we derive:

FA dFA k V f (V ) = F V =0 dV A0 FA where everything is known but V . We use numerical quadrature to evaluate the integrals.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23

from scipy.integrate import quad from scipy.optimize import fsolve k = 0.23 nu = 10 Ca0 = 3.0 Fa0 = Ca0 * nu Fa = 0.30 * nu # 1/min # L/min # mol / L

def integrand1(Fa): return 1.0 / Fa def integrand2(V): return -k / nu def func(V): I1, e1 = quad(integrand1, Fa0, Fa) I2, e2 = quad(integrand2, 0, V) return I1 - I2 guess = 120 # Liters sol, = fsolve(func, guess) print Volume = {0:1.2f}.format(sol)

Volume = 100.11 This also leaves something to be desired in complexity Many opportunities for mistakes in the derivation 36

Requires sophisticated thinking about the problem Other approaches require similar or more sophistication! 2.4.16 Solution by interpolation

Solve the problem Create interpolation function for the solution Solve the inverse problem V(Fa)
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

import numpy as np from scipy.integrate import odeint from scipy.interpolate import interp1d Ca0 = 3.0 v0 = 10.0 k = 0.23 # mol / L # L / min # 1 / min

def ode(Fa, V): Ca = Fa / v0 return -k * Ca Vspan = np.linspace(0, 200) # L sol = odeint(ode, Ca0 * v0, Vspan) interp_func = interp1d(sol[:, 0][::-1], Vspan[::-1], cubic) Ca_exit = 0.3 # mol / L Fa_exit = Ca_exit * v0 V_sol = interp_func(Fa_exit) print Solution is at {0} L.format(V_sol)

37

Solution is at 100.112342835 L this only works because V (FA ) is monotonic. That is a restriction on the interp1d function 2.4.17 Using events to stop integration

An alternative to the methods above is to use an ODE solver that is aware of events to stop the integration where you want it. pycse provides a function like this called odelay. You dene an event function that equals zero at the event. You specify if the event is terminal, and whether to the zero must be approached from above or below, or if all zeros count. Here is an example.
1 2 3 4 5 6 7 8 9 10 11 12 13 14

import numpy as np from pycse import odelay Ca0 = 3.0 v0 = 10.0 k = 0.23 # mol / L # L / min # 1 / min

Fa_Exit = 0.3 * v0 def ode(Fa, V): Ca = Fa / v0 return -k * Ca def event1(Fa, V):

38

15 16 17 18 19 20 21 22 23 24 25 26 27

isterminal = True direction = 0 value = Fa - Fa_Exit return value, isterminal, direction Vspan = np.linspace(0, 200) # L V, F, TE, YE, IE = odelay(ode, Ca0 * v0, Vspan, events=[event1]) print Solution is at {0} L.format(V[-1]) import matplotlib.pyplot as plt plt.plot(V, F) plt.show()

Solution is at 100.112421732 L As you can see, there are many ways to solve this problem It is not necessary to know every single way to do it, but knowing multiple ways increases your ability to solve other problems in the future

2.5

Mole balances with changing number of moles

The reason it is important to know whether the total number of moles is changing in a reactor is because we dene the concentration of a species in a owing system as Cj =
Fj

39

where is the total volumetric owrate, and we need these concentrations to evaluate the rate laws. For reactions with no change in total moles, no pressure drops, and isothermal conditions, is a constant. In all other cases, changes, and we have to compute to compute the concentrations for use in the rate laws. We know at the entrance of the reactor that P0 0 = FT 0 RT0 Z0 Here we have the initial pressure, volumetric owrate, temperature and compressibility factor. At some later point in the reactor or in time, we also have: P = FT RT Z Now, even if we are isothermal, so that T = T0 , isobaric, so that P = P0 , and there is no change in compressibility, i.e. Z = Z0 , if the total molar ow rate has changed, there will be a change in the volumetric ow. The ratio of these two equations leads to this expression for the volumetric ow rate in a reactor where the total number of moles is changing: 40

FT P 0 T Z = 0 F T 0 P T0 Z0

Here, the total molar ow is simply the sum of the molar ows of each species (including inert species) in the reactor, FT = N i=0 Fj . That means we now need to calculate the molar ow of each species, which in general means we will have a species mole balance for each species. For single reactions, it is often possible to relate these molar ows by stoichiometry. Consider this reaction, e.g. ethane cracking to ethylene and hydrogen. AB+C We want to design a plug ow reactor to consume 80% of the ethane that enters the reactor. It is given that rA = kCA with k = 0.072 1/s at 1000K. Let the initial molar owrate be 0.425 mol / s of pure A into the reactor at a total pressure of 6 atm. The initial concentration of A is simply CA0 = PA0 /(RT ). From this, we calculate the initial volumetric owrate: 0 = FA0 /CA0 rst we use the quantities package to do some unit conversions to get consistent SI units 41

1 2 3 4 5 6 7 8 9 10 11 12 13

import quantities as u PA0 = 6 * u.atm T = 1000 * u.K R = 8.314 * u.J / u.mol / u.K CA0 = PA0 /( R * T) print C_{{A0}} = {0}.format(CA0.simplified) FA0 = 0.425 * u.mol / u.s v0 = FA0 / CA0 print \\nu_0 = {0}.format(v0.simplified)

C_{A0} = 73.1236468607 mol/m**3 \nu_0 = 0.0058120733613 m**3/s If we assume there is no pressure drop, and that 80% of A has reacted at the exit, with pure A at the entrance, then we know that there will be 0.20 FA0 + 0.8 FA0 + 0.8 FA0 moles at the exit. Thus,
1 2 3 4 5 6 7 8 9 10 11 12 13 14

import quantities as u FA0 = 0.425 * u.mol / u.s PA0 = 6 * u.atm T = 1000 * u.K R = 8.314 * u.J / u.mol / u.K CA0 = PA0 /( R * T) v0 = FA0 / CA0 F_exit = 0.2 * FA0 + 0.8 * FA0 + 0.8 * FA0 v = v0 * F_exit / FA0 print Exit volumetric flowrate is {0}..format(v.simplified)

42

15 16 17 18 19

CA_exit = 0.2 * FA0 / v print Exit concentration of A is {0} which is not 0.2 * CA0 ({1}).format(CA_exit.simplified, (0.2 * CA0).simplified)

Exit volumetric flowrate is 0.0104617320503 m**3/s. Exit concentration of A is 8.12484965119 mol/m**3 which is not 0.2 * CA0 (14.6247293721 mol/m**3) You can see just by this algebra that the concentration changes because of the chemical reaction that consumes A, but also because the number of moles is changing, and the volumetric owrate is increasing. We have to account for that in our mole balances. The normal mole balance for a plug ow reactor looks like:
dFA dV

= rA . Here, it is convenient to invert the equa-

tion: = r1 A because then we can integrate over the molar ow to directly compute the volume. We compute the molar ows of B and C using the reaction extent: Fj = Fj 0 + j .
1 2

dV dFA

import numpy as np from scipy.integrate import odeint

43

3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32

Fa0 = 0.425 Fa_exit = 0.2 * Fa0 v0 = 0.0058120733613 k = 0.072

# mol / s

# m^3 / s # 1 / s

def dVdFa(V, Fa): xi = (Fa - Fa0) / (-1) # compute reaction extent Fb = xi * 1 Fc = xi * 1 Ft = Fa + Fb + Fc # total molar flow v = v0 * Ft / Fa0 # volumetric flow Ca = Fa / v ra = -k * Ca return 1.0 / ra Fspan = np.linspace(Fa0, Fa_exit) V0 = 0 sol = odeint(dVdFa, V0, Fspan) print At a volume of {0:1.2f} m^3 we achieve 80% conversion of A.format(sol[-1][0]) import matplotlib.pyplot as plt plt.plot(Fspan, sol) plt.xlabel(F$_A$ (mol/s)) plt.ylabel(Volume (m$^3$)) plt.savefig(images/changing-moles-pfr.png)

At a volume of 0.20 m^3 we achieve 80% conversion of A

44

An alternative approach, and one that is needed for multiple reactions, is to use a mole balance for each species:

dFA = rA dV dFB = rB dV dFC = rC dV

(10) (11) (12)

and to relate the rates of each species reaction rate to each other via stoichimetry: r=
rA 1

rB 1

rC 1

45

Here we cannot invert the ODE, because we have coupled odes.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37

import numpy as np from scipy.integrate import odeint Fa0 = 0.425 Fa_exit = 0.2 * Fa0 v0 = 0.0058120733613 k = 0.072 def dFdV(F, V): Fa = F[0] # we only need Fa for the rate law Ft = sum(F) # total flow rate v = v0 * Ft / Fa0 Ca = Fa / v ra = -k * Ca dFadV = ra dFbdV = -ra dFcdV = -ra return [dFadV, dFbdV, dFcdV] Vspan = np.linspace(0, 1) # m**3 # mol / s

# m^3 / s # 1 / s

F0 = [Fa0, 0, 0] # entrance conditions sol = odeint(dFdV, F0, Vspan) Fa = sol[:,0] from scipy.interpolate import interp1d f = interp1d(Vspan, Fa) from scipy.optimize import fsolve Vsol, = fsolve(lambda x: f(x) - 0.2 * Fa0, 0.2) print At a volume of {0:1.2f} m^3 we achieve 80% conversion of A.format(Vsol)

46

38 39 40 41 42 43 44 45 46 47

import matplotlib.pyplot as plt plt.plot(Vspan, sol) plt.xlabel(Volume (m$^3$)) plt.ylabel(F$_A$ (mol/s)) plt.legend([A, B, C]) plt.savefig(images/changing-moles-pfr-2.png) plt.show()

At a volume of 0.20 m^3 we achieve 80% conversion of A

This approach is more involved, but when there are multiple reactions, and net rates of reaction must be considered, this is the only way to proceed with reactor design.

47

2.5.1

Summary

It is important to keep track of when the number of moles in a reaction change because we dene the concentration of a species as Cj = Fj and depends on the total number of moles in the system.
FT P 0 T Z = 0 F T 0 P T0 Z0

We will see in a later section that pressure drops aect reactor design because of the change it causes in volumetric ow also.

2.6

Mole balances with pressure drops

We have previously seen that we must account for changing volumetric owrates in reactor design because the concentrations of species used in computing reaction rates are dependent on the volumetric owrate. This can be important even when the total molar ow is constant, if there is a pressure drop in the reactor, i.e. if the pressure at the entrance is not the same as the pressure at the exit of the reactor: 48

FT P0 T Z = 0 F T 0 P T0 Z0

Since the pressure drops through a reactor, if nothing else changes, the volumetric ow will increase (this is a consequence of conservation of mass). The consequence of this is the following: CA =
FA

FA FT 0 P 0 FT P0

This is especially important for packed bed reactors, which are often lled with catalyst beads that can impede the ow. Since we apply this specically to a packed bed reactor, it is convenient to work in terms of catalyst weight, rather than reactor volume. The two quantities are related by W = b V = c (1 )V where W is the weight, b is the bulk catalyst density, c is the density of solid catalyst, and is the porosity of the catalyst. We know how to develop mole balances for FA , dFj but these will lead to equations of the form dW = f (F, P ), which has an additional variable P in it. Now we need to have a quantitative expression for the pressure at some point in a reactor, as a function of the molar ows of each species. 49

The pressure drop through a packed bed can be modeled with the Ergun equation 2 . This is one of the more common approaches to considering pressure drops. The most important result is that
P0 T FT 0 = Ac (1 )c P T0 FT 0 where: (1) 150(1) + 1.75G 0 = G 3 Dp 0 gc Dp dP dW

0 is a constant that depends only on the properties of the packed bed, and the entrance gas conditions: Ac c G u Dp gc 0 bed cross-sectional area solid catalyst density catalyst porosity gas viscosity supercial mass velocity ( u) supercial velocity (volumetric ow / Ac ) catalyst bead diameter 32.174 lbm ft/s2 /lbf (in metric gc =1) inlet gas density

Clearly, we need additional data, but the data are all constants. In fact, it is customary to lump additional constants, and to dene:
2

See Fogler, section 4.5

50

20 = Ac c (1 )P0 and to dene y = P/P0 so that we can reexpress the dierential equation as: dy T FT dW = 2y T0 FT 0

This equation has an analytical solution when there is no change in the total number of moles. From here you can see that y will decrease in an isothermal, isomolar reaction due to the negative sign. This equation depends on FT , so it is coupled to the mole balances. So, we will typically have equations such as: dFA = rA dW dFB = rB dW . . . dy T FT = dW 2y T0 FT 0

(13) (14) (15) (16)

which must be numerically integrated with appropriate initial conditions. 2.6.1 A worked example with a pressure drop and inerts

We consider the partial oxidation of ethylene to ethylene oxide: 51

C2 H4 + 0.5 O2 C2 H4 O Oxygen is fed in a stoichiometric amount in the form of air. The rate law is given as rA = kCA CB . A is fed at a rate of Fa0 = 1.08 lbmol / h B is fed at a rate of 0.5F a0 FN 2 = FB 0.8/0.2 for the conditions and bed are provided as 0.0166 1 / (lbm cat). Let us estimate the catalyst weight required to achieve 60% conversion of A.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16

1/3

2/3

import numpy as np from scipy.integrate import odeint Fa0 Fb0 FI0 Fc0 = = = = 1.08 # lbmol / h 0.5 * Fa0 Fb0 * 0.8 / 0.2 # flow rate of N2 0.0

Ft0 = Fa0 + Fb0 + FI0 + Fc0 P0 = 10 # atm alpha = 0.0166 # 1 / lb_m cat k = 0.0141 # lb-mol / (atm * lb_m cat * h) def ode(F, W): Fa, Fb, Fc, y = F

52

17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

P = y * P0 Ft = sum(F) + FI0 # do not forget the inerts! Pa = Fa / Ft * P Pb = Fb / Ft * P ra = -k * Pa**(1.0/3.0) * Pb**(2.0 / 3.0) dFadW = ra dFbdW = 0.5 * ra dFcdW = -ra dydW = -alpha /(2 * y) * Ft / Ft0 return [dFadW, dFbdW, dFcdW, dydW] y0 = 1.0 # P0/P0 F0 = [Fa0, Fb0, Fc0, y0] Wspan = np.linspace(0, 50) # lb_m cat sol = odeint(ode, F0, Wspan) import matplotlib.pyplot as plt plt.plot(Wspan, sol[:,0:3]) plt.legend([A,B,C],loc=8) plt.xlabel(Catalyst weight ($lb_m$)) plt.ylabel(Molar flow (mol/min)) ax1 = plt.gca() ax2 = ax1.twinx() plt.plot(Wspan, sol[:,3], k--) plt.ylabel($P/P_0$) plt.legend([$P/P_0$],loc=NorthEast) plt.savefig(images/pressure-drop-pfr.png) plt.show()

53

2.7

Transient CSTR

We can model the startup of a CSTR as an ordinary dierential equation. We start with the usual mole balance:
dNA dt

= FA0 FA + V rA

and an initial condition on the concentration of A in the reactor. Suppose that the reactor starts out full of solvent, with no A present CA (t = 0) = 0. The reactor is at constant volume, so we rewrite the mole balance as: 54

dCA dt

= FA0 /V FA /V + rA

We will presume a rst order reaction, rA = kCA with k = 0.11 1/min. A ows into the reactor at a concentration of 0.5 mol/L at a rate of 1.5 L/min. The reactor is 2 L in volume. Let us plot the exit concentration as a function of time.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

import numpy as np from scipy.integrate import odeint CAin = 0.5 # mol/L v0 = 1.5 # L/min V = 2.0 # reactor volume (L) FA0 = CAin * v0 k = 0.11 # rate constant (1/min) def dCadt(Ca, t): rA = -k * Ca return FA0 / V - v0 * Ca / V +

rA

tspan = np.linspace(0.0, 20.0) Ca0 = 0.0 # initial condition in the tank sol = odeint(dCadt, Ca0, tspan) import matplotlib.pyplot as plt plt.plot(tspan, sol) plt.xlabel(Time (min)) plt.ylabel($C_A$ (mol/L)) plt.savefig(images/transient-cstr.png) plt.show()

55

You can see that the concentration initially increases That is because the tank is initially empty and lls up Eventually a steady state concentration occurs In this case, the conversion is low because the reaction rate is slow Note that unlike solving for the steady state solution using fsolve, here we do not need an initial guess. Instead, we start with an initial condition 56

There are scenarios where there are multiple steady satte solutions. In those cases the solution you get depends on the initial conditions This is analogous to the solution depending on the initial guess in a non-linear algebra problem

2.8

Summary

You should have learned: 1. How stoichiometry determines changes in the moles of species in a reaction 2. How the relative rates of species production are related by stoichiometry 3. Mole balances for a batch reactor, continuously stirred tank reactor, and plug ow reactor 4. Mole balances for reactors with pressure drops and for reactions that change the total number of moles You have seen examples of: 1. solving nonlinear equations 2. integrating ordinary dierential equations 57

3
3.1

Multiple reactions
Stoichiometry in multiple reactions

When we have multiple reactions, e.g. A 2B BC We have a scenario where a species maybe consumed and/or generated by multiple reactions. We can dene a reaction extent j for each reaction. then for each species we can determine the change in moles from all the reactions as: ni = ni0 + j ij j where ij is the stoichiometric coecient of species i in reaction j .

3.2

Rates for multiple reactions

When we have multiple reactions, e.g. A 2B BC 58

we have a scenario where a species maybe consumed and/or generated by multiple reactions. Each reaction will have its own reaction rate. We denote the rate of reaction i as ri . In this example, we might have r1 = k1 CA , and r2 = k2 CB . Then we have from reaction 1 that the rate of production of B is r1,B = 2r1 and the rate of consumption in reaction two is r2,B = r2 . The net rate of production of species B is the sum of these two species specic rates. rB = 2r1 r2 = 2k1 CA k2 CB This is the expression we would use in a species mole balance. For example, in a constant volume batch reactor we would have: = V rB = V (2k1 CA k2 CB ) In this example you also need another mole balance on species A. Critical points We need rate laws for each reaction 59
dNB dt

You derive the species rates using stoichiometry for each reaction You add all the species rates together to get the net rate of reaction for the species Let us work out this example completely. Let us consider a constant volume batch reactor.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29

import numpy as np from scipy.integrate import odeint k1 = 0.09 # 1/min k2 = 0.2 # 1/min CA0 = 2.5 # mol/L def batch(C, t): Ca, Cb = C r1 = k1 * Ca r2 = k2 * Cb ra = -r1 rb = 2.0 * r1 - r2 dCadt = ra dCbdt = rb return [dCadt, dCbdt] init = [CA0, 0.0] # initial conditions tspan = np.linspace(0, 30) # min sol = odeint(batch, init, tspan) import matplotlib.pyplot as plt plt.plot(tspan, sol) plt.xlabel(Time (min)) plt.ylabel(Conc (mol/L))

60

30 31 32 33

plt.legend([A,B]) plt.savefig(images/batch-multiple.png) plt.show()

You can see here that A continuously disappears. A is only consumed in the rst reaction. Initially, B increases as it is produced by the rst reaction. However, it begins to be consumed by reaction two, and eventually is completely consumed. If B was the desired product, you could maximize the yield by stopping the reaction after a short time.

61

3.3

Reversible chemical reactions

Reversible reactions are a special case of multiple reactions. We consider a forward and reverse reaction. For example, in the water gas shift reaction we could write the forward reaction as: CO + H2 O H2 + CO2 and the reverse reaction as: H2 + CO2 CO + H2 O We write this as CO + H2 O

H2 + CO2 .

All reactions are to some extent reversible A + bB + qQ + sS +

Thermodynamics denes the equilibrium distribution of reactants and products = Keq = eG/RT where G is the reaction Gibbs free energy dened by G = j j Gj a A is the activity of species A raised to the power. It is common to use concentration instead of activity, but you must remember this implies ideal activity, and that the equilibrium constant may end up having units. 62
s aq Q aS b a A aB

Recall that activity is dimensionless We sometimes express activity in terms of concentration using activity coecients aj = j Cj For ideal solutions, j = 1 3.3.1 A brief worked example.

The water gas shift reaction H2 O + CO CO2 + H2 has a Gibbs reaction energy of -730 cal/mol at 1000K. If you start with equimolar amounts of water and carbon monoxide at a total pressure of 10 atm, what is the equilibrium composition of gases in mol/L? There is no change in the number of moles for the reaction, the temperature is constant, and thus the volume is constant. We essentially only need to nd the equilibrium extent of reaction, and the problem is solved. We know in this case that Cj = Cj,0 + j . At equilibrium, we will have: K=
CC,eq CD,eq CA,eq CB,eq

(CC,0 +C eq )(CD,0 +D eq ) (CA,0 +A eq )(CB,0 +B eq )

63

After simplication, we have: K=


2 eq (CA,0 eq )2

So, we simply evaluate K for the conditions, and then solve for eq . Let CA0 = the concentration of A and B initially in the reactor. CA0 = PA0 /(RT ).
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22

import numpy as np R = 1.987 # cal / mol / K dG = -730 # cal / mol T = 1000.0 # K K = np.exp(-dG / R / T) print(K = {0}.format(K)) Pa0 = 5 # atm R = 0.082057 # L atm / (mol K) Ca0 = Pa0 / (R * T) def func(xi): return K - (xi**2) / (Ca0 - xi)**2 from scipy.optimize import fsolve guess = 0.05 xi_eq, = fsolve(func, guess) print xi_eq = ,xi_eq print C_A = {0:1.4f} mol / L.format(Ca0 - xi_eq) print C_C = {0:1.4f} mol / L.format(xi_eq)

K = 1.44395809814 xi_eq = 0.0332570531518 C_A = 0.0277 mol / L C_C = 0.0333 mol / L 64

An alternative formulation that uses the linear algebra notation follows. The advantage of this approach is that we do not need to derive the equation to solve, it is simply the denition of the equilibrium constant. The code, on the other hand, is a little more verbose, while simultaneously being more explicit.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24

import numpy R = 1.987 # dG = -730 # T = 1000.0 #

as np cal / mol / K cal / mol K

K = np.exp(-dG / R / T) print(K = {0}.format(K)) Pa0 = 5 # atm R = 0.082057 # L atm / (mol K) Ca0 = Pa0 / (R * T) def func(xi): nu = np.array([-1, -1, 1, 1]) # stoichiometric coefficients C0 = np.array([Ca0, Ca0, 0.0, 0.0]) # initial concentrations C = C0 + nu * xi return K - np.prod(C**nu) from scipy.optimize import fsolve guess = 0.05 xi_eq, = fsolve(func, guess) print C_A = {0:1.4f} mol / L.format(Ca0 - xi_eq) print C_C = {0:1.4f} mol / L.format(xi_eq)

K = 1.44395809814 C_A = 0.0277 mol / L C_C = 0.0333 mol / L 65

We get the same result as before. 3.3.2 Temperature dependent equilibrium constants

We dened the equilibrium constant as K = eG/(RT ) . Thus, the equilibrium constant is temperature dependent because of the T in the denominator of the exponential. It is also temperature dependent because G is temperature dependent. To incorporate the temperature dependence of the Gibbs free energy of a reaction, we need to compute the temperature dependence. The NIST Webbook provides data about a large number of compounds which can be used to compute reaction energies. Let us consider CO. At http://webbook.nist.gov/cgi/cbook.cgi? ID=C630080&Units=SI&Mask=1#Thermo-Gas you will nd the data needed to compute the Gibbs free energy of CO at arbitrary temperature and standard pressure.

66

You will nd the standard heat of formation and entropy, coecients of the Shomate polynomials which are used to calculate the enthalpy and entropy at non-standard temperatures. The Shomate polynomials are polynomials in t = T /1000. H = HF,298.15 + At + Bt2 /2 + Ct3 /3 + Dt4 /4 E/t + F H S = A ln(t) + Bt + Ct2 /2 + Dt3 /3 E/(2t2 ) + G With this information, we can calculate G for CO at any temperature: G = H T S . If we have this information for all of the species, then we can compute the reaction energy at any temperature. Grxn (T ) = GJ (T )
1 2 3 4 5 6 7 8 9 10 11 12

import numpy as np R = 8.314e-3 # kJ/mol/K P = 10.0 # atm, this is the total pressure in the reactor Po = 1.0 # atm, this is the standard state pressure species = [CO, H2O, CO2, H2] # Heats of formation at 298.15 K Hf298 = [-110.53, # CO

67

13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53

-241.826, # H2O -393.51, # CO2 0.0] # H2 # Shomate parameters for each species # CO H2O CO2 H2 WB = [[25.56759, 30.092, 24.99735, 33.066178], # [6.09613, 6.832514, 55.18696, -11.363417], # [4.054656, 6.793435, -33.69137, 11.432816], # [-2.671301, -2.53448, 7.948387, -2.772874], # [0.131021, 0.082139, -0.136638, -0.158558], # [-118.0089, -250.881, -403.6075, -9.980797],# [227.3665, 223.3967, 228.2431, 172.707974], # [-110.5271, -241.8264, -393.5224, 0.0]] # WB = np.array(WB).T def G_rxn(T): # Shomate equations t = T/1000 T_H = np.array([t, t**2 / 2.0, t**3 / 3.0, t**4 / 4.0, -1.0 / t, 1.0, 0.0, -1.0]) T_S = np.array([np.log(t), t, t**2 / 2.0, t**3 / 3.0, -1.0 / (2.0 * t**2), 0.0, 1.0, 0.0]) H = np.dot(WB, T_H) # (H - H_298.15) kJ/mol S = np.dot(WB, T_S / 1000.0) # absolute entropy kJ/mol/K Gjo = Hf298 + H - T * S # Gibbs energy of each component at 1000 K

A B C D E F G H

nu = np.array([-1, -1, 1, 1]) Grxn = np.dot(nu, Gjo) return Grxn print Reaction energy at 1000K = {0} kJ/mol.format(G_rxn(1000)) # print energy in different units import quantities as u e500 = (G_rxn(500.0) * 1000 * u.J / u.mol ).rescale(u.cal / u.mol) e1000 = (G_rxn(1000.0) * 1000 * u.J / u.mol ).rescale(u.cal / u.mol)

68

54 55 56

print At 1000 K the reaction energy is {0}.format(e1000) print At 500 K the reaction energy is {0}.format(e500)

Reaction energy at 1000K = -3.00803816667 kJ/mol At 1000 K the reaction energy is -718.938376354 cal/mol At 500 K the reaction energy is -4890.09976584 cal/mol You can see here that lower temperatures make the reaction much more exothermic. The equilibrium constant would be considerably larger, and the products more favored at the lower temperature. 3.3.3 Another view of chemical equilibrium

The composition at chemical equilibrium is the one that minimizes the Gibbs free energy of the mixture. We can use the data for computing the Gibbs free energy of pure components to illustrate this. We have to compute the Gibbs free energy of a species in the mixture, which is easy if we can assume an ideal mixture. Then we have Gj = Gj 0 + RT log(xj P/P0 ). 69

Here we will show that there is a reaction extent that minimizes the Gibbs free energy of the mixture.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

import numpy as T = 1000 # R = 8.314e-3 # R_ = 0.082057 #

np K kJ/mol/K L * atm / mol / K

P = 10.0 # atm, this is the total pressure in the reactor Po = 1.0 # atm, this is the standard state pressure species = [CO, H2O, CO2, H2] nu = np.array([-1, -1, 1, 1]) # stoichiometric coefficients # Heats of formation at 298.15 K Hf298 = [ -110.53, # CO -241.826, # H2O -393.51, # CO2 0.0] # H2 # Shomate parameters for each species # CO H2O CO2 H2 WB = [[25.56759, 30.092, 24.99735, 33.066178], # [6.09613, 6.832514, 55.18696, -11.363417], # [4.054656, 6.793435, -33.69137, 11.432816], # [-2.671301, -2.53448, 7.948387, -2.772874], # [0.131021, 0.082139, -0.136638, -0.158558], # [-118.0089, -250.881, -403.6075, -9.980797],# [227.3665, 223.3967, 228.2431, 172.707974], # [-110.5271, -241.8264, -393.5224, 0.0]] # WB = np.array(WB).T # Shomate equations t = T/1000 T_H = np.array([t, t**2 / 2.0, t**3 / 3.0, t**4 / 4.0, -1.0 / t, 1.0, 0.0, -1.0]) T_S = np.array([np.log(t), t, t**2 / 2.0, t**3 / 3.0,

A B C D E F G H

70

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65

-1.0 / (2.0 * t**2), 0.0, 1.0, 0.0]) H = np.dot(WB, T_H) # (H - H_298.15) kJ/mol S = np.dot(WB, T_S / 1000.0) # absolute entropy kJ/mol/K Gjo = Hf298 + H - T * S # Gibbs energy of each component at 1000 K

C0 = np.array([5.0, 5.0, 0.0, 0.0]) / R_ / T # initial concentrations @np.vectorize def G_tot(xi): C = C0 + xi * nu # change in moles from reaction extent x = C / C.sum() # mole fractions

# Species gibbs energies in mixture G = Gjo + R * T * np.log(x * P / Po) return np.dot(C, G)

XI = np.linspace(0, max(C0)) import matplotlib.pyplot as plt plt.plot(XI, G_tot(XI)) plt.xlabel($\\xi$ (mol)) plt.ylabel($G$ (kJ/mol)) plt.savefig(images/equilibrium-G.png) plt.show()

71

You have to be careful not to exceed the maximum . You can see there is a minimum in the Gibbs energy of the mixture, and it corresponds to the value we saw previously. We could nd the minimum numerically using optimization algorithms. For completeness we show that here:
1 2 3 4 5 6 7 8 9 10 11

import numpy as T = 1000 # R = 8.314e-3 # R_ = 0.082057 #

np K kJ/mol/K L * atm / mol / K

P = 10.0 # atm, this is the total pressure in the reactor Po = 1.0 # atm, this is the standard state pressure species = [CO, H2O, CO2, H2] nu = np.array([-1, -1, 1, 1]) # stoichiometric coefficients

72

12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52

# Heats of formation at 298.15 K Hf298 = [ -110.53, # CO -241.826, # H2O -393.51, # CO2 0.0] # H2 # Shomate parameters for each species # CO H2O CO2 H2 WB = [[25.56759, 30.092, 24.99735, 33.066178], # [6.09613, 6.832514, 55.18696, -11.363417], # [4.054656, 6.793435, -33.69137, 11.432816], # [-2.671301, -2.53448, 7.948387, -2.772874], # [0.131021, 0.082139, -0.136638, -0.158558], # [-118.0089, -250.881, -403.6075, -9.980797],# [227.3665, 223.3967, 228.2431, 172.707974], # [-110.5271, -241.8264, -393.5224, 0.0]] # WB = np.array(WB).T # Shomate equations t = T/1000 T_H = np.array([t, t**2 / 2.0, t**3 / 3.0, t**4 / 4.0, -1.0 / t, 1.0, 0.0, -1.0]) T_S = np.array([np.log(t), t, t**2 / 2.0, t**3 / 3.0, -1.0 / (2.0 * t**2), 0.0, 1.0, 0.0]) H = np.dot(WB, T_H) # (H - H_298.15) kJ/mol S = np.dot(WB, T_S / 1000.0) # absolute entropy kJ/mol/K Gjo = Hf298 + H - T * S # Gibbs energy of each component at 1000 K

A B C D E F G H

C0 = np.array([5.0, 5.0, 0.0, 0.0]) / R_ / T # initial concentrations @np.vectorize def G_tot(xi): C = C0 + xi * nu # change in moles from reaction extent x = C / C.sum() # mole fractions

73

53 54 55 56 57 58 59 60 61 62

# Species gibbs energies in mixture G = Gjo + R * T * np.log(x * P / Po) return np.dot(C, G) from scipy.optimize import fmin xi_guess = 0.03 sol, = fmin(G_tot, xi_guess) print The Gibbs free energy is minimized at xi = {0} mol/L.format(sol)

Optimization terminated successfully. Current function value: -46.204283 Iterations: 6 Function evaluations: 12 The Gibbs free energy is minimized at xi = 0.0331875 mol/L 3.3.4 Gibbs energy constrained minimization and the NIST webbook

We used the NIST webbook to compute a temperature dependent Gibbs energy of reaction, and then used a reaction extent variable to compute the equilibrium concentrations of each species for the water gas shift reaction. Here we look at the direct minimization of the Gibbs free energy of the species, with no assumptions about stoichiometry of reactions. We only apply the constraint of conservation of atoms. We use the NIST Webbook to provide the data for the Gibbs energy of each species. 74

As a reminder we consider equilibrium between the species CO, H2 O, CO2 and H2 , at 1000K, and 10 atm total pressure with an initial equimolar molar ow rate of CO and H2 O.
1 2 3 4 5 6 7

import numpy as np T = 1000 # K R = 8.314e-3 # kJ/mol/K P = 10.0 # atm, this is the total pressure in the reactor Po = 1.0 # atm, this is the standard state pressure

We are going to store all the data and calculations in vectors, so we need to assign each position in the vector to a species. Here are the denitions we use in this work. 1 2 3 4
1 2 3 4 5 6 7 8 9 10

CO H2O CO2 H2

species = [CO, H2O, CO2, H2] # Heats of formation at 298.15 K Hf298 = [ -110.53, -241.826, -393.51, 0.0]

# # # #

CO H2O CO2 H2

75

11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35

# Shomate parameters for each species # CO H2O CO2 H2 WB = [[25.56759, 30.092, 24.99735, 33.066178], # [6.09613, 6.832514, 55.18696, -11.363417], # [4.054656, 6.793435, -33.69137, 11.432816], # [-2.671301, -2.53448, 7.948387, -2.772874], # [0.131021, 0.082139, -0.136638, -0.158558], # [-118.0089, -250.881, -403.6075, -9.980797],# [227.3665, 223.3967, 228.2431, 172.707974], # [-110.5271, -241.8264, -393.5224, 0.0]] # WB = np.array(WB).T

A B C D E F G H

# Shomate equations t = T/1000 T_H = np.array([t, t**2 / 2.0, t**3 / 3.0, t**4 / 4.0, -1.0 / t, 1.0, 0.0, -1.0]) T_S = np.array([np.log(t), t, t**2 / 2.0, t**3 / 3.0, -1.0 / (2.0 * t**2), 0.0, 1.0, 0.0]) H = np.dot(WB, T_H) # (H - H_298.15) kJ/mol S = np.dot(WB, T_S/1000.0) # absolute entropy kJ/mol/K Gjo = Hf298 + H - T*S # Gibbs energy of each component at 1000 K

Now, construct the Gibbs free energy function, accounting for the change in activity due to concentration changes (ideal mixing).
1 2 3 4 5

def func(nj): nj = np.array(nj) Enj = np.sum(nj); Gj = Gjo / (R * T) + np.log(nj / Enj * P / Po) return np.dot(nj, Gj)

We impose the constraint that all atoms are conserved from the initial conditions to the equilib76

rium distribution of species. These constraints are in the form of Aeq n = beq , where n is the vector of mole numbers for each species.
1 2 3 4 5 6 7 8 9 10 11 12

Aeq = np.array([[ 1, [ 1, [ 0, # equimolar feed of beq = np.array([1, 2, 2]) 1 # # #

0, 1, 2, mol mol mol mol

1, 2, 0,

0], # C balance 0], # O balance 2]]) # H balance

H2O and 1 mol CO C fed O fed H fed

def ec1(nj): conservation of atoms constraint return np.dot(Aeq, nj) - beq

Now we are ready to solve the problem.


1 2 3 4 5

from scipy.optimize import fmin_slsqp n0 = [0.5, 0.5, 0.5, 0.5] # initial guesses N = fmin_slsqp(func, n0, f_eqcons=ec1) print N

>>> >>> Optimization terminated successfully. (Exit mode Current function value: -91.204832308 Iterations: 2 Function evaluations: 13 Gradient evaluations: 2 [ 0.45502309 0.45502309 0.54497691 0.54497691] 77

1. Compute mole fractions and partial pressures The pressures here are in good agreement with the pressures found by other methods. The minor disagreement (in the third or fourth decimal place) is likely due to convergence tolerances in the dierent algorithms used.
1 2 3 4 5

yj = N / np.sum(N) Pj = yj * P for s, y, p in zip(species, yj, Pj): print {0:10s}: {1:1.2f} {2:1.2f}.format(s, y, p)

>>> >>> ... ... CO H2O : 0.23 2.28 CO2 : 0.27 2.72 H2 : 0.27 2.72

: 0.23 2.28

2. Computing equilibrium constants We can compute the equilibrium constant for the reaction CO + H2 O CO2 + H2 . Compared to the value of K = 1.44 we found previously, the agreement is excellent. Note, that to dene an equilibrium constant it is necessary to specify a reaction, even though 78

it is not necessary to even consider a reaction to obtain the equilibrium distribution of species!
1 2 3

nuj = np.array([-1, -1, 1, 1]) K = np.prod(yj**nuj) print K

# stoichiometric coefficients of the reaction

>>> 1.43446295961 3.3.5 Finding equilibrium composition by direct minimization of Gibbs free energy on mole numbers

Adapted from problem 4.5 in Cutlip and Shacham Ethane and steam are fed to a steam cracker at a total pressure of 1 atm and at 1000K at a ratio of 4 mol H2O to 1 mol ethane. Estimate the equilibrium distribution of products (CH4 , C2 H4 , C2 H2 , CO2 , CO, O2 , H2 , H2 O, and C2 H6 ). Solution method: We will construct a Gibbs energy function for the mixture, and obtain the equilibrium composition by minimization of the function subject to elemental mass balance constraints.
1 2 3

import numpy as np R = 0.00198588 # kcal/mol/K

79

4 5 6 7 8 9 10

T = 1000 # K species = [CH4, C2H4, C2H2, CO2, CO, O2, H2, H2O, C2H6]

# $G_^\circ for each species. These are the heats of formation for each # species. Gjo = np.array([4.61, 28.249, 40.604, -94.61, -47.942, 0, 0, -46.03, 26.13]) # kcal/mo

1. The Gibbs energy of a mixture We start with G =


j

nj j .

Recalling that we dene j = G j + RT ln aj , and in the ideal gas limit, aj = yj P/P , and j that yj = nn . j Since in this problem, P = 1 atm, this leads to n Gj G j the function RT = + ln nn . nj RT j
j =1
1 2 3 4 5 6 7

import numpy as np def func(nj): nj = np.array(nj) Enj = np.sum(nj); G = np.sum(nj * (Gjo / R / T + np.log(nj / Enj))) return G

2. Linear equality constraints for atomic mass conservation The total number of each type of atom must be the same as what entered the reactor. These form equality constraints on the equilibrium composition. 80

We express these constraints as: Aeq n = b where n is a vector of the moles of each species present in the mixture.
1 2 3 4 5 6 7 8 9 10 11 12

Aeq = np.array([[0, [4, [1, # the incoming feed beq = np.array([4, 14, 2])

0, 4, 2,

0, 2, 2,

2, 0, 1,

1, 0, 1,

2, 0, 0,

0, 2, 0,

1, 2, 0,

0], 6], 2]])

# oxygen balance # hydrogen balance # carbon balance

was 4 mol H2O and 1 mol ethane # moles of oxygen atoms coming in # moles of hydrogen atoms coming in # moles of carbon atoms coming in

def ec1(n): equality constraint return np.dot(Aeq, n) - beq

Now we solve the problem.


1 2 3 4 5 6 7 8 9 10 11 12 13

# initial guess suggested in the example n0 = [1e-3, 1e-3, 1e-3, 0.993, 1.0, 1e-4, 5.992, 1.0, 1e-3] from scipy.optimize import fmin_slsqp X = fmin_slsqp(func, n0, f_eqcons=ec1, iter=300, acc=1e-12) for s,x in zip(species, X): print {0:10s} {1:1.4g}.format(s, x) # check that constraints were met print np.dot(Aeq, X) - beq print np.all( np.abs( np.dot(Aeq, X) - beq) < 1e-12)

>>> >>> >>> >>> Optimization terminated successfully. Current function value: -104.403947663 81

Iterations: 217 Function evaluations: 2937 Gradient evaluations: 217 >>> ... ... CH4 0.06694 C2H4 8.108e-08 C2H2 5.174e-08 CO2 0.5441 CO 1.389 O2 1.222e-14 H2 5.343 H2O 1.523 C2H6 8.44e-08 ... [ -1.66977543e-13 1.77635684e-15 True I found it necessary to tighten the accuracy parameter to get pretty good matches to the solutions found in Matlab. It was also necessary to increase the number of iterations. Even still, not all of the numbers match well, especially the very small numbers. You can, however, see that the constraints were satised pretty well. Interestingly there is a distribution of products! That is interesting because only steam and ethane enter the reactor, but a small fraction of methane is formed! 82

4.44089210e-16]

The main product is hydrogen. The stoichiometry of steam reforming is ideally C2 H6 +4H2 O 2CO2 +7H 2. Even though nearly all the ethane is consumed, we do not get the full yield of hydrogen. It appears that another equilibrium, one between CO, CO2 , H2 O and H2 , may be limiting that, since the rest of the hydrogen is largely in the water. It is also of great importance that we have not said anything about reactions, i.e. how these products were formed. The water gas shift reaction is: CO + H2 O CO2 + H2 . We can compute the Gibbs free energy of the reaction from the heats of formation of each species. Assuming these are the formation energies at 1000K, this is the reaction free energy at 1000K.
1 2 3 4 5

G_wgs = Gjo[3] + Gjo[6] - Gjo[4] - Gjo[7] print G_wgs K = np.exp(-G_wgs / (R*T)) print K

-0.638 >>> >>> 1.37887528109 83

3. Equilibrium constant based on mole numbers One normally uses activities to dene the equilibrium constant. Since there are the same number of moles on each side of the reaction all factors that convert mole numbers to activity, concentration or pressure cancel, so we simply consider the ratio of mole numbers here.
1

print (X[3] * X[6]) / (X[4] * X[7])

1.37450039394 This is close, but not exactly the same as the equilibrium constant computed above. They should be exactly the same, and the dierence is due to convergence errors in the solution to the problem. Clearly, there is an equilibrium between these species that prevents the complete reaction of steam reforming. 4. Summary This is an appealing way to minimize the Gibbs energy of a mixture. No assumptions about reactions are necessary, and the constraints are easy to identify. The Gibbs energy function is especially easy to code. 84

3.3.6

Equilibria with multiple reactions

We use the same fundamental approaches to solving equilibrium problems when there are multiple reactions In fact, we do not need to consider reactions at all if we know the Gibbs free energies of each species Let us consider this set of reactions, where all species are in the gas phase. Assume we start with equimolar amounts of A and B, and a total pressure of 2.5 atm at 400 K. A+B A+B C with K1 = 108 D with K2 = 284

We want to know what the equilibrium composition for these reactions are. aC We have two equations: K1 = aA aB and aD K2 = aA aB We know the activity of a gas species is aj = Pj /1atm or equivalently in mole fraction: aj = xj P/1atm. We dene reaction extents for each reaction: 1 and 2 85

Then: nA = nA0 1 2 nB = nB 0 1 2 nC = 1 nD = 2 ntotal = nt0 1 2 (17) (18) (19) (20) (21)

We can dene mole fractions from these equations which allow us to express the equilibrium equations in two unknowns. It is convenient to normalize all equations by nt0 , which leads to these denitions for the mole fractions:

yA0 1 2 1 1 2 yB 0 1 2 yB = 1 1 2 1 yC = 1 1 2 2 yD = 1 1 2 yA =

(22) (23) (24) (25) (26)

86

These are plugged into the activity aj = yj P/1atm. Here is the code we use to solve this problem.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30

ya0 = 0.5 # initial mole fraction of A yb0 = 0.5 # initial mole fraction of B P = 2.5 # initial pressure in atm def xj(extent): convenience function to calculate mole fractions ext1, ext2 = extent ya = (ya0 - ext1 - ext2) / (1.0 - ext1 - ext2) yb = (yb0 - ext1 - ext2) / (1.0 - ext1 - ext2) yc = (ext1) / (1.0 - ext1 - ext2) yd = (ext2) / (1.0 - ext1 - ext2) return [ya, yb, yc, yd] def func(extent): zeros function for fsolve ya, yb, yc, yd = xj(extent) eq1 = 108.0 - (yc * P)/(ya * P * yb * P) eq2 = 284.0 - (yd * P)/(ya * P * yb * P) return [eq1, eq2] from scipy.optimize import fsolve guess = [0.1, 0.39] sol = fsolve(func, guess) print The reaction extents are:\n,sol print The mole fractions are: \n,xj(sol)

The reaction extents are: [ 0.13335692 0.35067931] The mole fractions are: 87

[0.030939713802784111, 0.030939713802784111, 0.258461790353 There are signicant amounts of each product Note that other initial guesses give unphysical solutions, i.e. negative mole fractions. Also note that this solution applies to a constant total pressure which means in this case the volume must be changing since there is a change in the number of moles You would get a dierent result in a constant volume reactor where the total pressure changes There is a constraint on the two reaction extents. since no mole fraction can be negative, 1 +2 yA0 Other solutions violate this constraint You may have to use constrained optimization to nd physical solutions

3.4

Rate laws for reversible reactions

We can think of reversible reactions as two reactions going in opposite directions. A+B C + D can be thought of as: 88

A+B C +D C +D A+B Each reaction has a forward reaction rate, e.g.: r1 = k1 CA CB r2 = k2 CC CD Now, to nd the rate that species A is "generated" we have: r1A = r1 and r2A = r2 , and the net rate is rA = r1A + r2A = k1 CA CB + k2 CC CD . At equilibrium, the net rate must be zero, which means: k1 CA,eq CB,eq = k2 CC,eq CD,eq or: CC,eq CD,eq k1 = k2 CA,eq CB,eq = Keq You can see that between k1 , k2 and Keq , only two of them are independent. i.e. k2 = k1 /Keq . Thus, we may also see net reaction rates for equilibrium reaction rates written as:
C CD rA = k1 (CA CB CK ) eq

It is important that these constraints exist, so that thermodynamics are not violated. 89

3.4.1

A CSTR with a reversible reaction

Recall the water gas shift reaction we discussed before H2 O + CO CO2 + H2 . We previously calculated the equilibrium coecient to be 1.44 at 1000K. Assume the reaction is elementary, and the forward rate constant is k1 = 0.02 L / (mol * s) The reactor is initially fed pure A and B at concentrations of 0.05 mol / L. What is the exit concentration of A? the reactor volume is 10 L, and the volumetric ow rate into the reactor is 0.01 L / s.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17

from scipy.optimize import fsolve Keq = 1.44395809814 v0 = 0.01 # L / s V = 10 # L k1 = 0.02 # L / mol / s Ca0 = Cb0 = 0.05 # mol / L Cc0 = Cd0 = 0.0 Fa0 = v0 * Ca0 def cstr(Ca): xi = (Ca - Ca0) / (-1) # compute reaction extent Cb = Cb0 - xi Cc = Cc0 + xi

90

18 19 20 21 22 23 24 25 26 27

Cd = Cd0 + xi ra = -k1 * (Ca * Cb - (Cc * Cd) / Keq) return Fa0 - Ca * v0 + V * ra guess = 0.2 ca_exit, = fsolve(cstr, guess) print the exit concentration of C_A is {0:1.4f} mol / L.format(ca_exit) print the exit concentration of C_C is {0:1.4f} mol / L.format(Ca0 - ca_exit)

the exit concentration of C_A is 0.0327 mol / L the exit concentration of C_C is 0.0173 mol / L There is less C produced than you would expect from the equilibrium composition The reactants are not in the reactor long enough to reach equilibrium You can explore this solution. Try using a lower volumetric ow rate, or a larger volume reactor. You will see that the concentrations converge to the equilibrium limit we computed before.

3.5

Mole balances with multiple reactions

There is nothing particularly new in mole balances with multiple reactions We still write species based mole balances 91

We use the net rate law for each species This typically leads to coupled equations For CSTRs these are often coupled nonlinear algebra equations For PFRs these are often coupled dierential equations 3.5.1 Multiple reactions in a CSTR

We consider a reactor design with multiple reactions Mesitylene (trimethyl benzene) can be hydrogenated to form m-xylene, which can be further hydrogenated to toluene The reactions we consider are:

M + H2 X + CH4 X + H2 T + CH4

(27) (28)

The reaction is carried out isothermally at 1500 R at 35 atm. The feed is 2/3 hydrogen and 1/3 mesitylene 92

The volumetric feed rate is 476 cubic feet per hour and the reactor volume is 238 cubic feet The rate laws are

0.5 r1 = k1 CM CH 0.5 r2 = k2 CX CH

(29) (30)

The rate constants are: k1 = 55.20(ft3 /lb mol)0.5 /h k2 = 30.20(ft3 /lb mol)0.5 /h (31) (32) (33)

Here is the code we need to setup and solve this problem.


1 2 3 4 5 6 7 8 9 10 11 12 13

def funcC(C): vo = 476.0 # ft^3 / hr V = 238.0 # ft^3 Po = 35.0 # atm T = 1500.0 # Rankine R = 0.73 # in appropriate units CTo = Po / R / T Cmo = CTo / 3.0 Cho = CTo * 2.0 / 3.0 Cxo = 0.0 Cmeo = 0.0 Ctolo = 0.0

93

14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45

tau = V / vo CM, CH, CX, CMe, CT = C # rate laws k1 = 55.20 # (ft^3/lbmol)^0.5/h k2 = 30.20 # (ft^3/lbmol)^0.5/h r1m = -k1 * CM * CH**0.5 r2t = k2 * CX * CH**0.5 # net rates rM = r1m rH = r1m - r2t rX = -r1m - r2t rMe = -r1m + r2t rT = r2t return [tau tau tau tau tau * * * * * (-rM) - Cmo + (-rH) - Cho + (-rX) - Cxo + (-rMe) - Cmeo (-rT) - Ctolo CM, CH, CX, + CMe, + CT]

initGuesses = [0.002, 0.002, 0.002, 0.002, 0.002] from scipy.optimize import fsolve exitC = fsolve(funcC, initGuesses) species = [M, H, X, Me, T] for s,C in zip(species, exitC): print {0:^3s}{1:1.5f} lbmol/ft^3.format(s,C)

M H X Me T

0.00294 0.00905 0.00317 0.01226 0.00455

lbmol/ft^3 lbmol/ft^3 lbmol/ft^3 lbmol/ft^3 lbmol/ft^3 94

3.5.2

Multiple reactions in a PFR

Now we solve the same problem in a PFR.


1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36

import numpy as np from scipy.integrate import odeint vo = 476.0 # ft^3 / hr Po = 35.0 # atm T = 1500.0 # Rankine R = 0.73 # in appropriate units CTo = Po / R / T Fto = CTo * vo # initial molar flows Fmo = Fto / 3.0 Fho = Fto * 2.0 / 3.0 Fxo = 0.0 Fmeo = 0.0 Ftolo = 0.0 def dFdV(F, t): PFR moe balances Ft = F.sum() v = vo * Ft / Fto C = F / v CM, CH, CX, CMe, CT = C # rate laws k1 = 55.20 k2 = 30.20 r1m = -k1 * CM * CH**0.5 r2t = k2 * CX * CH**0.5 # net rates rM = r1m rH = r1m - r2t rX = -r1m - r2t rMe = -r1m + r2t

95

37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72

rT = r2t dFMdV = rM dFHdV = rH dFXdV = rX dFMedV = rMe dFTdV = rT return [ dFMdV, dFHdV, dFXdV, dFMedV, dFTdV ] Finit = [Fmo, Fho, Fxo, Fmeo, Ftolo] Vspan = np.linspace(0.0, 238.0) sol = odeint(dFdV, Finit, Vspan) Ft = sol.sum(axis=1) v = vo * Ft / Fto FM FH FX FMe FT = = = = = sol[:,0] sol[:,1] sol[:,2] sol[:,3] sol[:,4] # sum each row

tau = Vspan / vo import matplotlib.pyplot as plt plt.plot(tau, FM / v, label=$C_M$) plt.plot(tau, FH / v, label=$C_H$) plt.plot(tau, FX / v, label=$C_X$) plt.legend(loc=best) plt.xlabel($\\tau$ (hr)) plt.ylabel(Concentration (lbmol/ft$^3$)) plt.savefig(images/multiple-reactions-pfr.png) plt.show()

96

You can see that the basic approach is the same as for a single reaction the code is just a lot longer In this example it was not necessary to compute the total molar ow. Inspection shows that it is a constant. Hence, the volumetric ow is also constant.

97