Cantera  4.0.0a2
Loading...
Searching...
No Matches
EEDFTwoTermApproximation.cpp
Go to the documentation of this file.
1/**
2 * @file EEDFTwoTermApproximation.cpp
3 * EEDF Two-Term approximation solver. Implementation file for class
4 * EEDFTwoTermApproximation.
5 */
6
7// This file is part of Cantera. See License.txt in the top-level directory or
8// at https://cantera.org/license.txt for license and copyright information.
9
12#include "cantera/numerics/eigen_dense.h"
16#include <numbers>
17
18namespace Cantera
19{
20
21typedef Eigen::SparseMatrix<double> SparseMat;
22
23EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase* s)
24{
25 // store a pointer to s.
26 m_phase = s;
27 m_first_call = true;
28 m_has_EEDF = false;
29 m_gamma = pow(2.0 * ElectronCharge / ElectronMass, 0.5);
30}
31
32void EEDFTwoTermApproximation::setLinearGrid(double kTe_max, size_t ncell)
33{
34 m_points = ncell;
35 m_gridCenter.resize(m_points);
36 m_gridEdge.resize(m_points + 1);
37 m_f0.resize(m_points);
38 m_f0_edge.resize(m_points + 1);
39 for (size_t j = 0; j < m_points; j++) {
40 m_gridCenter[j] = kTe_max * ( j + 0.5 ) / m_points;
41 m_gridEdge[j] = kTe_max * j / m_points;
42 }
43 m_gridEdge[m_points] = kTe_max;
45}
46
47
48void EEDFTwoTermApproximation::setQuadraticGrid(double kTe_max, size_t ncell)
49{
50 m_points = ncell;
51
52 m_gridCenter.resize(m_points);
53 m_gridEdge.resize(m_points + 1);
54 m_f0.resize(m_points);
55 m_f0_edge.resize(m_points + 1);
56
57 double n = static_cast<double>(m_points);
58
59 for (size_t j = 0; j <= m_points; j++) {
60 double x = static_cast<double>(j);
61 m_gridEdge[j] = kTe_max * x * (x + 1.0) / (n * (n + 1.0));
62 }
63
64 for (size_t j = 0; j < m_points; j++) {
65 m_gridCenter[j] = 0.5 * (m_gridEdge[j] + m_gridEdge[j + 1]);
66 }
67
69}
70
71void EEDFTwoTermApproximation::setGeometricGrid(double kTe_max, size_t ncell,
72 double ratio)
73{
74 // First, a few checks to make sure the parameters are valid.
75 if (ncell == 0) {
76 throw CanteraError("EEDFTwoTermApproximation::setDirectGeometricGrid",
77 "Number of cells must be positive.");
78 }
79
80 if (kTe_max <= 0.0) {
81 throw CanteraError("EEDFTwoTermApproximation::setDirectGeometricGrid",
82 "Maximum electron energy must be positive.");
83 }
84
85 if (ratio <= 0.0) {
86 throw CanteraError("EEDFTwoTermApproximation::setDirectGeometricGrid",
87 "Geometric ratio must be positive.");
88 }
89
90 if (std::abs(ratio - 1.0) < 1e-14) {
91 setLinearGrid(kTe_max, ncell);
92 return;
93 }
94
95 if (ratio < 1.0) {
96 throw CanteraError("EEDFTwoTermApproximation::setDirectGeometricGrid",
97 "For an increasing direct geometric grid, ratio must be larger than 1.");
98 }
99
100 m_points = ncell;
101 m_gridCenter.resize(m_points);
102 m_gridEdge.resize(m_points + 1);
103 m_f0.resize(m_points);
104 m_f0_edge.resize(m_points + 1);
105
106 // The zero-energy boundary cannot belong to a positive geometric progression.
107 // Therefore the first grid point is set to 0 and we impose a geometric progression
108 // only from the second grid point only. This second grid point is computed to match
109 // the requested number of cells, maximum grid energy and geometric ratio as
110 // follows:
111 //
112 // E_1 = kTe_max / ratio^(N - 1)
113 //
114 // and then the progression is defined as follows:
115 //
116 // E_0 = 0
117 // E_j = E_1 * ratio^(j - 1), j = 1, ..., N
118 // E_N = kTe_max
119
120 m_gridEdge[0] = 0.0;
121
122 double firstEdge = kTe_max / std::pow(ratio, static_cast<double>(m_points - 1));
123
124 m_gridEdge[1] = firstEdge;
125
126 for (size_t j = 2; j <= m_points; j++) {
127 m_gridEdge[j] = m_gridEdge[j - 1] * ratio;
128 }
129
130 // Avoid any rounding errors on the final boundary by imposing the requested value:
131 m_gridEdge[m_points] = kTe_max;
132
133 for (size_t j = 0; j < m_points; j++) {
134 m_gridCenter[j] = 0.5 * (m_gridEdge[j] + m_gridEdge[j + 1]);
135 }
136
137 setGridCache();
138 m_geometricRatio = ratio;
139}
140
141void EEDFTwoTermApproximation::setCustomGrid(span<const double> levels)
142{
143 checkArraySize("EEDFTwoTermApproximation::setCustomGrid", levels.size(), 2);
144
145 m_points = levels.size() - 1;
146
147 m_gridCenter.resize(m_points);
148 m_gridEdge.resize(m_points + 1);
149 m_f0.resize(m_points);
150 m_f0_edge.resize(m_points + 1);
151
152 for (size_t j = 0; j < m_points + 1; j++) {
153 if (!std::isfinite(levels[j])) {
154 throw CanteraError("EEDFTwoTermApproximation::setCustomGrid",
155 "Energy grid contains a non-finite value.");
156 }
157 if (levels[j] < 0.0) {
158 throw CanteraError("EEDFTwoTermApproximation::setCustomGrid",
159 "Energy grid values must be non-negative.");
160 }
161 if (j > 0 && levels[j] <= levels[j - 1]) {
162 throw CanteraError("EEDFTwoTermApproximation::setCustomGrid",
163 "Energy grid values must be strictly increasing.");
164 }
165
166 m_gridEdge[j] = levels[j];
167 }
168
169 for (size_t j = 0; j < m_points; j++) {
170 m_gridCenter[j] = 0.5 * (m_gridEdge[j] + m_gridEdge[j + 1]);
171 }
172
173 setGridCache();
174}
175
177{
178 if (m_first_call) {
180 m_first_call = false;
181 }
182
185
186 const double EN = m_phase->reducedElectricField();
187
188 // Multiplicative factor converts from Td to SI units
189 if (EN <= m_thresholdToMaxwellian*1e-21) {
190 const double kTgas = Boltzmann * m_phase->temperature() / ElectronCharge;
192 } else {
193 if (!m_has_EEDF) {
195 }
196
197 converge(m_f0);
198
199 if (m_adaptGrid) {
201 }
202 }
203
204 // write the EEDF at grid edges
205 vector<double> f(m_f0.data(), m_f0.data() + m_f0.rows() * m_f0.cols());
206 vector<double> x(m_gridCenter.data(), m_gridCenter.data() + m_gridCenter.rows() * m_gridCenter.cols());
207 for (size_t i = 0; i < m_points + 1; i++) {
208 m_f0_edge[i] = linearInterp(m_gridEdge[i], x, f);
209 }
210
211 m_has_EEDF = true;
212
213 // update electron mobility
215 return 0;
216}
217
219 double x, span<const double> xpts, span<const double> fpts, double below_value,
220 double above_value)
221{
222 AssertThrowMsg(!xpts.empty(), "linearInterpBounded", "x data empty");
223 AssertThrowMsg(!fpts.empty(), "linearInterpBounded", "f(x) data empty");
224 AssertThrowMsg(xpts.size() == fpts.size(), "linearInterpBounded",
225 "len(xpts) = {}, len(fpts) = {}", xpts.size(), fpts.size());
226
227 if (x < xpts.front()) {
228 return below_value;
229 }
230
231 if (x > xpts.back()) {
232 return above_value;
233 }
234
235 return linearInterp(x, xpts, fpts);
236}
237
239 const Eigen::VectorXd& oldGridCenter, const Eigen::VectorXd& oldF0)
240{
241 if (oldGridCenter.size() != oldF0.size() || oldGridCenter.size() < 2) {
242 throw CanteraError("EEDFTwoTermApproximation::projectPreviousEEDFOnCurrentGrid",
243 "Previous EEDF and grid must have matching sizes of at least two points.");
244 }
245
246 const double fFloor = 1e-300;
247
248 vector<double> oldGrid(oldGridCenter.data(),
249 oldGridCenter.data() + oldGridCenter.size());
250
251 vector<double> oldF(oldF0.data(),
252 oldF0.data() + oldF0.size());
253
254 for (size_t j = 0; j < m_points; j++) {
255 m_f0(j) = std::max(fFloor,
256 linearInterpBounded(m_gridCenter[j], oldGrid, oldF, fFloor, fFloor));
257 }
258
259 double fnorm = norm(m_f0, m_gridCenter);
260
261 if (!std::isfinite(fnorm) || fnorm <= 0.0) {
262 throw CanteraError("EEDFTwoTermApproximation::projectPreviousEEDFOnCurrentGrid",
263 "Invalid norm after projecting previous EEDF onto the adapted grid.");
264 }
265
266 m_f0 /= fnorm;
267}
268
270 const double fFloor = 1e-300;
271
272 for (size_t n = 0; n < m_maxGridAdaptIterations; n++) {
273 double fLeft = std::max(std::abs(m_f0(0)), fFloor);
274 double fRight = std::max(std::abs(m_f0(m_points - 1)), fFloor);
275 double decades = std::log10(fLeft) - std::log10(fRight);
276
277 if (!std::isfinite(decades)) {
278 throw CanteraError("EEDFTwoTermApproximation::adaptEnergyGrid",
279 "Non-finite EEDF decay detected during grid adaptation.");
280 }
281
282 if (decades < m_minEedfDecay) {
283 // The right boundary is too low: the tail has not decayed enough.
284 double newMaxEnergy = m_kTeMax * (1.0 + m_gridUpdateFactor);
285 Eigen::VectorXd oldGridCenter = m_gridCenter;
286 Eigen::VectorXd oldF0 = m_f0;
287 updateGrid(newMaxEnergy);
288 if (m_maxwellianReset) {
290 } else {
291 projectPreviousEEDFOnCurrentGrid(oldGridCenter, oldF0);
292 }
294 converge(m_f0);
295
296 } else if (decades > m_maxEedfDecay) {
297 // The right boundary is unnecessarily high.
298 double newMaxEnergy = m_kTeMax / (1.0 + m_gridUpdateFactor);
299 Eigen::VectorXd oldGridCenter = m_gridCenter;
300 Eigen::VectorXd oldF0 = m_f0;
301 updateGrid(newMaxEnergy);
302 if (m_maxwellianReset) {
304 } else {
305 projectPreviousEEDFOnCurrentGrid(oldGridCenter, oldF0);
306 }
308 converge(m_f0);
309
310 } else {
311 break;
312 }
313 }
314}
315
317{
318 if (!std::isfinite(kTe) || kTe <= 0.0) {
319 throw CanteraError("EEDFTwoTermApproximation::setMaxwellianDistribution",
320 "Invalid electron temperature for Maxwellian EEDF: {}", kTe);
321 }
322
323 const double prefactor = 2.0 * std::numbers::inv_sqrtpi * std::pow(kTe, -1.5);
324
325 for (size_t j = 0; j < m_points; j++) {
326 m_f0(j) = prefactor * std::exp(-m_gridCenter[j] / kTe);
327 }
328
329 double fNorm = norm(m_f0, m_gridCenter);
330
331 if (!std::isfinite(fNorm) || fNorm <= 0.0) {
332 throw CanteraError("EEDFTwoTermApproximation::setMaxwellianDistribution",
333 "Invalid normalization factor for Maxwellian EEDF.");
334 }
335
336 m_f0 /= fNorm;
337}
338
339void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0)
340{
341 double err0 = 0.0;
342 double err1 = 0.0;
343 double delta = m_delta0;
344
345 if (m_maxn == 0) {
346 throw CanteraError("EEDFTwoTermApproximation::converge",
347 "m_maxn is zero; no iterations will occur.");
348 }
349 if (m_points == 0) {
350 throw CanteraError("EEDFTwoTermApproximation::converge",
351 "m_points is zero; the EEDF grid is empty.");
352 }
353 if (isnan(delta) || delta == 0.0) {
354 throw CanteraError("EEDFTwoTermApproximation::converge",
355 "m_delta0 is NaN or zero; solver cannot update.");
356 }
357
358 for (size_t n = 0; n < m_maxn; n++) {
359 if (0.0 < err1 && err1 < err0) {
360 delta *= log(m_factorM) / (log(err0) - log(err1));
361 }
362
363 Eigen::VectorXd f0_old = f0;
364 f0 = iterate(f0_old, delta);
365 checkFinite("EEDFTwoTermApproximation::converge: f0", asSpan(f0));
366
367 err0 = err1;
368 Eigen::VectorXd Df0 = (f0_old - f0).cwiseAbs();
369 err1 = norm(Df0, m_gridCenter);
370 if (err1 < m_rtol) {
371 break;
372 } else if (n == m_maxn - 1) {
373 throw CanteraError("WeaklyIonizedGas::converge", "Convergence failed");
374 }
375 }
376}
377
378Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, double delta)
379{
380 // CQM multiple call to vector_* and matrix_*
381 // probably extremely ineficient
382 // must be refactored!!
383
384 SparseMat PQ(m_points, m_points);
385 vector<double> g = vector_g(f0);
386
387 for (size_t k : m_phase->kInelastic()) {
388 SparseMat Q_k = matrix_Q(g, k);
389 SparseMat P_k = matrix_P(g, k);
390 PQ += (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]];
391 }
392
393 SparseMat A = matrix_A(f0);
394 SparseMat I(m_points, m_points);
395 for (size_t i = 0; i < m_points; i++) {
396 I.insert(i,i) = 1.0;
397 }
398 A -= PQ;
399 A *= delta;
400 A += I;
401
402 // SparseLU :
403 Eigen::SparseLU<SparseMat> solver(A);
404 if (solver.info() == Eigen::NumericalIssue) {
405 throw CanteraError("EEDFTwoTermApproximation::iterate",
406 "Error SparseLU solver: NumericalIssue");
407 } else if (solver.info() == Eigen::InvalidInput) {
408 throw CanteraError("EEDFTwoTermApproximation::iterate",
409 "Error SparseLU solver: InvalidInput");
410 }
411 if (solver.info() != Eigen::Success) {
412 throw CanteraError("EEDFTwoTermApproximation::iterate",
413 "Error SparseLU solver", "Decomposition failed");
414 return f0;
415 }
416
417 // solve f0
418 Eigen::VectorXd f1 = solver.solve(f0);
419 if(solver.info() != Eigen::Success) {
420 throw CanteraError("EEDFTwoTermApproximation::iterate", "Solving failed");
421 return f0;
422 }
423
424 checkFinite("EEDFTwoTermApproximation::converge: f0", asSpan(f1));
425 f1 /= norm(f1, m_gridCenter);
426 return f1;
427}
428
429double EEDFTwoTermApproximation::integralPQ(double a, double b, double u0, double u1,
430 double g, double x0)
431{
432 double A1;
433 double A2;
434 if (g != 0.0) {
435 double expm1a = expm1(g * (-a + x0));
436 double expm1b = expm1(g * (-b + x0));
437 double ag = a * g;
438 double ag1 = ag + 1;
439 double bg = b * g;
440 double bg1 = bg + 1;
441 A1 = (expm1a * ag1 + ag - expm1b * bg1 - bg) / (g*g);
442 A2 = (expm1a * (2 * ag1 + ag * ag) + ag * (ag + 2) -
443 expm1b * (2 * bg1 + bg * bg) - bg * (bg + 2)) / (g*g*g);
444 } else {
445 A1 = 0.5 * (b*b - a*a);
446 A2 = 1.0 / 3.0 * (b*b*b - a*a*a);
447 }
448
449 // The interpolation formula of u(x) = c0 + c1 * x
450 double c0 = (a * u1 - b * u0) / (a - b);
451 double c1 = (u0 - u1) / (a - b);
452
453 return c0 * A1 + c1 * A2;
454}
455
456vector<double> EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0)
457{
458 vector<double> g(m_points, 0.0);
459 const double f_min = 1e-300; // Smallest safe floating-point value
460
461 // Handle first point (i = 0)
462 double f1 = std::max(f0(1), f_min);
463 double f0_ = std::max(f0(0), f_min);
464 g[0] = log(f1 / f0_) / (m_gridCenter[1] - m_gridCenter[0]);
465
466 // Handle last point (i = N)
467 size_t N = m_points - 1;
468 double fN = std::max(f0(N), f_min);
469 double fNm1 = std::max(f0(N - 1), f_min);
470 g[N] = log(fN / fNm1) / (m_gridCenter[N] - m_gridCenter[N - 1]);
471
472 // Handle interior points
473 for (size_t i = 1; i < N; ++i) {
474 double f_up = std::max(f0(i + 1), f_min);
475 double f_down = std::max(f0(i - 1), f_min);
476 g[i] = log(f_up / f_down) / (m_gridCenter[i + 1] - m_gridCenter[i - 1]);
477 }
478 return g;
479}
480
481SparseMat EEDFTwoTermApproximation::matrix_P(span<const double> g, size_t k)
482{
483 SparseTriplets tripletList;
484 for (size_t n = 0; n < m_eps[k].size(); n++) {
485 double eps_a = m_eps[k][n][0];
486 double eps_b = m_eps[k][n][1];
487 double sigma_a = m_sigma[k][n][0];
488 double sigma_b = m_sigma[k][n][1];
489 auto j = static_cast<SparseMat::StorageIndex>(m_j[k][n]);
490 double r = integralPQ(eps_a, eps_b, sigma_a, sigma_b, g[j], m_gridCenter[j]);
491 double p = m_gamma * r;
492
493 tripletList.emplace_back(j, j, p);
494 }
495 SparseMat P(m_points, m_points);
496 P.setFromTriplets(tripletList.begin(), tripletList.end());
497 return P;
498}
499
500SparseMat EEDFTwoTermApproximation::matrix_Q(span<const double> g, size_t k)
501{
502 SparseTriplets tripletList;
503 for (size_t n = 0; n < m_eps[k].size(); n++) {
504 double eps_a = m_eps[k][n][0];
505 double eps_b = m_eps[k][n][1];
506 double sigma_a = m_sigma[k][n][0];
507 double sigma_b = m_sigma[k][n][1];
508 auto i = static_cast<SparseMat::StorageIndex>(m_i[k][n]);
509 auto j = static_cast<SparseMat::StorageIndex>(m_j[k][n]);
510 double r = integralPQ(eps_a, eps_b, sigma_a, sigma_b, g[j], m_gridCenter[j]);
511 double q = m_inFactor[k] * m_gamma * r;
512
513 tripletList.emplace_back(i, j, q);
514 }
515 SparseMat Q(m_points, m_points);
516 Q.setFromTriplets(tripletList.begin(), tripletList.end());
517 return Q;
518}
519
520SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0)
521{
522 vector<double> a0(m_points + 1);
523 vector<double> a1(m_points + 1);
524 size_t N = m_points - 1;
525 // Scharfetter-Gummel scheme
526 double nu = netProductionFrequency(f0);
527 a0[0] = NAN;
528 a1[0] = NAN;
529 a0[N+1] = NAN;
530 a1[N+1] = NAN;
531
532 double nDensity = m_phase->molarDensity() * Avogadro;
533 double alpha;
534 double E = m_phase->electricField();
535 if (m_growth == "spatial") {
536 double mu = electronMobility(f0);
537 double D = electronDiffusivity(f0);
538 alpha = (mu * E - sqrt(pow(mu * E, 2) - 4 * D * nu * nDensity)) / 2.0 / D / nDensity;
539 } else {
540 alpha = 0.0;
541 }
542
543 double sigma_tilde;
544 double omega = 2 * Pi * m_phase->electricFieldFrequency();
545 for (size_t j = 1; j < m_points; j++) {
546 if (m_growth == "temporal") {
547 sigma_tilde = m_totalCrossSectionEdge[j] + nu / pow(m_gridEdge[j], 0.5) / m_gamma;
548 } else {
549 sigma_tilde = m_totalCrossSectionEdge[j];
550 }
551 double q = omega / (nDensity * m_gamma * pow(m_gridEdge[j], 0.5));
552 double W = -m_gamma * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j];
553 double F = sigma_tilde * sigma_tilde / (sigma_tilde * sigma_tilde + q * q);
554 double DA = m_gamma / 3.0 * pow(E / nDensity, 2.0) * m_gridEdge[j];
556 double D = DA / sigma_tilde * F + DB;
557 if (m_growth == "spatial") {
558 W -= m_gamma / 3.0 * 2 * alpha * E / nDensity * m_gridEdge[j] / sigma_tilde;
559 }
560
561 double z = W * (m_gridCenter[j] - m_gridCenter[j-1]) / D;
562 if (!std::isfinite(z)) {
563 throw CanteraError("matrix_A", "Non-finite Peclet number encountered");
564 }
565 if (std::abs(z) > 500) {
566 warn_user("EEDFTwoTermApproximation::matrix_A",
567 "Large Peclet number z = {:.3e} at j = {}. "
568 "W = {:.3e}, D = {:.3e}, E/N = {:.3e}\n",
569 z, j, W, D, E / nDensity);
570 }
571 a0[j] = W / (1 - std::exp(-z));
572 a1[j] = W / (1 - std::exp(z));
573 }
574
575 SparseTriplets tripletList;
576 // center diagonal
577 // zero flux b.c. at energy = 0
578 tripletList.emplace_back(0, 0, a0[1]);
579
580 for (size_t j = 1; j < m_points - 1; j++) {
581 tripletList.emplace_back(j, j, a0[j+1] - a1[j]);
582 }
583
584 // upper diagonal
585 for (size_t j = 0; j < m_points - 1; j++) {
586 tripletList.emplace_back(j, j+1, a1[j+1]);
587 }
588
589 // lower diagonal
590 for (size_t j = 1; j < m_points; j++) {
591 tripletList.emplace_back(j, j-1, -a0[j]);
592 }
593
594 // zero flux b.c.
595 tripletList.emplace_back(N, N, -a1[N]);
596
597 SparseMat A(m_points, m_points);
598 A.setFromTriplets(tripletList.begin(), tripletList.end());
599
600 //plus G
601 SparseMat G(m_points, m_points);
602 if (m_growth == "temporal") {
603 for (size_t i = 0; i < m_points; i++) {
604 G.insert(i, i) = 2.0 / 3.0 * (pow(m_gridEdge[i+1], 1.5) - pow(m_gridEdge[i], 1.5)) * nu;
605 }
606 } else if (m_growth == "spatial") {
607 double nDensity = m_phase->molarDensity() * Avogadro;
608 for (size_t i = 0; i < m_points; i++) {
609 double sigma_c = 0.5 * (m_totalCrossSectionEdge[i] + m_totalCrossSectionEdge[i + 1]);
610 G.insert(i, i) = - alpha * m_gamma / 3 * (alpha * (pow(m_gridEdge[i + 1], 2) - pow(m_gridEdge[i], 2)) / sigma_c / 2
611 - E / nDensity * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i]));
612 }
613 }
614 return A + G;
615}
616
618{
619 double nu = 0.0;
620 vector<double> g = vector_g(f0);
621
622 for (size_t k = 0; k < m_phase->nCollisions(); k++) {
623 if (m_phase->collisionRate(k)->kind() == "ionization" ||
624 m_phase->collisionRate(k)->kind() == "attachment") {
625 SparseMat PQ = (matrix_Q(g, k) - matrix_P(g, k)) *
627 Eigen::VectorXd s = PQ * f0;
628 checkFinite("EEDFTwoTermApproximation::netProductionFrequency: s",
629 asSpan(s));
630 nu += s.sum();
631 }
632 }
633 return nu;
634}
635
636double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0)
637{
638 vector<double> y(m_points, 0.0);
639 double nu = netProductionFrequency(f0);
640 for (size_t i = 0; i < m_points; i++) {
641 if (m_gridCenter[i] != 0.0) {
642 y[i] = m_gridCenter[i] * f0(i) /
643 (m_totalCrossSectionCenter[i] + nu / m_gamma / pow(m_gridCenter[i], 0.5));
644 }
645 }
646 double nDensity = m_phase->molarDensity() * Avogadro;
647 auto f = Eigen::Map<const Eigen::ArrayXd>(y.data(), y.size());
648 auto x = Eigen::Map<const Eigen::ArrayXd>(m_gridCenter.data(), m_gridCenter.size());
649 return 1./3. * m_gamma * simpson(f, x) / nDensity;
650}
651
652double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0)
653{
654 double nu = netProductionFrequency(f0);
655 vector<double> y(m_points + 1, 0.0);
656 for (size_t i = 1; i < m_points; i++) {
657 // calculate df0 at i-1/2
658 double df0 = (f0(i) - f0(i-1)) / (m_gridCenter[i] - m_gridCenter[i-1]);
659 if (m_gridEdge[i] != 0.0) {
660 y[i] = m_gridEdge[i] * df0 /
661 (m_totalCrossSectionEdge[i] + nu / m_gamma / pow(m_gridEdge[i], 0.5));
662 }
663 }
664 double nDensity = m_phase->molarDensity() * Avogadro;
665 return -1./3. * m_gamma * simpson(asVectorXd(y), asVectorXd(m_gridEdge)) / nDensity;
666}
667
669{
670 // set up target index
671 m_kTargets.resize(m_phase->nCollisions());
673 m_inFactor.resize(m_phase->nCollisions());
674 for (size_t k = 0; k < m_phase->nCollisions(); k++) {
676 // Check if it is a new target or not :
677 auto it = find(m_k_lg_Targets.begin(), m_k_lg_Targets.end(), m_kTargets[k]);
678
679 if (it == m_k_lg_Targets.end()){
680 m_k_lg_Targets.push_back(m_kTargets[k]);
681 m_klocTargets[k] = m_k_lg_Targets.size() - 1;
682 } else {
683 m_klocTargets[k] = distance(m_k_lg_Targets.begin(), it);
684 }
685
686 const auto& kind = m_phase->collisionRate(k)->kind();
687
688 if (kind == "ionization") {
689 m_inFactor[k] = 2;
690 } else if (kind == "attachment") {
691 m_inFactor[k] = 0;
692 } else {
693 m_inFactor[k] = 1;
694 }
695 }
696
697 m_X_targets.resize(m_k_lg_Targets.size());
698 m_X_targets_prev.resize(m_k_lg_Targets.size());
699 for (size_t k = 0; k < m_X_targets.size(); k++) {
700 size_t k_glob = m_k_lg_Targets[k];
701 m_X_targets[k] = m_phase->moleFraction(k_glob);
703 }
704
705 // set up indices of species which has no cross-section data
706 for (size_t k = 0; k < m_phase->nSpecies(); k++) {
707 auto it = std::find(m_kTargets.begin(), m_kTargets.end(), k);
708 if (it == m_kTargets.end()) {
709 m_kOthers.push_back(k);
710 }
711 }
712}
713
715{
716 // Compute sigma_m and sigma_\epsilon
719}
720
721// Update the species mole fractions used for EEDF computation
723{
724 double tmp_sum = 0.0;
725 for (size_t k = 0; k < m_X_targets.size(); k++) {
727 tmp_sum = tmp_sum + m_phase->moleFraction(m_k_lg_Targets[k]);
728 }
729
730 // Normalize the mole fractions to unity:
731 for (size_t k = 0; k < m_X_targets.size(); k++) {
732 m_X_targets[k] = m_X_targets[k] / tmp_sum;
733 }
734}
735
737{
739 m_totalCrossSectionEdge.assign(m_points + 1, 0.0);
740 for (size_t k = 0; k < m_phase->nCollisions(); k++) {
741 auto x = m_phase->collisionRate(k)->energyLevels();
742 auto y = m_phase->collisionRate(k)->crossSections();
743
744 for (size_t i = 0; i < m_points; i++) {
746 linearInterp(m_gridCenter[i], x, y);
747 }
748 for (size_t i = 0; i < m_points + 1; i++) {
750 linearInterp(m_gridEdge[i], x, y);
751 }
752 }
753}
754
756{
757 m_sigmaElastic.clear();
758 m_sigmaElastic.resize(m_points, 0.0);
759 for (size_t k : m_phase->kElastic()) {
760 auto x = m_phase->collisionRate(k)->energyLevels();
761 auto y = m_phase->collisionRate(k)->crossSections();
762 // Note:
763 // moleFraction(m_kTargets[k]) <=> m_X_targets[m_klocTargets[k]]
764 double mass_ratio = ElectronMass / (m_phase->molecularWeight(m_kTargets[k]) / Avogadro);
765 for (size_t i = 0; i < m_points; i++) {
766 m_sigmaElastic[i] += 2.0 * mass_ratio * m_X_targets[m_klocTargets[k]] *
767 linearInterp(m_gridEdge[i], x, y);
768 }
769 }
770}
771
773{
774 m_sigma.clear();
775 m_sigma.resize(m_phase->nCollisions());
776 m_eps.clear();
777 m_eps.resize(m_phase->nCollisions());
778 m_j.clear();
779 m_j.resize(m_phase->nCollisions());
780 m_i.clear();
781 m_i.resize(m_phase->nCollisions());
782 for (size_t k = 0; k < m_phase->nCollisions(); k++) {
783 auto& collision = m_phase->collisionRate(k);
784 auto x = collision->energyLevels();
785 auto y = collision->crossSections();
786 vector<double> eps1(m_points + 1);
787 int shiftFactor = (collision->kind() == "ionization") ? 2 : 1;
788
789 for (size_t i = 0; i < m_points + 1; i++) {
790 eps1[i] = clip(shiftFactor * m_gridEdge[i] + collision->threshold(),
791 m_gridEdge[0] + 1e-9, m_gridEdge[m_points] - 1e-9);
792 }
793 vector<double> nodes = eps1;
794 for (size_t i = 0; i < m_points + 1; i++) {
795 if (m_gridEdge[i] >= eps1[0] && m_gridEdge[i] <= eps1[m_points]) {
796 nodes.push_back(m_gridEdge[i]);
797 }
798 }
799 for (size_t i = 0; i < x.size(); i++) {
800 if (x[i] >= eps1[0] && x[i] <= eps1[m_points]) {
801 nodes.push_back(x[i]);
802 }
803 }
804
805 std::sort(nodes.begin(), nodes.end());
806 auto last = std::unique(nodes.begin(), nodes.end());
807 nodes.resize(std::distance(nodes.begin(), last));
808 vector<double> sigma0(nodes.size());
809 for (size_t i = 0; i < nodes.size(); i++) {
810 sigma0[i] = linearInterp(nodes[i], x, y);
811 }
812
813 // search position of cell j
814 for (size_t i = 1; i < nodes.size(); i++) {
815 auto low = std::lower_bound(m_gridEdge.begin(), m_gridEdge.end(), nodes[i]);
816 m_j[k].push_back(low - m_gridEdge.begin() - 1);
817 }
818
819 // search position of cell i
820 for (size_t i = 1; i < nodes.size(); i++) {
821 auto low = std::lower_bound(eps1.begin(), eps1.end(), nodes[i]);
822 m_i[k].push_back(low - eps1.begin() - 1);
823 }
824
825 // construct sigma
826 for (size_t i = 0; i < nodes.size() - 1; i++) {
827 m_sigma[k].push_back({sigma0[i], sigma0[i+1]});
828 }
829
830 // construct eps
831 for (size_t i = 0; i < nodes.size() - 1; i++) {
832 m_eps[k].push_back({nodes[i], nodes[i+1]});
833 }
834
835 // construct sigma_offset
836 vector<double> x_offset(collision->energyLevels().begin(),
837 collision->energyLevels().end());
838 for (auto& element : x_offset) {
839 element -= collision->threshold();
840 }
841 }
842}
843
844double EEDFTwoTermApproximation::norm(const Eigen::VectorXd& f, const Eigen::VectorXd& grid)
845{
846 string m_quadratureMethod = "simpson";
847 Eigen::VectorXd p(f.size());
848 for (int i = 0; i < f.size(); i++) {
849 p[i] = f(i) * pow(grid[i], 0.5);
850 }
851 return numericalQuadrature(m_quadratureMethod, p, grid);
852}
853
855 double initialMaxEnergy, size_t nGridCells, const string& gridType)
856{
857 if (!std::isfinite(initialMaxEnergy) || initialMaxEnergy <= 0.0) {
858 throw CanteraError("EEDFTwoTermApproximation::setInitialGridParameters",
859 "initialMaxEnergy must be finite and greater than zero.");
860 }
861
862 if (nGridCells == 0) {
863 throw CanteraError("EEDFTwoTermApproximation::setInitialGridParameters",
864 "nGridCells must be greater than zero.");
865 }
866
867 if (gridType != "linear" &&
868 gridType != "quadratic" &&
869 gridType != "geometric") {
870 throw CanteraError("EEDFTwoTermApproximation::setInitialGridParameters",
871 "Unknown energy grid type '{}'. Expected linear, quadratic or geometric.",
872 gridType);
873 }
874
875 m_kTeMax = initialMaxEnergy;
876 m_initialGridCells = nGridCells;
877 m_gridType = gridType;
878}
879
881{
882 m_adaptGrid = enabled;
883}
884
886 double minDecayDecades, double maxDecayDecades, double updateFactor,
887 size_t maxIterations, bool maxwellianReset)
888{
889 if (!std::isfinite(minDecayDecades) || !std::isfinite(maxDecayDecades) ||
890 minDecayDecades <= 0.0 || maxDecayDecades <= minDecayDecades) {
891 throw CanteraError("EEDFTwoTermApproximation::setGridAdaptationParameters",
892 "Require 0 < min_decay_decades < max_decay_decades.");
893 }
894
895 if (!std::isfinite(updateFactor) || updateFactor <= 0.0) {
896 throw CanteraError("EEDFTwoTermApproximation::setGridAdaptationParameters",
897 "update_factor must be finite and greater than zero.");
898 }
899
900 if (maxIterations == 0) {
901 throw CanteraError("EEDFTwoTermApproximation::setGridAdaptationParameters",
902 "max_iterations must be greater than zero.");
903 }
904
905 m_minEedfDecay = minDecayDecades;
906 m_maxEedfDecay = maxDecayDecades;
907 m_gridUpdateFactor = updateFactor;
908 m_maxGridAdaptIterations = maxIterations;
909 m_maxwellianReset = maxwellianReset;
910}
911
913{
914 if (!std::isfinite(maxEnergy) || maxEnergy <= 0.0) {
915 throw CanteraError("EEDFTwoTermApproximation::updateGrid",
916 "Maximum grid energy must be finite and greater than zero.");
917 }
918
919 m_kTeMax = maxEnergy;
920
921 if (m_gridType == "linear") {
923 } else if (m_gridType == "quadratic") {
925 } else if (m_gridType == "geometric") {
927 } else {
928 throw CanteraError("EEDFTwoTermApproximation::updateGrid",
929 "Unknown energy grid type '{}'.", m_gridType);
930 }
931
932 m_has_EEDF = false;
933}
934
935}
EEDF Two-Term approximation solver.
Header for plasma reaction rates parameterized by electron collision cross section and electron energ...
Header file for class PlasmaPhase.
Base class for exceptions thrown by Cantera classes.
double m_rtol
Error tolerance for convergence.
Eigen::VectorXd m_f0
Normalized electron energy distribution function.
vector< double > m_gridEdge
Grid of electron energy (cell boundary i-1/2) [eV].
void enableGridAdaptation(bool enabled)
Enable or disable automatic grid adaptation for the EEDF solver energy grid.
vector< vector< size_t > > m_i
Location of cell i for grid cache.
void calculateTotalCrossSection()
Compute the total (elastic + inelastic) cross section.
vector< vector< size_t > > m_j
Location of cell j for grid cache.
vector< double > m_X_targets_prev
Previous mole fraction of targets used to compute eedf.
double m_thresholdToMaxwellian
The threshold in reduced electric field [townsend, Td] below which no EEDF will be computed,...
void projectPreviousEEDFOnCurrentGrid(const Eigen::VectorXd &oldGridCenter, const Eigen::VectorXd &oldF0)
Projects a previously converged EEDF onto the current energy grid.
void adaptEnergyGrid()
Runs the energy grid adaptation script when this feature is activated.
vector< vector< vector< double > > > m_eps
The energy boundaries of the overlap of cell i and j.
double m_maxEedfDecay
Maximum amount of decades decay at the tail of the EEDF when grid adaptation is on.
void setGeometricGrid(double kTe_max, size_t ncell, double ratio=1.01)
Sets a geometric energy grid for the EEDF solver, defined by the maximum energy and the number of gri...
vector< vector< vector< double > > > m_sigma
Cross section at the boundaries of the overlap of cell i and j.
void setGridAdaptationParameters(double minDecayDecades, double maxDecayDecades, double updateFactor, size_t maxIterations, bool maxwellianReset)
Set parameters controlling automatic adaptation of the EEDF energy grid.
vector< size_t > m_k_lg_Targets
Local to global indices.
string m_gridType
Energy grid spacing type. Can be linear, quadratic or geometric.
bool m_first_call
First call to calculateDistributionFunction.
double m_kTeMax
Maximum value of the energy grid [eV].
size_t m_initialGridCells
Number of cells for the starting energy grid.
double m_gamma
Defined by the formula: pow(2.0 * ElectronCharge / ElectronMass, 0.5) and comupted during phase initi...
void converge(Eigen::VectorXd &f0)
Iterate f0 (EEDF) until convergence.
double m_delta0
Formerly options for the EEDF solver.
double electronDiffusivity(const Eigen::VectorXd &f0)
Diffusivity.
PlasmaPhase * m_phase
Pointer to the PlasmaPhase object used to initialize this object.
double norm(const Eigen::VectorXd &f, const Eigen::VectorXd &grid)
Compute the L1 norm of a function f defined over a given energy grid.
void updateMoleFractions()
Update the vector of species mole fractions.
vector< size_t > m_kTargets
List of target species indices in global Cantera numbering (1 index per cs)
void setCustomGrid(span< const double > levels)
Sets a custom energy grid for the EEDF solver, defined by the user-provided vector of energy levels.
void setQuadraticGrid(double kTe_max, size_t ncell)
Sets a quadratic energy grid for the EEDF solver, defined by the maximum energy and the number of gri...
double electronMobility(const Eigen::VectorXd &f0)
Mobility.
void setMaxwellianDistribution(double kTe)
Sets a Maxwellian distribution with the specified electron temperature [eV].
Eigen::VectorXd m_gridCenter
Grid of electron energy (cell center) [eV].
size_t m_maxn
Maximum number of iterations.
Eigen::SparseMatrix< double > matrix_A(const Eigen::VectorXd &f0)
Matrix A (Ax = b) of the equation of EEDF, which is discretized by the exponential scheme of Scharfet...
Eigen::SparseMatrix< double > matrix_Q(span< const double > g, size_t k)
The matrix of scattering-in.
Eigen::VectorXd iterate(const Eigen::VectorXd &f0, double delta)
An iteration of solving electron energy distribution function.
double m_gridUpdateFactor
Factor by which the EEDF grid maximum energy is increased of shrunk when grid adaptation is on.
double m_electronMobility
Electron mobility [m²/V·s].
void setInitialGridParameters(double initialMaxEnergy, size_t nGridCells, const string &gridType)
Set the initial grid parameters used by generated EEDF grids.
size_t m_points
The number of points in the EEDF grid.
vector< size_t > m_kOthers
Indices of species which has no cross-section data.
double m_factorM
The factor for step size change.
void calculateTotalElasticCrossSection()
Compute the total elastic collision cross section.
double m_minEedfDecay
Minimum amount of decades decay at the tail of the EEDF when grid adaptation is on.
void updateCrossSections()
Update the total cross sections based on the current state.
size_t m_maxGridAdaptIterations
Maximum number of iterations on the maximum energy accepted for grid adaptation.
double m_geometricRatio
In the case where a geometric grid is employed, this stores the corresponding geometric ratio.
void initSpeciesIndexCrossSections()
Initialize species indices associated with cross-section data.
double m_init_kTe
The initial electron temperature [eV].
double netProductionFrequency(const Eigen::VectorXd &f0)
Reduced net production frequency.
double linearInterpBounded(double x, span< const double > xpts, span< const double > fpts, double below_value, double above_value)
An extension of the linearInterp function that returns specified values when the input is out of boun...
bool m_maxwellianReset
Boolean flag to reset the EEDF to a Maxwellian distribution at the gas temperature when the grid is a...
std::string m_growth
The growth model of EEDF.
void setGridCache()
Build or rebuild the grid-dependent cache used for scattering matrices.
int calculateDistributionFunction()
compute the EEDF given an electric field CQM The solver will take the species to consider and the set...
vector< double > m_totalCrossSectionEdge
Total electron cross section on the cell boundary (i-1/2) of energy grid.
Eigen::SparseMatrix< double > matrix_P(span< const double > g, size_t k)
The matrix of scattering-out.
vector< double > m_f0_edge
EEDF at grid edges (cell boundaries)
bool m_adaptGrid
Flag activating or deactivating automatic grid adaptation.
vector< size_t > m_klocTargets
List of target species indices in local X EEDF numbering (1 index per cs)
vector< double > m_X_targets
Mole fraction of targets.
void updateGrid(double maxEnergy)
Updates the grid according to the grid type and the new maximum energy when running grid adaptation.
double integralPQ(double a, double b, double u0, double u1, double g, double x0)
The integral in [a, b] of assuming that u is linear with u(a) = u0 and u(b) = u1.
vector< double > m_totalCrossSectionCenter
Total electron cross section on the cell center of energy grid.
vector< double > vector_g(const Eigen::VectorXd &f0)
Vector g is used by matrix_P() and matrix_Q().
vector< double > m_sigmaElastic
Vector of total elastic cross section weighted with mass ratio.
void setLinearGrid(double kTe_max, size_t ncell)
Sets a linear energy grid for the EEDF solver, defined by the maximum energy and the number of grid c...
virtual double molarDensity() const
Molar density (kmol/m^3).
Definition Phase.cpp:597
size_t nSpecies() const
Returns the number of species in the phase.
Definition Phase.h:247
double temperature() const
Temperature (K).
Definition Phase.h:586
double moleFraction(size_t k) const
Return the mole fraction of a single species.
Definition Phase.cpp:457
double molecularWeight(size_t k) const
Molecular weight of species k.
Definition Phase.cpp:398
Base class for handling plasma properties, specifically focusing on the electron energy distribution.
double electricFieldFrequency() const
Get the frequency of the applied electric field [Hz].
size_t nCollisions() const
Number of electron collision cross sections.
double electricField() const
Get the applied electric field strength [V/m].
const vector< size_t > & kElastic() const
Get the indices for elastic electron collisions.
const shared_ptr< ElectronCollisionPlasmaRate > collisionRate(size_t i) const
Get the ElectronCollisionPlasmaRate object associated with electron collision i.
double reducedElectricField() const
Calculate the degree of ionization.
size_t targetIndex(size_t i) const
Return the target of a specific process.
const vector< size_t > & kInelastic() const
Get the indicies for inelastic electron collisions.
Definitions for the classes that are thrown when Cantera experiences an error condition (also contain...
Header for a file containing miscellaneous numerical functions.
#define AssertThrowMsg(expr, procedure,...)
Assertion must be true or an error is thrown.
T clip(const T &value, const T &lower, const T &upper)
Clip value such that lower <= value <= upper.
Definition global.h:326
double numericalQuadrature(const string &method, const Eigen::ArrayXd &f, const Eigen::ArrayXd &x)
Numerical integration of a function.
Definition funcs.cpp:116
double simpson(const Eigen::ArrayXd &f, const Eigen::ArrayXd &x)
Numerical integration of a function using Simpson's rule with flexibility of taking odd and even numb...
Definition funcs.cpp:91
double linearInterp(double x, span< const double > xpts, span< const double > fpts)
Linearly interpolate a function defined on a discrete grid.
Definition funcs.cpp:13
const double Boltzmann
Boltzmann constant [J/K].
Definition ct_defs.h:87
const double Avogadro
Avogadro's Number [number/kmol].
Definition ct_defs.h:84
const double ElectronCharge
Elementary charge [C].
Definition ct_defs.h:93
const double Pi
Pi.
Definition ct_defs.h:71
const double ElectronMass
Electron Mass [kg].
Definition ct_defs.h:114
void warn_user(const string &method, const string &msg, const Args &... args)
Print a user warning raised from method as CanteraWarning.
Definition global.h:263
Namespace for the Cantera kernel.
Definition AnyMap.cpp:595
void checkFinite(const double tmp)
Check to see that a number is finite (not NaN, +Inf or -Inf)
MappedVector asVectorXd(vector< double > &v)
Convenience wrapper for accessing std::vector as an Eigen VectorXd.
Definition eigen_dense.h:60
span< double > asSpan(Eigen::DenseBase< Derived > &v)
Convenience wrapper for accessing Eigen vector/array/map data as a span.
Definition eigen_dense.h:46
void checkArraySize(const char *procedure, size_t available, size_t required)
Wrapper for throwing ArraySizeError.