<?xml version="1.0" encoding="UTF-8"?>

<rss version="2.0"
     xmlns:content="http://purl.org/rss/1.0/modules/content/"
     xmlns:sy="http://purl.org/rss/1.0/modules/syndication/"
     xmlns:atom="http://www.w3.org/2005/Atom"
     xmlns:dc="http://purl.org/dc/elements/1.1/"
     xmlns:wfw="http://wellformedweb.org/CommentAPI/"
     >
  <channel>
    <atom:link href="http://kitchingroup.cheme.cmu.edu/blog/feed/index.xml" rel="self" type="application/rss+xml" />
    <title>The Kitchin Research Group</title>
    <link>https://kitchingroup.cheme.cmu.edu/blog</link>
    <description>Chemical Engineering at Carnegie Mellon University</description>
    <pubDate>Sat, 01 Nov 2025 13:47:46 GMT</pubDate>
    <generator>Blogofile</generator>
    <sy:updatePeriod>hourly</sy:updatePeriod>
    <sy:updateFrequency>1</sy:updateFrequency>
    
    <item>
      <title>Modeling a transient plug flow reactor</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2013/03/06/Modeling-a-transient-plug-flow-reactor</link>
      <pubDate>Wed, 06 Mar 2013 15:51:44 EST</pubDate>
      <category><![CDATA[animation]]></category>
      <category><![CDATA[pde]]></category>
      <guid isPermaLink="false">oErIFo4T0c1Hz6pZAjHjrMITWks=</guid>
      <description>Modeling a transient plug flow reactor</description>
      <content:encoded><![CDATA[


&lt;p&gt;
&lt;a href="http://matlab.cheme.cmu.edu/2011/11/17/modeling-a-transient-plug-flow-reactor" &gt;Matlab post&lt;/a&gt;



&lt;/p&gt;

&lt;p&gt;
The PDE that describes the transient behavior of a plug flow reactor with constant volumetric flow rate is:
&lt;/p&gt;

&lt;p&gt;
\( \frac{\partial C_A}{\partial dt} = -\nu_0 \frac{\partial C_A}{\partial dV} + r_A \).
&lt;/p&gt;

&lt;p&gt;
To solve this numerically in python, we will utilize the method of lines. The idea is to discretize the reactor in volume, and approximate the spatial derivatives by finite differences. Then we will have a set of coupled ordinary differential equations that can be solved in the usual way. Let us simplify the notation with \(C = C_A\), and let \(r_A = -k C^2\). Graphically this looks like this:
&lt;/p&gt;

&lt;p&gt;&lt;img src="/img/./images/pde-method-of-lines.png"&gt;&lt;p&gt;

&lt;p&gt;
This leads to the following set of equations:
&lt;/p&gt;

\begin{eqnarray}
\frac{dC_0}{dt} &amp;=&amp; 0 \text{ (entrance concentration never changes)} \\
\frac{dC_1}{dt} &amp;=&amp; -\nu_0 \frac{C_1 - C_0}{V_1 - V_0} - k C_1^2 \\
\frac{dC_2}{dt} &amp;=&amp; -\nu_0 \frac{C_2 - C_1}{V_2 - V_1} - k C_2^2 \\
\vdots \\
\frac{dC_4}{dt} &amp;=&amp; -\nu_0 \frac{C_4 - C_3}{V_4 - V_3} - k C_4^2 
\end{eqnarray}

&lt;p&gt;
Last, we need initial conditions for all the nodes in the discretization. Let us assume the reactor was full of empty solvent, so that \(C_i = 0\) at \(t=0\). In the next block of code, we get the transient solutions, and the steady state solution.
&lt;/p&gt;
&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; numpy &lt;span style="color: #8b0000;"&gt;as&lt;/span&gt; np
&lt;span style="color: #8b0000;"&gt;from&lt;/span&gt; scipy.integrate &lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; odeint

Ca0 = 2     &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;Entering concentration&lt;/span&gt;
vo = 2      &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;volumetric flow rate&lt;/span&gt;
volume = 20 &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;total volume of reactor, spacetime = 10&lt;/span&gt;
k = 1       &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;reaction rate constant&lt;/span&gt;

N = 100     &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;number of points to discretize the reactor volume on&lt;/span&gt;

init = np.zeros(N)    &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;Concentration in reactor at t = 0&lt;/span&gt;
init[0] = Ca0         &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;concentration at entrance&lt;/span&gt;

V = np.linspace(0, volume, N) &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;discretized volume elements&lt;/span&gt;
tspan = np.linspace(0, 25)    &lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;time span to integrate over&lt;/span&gt;

&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;method_of_lines&lt;/span&gt;(C, t):
    &lt;span style="color: #228b22;"&gt;'coupled ODES at each node point'&lt;/span&gt;
    D = -vo * np.diff(C) / np.diff(V) - k * C[1:]**2
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; np.concatenate([[0], &lt;span style="color: #ff0000; font-weight: bold;"&gt;#&lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;C0 is constant at entrance&lt;/span&gt;
                            D])


sol = odeint(method_of_lines, init, tspan)

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;steady state solution&lt;/span&gt;
&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;pfr&lt;/span&gt;(C, V):
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; 1.0 / vo * (-k * C**2)

ssol = odeint(pfr, Ca0, V)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The transient solution contains the time dependent behavior of each node in the discretized reactor. Each row contains the concentration as a function of volume at a specific time point. For example, we can plot the concentration of A at the exit vs. time (that is, the last entry of each row) as:
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #8b0000;"&gt;as&lt;/span&gt; plt
plt.plot(tspan, sol[:, -1])
plt.xlabel(&lt;span style="color: #228b22;"&gt;'time'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #228b22;"&gt;'$C_A$ at exit'&lt;/span&gt;)
plt.savefig(&lt;span style="color: #228b22;"&gt;'images/transient-pfr-1.png'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;pre class="example"&gt;
[&amp;lt;matplotlib.lines.Line2D object at 0x05A18830&amp;gt;]
&amp;lt;matplotlib.text.Text object at 0x059FE1D0&amp;gt;
&amp;lt;matplotlib.text.Text object at 0x05A05270&amp;gt;
&lt;/pre&gt;


&lt;p&gt;&lt;img src="/img/./images/transient-pfr-1.png"&gt;&lt;p&gt;

&lt;p&gt;
After approximately one space time, the steady state solution is reached at the exit. For completeness, we also examine the steady state solution.
&lt;/p&gt;
&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;plt.figure()
plt.plot(V, ssol, label=&lt;span style="color: #228b22;"&gt;'Steady state'&lt;/span&gt;)
plt.plot(V, sol[-1], label=&lt;span style="color: #228b22;"&gt;'t = {}'&lt;/span&gt;.format(tspan[-1]))
plt.xlabel(&lt;span style="color: #228b22;"&gt;'Volume'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #228b22;"&gt;'$C_A$'&lt;/span&gt;)
plt.legend(loc=&lt;span style="color: #228b22;"&gt;'best'&lt;/span&gt;)
plt.savefig(&lt;span style="color: #228b22;"&gt;'images/transient-pfr-2.png'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;&lt;img src="/img/./images/transient-pfr-2.png"&gt;&lt;p&gt;

&lt;p&gt;
There is some minor disagreement between the final transient solution and the steady state solution. That is due to the approximation in discretizing the reactor volume. In this example we used 100 nodes. You get better agreement with a larger number of nodes, say 200 or more. Of course, it takes slightly longer to compute then, since the number of coupled odes is equal to the number of nodes.
&lt;/p&gt;

&lt;p&gt;
We can also create an animated gif to show how the concentration of A throughout the reactor varies with time. Note, I had to install ffmpeg (&lt;a href="http://ffmpeg.org/" &gt;http://ffmpeg.org/&lt;/a&gt;) to save the animation.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;

&lt;pre class="src src-python"&gt;&lt;span style="color: #8b0000;"&gt;from&lt;/span&gt; matplotlib &lt;span style="color: #8b0000;"&gt;import&lt;/span&gt; animation

&lt;span style="color: #ff0000; font-weight: bold;"&gt;# &lt;/span&gt;&lt;span style="color: #ff0000; font-weight: bold;"&gt;make empty figure&lt;/span&gt;
fig = plt.figure()
ax = plt.axes(xlim=(0, 20), ylim=(0, 2))
line, = ax.plot(V, init, lw=2)

&lt;span style="color: #8b0000;"&gt;def&lt;/span&gt; &lt;span style="color: #8b2323;"&gt;animate&lt;/span&gt;(i):
    line.set_xdata(V)
    line.set_ydata(sol[i])
    ax.set_title(&lt;span style="color: #228b22;"&gt;'t = {0}'&lt;/span&gt;.format(tspan[i]))
    ax.figure.canvas.draw() 
    &lt;span style="color: #8b0000;"&gt;return&lt;/span&gt; line,
    

anim = animation.FuncAnimation(fig, animate, frames=50,  blit=&lt;span style="color: #8b0000;"&gt;True&lt;/span&gt;)

anim.save(&lt;span style="color: #228b22;"&gt;'images/transient_pfr.mp4'&lt;/span&gt;, fps=10)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
&lt;a href="http://jkitchin.github.com/media/transient_pfr.mp4" &gt;http://jkitchin.github.com/media/transient_pfr.mp4&lt;/a&gt;
&lt;/p&gt;

&lt;p&gt;
You can see from the animation that after about 10 time units, the solution is not changing further, suggesting steady state has been reached.
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2013 by John Kitchin. See the &lt;a href="/copying.html"&gt;License&lt;/a&gt; for information about copying.&lt;p&gt;&lt;p&gt;&lt;a href="/org/2013/03/06/Modeling-a-transient-plug-flow-reactor.org"&gt;org-mode source&lt;/a&gt;&lt;p&gt;]]></content:encoded>
    </item>
  </channel>
</rss>
