<?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>Solving differential algebraic equations with help from autograd</title>
      <link>https://kitchingroup.cheme.cmu.edu/blog/2019/09/22/Solving-differential-algebraic-equations-with-help-from-autograd</link>
      <pubDate>Sun, 22 Sep 2019 12:59:25 EDT</pubDate>
      <category><![CDATA[dae]]></category>
      <category><![CDATA[autograd]]></category>
      <category><![CDATA[ode]]></category>
      <guid isPermaLink="false">eATX_e4VOuQRCAt5B_CujmZtj4w=</guid>
      <description>Solving differential algebraic equations with help from autograd</description>
      <content:encoded><![CDATA[


&lt;p&gt;
This problem is adapted from one in "Problem Solving in Chemical Engineering with Numerical Methods, Michael B. Cutlip, Mordechai Shacham".
&lt;/p&gt;

&lt;p&gt;
In the binary, batch distillation of benzene (1) and toluene (2), the moles of liquid \(L\) remaining as a function of the mole fraction of toluene (\(x_2\)) is expressed by:
&lt;/p&gt;

&lt;p&gt;
\(\frac{dL}{dx_2} = \frac{L}{x_2 (k_2 - 1)}\)
&lt;/p&gt;

&lt;p&gt;
where \(k_2\) is the vapor liquid equilibrium ratio for toluene. This can be computed as:
&lt;/p&gt;

&lt;p&gt;
\(k_i = P_i / P\) where \(P_i = 10^{A_i + \frac{B_i}{T +C_i}}\) and that pressure is in mmHg, and the temperature is in degrees Celsius.
&lt;/p&gt;

&lt;p&gt;
One difficulty in solving this problem is that the temperature is not constant; it changes with the composition. We know that the temperature changes to satisfy this constraint  \(k_1(T) x_1 + k_2(T) x_2 = 1\).
&lt;/p&gt;

&lt;p&gt;
Sometimes, one can solve for T directly, and substitute it into the first ODE, but this is not a possibility here. One way you might solve this is to use the constraint to find \(T\) inside an ODE function, but that is tricky; nonlinear algebra solvers need a guess and don't always converge, or may converge to non-physical solutions. They also require iterative solutions, so they will be slower than an approach where we just have to integrate the solution.  A better way is to derive a second ODE \(dT/dx_2\) from the constraint.  The constraint is implicit in \(T\), so We  compute it as \(dT/dx_2 = -df/dx_2 / df/dT\) where \(f(x_2, T) = k_1(T) x_1 + k_2(T) x_2  - 1 = 0\). This equation is used to compute the bubble point temperature. Note, it is possible to derive these analytically, but who wants to?  We can use autograd to get those derivatives for us instead.
&lt;/p&gt;

&lt;p&gt;
The following information is given:
&lt;/p&gt;

&lt;p&gt;
The total pressure is fixed at 1.2 atm, and the distillation starts at \(x_2=0.4\). There are initially 100 moles in the distillation.
&lt;/p&gt;

&lt;table border="2" cellspacing="0" cellpadding="6" rules="groups" frame="hsides"&gt;


&lt;colgroup&gt;
&lt;col  class="org-left" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;

&lt;col  class="org-right" /&gt;
&lt;/colgroup&gt;
&lt;thead&gt;
&lt;tr&gt;
&lt;th scope="col" class="org-left"&gt;species&lt;/th&gt;
&lt;th scope="col" class="org-right"&gt;A&lt;/th&gt;
&lt;th scope="col" class="org-right"&gt;B&lt;/th&gt;
&lt;th scope="col" class="org-right"&gt;C&lt;/th&gt;
&lt;/tr&gt;
&lt;/thead&gt;
&lt;tbody&gt;
&lt;tr&gt;
&lt;td class="org-left"&gt;benzene&lt;/td&gt;
&lt;td class="org-right"&gt;6.90565&lt;/td&gt;
&lt;td class="org-right"&gt;-1211.033&lt;/td&gt;
&lt;td class="org-right"&gt;220.79&lt;/td&gt;
&lt;/tr&gt;

&lt;tr&gt;
&lt;td class="org-left"&gt;toluene&lt;/td&gt;
&lt;td class="org-right"&gt;6.95464&lt;/td&gt;
&lt;td class="org-right"&gt;-1344.8&lt;/td&gt;
&lt;td class="org-right"&gt;219.482&lt;/td&gt;
&lt;/tr&gt;
&lt;/tbody&gt;
&lt;/table&gt;

&lt;p&gt;
We have to start by finding the initial temperature from the constraint.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; autograd.numpy &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; np
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; autograd &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; grad
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.integrate &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; solve_ivp
&lt;span style="color: #0000FF;"&gt;from&lt;/span&gt; scipy.optimize &lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; fsolve
%matplotlib inline
&lt;span style="color: #0000FF;"&gt;import&lt;/span&gt; matplotlib.pyplot &lt;span style="color: #0000FF;"&gt;as&lt;/span&gt; plt

&lt;span style="color: #BA36A5;"&gt;P&lt;/span&gt; = 760 * 1.2 &lt;span style="color: #8D8D84;"&gt;# &lt;/span&gt;&lt;span style="color: #8D8D84; font-style: italic;"&gt;mmHg&lt;/span&gt;
&lt;span style="color: #BA36A5;"&gt;A1&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;B1&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;C1&lt;/span&gt; = 6.90565, -1211.033,  220.79
&lt;span style="color: #BA36A5;"&gt;A2&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;B2&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;C2&lt;/span&gt; = 6.95464, -1344.8, 219.482

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;k1&lt;/span&gt;(T):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 10**(A1 + B1 / (C1 + T)) / P

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;k2&lt;/span&gt;(T):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; 10**(A2 + B2 / (C2 + T)) / P

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;f&lt;/span&gt;(x2, T):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;x1&lt;/span&gt; = 1 - x2
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; k1(T) * x1 + k2(T) * x2 - 1

T0, = fsolve(&lt;span style="color: #0000FF;"&gt;lambda&lt;/span&gt; T: f(0.4, T), 96)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'The initial temperature is {T0:1.2f} degC.'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
The initial temperature is 95.59 degC.
&lt;/p&gt;

&lt;p&gt;
Next, we compute the derivative we need. This derivative is derived from the constraint, which should ensure that the temperature changes as required to maintain the constraint.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #BA36A5;"&gt;dfdx2&lt;/span&gt; = grad(f, 0)
&lt;span style="color: #BA36A5;"&gt;dfdT&lt;/span&gt; = grad(f, 1)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;dTdx2&lt;/span&gt;(x2, T):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; -dfdx2(x2, T) / dfdT(x2, T)

&lt;span style="color: #0000FF;"&gt;def&lt;/span&gt; &lt;span style="color: #006699;"&gt;ode&lt;/span&gt;(x2, X):
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;L&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;T&lt;/span&gt; = X
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #BA36A5;"&gt;dLdx2&lt;/span&gt; = L / (x2 * (k2(T) - 1))
&lt;span style="color: #9B9B9B; background-color: #EDEDED;"&gt; &lt;/span&gt;   &lt;span style="color: #0000FF;"&gt;return&lt;/span&gt; [dLdx2, dTdx2(x2, T)]
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
Next we solve and plot the ODE.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #BA36A5;"&gt;x2span&lt;/span&gt; = (0.4, 0.8)
&lt;span style="color: #BA36A5;"&gt;X0&lt;/span&gt; = (100, T0)
&lt;span style="color: #BA36A5;"&gt;sol&lt;/span&gt; = solve_ivp(ode, x2span, X0, max_step=0.01)

plt.plot(sol.t, sol.y.T)
plt.legend([&lt;span style="color: #008000;"&gt;'L'&lt;/span&gt;, &lt;span style="color: #008000;"&gt;'T'&lt;/span&gt;]);
plt.xlabel(&lt;span style="color: #008000;"&gt;'$x_2$'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'L, T'&lt;/span&gt;)
&lt;span style="color: #BA36A5;"&gt;x2&lt;/span&gt; = sol.t
&lt;span style="color: #BA36A5;"&gt;L&lt;/span&gt;, &lt;span style="color: #BA36A5;"&gt;T&lt;/span&gt; = sol.y
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(f&lt;span style="color: #008000;"&gt;'At x2={x2[-1]:1.2f} there are {L[-1]:1.2f} moles of liquid left at {T[-1]:1.2f} degC'&lt;/span&gt;)
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
At x2=0.80 there are 14.04 moles of liquid left at 108.57 degC
&lt;/p&gt;

&lt;pre class="example"&gt;
&amp;lt;Figure size 432x288 with 1 Axes&amp;gt;
&lt;/pre&gt;


&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/a75e63c53e3c2cb02c40c808789084c337e174ff.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;

&lt;p&gt;
You can see that the liquid level drops, and the temperature rises.
&lt;/p&gt;

&lt;p&gt;
Let's double check that the constraint is actually met. We do that qualitatively here by plotting it, and quantitatively by showing all values are close to 0.
&lt;/p&gt;

&lt;div class="org-src-container"&gt;
&lt;pre class="src src-ipython"&gt;&lt;span style="color: #BA36A5;"&gt;constraint&lt;/span&gt; = k1(T) * (1 - x2) + k2(T) * x2 - 1
plt.plot(x2, constraint)
plt.ylim([-1, 1])
plt.xlabel(&lt;span style="color: #008000;"&gt;'$x_2$'&lt;/span&gt;)
plt.ylabel(&lt;span style="color: #008000;"&gt;'constraint value'&lt;/span&gt;)
&lt;span style="color: #0000FF;"&gt;print&lt;/span&gt;(np.allclose(constraint, np.zeros_like(constraint)))
constraint
&lt;/pre&gt;
&lt;/div&gt;

&lt;p&gt;
True
&lt;/p&gt;

&lt;pre class="example"&gt;
array([ 2.22044605e-16,  4.44089210e-16,  2.22044605e-16,  0.00000000e+00,
        1.11022302e-15,  0.00000000e+00,  6.66133815e-16,  0.00000000e+00,
       -2.22044605e-16,  1.33226763e-15,  8.88178420e-16, -4.44089210e-16,
        4.44089210e-16,  1.11022302e-15, -2.22044605e-16,  0.00000000e+00,
       -2.22044605e-16, -1.11022302e-15,  4.44089210e-16,  0.00000000e+00,
       -4.44089210e-16,  4.44089210e-16, -6.66133815e-16, -4.44089210e-16,
        4.44089210e-16, -1.11022302e-16, -8.88178420e-16, -8.88178420e-16,
       -9.99200722e-16, -3.33066907e-16, -7.77156117e-16, -2.22044605e-16,
       -9.99200722e-16, -1.11022302e-15, -3.33066907e-16, -1.99840144e-15,
       -1.33226763e-15, -2.44249065e-15, -1.55431223e-15, -6.66133815e-16,
       -2.22044605e-16])
&lt;/pre&gt;


&lt;pre class="example"&gt;
&amp;lt;Figure size 432x288 with 1 Axes&amp;gt;
&lt;/pre&gt;


&lt;p&gt;
&lt;figure&gt;&lt;img src="/media/bb2b32002658b8724d214f2441c9f55a97c565c8.png"&gt;&lt;/figure&gt; 
&lt;/p&gt;


&lt;p&gt;
So indeed, the constraint is met! Once again, autograd comes to the rescue in making a computable derivative from an algebraic constraint so that we can solve a DAE as a set of ODEs using our regular machinery. Nice work autograd!
&lt;/p&gt;
&lt;p&gt;Copyright (C) 2019 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/2019/09/22/Solving-differential-algebraic-equations-with-help-from-autograd.org"&gt;org-mode source&lt;/a&gt;&lt;/p&gt;
&lt;p&gt;Org-mode version = 9.2.3&lt;/p&gt;]]></content:encoded>
    </item>
  </channel>
</rss>
