rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
ReactorRHS.cpp
Go to the documentation of this file.
1#include "ReactorRHS.h"
2#include "units.h"
3
8
9#include <algorithm>
10#include <cmath>
11#include <sstream>
12#include <string>
13
14namespace rizer {
15
16namespace {
17// Exact twin of rizer.plasma.plasma_extension._reaction_involves_electron: the
18// electron participates iff it appears in the reaction *equation*. Cantera cancels a
19// spectator electron (one on both sides, e.g. "CH4 + e- => CH3 + H + e-") from the
20// reactant/product stoichiometry, so a species-map test would miss such electron-
21// impact excitation/dissociation reactions. Both implementations replace the
22// reaction-arrow and stoichiometric-plus characters '+','=','<','>' with spaces and
23// then look for the electron as a standalone token; they MUST stay byte-for-byte
24// equivalent so the native and Python reactors select the same electron-impact
25// reactions for the inelastic electron-to-gas power. Change them together.
29bool equationHasElectron(const std::string& equation, const std::string& electron)
30{
31 std::string s = equation;
32 for (char& ch : s) {
33 if (ch == '+' || ch == '=' || ch == '<' || ch == '>') ch = ' ';
34 }
35 std::istringstream iss(s);
36 std::string token;
37 while (iss >> token) {
38 if (token == electron) return true;
39 }
40 return false;
41}
42} // namespace
43
44// Physical constants from units.h (mirror rizer/misc/units.py) rather than
45// Cantera's CODATA-2018 values, to match the Python reactor reference.
46constexpr double GasConstant = units::R_kmol; // J/kmol/K
47constexpr double Boltzmann = units::k_b; // J/K
48
49ReactorRHS::ReactorRHS(std::shared_ptr<Cantera::Solution> sol, Config cfg)
50 : m_sol(std::move(sol))
51 , m_nsp(m_sol->thermo()->nSpecies())
52 , m_ie(m_sol->thermo()->speciesIndex("e-"))
53 , m_reacting(cfg.reacting)
54 , m_collision(std::move(cfg.collision))
55 // The legacy fallback members (m_sigma, m_nu_m_tab, m_nu_m, m_nu_E) are always
56 // built from cfg here, but transport() only reads them when m_collision.empty()
57 // is true -- i.e. only one of the two closures is ever actually exercised at
58 // evaluation time.
59 , m_sigma(PropertyTable::fromTableOrScalar(cfg.sigma_T, cfg.sigma_v))
60 , m_nu_m_tab(PropertyTable::fromTableOrScalar(cfg.nu_m_T, cfg.nu_m_v))
61 , m_nu_m(cfg.nu_m)
62 , m_nu_E(cfg.nu_E)
63{
64 if (m_ie == Cantera::npos) {
65 throw std::invalid_argument("ReactorRHS: mechanism has no 'e-' species.");
66 }
67 m_wt.assign(m_sol->thermo()->molecularWeights().begin(),
68 m_sol->thermo()->molecularWeights().end());
69 m_Yc.resize(m_nsp);
70 m_wdot.resize(m_nsp);
71 m_uk.resize(m_nsp);
72 m_cpR.resize(m_nsp);
73 m_cp_mass.resize(m_nsp);
74 m_X.resize(m_nsp);
75 m_nk.resize(m_nsp);
76 const std::size_t nR = m_sol->kinetics()->nReactions();
77 m_dU.resize(nR);
78 m_rop.resize(nR);
79 m_is_electron_rxn.assign(nR, 0);
80 // Select electron-impact reactions exactly as the Python reference does: the
81 // electron participates iff it appears in the reaction equation. This covers
82 // two-temperature-plasma, three-body-two-temperature-plasma,
83 // reverse-two-temperature-plasma, Druyvesteyn and janev-* reactions -- including
84 // those with a spectator electron that Cantera cancels from the stoichiometry.
85 const std::string e_name = m_sol->thermo()->speciesName(m_ie);
86 for (std::size_t i = 0; i < nR; i++) {
87 if (equationHasElectron(m_sol->kinetics()->reaction(i)->equation(), e_name)) {
88 m_is_electron_rxn[i] = 1;
89 }
90 }
91}
92
93std::string ReactorRHS::speciesName(std::size_t k) const
94{
95 return m_sol->thermo()->speciesName(k);
96}
97
98void ReactorRHS::setState(double Tg, double Te, const double* Y, double rho, double& P,
99 double& Xe, double& Tmean, double& n_e) const
100{
101 auto thermo = m_sol->thermo();
102 // Clip any negative mass fractions (roundoff from the ODE integrator) to zero
103 // before handing them to Cantera; setMassFractions_NoNorm keeps them
104 // un-renormalized so Y still sums to (numerically) 1 as the integrator expects.
105 for (std::size_t k = 0; k < m_nsp; k++) m_Yc[k] = (Y[k] > 0.0) ? Y[k] : 0.0;
106 const double Tg_c = std::max(Tg, 200.0);
107 const double Te_c = std::max(Te, 200.0);
108 thermo->setMassFractions_NoNorm(Cantera::span<const double>(m_Yc.data(), m_nsp));
109 thermo->setElectronTemperature(Te_c);
110 thermo->setState_TD(Tg_c, rho);
111 P = thermo->pressure();
112 Xe = thermo->moleFraction(m_ie);
113 // Two-temperature EOS mean temperature (PlasmaPhase convention),
114 // @f$P = n_\text{tot} k_B T_\text{mean}@f$, with Tmean the mole-fraction-weighted
115 // average of Tg and Te. n_e below uses Tmean (not Tg) to stay consistent with how
116 // Cantera's PlasmaPhase computes P internally.
117 Tmean = (1.0 - Xe) * Tg_c + Xe * Te_c;
118 n_e = Xe * P / (Boltzmann * Tmean);
119}
120
121void ReactorRHS::transport(double Te, double n_e, double P, double Tmean, double& sigma,
122 double& nu_E, double& cond_maxwell_2) const
123{
124 const double Te_c = std::max(Te, 200.0);
125 // Closure selection: the composition-resolved CollisionModel is preferred and used
126 // exclusively whenever it was supplied (m_collision non-empty); the legacy
127 // tabulated/scalar closure below is only reached when no CollisionModel was given.
128 // Exactly one branch executes per call -- see the Config comment in ReactorRHS.h.
129 if (!m_collision.empty()) {
130 // Composition-resolved: convert Cantera's mole fractions X_k [-] to
131 // per-species number densities n_k [1/m^3] via the ideal-gas number density
132 // @f$n_\text{tot} = P/(k_B T_\text{mean})@f$, since CollisionModel::evaluate()
133 // needs absolute densities, not fractions.
134 const double n_tot = P / (Boltzmann * Tmean);
135 m_sol->thermo()->getMoleFractions(Cantera::span<double>(m_X.data(), m_nsp));
136 for (std::size_t k = 0; k < m_nsp; k++) m_nk[k] = m_X[k] * n_tot;
137 m_collision.evaluate(Te_c, n_e, m_nk.data(), m_nsp, sigma, nu_E, cond_maxwell_2);
138 } else {
139 // Legacy closure: prefer nu_m(Te) (table or scalar) and derive sigma from the
140 // Drude relation @f$\sigma = n_e e^2 / (m_e \nu_m)@f$; if no nu_m is available
141 // at all (nu_m_eff <= 0), fall back further to a directly tabulated sigma(Te).
142 // nu_E is always the fixed scalar in this legacy path (no Te-dependent table
143 // for nu_E). cond_maxwell_2 needs per-species composition, unavailable here.
144 const double nu_m_eff = m_nu_m_tab.empty() ? m_nu_m : m_nu_m_tab.eval(Te_c);
145 sigma = (nu_m_eff > 0.0)
146 ? n_e * units::e * units::e / (units::m_e * nu_m_eff)
147 : m_sigma.eval(Te_c);
148 nu_E = m_nu_E;
149 cond_maxwell_2 = 0.0;
150 }
151}
152
153void ReactorRHS::rates(double Tg, double Te, const double* Y, double rho, double E,
154 double& dTg, double& dTe, double* dY, double& lhs_Tg,
155 double& lhs_Te, Diagnostics* diag) const
156{
157 auto thermo = m_sol->thermo();
158 auto kin = m_sol->kinetics();
159
160 double P, Xe, Tmean, n_e;
161 setState(Tg, Te, Y, rho, P, Xe, Tmean, n_e);
162 const double Tg_c = std::max(Tg, 200.0);
163 const double Te_c = std::max(Te, 200.0);
164
165 thermo->getCp_R(m_cpR);
166 const double We = m_wt[m_ie];
167 const double cve = 1.5 * GasConstant / We;
168
169 // Mass-basis species heat capacity c_p,k = (Cp_k/R)*R/W_k [J/kg/K],
170 // exposed via speciesCp() for PlasmaChannel1D's enthalpy-diffusion
171 // energy-equation term. The electron slot instead mirrors the analytic
172 // monatomic-ideal-gas value c_p,e = cv_e + R/W_e (derived from cve just
173 // above, not from getCp_R()), for consistency with how cve/lhs_Te
174 // already bypass Cantera's own electron Cp elsewhere in this function.
175 for (std::size_t k = 0; k < m_nsp; k++) {
176 m_cp_mass[k] = m_cpR[k] * GasConstant / m_wt[k];
177 }
178 m_cp_mass[m_ie] = cve + GasConstant / We;
179
180 // Gas-side heat capacity at constant volume, @f$c_v/R = c_p/R - 1@f$ per species
181 // (ideal gas), mass-weighted over the heavy species only (electron excluded -- it
182 // has its own energy equation with capacitance lhs_Te below) and converted to a
183 // per-volume capacitance via rho: cv_heavy has units J/(kg K) before the rho
184 // multiply.
185 double cv_heavy = 0.0;
186 for (std::size_t k = 0; k < m_nsp; k++) {
187 if (k == m_ie) continue;
188 cv_heavy += m_Yc[k] * (m_cpR[k] - 1.0) * GasConstant / m_wt[k];
189 }
190 // Electron-energy capacitance @f$Y_e \rho c_{v,e}@f$; Y_e is floored away from
191 // zero so lhs_Te stays finite/positive even when the electron mass fraction
192 // underflows to ~0.
193 const double Ye_cap = std::max(m_Yc[m_ie], 1.0e-12);
194 lhs_Te = Ye_cap * cve * rho;
195 lhs_Tg = std::max(cv_heavy * rho, 1.0e-3);
196
197 double sigma, nu_E, cond_maxwell_2;
198 transport(Te_c, n_e, P, Tmean, sigma, nu_E, cond_maxwell_2);
199
200 // Elastic electron-heavy energy exchange (classic elastic-collision power
201 // transfer rate from electrons to the heavy gas, positive when Te > Tg):
202 // @f[
203 // P_\text{el} = \nu_E n_e \frac{3}{2} k_B (T_e - T_g)
204 // @f]
205 // It appears with a minus sign in the electron balance and a plus sign in the
206 // gas balance below (energy conserved between the two) [W/m^3].
207 const double P_el = nu_E * n_e * 1.5 * Boltzmann * (Te_c - Tg_c);
208 // Joule heating, @f$P_\text{joule} = \sigma E^2@f$ [W/m^3]: the resistive power
209 // deposited into the electron population by the electric field; it only enters
210 // the electron-energy balance (dTe), never the gas balance (dTg) directly.
211 const double P_joule = sigma * E * E;
212
213 double P_chem = 0.0, P_chem_e = 0.0, P_inel = 0.0;
214 for (std::size_t k = 0; k < m_nsp; k++) dY[k] = 0.0;
215 if (m_reacting) {
216 kin->getNetProductionRates(m_wdot);
217 kin->getNetRatesOfProgress(m_rop);
218 thermo->getPartialMolarIntEnergies(m_uk);
219 // Internal-energy change per reaction (constant-volume reactor), matching
220 // the Python reference: @f$dU_i = \sum_k \nu_{ki} u_k@f$ =
221 // getReactionDelta(u). Using dU (not dH) keeps the R*T_k pressure-work of
222 // created particles out of the electron->gas inelastic exchange (see
223 // plasma_power_inelastic).
224 kin->getReactionDelta(Cantera::span<const double>(m_uk.data(), m_nsp),
225 Cantera::span<double>(m_dU.data(), m_dU.size()));
226 for (std::size_t k = 0; k < m_nsp; k++) {
227 // m_wdot[k] is the net production rate in [kmol/m^3/s] (Cantera
228 // convention); multiplying by the molecular weight [kg/kmol] and
229 // dividing by rho [kg/m^3] converts it to the mass-fraction time
230 // derivative @f$dY_k = \dot\omega_k W_k / \rho@f$ [1/s].
231 dY[k] = m_wdot[k] * m_wt[k] / rho;
232 if (k != m_ie) P_chem += m_uk[k] * m_wdot[k];
233 }
234 P_chem_e = m_wdot[m_ie] * cve * We * Te_c;
235 for (std::size_t i = 0; i < m_rop.size(); i++) {
236 if (m_is_electron_rxn[i]) P_inel += m_dU[i] * m_rop[i];
237 }
238 }
239
240 dTe = (-P_chem_e - P_el - P_inel + P_joule) / lhs_Te;
241 dTg = (-P_chem + P_el + P_inel) / lhs_Tg;
242
243 if (!std::isfinite(dTe)) dTe = 0.0;
244 if (!std::isfinite(dTg)) dTg = 0.0;
245 for (std::size_t k = 0; k < m_nsp; k++) {
246 if (!std::isfinite(dY[k])) dY[k] = 0.0;
247 }
248
249 if (diag != nullptr) {
250 diag->P_elastic = P_el;
251 diag->P_inelastic = P_inel;
252 diag->P_chemical = P_chem;
253 diag->P_chemical_e = P_chem_e;
254 diag->P_Joule = P_joule;
255 // Maxwellian-distribution conditions (Mitchner VIII-3.8): C1 = nu_ee/m_e,
256 // C2 = sum_h nu_eh/m_h (cond_maxwell_2 above, from the same transport()
257 // call that produced sigma/nu_E for this state).
258 const double nu_ee = CollisionModel::electronElectronCollisionFrequency(n_e, Te_c);
259 diag->cond_maxwell_1 = nu_ee / units::m_e;
260 diag->cond_maxwell_2 = cond_maxwell_2;
261 }
262}
263
264double ReactorRHS::conductivity(double Tg, double Te, const double* Y, double rho) const
265{
266 double P, Xe, Tmean, n_e;
267 setState(Tg, Te, Y, rho, P, Xe, Tmean, n_e);
268 double sigma, nu_E, cond_maxwell_2;
269 transport(std::max(Te, 200.0), n_e, P, Tmean, sigma, nu_E, cond_maxwell_2);
270 return sigma;
271}
272
273double ReactorRHS::electronDensity(double Tg, double Te, const double* Y,
274 double rho) const
275{
276 double P, Xe, Tmean, n_e;
277 setState(Tg, Te, Y, rho, P, Xe, Tmean, n_e);
278 return n_e;
279}
280
281} // 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...
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.
double conductivity(double Tg, double Te, const double *Y, double rho) const
Electrical conductivity [S/m] at a node state.
std::string speciesName(std::size_t k) const
Name of species k in the mechanism.
ReactorRHS(std::shared_ptr< Cantera::Solution > sol, Config cfg)
Construct a ReactorRHS for a given plasma Solution and configuration.
double electronDensity(double Tg, double Te, const double *Y, double rho) const
Electron number density [1/m^3], .
void rates(double Tg, double Te, const double *Y, double rho, double E, double &dTg, double &dTe, double *dY, double &lhs_Tg, double &lhs_Te, Diagnostics *diag=nullptr) const
Single-cell RHS (no transport, no volume work).
std::size_t nSpecies() const
Definition ReactorRHS.h:92
const double Boltzmann
const size_t npos
constexpr double k_b
Boltzmann constant [J/K]. Exact (CODATA).
Definition units.h:28
constexpr double R_kmol
Ideal gas constant [J/(kmol K)] (matches Cantera's GasConstant).
Definition units.h:60
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
constexpr double Boltzmann
constexpr double GasConstant
Optional power / Maxwellian-condition breakdown filled by rates() when a non-null pointer is passed.
Definition ReactorRHS.h:43
double cond_maxwell_1
nu_ee/m_e [1/kg/s] (Mitchner VIII-3.8, term 1)
Definition ReactorRHS.h:49
double P_Joule
Joule heating power [W/m^3].
Definition ReactorRHS.h:48
double P_chemical_e
Electron chemical power [W/m^3].
Definition ReactorRHS.h:47
double P_chemical
Heavy-species chemical power [W/m^3].
Definition ReactorRHS.h:46
double P_inelastic
Inelastic electron-impact-reaction power [W/m^3].
Definition ReactorRHS.h:45
double P_elastic
Elastic electron-heavy exchange power [W/m^3].
Definition ReactorRHS.h:44
double cond_maxwell_2
sum_h nu_eh/m_h [1/kg/s] (Mitchner VIII-3.8, term 2)
Definition ReactorRHS.h:50
Physical constants and unit conversions, in SI by default.