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