rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
CollisionModel.cpp
Go to the documentation of this file.
1#include "CollisionModel.h"
2#include "units.h"
3
4#include <algorithm>
5#include <cmath>
6
7namespace rizer {
8
9namespace {
10
11// Physical constants from units.h (mirror rizer/misc/units.py) so the C++
12// collision physics reproduces the Python PlasmaExtension reference to
13// interpolation error. Short local aliases keep the formula bodies readable.
14constexpr double kE = units::e; // elementary charge [C]
15constexpr double kKB = units::k_b; // Boltzmann constant [J/K]
16constexpr double kME = units::m_e; // electron mass [kg]
17constexpr double kEPS0 = units::epsilon_0; // vacuum permittivity [F/m]
18constexpr double kNA = units::N_a; // Avogadro [1/mol]
19constexpr double kPI = units::pi;
20
21// Electron thermal velocity (equations.py):
22// @f[
23// v_\text{th} = \sqrt{\frac{8 k_B T_e}{\pi m_e}} \quad [\text{m/s}].
24// @f]
25// C++ counterpart of rizer.plasma.equations.electron_thermal_velocity.
26// \param Te Electron temperature [K]
27inline double thermalVelocity(double Te)
28{
29 return std::sqrt(8.0 * kKB * Te / (kPI * kME));
30}
31
32// Mitchner Coulomb logarithm, floored at 1 for robustness during violent
33// transients (the Python model raises for lnLambda <= 0 and warns for <= 1;
34// a small positive floor keeps nu_ei finite and positive so the stiff
35// solver never sees a non-physical collision frequency):
36// @f[
37// \ln\Lambda = \ln\!\left(\frac{\lambda_D}{\overline{b_0}}\right), \qquad
38// \lambda_D = \sqrt{\frac{\varepsilon_0 k_B T_e}{n_e e^2}}, \qquad
39// \overline{b_0} = \frac{Z e^2}{12 \pi \varepsilon_0 k_B T_e}.
40// @f]
41// C++ counterpart of rizer.plasma.equations.coulomb_logarithm(model="Mitchner").
42// \param n_e Electron number density [m^-3]
43// \param Te Electron temperature [K]
44// \param Z Ion charge state (>0) of the colliding heavy species
45inline double coulombLogMitchner(double n_e, double Te, int Z)
46{
47 if (n_e <= 0.0 || Te <= 0.0) return 1.0;
48 const double lambda_D = std::sqrt(kEPS0 * kKB * Te / (n_e * kE * kE));
49 const double b0_bar = static_cast<double>(Z) * kE * kE
50 / (12.0 * kPI * kEPS0 * kKB * Te);
51 const double lnL = std::log(lambda_D / b0_bar);
52 return (lnL > 1.0) ? lnL : 1.0;
53}
54
55// Mitchner II-8.11e prefactor (includes Z^2):
56// @f[
57// c_\text{st}(Z) = \frac{4 \sqrt{2 \pi}}{3}
58// \left(\frac{m_e}{k_B}\right)^{1.5}
59// \left(\frac{e^2}{4 \pi \varepsilon_0 m_e}\right)^2 Z^2.
60// @f]
61// Shared by the per-ion-species prefactor (CollisionModel ctor) and the
62// electron-electron collision frequency (Z=1) below.
63// \param Z Charge state (ion charge number, or 1 for electron-electron)
64double ionCollisionPrefactor(int Z)
65{
66 const double a = 4.0 * std::sqrt(2.0 * kPI) / 3.0;
67 const double b = std::pow(kME / kKB, 1.5);
68 const double c = kE * kE / (4.0 * kPI * kEPS0 * kME);
69 return a * b * c * c * static_cast<double>(Z) * static_cast<double>(Z);
70}
71
72} // namespace
73
74double CollisionModel::meanCrossSection(const std::vector<double>& energy_J,
75 const std::vector<double>& sigma_m2, double Te,
76 double x_max, int N)
77{
78 // Trapezoid quadrature on a uniform x-grid of N points for (Mitchner
79 // II-6.30):
80 // @f[
81 // \overline{Q}(T_e) = \frac{2}{3} \int_0^{x_\text{max}}
82 // x^2 e^{-x} Q(k_B T_e x)\, dx,
83 // @f]
84 // identical to the Python TabulatedSpeciesCrossSection
85 // .get_mean_cross_section (x_max=20, N=1000), which is the C++
86 // counterpart of this method.
87 if (N < 2 || energy_J.empty()) return 0.0;
88 // np.interp default: linear interpolation, holding the nearest edge value
89 // outside the table (constant extrapolation) rather than zeroing -- exactly
90 // PropertyTable::eval()'s contract. Hard-zeroing biases the averaged cross
91 // section -- and hence k(Te) -- low when Te samples energies beyond the
92 // tabulated range. Built once here (not per query) since energy_J/sigma_m2
93 // are fixed across this loop.
94 const PropertyTable sigma_table = PropertyTable::fromTableOrScalar(energy_J, sigma_m2);
95 const double dx = x_max / static_cast<double>(N - 1);
96 double integral = 0.0;
97 double prev = 0.0; // integrand at x=0 is 0 (x^2 factor)
98 for (int i = 1; i < N; i++) {
99 const double x = dx * static_cast<double>(i);
100 const double energy = kKB * Te * x;
101 const double Q = sigma_table.eval(energy);
102 const double cur = (2.0 / 3.0) * x * x * std::exp(-x) * Q;
103 integral += 0.5 * (prev + cur) * dx;
104 prev = cur;
105 }
106 return integral;
107}
108
110 const std::vector<double>& energy_J, const std::vector<double>& sigma_m2,
111 const std::vector<double>& Te, double x_max, int N)
112{
113 std::vector<double> out(Te.size());
114 for (std::size_t i = 0; i < Te.size(); i++) {
115 out[i] = meanCrossSection(energy_J, sigma_m2, Te[i], x_max, N);
116 }
117 return out;
118}
119
120CollisionModel::CollisionModel(const std::vector<SpeciesSpec>& specs, double Te_min,
121 double Te_max, std::size_t Te_n, bool spitzer_correction)
122 : m_spitzer(spitzer_correction)
123{
124 // Log-spaced Te grid for the precomputed Qbar(Te) tables (smooth function;
125 // log spacing resolves the low-Te knee). PropertyTable handles non-uniform.
126 if (Te_n < 2) Te_n = 2;
127 std::vector<double> Te_grid(Te_n);
128 const double llo = std::log(Te_min), lhi = std::log(Te_max);
129 for (std::size_t i = 0; i < Te_n; i++) {
130 Te_grid[i] = std::exp(llo + (lhi - llo) * static_cast<double>(i)
131 / static_cast<double>(Te_n - 1));
132 }
133
134 m_species.resize(specs.size());
135 for (std::size_t k = 0; k < specs.size(); k++) {
136 const SpeciesSpec& s = specs[k];
137 Sp& sp = m_species[k];
138 if (s.molar_mass > 0.0) {
139 // m_h [kg] = molar_mass [kg/mol] / N_A, so N_A / molar_mass = 1 / m_h;
140 // molar_mass is already SI (kg/mol) here -- the Python side converts
141 // from g/mol via *1e-3 before this ever reaches C++ (see
142 // unpack_momentum_transfer_models / PlasmaChannel species setup).
143 sp.two_me_over_mh = 2.0 * kME * kNA / s.molar_mass; // 2 m_e / m_h
144 sp.inv_mh = kNA / s.molar_mass; // 1 / m_h
145 }
146 if (s.is_electron) {
147 sp.kind = Kind::Skip;
148 } else if (s.Z > 0) {
149 sp.kind = Kind::Ion;
150 sp.Z = s.Z;
151 // Precomputed once here exactly as
152 // IonMomentumTransferCollisionFrequencyModel.__init__ does in the
153 // Python reference (rizer/plasma/collision_frequency.py).
154 sp.cst = ionCollisionPrefactor(s.Z);
155 } else if (!s.energy_J.empty() && s.energy_J.size() == s.sigma_m2.size()) {
156 sp.kind = Kind::Tabulated;
157 // C++ counterpart of TabulatedSpeciesCrossSection: bake the Maxwellian
158 // average into a Te table once at construction (build time), rather
159 // than integrating live every RHS evaluation.
160 std::vector<double> Qbar =
162 sp.Qbar = PropertyTable(Te_grid, Qbar);
163 } else if (s.radius > 0.0) {
164 sp.kind = Kind::HardSphere;
165 sp.Qbar = PropertyTable::constant(4.0 / 3.0 * kPI * s.radius * s.radius);
166 } else {
167 sp.kind = Kind::Skip; // no model -> ignored in the heavy sums
168 }
169 }
170}
171
172void CollisionModel::evaluate(double Te, double n_e, const double* n_k, std::size_t nsp,
173 double& sigma, double& nu_E, double& cond_maxwell_2) const
174{
175 sigma = 0.0;
176 nu_E = 0.0;
177 cond_maxwell_2 = 0.0;
178 if (m_species.empty() || Te <= 0.0) return;
179
180 const double v_th = thermalVelocity(Te);
181 double nu_eH = 0.0, nu_ions = 0.0, nu_E_sum = 0.0, cond_maxwell_2_sum = 0.0;
182
183 const std::size_t K = std::min(nsp, m_species.size());
184 for (std::size_t k = 0; k < K; k++) {
185 const Sp& sp = m_species[k];
186 if (sp.kind == Kind::Skip) continue;
187 const double n = n_k[k];
188 if (n <= 0.0) continue;
189
190 double nu1 = 0.0;
191 if (sp.kind == Kind::Ion) {
192 // Mitchner II-8.10, the C++ counterpart of
193 // IonMomentumTransferCollisionFrequencyModel
194 // .get_mean_momentum_transfer_collision_frequency:
195 // @f[
196 // \nu_{ei} = n_i \, c_\text{st} \, T_e^{-1.5} \, \ln\Lambda.
197 // @f]
198 const double lnL = coulombLogMitchner(n_e, Te, sp.Z);
199 nu1 = n * sp.cst * std::pow(Te, -1.5) * lnL;
200 nu_ions += nu1;
201 } else { // Tabulated or HardSphere
202 // Mitchner II-6.29, the C++ counterpart of
203 // MomentumTransferCollisionFrequencyModel
204 // .get_mean_momentum_transfer_collision_frequency:
205 // @f[
206 // \nu_{eh} = n_h \, \overline{Q}(T_e) \, v_\text{th}.
207 // @f]
208 nu1 = n * sp.Qbar.eval(Te) * v_th;
209 }
210 nu_eH += nu1;
211 // Elastic energy-exchange frequency contribution (Mitchner II-7.6),
212 // the C++ counterpart of EnergyExchangeCollisionFrequencyModel
213 // .get_mean_energy_exchange_collision_frequency:
214 // @f[
215 // \nu_{eh}^E = \frac{2 m_e}{m_h} \nu_{eh}.
216 // @f]
217 nu_E_sum += sp.two_me_over_mh * nu1;
218 // Second Maxwellian-distribution condition term (Mitchner VIII-3.8),
219 // the C++ counterpart of
220 // PlasmaExtension.conditions_for_maxwellian_distribution's
221 // cond_maxwell_2: @f$\sum_h \bar\nu_{eh}/m_h@f$.
222 cond_maxwell_2_sum += sp.inv_mh * nu1;
223 }
224
225 nu_E = nu_E_sum;
226 cond_maxwell_2 = cond_maxwell_2_sum;
227 if (nu_eH > 0.0 && n_e > 0.0) {
228 sigma = n_e * kE * kE / (kME * nu_eH);
229 // Electron-electron collisions raise sigma above the Lorentz value by up to
230 // the Spitzer factor 1.98 for a fully ionized, singly charged plasma
231 // (Mitchner II-13.18). Bridge it smoothly with the ion collision fraction
232 // @f$f_\text{ion} = \nu_\text{ions} / \nu_{eH}@f$ (1 -> neutral-dominated,
233 // 1.98 -> fully ionized) instead of switching abruptly once nu_ions
234 // exceeds nu_neutrals.
235 if (m_spitzer) {
236 const double f_ion = nu_ions / nu_eH;
237 sigma *= 1.0 + 0.98 * f_ion;
238 }
239 }
240}
241
242double CollisionModel::conductivity(double Te, double n_e, const double* n_k,
243 std::size_t nsp) const
244{
245 double sigma = 0.0, nu_E = 0.0, cond_maxwell_2 = 0.0;
246 evaluate(Te, n_e, n_k, nsp, sigma, nu_E, cond_maxwell_2);
247 return sigma;
248}
249
251{
252 // Mitchner II-8.11e with Z=1, the C++ counterpart of
253 // PlasmaExtension.conditions_for_maxwellian_distribution's nu_ee: same
254 // closed form as the ion collision frequency (ionCollisionPrefactor
255 // above), evaluated with n=n_e, Z=1.
256 if (n_e <= 0.0 || Te <= 0.0) return 0.0;
257 static const double cst_ee = ionCollisionPrefactor(1);
258 const double lnL = coulombLogMitchner(n_e, Te, 1);
259 return n_e * cst_ee * std::pow(Te, -1.5) * lnL;
260}
261
262} // namespace rizer
static double electronElectronCollisionFrequency(double n_e, double Te)
Electron-electron collision frequency [1/s] (Mitchner II-8.11e with Z=1), the closed-form ingredient...
double conductivity(double Te, double n_e, const double *n_k, std::size_t nsp) const
Conductivity only [S/m].
static double meanCrossSection(const std::vector< double > &energy_J, const std::vector< double > &sigma_m2, double Te, double x_max=20.0, int N=1000)
The Maxwellian momentum-transfer kernel Qbar(Te) [m^2] (single source of truth; also exposed to Pytho...
static std::vector< double > meanCrossSectionTable(const std::vector< double > &energy_J, const std::vector< double > &sigma_m2, const std::vector< double > &Te, double x_max=20.0, int N=1000)
Tabulated variant of meanCrossSection, evaluated at each Te in the input vector.
void evaluate(double Te, double n_e, const double *n_k, std::size_t nsp, double &sigma, double &nu_E, double &cond_maxwell_2) const
Composition-resolved electron transport.
static PropertyTable constant(double value)
A two-point constant table returning value for any T.
double eval(double T) const
Linearly interpolated value at T; clamped to the table endpoints.
static PropertyTable fromTableOrScalar(const std::vector< double > &x, const std::vector< double > &v)
Build a table from a grid/value pair, a single scalar, or neither.
constexpr double k_b
Boltzmann constant [J/K]. Exact (CODATA).
Definition units.h:28
constexpr double epsilon_0
Vacuum permittivity [F/m] (CODATA 2022).
Definition units.h:42
constexpr double pi
pi (matches numpy's np.pi used in units.py).
Definition units.h:51
constexpr double N_a
Avogadro's number [1/mol]. Exact (CODATA).
Definition units.h:30
constexpr double e
Elementary charge [C]. Exact (CODATA).
Definition units.h:26
constexpr double m_e
Electron mass [kg] (CODATA 2022).
Definition units.h:36
Per-species spec, in species-index order.
bool is_electron
true for the electron species (skipped)
std::vector< double > energy_J
tabulated cross-section abscissa [J]
double molar_mass
[kg/mol], for the elastic 2 m_e/m_h factor
double radius
hard-sphere radius [m] (>0 if used)
std::vector< double > sigma_m2
tabulated cross-section [m^2]
int Z
ion charge number (>0 marks an ion)
Physical constants and unit conversions, in SI by default.