Equilibrium by Gibbs minimization

Equilibrium criteria is

$$(dG)_{T,P}=0$$$$G = G(T,P,n_i)$$$$dG = \frac{\partial G}{\partial T}dT + \frac{\partial G}{\partial P}dP + \sum_i\frac{\partial G}{\partial n_i}dn_i$$$$(dG)_{T,P}= \sum_i\frac{\partial G}{\partial n_i}dn_i$$ $$dG = \sum_i\frac{\partial G}{\partial n_i}dn_i = \sum_i\mu_idn_i = \nabla G\cdot\mathbf{dn}$$

* $\nabla$ is the gradient in the $n_i$ space.

$$\nabla\frac{G}{RT} = 0$$$$\sum_i\frac{\mu_i}{RT} = 0$$ $$ \begin{align*} \frac{\mu_i}{RT}&= \frac{g_{i,f}^o}{RT} + \ln\left(\frac{P_i}{P^o}\right), \\ &= \underbrace{\frac{g_{i,f}^o}{RT} + \ln\left(\frac{P}{P^o}\right)}_{\hat{g}_i} + \ln\left(\frac{n_i}{n_t}\right). \end{align*} $$$$\frac{\mu_i}{RT} = \hat{g}_i + \ln\left(\frac{n_i}{n_t}\right)$$ $$\nabla\frac{G}{RT} = \sum_i\left[\hat{g}_i + \ln\left(\frac{n_i}{n_t}\right)\right]\mathbf{e_i} = 0,\phantom{xxx} i=1,\,N_s$$ Here, $\mathbf{e_i}$ is a unit vector in the species $i$ direction.

Elemental constraints

$$A_k = \sum_i n_ia_{i,k},\phantom{xxx} k=1,\,N_e$$

Lagrange multipliers

Lagrange Multipliers

More dimensions and constraints

Equilibrium

For equilibrium $f\equiv G/RT$, and $c\equiv c_k$, given above. Hence,

$$\nabla\frac{G}{RT} - \sum_k\lambda_k\nabla c_k= 0.$$

* The gradient operators each have $N_s$ additive terms, one for each species. For the whole equation to be 0, each term will be zero. This gives

$$\hat{g}_i + \ln\left(\frac{n_i}{n_t}\right) - \sum_k\lambda_ka_{i,k}=0,\phantom{xxxx}i=1,\,N_s$$

Along with the constraint equations

$$\sum_in_ia_{i,k}=A_k, \phantom{xxx}k=1,N_e,$$

and the normalization condition

$$n_t=\sum_in_i.$$ $$x_i = \exp\left(-\hat{g}_i + \sum_ka_{i,k}\lambda_k\right), \phantom{xxx} i=1,N_s\tag{1}$$ $$\sum_ix_ia_{i,k} = A_k/n_t, \phantom{xxx} k=1,N_e\tag{2}$$$$\sum_ix_i = 1.\tag{3}$$

* We have $N_s+N_e+1$ equations in unknowns $x_i$, $\lambda_k$ and $n_t$

Reduce equations

$$\sum_ia_{i,k}\exp\left(-\hat{g}_i + \sum_{j}\lambda_j a_{i,j}\right) = A_k/n_t,\phantom{xxxx} k=1,\,N_e$$ $$\sum_i\exp\left(-\hat{g}_i + \sum_k\lambda_ka_{i,k}\right) = 1$$

Matrix form

$$\mathbf{[a]}^{T}\exp(-\mathbf{\hat{g}}+\mathbf{[a]}\boldsymbol{\lambda}) = \mathbf{A}/n_t,$$ $$\sum_i\exp(-\mathbf{\hat{g}}+\mathbf{[a]}\boldsymbol{\lambda}) = 1.$$ $$\mathbf{x} = \exp(-\mathbf{\hat{g}} + \mathbf{[a]}\boldsymbol{\lambda})$$
species C H O N
$CH_4$ 1 4 0 0
$CO_2$ 1 0 2 0
$CO$ 1 0 1 0
$H_2O$ 0 2 1 0
$N_2$ 0 0 0 2
$H_2$ 0 2 0 0

Element potentials

Consider the green equation

$$\mathbf{x} = \exp(-\mathbf{\hat{g}} + \mathbf{[a]}\boldsymbol{\lambda})$$

We can rearrange this to

$$\mathbf{[a]}\boldsymbol{\lambda} = \mathbf{\hat{g}} + \ln(x) = \boldsymbol{\mu}/RT $$

The first and last parts are

$$\mathbf{[a]}\boldsymbol{\lambda} = \boldsymbol{\mu}/RT $$

Chemical potentials $\mu_i$ are linear combinations of element potentials $\lambda_k$.

$$\sum_k\lambda_ka_{i,k} = \mu_i/RT$$

Solution approach

Solver

Analytic Jacobian

Nonlinear solvers based on Newton’s method use the Jacobian matrix. This can be computed numerically, but when an analytical Jacobian is available it is best to use it.

Write our equations as $f$ and $h$:

$$f_k = \sum_ia_{i,k}\exp\left(-\hat{g}_i + \sum_{j}\lambda_j a_{i,j}\right) -A_k/n_t= 0,\phantom{xxxx} k=1,\,N_e$$ $$h = \sum_i\exp\left(-\hat{g}_i + \sum_k\lambda_ka_{i,k}\right) -1 = 0$$

