Cantera  2.5.1
IonGasTransport.cpp
Go to the documentation of this file.
1 //! @file IonGasTransport.cpp
2 
3 // This file is part of Cantera. See License.txt in the top-level directory or
4 // at https://cantera.org/license.txt for license and copyright information.
5 
9 #include "MMCollisionInt.h"
10 
11 namespace Cantera
12 {
13 IonGasTransport::IonGasTransport() :
14  m_kElectron(npos)
15 {
16 }
17 
18 void IonGasTransport::init(thermo_t* thermo, int mode, int log_level)
19 {
20  m_thermo = thermo;
21  m_nsp = m_thermo->nSpecies();
22  m_mode = mode;
23  if (m_mode == CK_Mode) {
24  throw CanteraError("IonGasTransport::init",
25  "mode = CK_Mode, which is an outdated lower-order fit.");
26  }
27  m_log_level = log_level;
28  // make a local copy of species charge
29  for (size_t k = 0; k < m_nsp; k++) {
30  m_speciesCharge.push_back(m_thermo->charge(k));
31  }
32 
33  // Find the index of electron
34  if (m_thermo->speciesIndex("E") != npos ) {
35  m_kElectron = m_thermo->speciesIndex("E");
36  }
37 
38  // Find indices for charge of species
39  for (size_t k = 0; k < m_nsp; k++) {
40  if (m_speciesCharge[k] != 0){
41  if (k != m_kElectron) {
42  m_kIon.push_back(k);
43  }
44  } else {
45  m_kNeutral.push_back(k);
46  }
47  }
48  // set up O2/O2- collision integral [A^2]
49  // Data taken from Prager (2005)
50  const vector_fp temp{300.0, 400.0, 500.0, 600.0, 800.0, 1000.0,
51  1200.0, 1500.0, 2000.0, 2500.0, 3000.0, 4000.0};
52  const vector_fp om11_O2{120.0, 107.0, 98.1, 92.1, 83.0, 77.0,
53  72.6, 67.9, 62.7, 59.3, 56.7, 53.8};
54  vector_fp w(temp.size(),-1);
55  int degree = 5;
56  m_om11_O2.resize(degree + 1);
57  polyfit(temp.size(), degree, temp.data(), om11_O2.data(),
58  w.data(), m_om11_O2.data());
59  // set up Monchick and Mason parameters
60  setupCollisionParameters();
61  // set up n64 parameters
62  setupN64();
63  // setup collision integrals
64  setupCollisionIntegral();
65  m_molefracs.resize(m_nsp);
66  m_spwork.resize(m_nsp);
67  m_visc.resize(m_nsp);
68  m_sqvisc.resize(m_nsp);
69  m_phi.resize(m_nsp, m_nsp, 0.0);
70  m_bdiff.resize(m_nsp, m_nsp);
71  m_cond.resize(m_nsp);
72 
73  // make a local copy of the molecular weights
74  m_mw = m_thermo->molecularWeights();
75 
76  m_wratjk.resize(m_nsp, m_nsp, 0.0);
77  m_wratkj1.resize(m_nsp, m_nsp, 0.0);
78  for (size_t j = 0; j < m_nsp; j++) {
79  for (size_t k = j; k < m_nsp; k++) {
80  m_wratjk(j,k) = sqrt(m_mw[j]/m_mw[k]);
81  m_wratjk(k,j) = sqrt(m_wratjk(j,k));
82  m_wratkj1(j,k) = sqrt(1.0 + m_mw[k]/m_mw[j]);
83  }
84  }
85 }
86 
87 double IonGasTransport::viscosity()
88 {
89  update_T();
90  update_C();
91 
92  if (m_visc_ok) {
93  return m_viscmix;
94  }
95 
96  double vismix = 0.0;
97  // update m_visc and m_phi if necessary
98  if (!m_viscwt_ok) {
99  updateViscosity_T();
100  }
101 
102  multiply(m_phi, m_molefracs.data(), m_spwork.data());
103 
104  for (size_t k : m_kNeutral) {
105  vismix += m_molefracs[k] * m_visc[k]/m_spwork[k]; //denom;
106  }
107  m_viscmix = vismix;
108  return vismix;
109 }
110 
111 double IonGasTransport::thermalConductivity()
112 {
113  update_T();
114  update_C();
115  if (!m_spcond_ok) {
116  updateCond_T();
117  }
118  if (!m_condmix_ok) {
119  doublereal sum1 = 0.0, sum2 = 0.0;
120  for (size_t k : m_kNeutral) {
121  sum1 += m_molefracs[k] * m_cond[k];
122  sum2 += m_molefracs[k] / m_cond[k];
123  }
124  m_lambda = 0.5*(sum1 + 1.0/sum2);
125  m_condmix_ok = true;
126  }
127  return m_lambda;
128 }
129 
130 double IonGasTransport::electricalConductivity()
131 {
132  vector_fp mobi(m_nsp);
133  getMobilities(&mobi[0]);
134  double p = m_thermo->pressure();
135  double sum = 0.0;
136  for (size_t k : m_kIon) {
137  double ND_k = m_molefracs[k] * p / m_kbt;
138  sum += ND_k * std::abs(m_speciesCharge[k]) * ElectronCharge * mobi[k];
139  }
140  if (m_kElectron != npos) {
141  sum += m_molefracs[m_kElectron] * p / m_kbt *
142  ElectronCharge * mobi[m_kElectron];
143  }
144  return sum;
145 }
146 
147 void IonGasTransport::fitDiffCoeffs(MMCollisionInt& integrals)
148 {
149  GasTransport::fitDiffCoeffs(integrals);
150 
151  // number of points to use in generating fit data
152  const size_t np = 50;
153  int degree = 4;
154  double dt = (m_thermo->maxTemp() - m_thermo->minTemp())/(np-1);
155  vector_fp tlog(np);
156  vector_fp w(np);
157 
158  // generate array of log(t) values
159  for (size_t n = 0; n < np; n++) {
160  double t = m_thermo->minTemp() + dt*n;
161  tlog[n] = log(t);
162  }
163 
164  // vector of polynomial coefficients
165  vector_fp c(degree + 1);
166  double err = 0.0, relerr = 0.0,
167  mxerr = 0.0, mxrelerr = 0.0;
168 
169  vector_fp diff(np + 1);
170  // The array order still not ideal
171  for (size_t k = 0; k < m_nsp; k++) {
172  for (size_t j = k; j < m_nsp; j++) {
173  if (m_alpha[k] == 0.0 || m_alpha[j] == 0.0 ||
174  k == m_kElectron || j == m_kElectron) {
175  continue;
176  }
177  if (m_speciesCharge[k] == 0) {
178  if (m_speciesCharge[j] == 0) {
179  continue;
180  }
181  } else {
182  if (m_speciesCharge[j] != 0) {
183  continue;
184  }
185  }
186  for (size_t n = 0; n < np; n++) {
187  double t = m_thermo->minTemp() + dt*n;
188  double eps = m_epsilon(j,k);
189  double tstar = Boltzmann * t/eps;
190  double sigma = m_diam(j,k);
191  double om11 = omega11_n64(tstar, m_gamma(j,k))
192  * Pi * sigma * sigma;
193  // Stockmayer-(n,6,4) model is not suitable for collision
194  // between O2/O2- due to resonant charge transfer.
195  // Therefore, the experimental collision data is used instead.
196  if ((k == m_thermo->speciesIndex("O2-") ||
197  j == m_thermo->speciesIndex("O2-")) &&
198  (k == m_thermo->speciesIndex("O2") ||
199  j == m_thermo->speciesIndex("O2"))) {
200  om11 = poly5(t, m_om11_O2.data()) / 1e20;
201  }
202  double diffcoeff = 3.0/16.0 * sqrt(2.0 * Pi/m_reducedMass(k,j))
203  * pow(Boltzmann * t, 1.5) / om11;
204 
205  diff[n] = diffcoeff/pow(t, 1.5);
206  w[n] = 1.0/(diff[n]*diff[n]);
207  }
208  polyfit(np, degree, tlog.data(), diff.data(), w.data(), c.data());
209 
210  for (size_t n = 0; n < np; n++) {
211  double val, fit;
212  double t = exp(tlog[n]);
213  double pre = pow(t, 1.5);
214  val = pre * diff[n];
215  fit = pre * poly4(tlog[n], c.data());
216  err = fit - val;
217  relerr = err/val;
218  mxerr = std::max(mxerr, fabs(err));
219  mxrelerr = std::max(mxrelerr, fabs(relerr));
220  }
221  size_t sum = k * (k + 1) / 2;
222  m_diffcoeffs[k*m_nsp+j-sum] = c;
223  if (m_log_level >= 2) {
224  writelog(m_thermo->speciesName(k) + "__" +
225  m_thermo->speciesName(j) + ": [" + vec2str(c) + "]\n");
226  }
227  }
228  }
229 
230  if (m_log_level) {
231  writelogf("Maximum binary diffusion coefficient absolute error:"
232  " %12.6g\n", mxerr);
233  writelogf("Maximum binary diffusion coefficient relative error:"
234  "%12.6g", mxrelerr);
235  }
236 }
237 
238 void IonGasTransport::setupN64()
239 {
240  m_gamma.resize(m_nsp, m_nsp, 0.0);
241  for (size_t i : m_kIon) {
242  for (size_t j : m_kNeutral) {
243  if (m_alpha[j] != 0.0 && m_alpha[i] != 0.0) {
244  double r_alpha = m_alpha[i] / m_alpha[j];
245  // save a copy of polarizability in Angstrom
246  double alphaA_i = m_alpha[i] * 1e30;
247  double alphaA_j = m_alpha[j] * 1e30;
248  // The ratio of dispersion to induction forces
249  double xi = alphaA_i / (m_speciesCharge[i] * m_speciesCharge[i] *
250  (1.0 + pow(2 * r_alpha, 2./3.)) * sqrt(alphaA_j));
251 
252  // the collision diameter
253  double K1 = 1.767;
254  double kappa = 0.095;
255  m_diam(i,j) = K1 * (pow(m_alpha[i], 1./3.) + pow(m_alpha[j], 1./3.)) /
256  pow(alphaA_i * alphaA_j * (1.0 + 1.0 / xi), kappa);
257 
258  // The original K2 is 0.72, but Han et al. suggested that K2 = 1.44
259  // for better fit.
260  double K2 = 1.44;
261  double epsilon = K2 * ElectronCharge * ElectronCharge *
262  m_speciesCharge[i] * m_speciesCharge[i] *
263  m_alpha[j] * (1.0 + xi) /
264  (8 * Pi * epsilon_0 * pow(m_diam(i,j),4));
265  if (epsilon != 0.0) {
266  m_epsilon(i,j) = epsilon;
267  }
268 
269  // Calculate dispersion coefficient and quadrupole polarizability
270  // from curve fitting if not available.
271  // Neutrals
272  if (m_disp[j] == 0.0) {
273  m_disp[j] = exp(1.8846*log(alphaA_j)-0.4737)* 1e-50;
274  }
275  if (m_quad_polar[j] == 0.0) {
276  m_quad_polar[j] = 2.0 * m_disp[j];
277  }
278  // Ions
279  if (m_disp[i] == 0.0) {
280  if (m_speciesCharge[i] > 0) {
281  m_disp[i] = exp(1.8853*log(alphaA_i)+0.2682)* 1e-50;
282  } else {
283  m_disp[i] = exp(3.2246*log(alphaA_i)-3.2397)* 1e-50;
284  }
285  }
286 
287  // The binary dispersion coefficient is determined by the combination rule
288  // Reference:
289  // Tang, K. T. "Dynamic polarizabilities and van der Waals coefficients."
290  // Physical Review 177.1 (1969): 108.
291  double C6 = 2.0 * m_disp[i] * m_disp[j] /
292  (1.0/r_alpha * m_disp[i] + r_alpha * m_disp[j]);
293 
294  m_gamma(i,j) = (2.0 / pow(m_speciesCharge[i],2) * C6 + m_quad_polar[j]) /
295  (m_alpha[j] * m_diam(i,j) * m_diam(i,j));//Dimensionless
296 
297  // properties are symmetric
298  m_diam(j,i) = m_diam(i,j);
299  m_epsilon(j,i) = m_epsilon(i,j);
300  m_gamma(j,i) = m_gamma(i,j);
301  }
302  }
303  }
304 }
305 
306 double IonGasTransport::omega11_n64(const double tstar, const double gamma)
307 {
308  double logtstar = log(tstar);
309  double om11 = 0.0;
310  if (tstar < 0.01) {
311  throw CanteraError("IonGasTransport::omega11_n64",
312  "tstar = {} is smaller than 0.01", tstar);
313  } else if (tstar <= 0.04) {
314  // for interval 0.01 to 0.04, SSE = 0.006; R^2 = 1; RMSE = 0.020
315  om11 = 2.97 - 12.0 * gamma
316  - 0.887 * logtstar
317  + 3.86 * gamma * gamma
318  - 6.45 * gamma * logtstar
319  - 0.275 * logtstar * logtstar
320  + 1.20 * gamma * gamma * logtstar
321  - 1.24 * gamma * logtstar * logtstar
322  - 0.164 * pow(logtstar,3);
323  } else if (tstar <= 1000) {
324  // for interval 0.04 to 1000, SSE = 0.282; R^2 = 1; RMSE = 0.033
325  om11 = 1.22 - 0.0343 * gamma
326  + (-0.769 + 0.232 * gamma) * logtstar
327  + (0.306 - 0.165 * gamma) * logtstar * logtstar
328  + (-0.0465 + 0.0388 * gamma) * pow(logtstar,3)
329  + (0.000614 - 0.00285 * gamma) * pow(logtstar,4)
330  + 0.000238 * pow(logtstar,5);
331  } else {
332  throw CanteraError("IonGasTransport::omega11_n64",
333  "tstar = {} is larger than 1000", tstar);
334  }
335  return om11;
336 }
337 
338 void IonGasTransport::getMixDiffCoeffs(double* const d)
339 {
340  update_T();
341  update_C();
342 
343  // update the binary diffusion coefficients if necessary
344  if (!m_bindiff_ok) {
345  updateDiff_T();
346  }
347 
348  double mmw = m_thermo->meanMolecularWeight();
349  double p = m_thermo->pressure();
350  if (m_nsp == 1) {
351  d[0] = m_bdiff(0,0) / p;
352  } else {
353  for (size_t k = 0; k < m_nsp; k++) {
354  if (k == m_kElectron) {
355  d[k] = 0.4 * m_kbt / ElectronCharge;
356  } else {
357  double sum2 = 0.0;
358  for (size_t j : m_kNeutral) {
359  if (j != k) {
360  sum2 += m_molefracs[j] / m_bdiff(j,k);
361  }
362  }
363  if (sum2 <= 0.0) {
364  d[k] = m_bdiff(k,k) / p;
365  } else {
366  d[k] = (mmw - m_molefracs[k] * m_mw[k])/(p * mmw * sum2);
367  }
368  }
369  }
370  }
371 }
372 
373 void IonGasTransport::getMobilities(double* const mobi)
374 {
375  update_T();
376  update_C();
377 
378  // update the binary diffusion coefficients if necessary
379  if (!m_bindiff_ok) {
380  updateDiff_T();
381  }
382  double p = m_thermo->pressure();
383  for (size_t k = 0; k < m_nsp; k++) {
384  if (k == m_kElectron) {
385  mobi[k] = 0.4;
386  } else {
387  mobi[k] = 0.0;
388  }
389  }
390  for (size_t k : m_kIon) {
391  double sum = 0.0;
392  for (size_t j : m_kNeutral) {
393  double bmobi = m_bdiff(k,j) * ElectronCharge / m_kbt;
394  sum += m_molefracs[j] / bmobi;
395  }
396  mobi[k] = 1.0 / sum / p;
397  }
398 }
399 
400 }
Monk and Monchick collision integrals.
Base class for exceptions thrown by Cantera classes.
Definition: ctexceptions.h:61
Calculation of Collision integrals.
size_t nSpecies() const
Returns the number of species in the phase.
Definition: Phase.h:285
Base class for a phase with thermodynamic properties.
Definition: ThermoPhase.h:102
const size_t npos
index returned by functions to indicate "no position"
Definition: ct_defs.h:188
const double Boltzmann
Boltzmann constant [J/K].
Definition: ct_defs.h:66
std::vector< double > vector_fp
Turn on the use of stl vectors for the basic array type within cantera Vector of doubles.
Definition: ct_defs.h:180
const double epsilon_0
Permittivity of free space [F/m].
Definition: ct_defs.h:129
const double ElectronCharge
Elementary charge [C].
Definition: ct_defs.h:72
const double Pi
Pi.
Definition: ct_defs.h:53
void writelog(const std::string &fmt, const Args &... args)
Write a formatted message to the screen.
Definition: global.h:158
void writelogf(const char *fmt, const Args &... args)
Write a formatted message to the screen.
Definition: global.h:179
Namespace for the Cantera kernel.
Definition: AnyMap.cpp:264
void multiply(const DenseMatrix &A, const double *const b, double *const prod)
Multiply A*b and return the result in prod. Uses BLAS routine DGEMV.
std::string vec2str(const vector_fp &v, const std::string &fmt, const std::string &sep)
Convert a vector to a string (separated by commas)
Definition: stringUtils.cpp:34
R poly5(D x, R *c)
Templated evaluation of a polynomial of order 5.
Definition: utilities.h:479
R poly4(D x, R *c)
Evaluates a polynomial of order 4.
Definition: utilities.h:491
double polyfit(size_t n, size_t deg, const double *xp, const double *yp, const double *wp, double *pp)
Fits a polynomial function to a set of data points.
Definition: polyfit.cpp:14
Contains declarations for string manipulation functions within Cantera.