The Jacobian matrix is then

$$J = \begin{pmatrix} \frac{\partial f_1}{\partial \lambda_1} & \ldots & \frac{\partial f_1}{\partial \lambda_{N_e}} & \frac{\partial f_1}{\partial n_t} & \\ \vdots & \ldots & \vdots & \vdots \\ \\ \frac{\partial f_{N_e}}{\partial \lambda_1} & \ldots & \frac{\partial f_{N_e}}{\partial \lambda_{N_e}} & \frac{\partial f_{N_e}}{\partial n_t} & \\ \frac{\partial h}{\partial \lambda_1} & \ldots & \frac{\partial h}{\partial \lambda_{N_e}} & \frac{\partial h}{\partial n_t} \end{pmatrix}$$

The four main components are given by

$$\begin{align} &\frac{\partial f_k}{\partial\lambda_m} = \sum_ia_{i,k}\exp\left(-\hat{g}_i+\sum_ja_{i,j}\lambda_j\right)a_{i,m}, \\ &\frac{\partial f_k}{\partial n_t } = A_k/n_t^2, \\ &\frac{\partial h}{\partial \lambda_m} = \sum_i\exp\left(-\hat{g}_i+\sum_ka_{i,k}\lambda_k\right)a_{i,m}, \\ &\frac{\partial h}{\partial n_t} = 0. \end{align}$$
import numpy as np
import cantera as ct
from scipy.optimize import fsolve
import matplotlib.pyplot as plt
from streams import streams
ξ = 0.1
T = 1600.0
P = 101325

strm = streams({"O2":1,"N2":3.76}, {"CH4":1}, 300, 300, 101325, "./simple.yaml")
#strm = streams({"O2":1,"N2":3.76}, {"CH4":1}, 300, 300, 101325, "gri30.yaml")

spNames  = ["CH4", "O2", "N2", "CO2", "H2O", "CO", "H2", "OH", "O"]
elNames  = ['C', 'H', 'O', 'N']

isp = [strm.gas.species_index(i) for i in spNames]
Nsp = len(spNames)
Nel = len(elNames)

xmix = strm.get_x_from_y(strm.get_mixing_state(ξ)[0])
strm.gas.TPX = T,P,xmix

#------------ get a

a = np.array([[strm.gas.n_atoms(isp[i],elNames[k]) for k in range(Nel)] 
              for i in range(Nsp)])

#------------ get A

A = np.dot(a.T,xmix[isp])

#------------ get ghat

ghat = (strm.gas.standard_gibbs_RT + np.log(P/101325))[isp]

#------------ solver function

def FeqTP(λnt):
    λ = λnt[:-1]
    nt = λnt[-1]
    
    F = np.zeros(len(λnt))
    F[:-1] = np.dot(a.T, np.exp(-ghat+np.dot(a,λ))) - A/nt
    F[-1]  = np.sum(np.exp(-ghat+np.dot(a,λ))) - 1.0
    
    return F

#------------ Jacobian function

def getJac(λnt):
    λ = λnt[:-1]
    nt = λnt[-1]
    
    n = len(λnt)
    J = np.zeros((n,n))
    
    egaλ = np.exp(-ghat+np.dot(a,λ))
    
    for k in range(n-1):
        for m in range(n-1):
            J[k,m] = np.sum(a[:,k]*a[:,m]*egaλ)
        m = n-1
        J[k,m] = A[k]/(nt*nt)
    k = n-1
    for m in range(n-1):
        J[k,m] = np.sum(a[:,m]*egaλ)
    m = n-1
    J[k,m] = 0.0
    
    return J

#------------ initial guesses for nt, and λ (make sure consistent with A above), then solve

x = strm.get_pCC(ξ, getYorX='x')[isp] + 1E-15      # products of complete combustion as a guess.
λ = np.linalg.solve(np.dot(a.T,a), np.dot(a.T, ghat+np.log(x)))   # linear least squares
λnt_guess = np.append(λ, 1)

λnt = fsolve(FeqTP, λnt_guess, factor=0.001, maxfev=10000, fprime=getJac)

#------------ recover composition

x = np.exp(-ghat + np.dot(a,λnt[:-1]))

#------------ compare to Cantera

xmix = strm.get_x_from_y(strm.get_mixing_state(ξ)[0])
strm.gas.TPX = T,P,xmix
strm.gas.equilibrate("TP")
xCantera = strm.gas.X

#------------ output results

print("Species    XCantera      XSolver")
print("-----------------------------------")
for i in range(Nsp):
    print(f"{spNames[i]:8s} {xCantera[isp][i]:.6e}  {x[i]:.6e}")
Species    XCantera      XSolver
-----------------------------------
CH4      5.137512e-09  5.137512e-09
O2       2.846952e-11  2.846952e-11
N2       5.685436e-01  5.685436e-01
CO2      3.037884e-02  3.037884e-02
H2O      1.282186e-01  1.282186e-01
CO       1.134398e-01  1.134398e-01
H2       1.594184e-01  1.594184e-01
OH       6.834862e-07  6.834862e-07
O        7.735590e-11  7.735590e-11