rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
PlasmaChannel1D.cpp
Go to the documentation of this file.
1#include "PlasmaChannel1D.h"
4#include "units.h"
5
10
11#include <algorithm>
12#include <cmath>
13#include <stdexcept>
14
15namespace rizer {
16
17// Ideal gas constant on the kmol basis (matches Cantera's molecularWeights(),
18// which are in kg/kmol), used by the species-diffusion compressibility term
19// below. Mirrors the same constant/convention in ReactorRHS.cpp.
20constexpr double GasConstant = units::R_kmol; // J/kmol/K
21
22namespace {
23// Register the custom plasma rates (janev / reverse-two-temperature-plasma)
24// before loading the mechanism, so YAML files that use them load in C++.
25// Idempotent — safe to call on every construction.
29loadMechanism(const std::string& mech, const std::string& phase)
30{
32 return Cantera::newSolution(mech, phase);
33}
34} // namespace
35
37 : Cantera::Domain1D(/*nv placeholder*/1, cfg.n_points)
38 , m_solution(loadMechanism(cfg.mech, cfg.phase))
39 , m_n_species(m_solution->thermo()->nSpecies())
40 , m_electron_index(m_solution->thermo()->speciesIndex("e-"))
41 , m_outer_radius(cfg.R_max)
42 , m_rho(cfg.rho)
43 , m_electric_field(cfg.electric_field)
44 , m_species_diffusivity(cfg.D_species)
45 , m_reacting(cfg.reacting)
46 , m_ambient_temperature(cfg.T_amb)
47 , m_Y0(cfg.Y0)
48 , m_kappa(PropertyTable::fromTableOrScalar(cfg.kappa_T, cfg.kappa_v))
49 , m_kappa_e(PropertyTable::fromTableOrScalar(cfg.kappa_e_T, cfg.kappa_e_v))
50 , m_initTg(PropertyTable::fromTableOrScalar(cfg.initTg_r, cfg.initTg_v))
51 , m_initTe(PropertyTable::fromTableOrScalar(cfg.initTe_r, cfg.initTe_v))
52{
53 // An explicit grid (if given) defines the mesh, n_points and R_max.
54 const bool explicit_grid = cfg.grid.size() >= 2;
55 const std::size_t n_points = explicit_grid ? cfg.grid.size() : cfg.n_points;
56 if (explicit_grid) {
57 m_outer_radius = cfg.grid.back();
58 } else if (cfg.R_max <= 0.0 || cfg.n_points < 1) {
59 throw std::invalid_argument("PlasmaChannel1D: need R_max>0, n_points>=1 "
60 "(or supply an explicit grid of >=2 points).");
61 }
62 if (m_electron_index == Cantera::npos) {
63 throw std::invalid_argument("PlasmaChannel1D: mechanism has no 'e-' species.");
64 }
65 if (m_Y0.size() != m_n_species) {
66 throw std::invalid_argument("PlasmaChannel1D: Y0 length != nSpecies.");
67 }
68 // A composition-resolved collision model (cfg.specs, built from Python's
69 // `mtcf` list of MomentumTransferCollisionFrequencyModel) and the legacy
70 // sigma/nu_m/nu_E closure are two different ways to get sigma/nu_E into
71 // the SAME ReactorRHS::Config.collision slot; mixing them would silently
72 // pick one (whichever ReactorRHS::transport() checks first) and ignore
73 // the other's inputs, so reject the combination outright instead of
74 // guessing which one the caller meant.
75 // WHY this matters physically (see ARCHITECTURE.md "Composition-resolved
76 // sigma/nu_E"): sigma is far more sensitive to the *ionization state* than
77 // to bulk composition. In a fast NRP-style discharge, electron-impact
78 // ionization can run orders of magnitude ahead of the channel's initial
79 // (often near-neutral) seed composition within a couple of nanoseconds.
80 // cfg.specs recomputes sigma/nu_E live from the *evolving* composition
81 // every eval() call, tracking that; the legacy sigma(Te)/nu_m(Te) tables
82 // are frozen at whatever composition/shape they were tabulated for, and
83 // can only re-scale with Te, not with the actual evolving n_e/ionization
84 // fraction. Silently combining the two would recombine a frozen sigma(Te)
85 // *shape* with the channel's own independently-evolving n_e(t), which is
86 // exactly the ~10% error the composition-resolved model was added to fix.
87 const bool have_collision = !cfg.specs.empty();
88 const bool have_legacy_closure = !cfg.sigma_T.empty() || !cfg.sigma_v.empty()
89 || !cfg.nu_m_T.empty() || !cfg.nu_m_v.empty()
90 || cfg.nu_m > 0.0 || cfg.nu_E != 0.0;
91 if (have_collision && have_legacy_closure) {
92 throw std::invalid_argument(
93 "PlasmaChannel1D: cfg.specs (composition-resolved collision model) "
94 "cannot be combined with sigma_T/sigma_v, nu_m, nu_m_T/nu_m_v, or "
95 "nu_E (the legacy closure) -- pick one.");
96 }
97 // Single-cell 2T physics (chemistry/Joule/exchange) shared with the 0D reactor.
98 // The field E is supplied per eval via setElectricField().
99 {
100 ReactorRHS::Config reactor_config;
101 reactor_config.reacting = cfg.reacting;
102 reactor_config.species_h0k = cfg.species_h0k;
103 if (have_collision) {
104 // Same pattern as Plasma0DReactor::Plasma0DReactor: molar_mass and
105 // is_electron are derived from the mechanism, not the caller, since
106 // they must exactly match this domain's own species indexing.
107 std::vector<CollisionModel::SpeciesSpec> specs = cfg.specs;
108 specs.resize(m_n_species);
109 const auto& molecular_weights = m_solution->thermo()->molecularWeights();
110 for (std::size_t k = 0; k < m_n_species; k++) {
111 // kg/kmol -> kg/mol
112 specs[k].molar_mass = molecular_weights[k] * 1.0e-3;
113 specs[k].is_electron = (k == m_electron_index);
114 }
115 reactor_config.collision =
116 CollisionModel(specs, cfg.Te_min, cfg.Te_max, cfg.Te_n, cfg.spitzer);
117 } else {
118 reactor_config.sigma_T = cfg.sigma_T; reactor_config.sigma_v = cfg.sigma_v;
119 reactor_config.nu_m_T = cfg.nu_m_T; reactor_config.nu_m_v = cfg.nu_m_v;
120 reactor_config.nu_m = cfg.nu_m; reactor_config.nu_E = cfg.nu_E;
121 }
122 m_reactor_rhs =
123 std::make_unique<ReactorRHS>(m_solution, std::move(reactor_config));
124 }
125 // Initial radial composition profile: one Tg-style table per species.
126 if (!cfg.initY_r.empty() && cfg.initY_r.size() >= 2
127 && cfg.initY_v.size() == cfg.initY_r.size() * m_n_species) {
128 const std::size_t n_radii = cfg.initY_r.size();
129 m_initY.reserve(m_n_species);
130 for (std::size_t k = 0; k < m_n_species; k++) {
131 std::vector<double> column(n_radii);
132 for (std::size_t i = 0; i < n_radii; i++) {
133 column[i] = cfg.initY_v[i * m_n_species + k];
134 }
135 m_initY.push_back(PropertyTable::fromTableOrScalar(cfg.initY_r, column));
136 }
137 }
138 m_has_transport =
139 !m_kappa.empty() || !m_kappa_e.empty() || m_species_diffusivity > 0.0;
140
141 Cantera::Domain1D::resize(2 + m_n_species, n_points);
142 if (explicit_grid) {
143 setupGrid(Cantera::span<const double>(cfg.grid.data(), cfg.grid.size()));
144 } else if (cfg.n_points >= 2) {
145 setupUniformGrid(cfg.n_points, cfg.R_max, 0.0);
146 } // n_points==1: keep the single node at r=0 (resize left m_z={0});
147 // setupUniformGrid would divide by (points-1)=0.
148
149 setComponentName(0, "Tg");
150 setComponentName(1, "Te");
151 for (std::size_t k = 0; k < m_n_species; k++) {
152 setComponentName(2 + k, m_solution->thermo()->speciesName(k));
153 }
154 // Newton step-limiting bounds, not a hard clamp (resetBadValues() is the
155 // clamp): 200 K keeps both temperatures comfortably above 0 K/the thermo
156 // fit floor without constraining genuine cooling; 1e6 K is a generous
157 // ceiling no physical run should approach. Species bounds allow a small
158 // negative undershoot (-1e-12) since Newton trial states can dip slightly
159 // below zero for a trace species without being "bad" -- resetBadValues()
160 // clips to [0,1] only after repeated Newton failures, not on every step.
161 setBounds(0, 200.0, 1.0e6);
162 setBounds(1, 200.0, 1.0e6);
163 for (std::size_t k = 0; k < m_n_species; k++) {
164 setBounds(2 + k, -1.0e-12, 2.0);
165 }
166 // Transient absolute tolerance is 100x tighter than steady (1e-12 vs
167 // 1e-10), the same ratio ThermalPlasmaColumn1D uses for its own T
168 // component -- a deliberate, consistent choice across this codebase's
169 // Domain1D subclasses, not an arbitrary pair of numbers.
170 setSteadyTolerances(1.0e-4, 1.0e-10);
171 setTransientTolerances(1.0e-4, 1.0e-12);
172}
173
174// Map a Domain1D component index to its name ("Tg", "Te", or a species name),
175// the inverse of componentIndex(). Required by Cantera's Domain1D interface
176// (used e.g. for solution output, error messages, and by our own callers in
177// TransientPlasmaChannelSolver.cpp to enumerate/report components by name).
178std::string PlasmaChannel1D::componentName(std::size_t n) const
179{
180 if (n == 0) return "Tg";
181 if (n == 1) return "Te";
182 return m_solution->thermo()->speciesName(n - 2);
183}
184
185// Map a component name back to its Domain1D index (inverse of componentName()).
186// The `checkAlias` argument from the Domain1D base signature is unused: this
187// domain has no aliases, only the canonical "Tg"/"Te"/species names.
188std::size_t PlasmaChannel1D::componentIndex(const std::string& name, bool) const
189{
190 if (name == "Tg") return 0;
191 if (name == "Te") return 1;
192 std::size_t k = m_solution->thermo()->speciesIndex(name);
193 if (k != Cantera::npos) return 2 + k;
194 throw std::invalid_argument("PlasmaChannel1D: no component '" + name + "'.");
195}
196
197bool PlasmaChannel1D::hasComponent(const std::string& name, bool) const
198{
199 return name == "Tg" || name == "Te"
200 || m_solution->thermo()->speciesIndex(name) != Cantera::npos;
201}
202
203// Read one component's radial profile out of the domain's own solution buffer
204// (m_state, populated by Cantera after getInitialSoln()/a solve). Used by
205// TransientPlasmaChannelSolver::solveChannelTransient to seed its flat state
206// vector `x` from the freshly-initialized profile (see the comment there on
207// why getState() can't be used for that instead).
208void PlasmaChannel1D::getValues(const std::string& component,
210{
211 if (!m_state) {
212 throw std::runtime_error("PlasmaChannel1D::getValues: not installed.");
213 }
214 std::size_t comp = componentIndex(component);
215 const double* soln = m_state->data() + loc();
216 for (std::size_t j = 0; j < m_points; j++) {
217 values[j] = soln[index(comp, j)];
218 }
219}
220
221// Cantera calls this once per (component, node) to seed the initial guess
222// before the first solve. Falls back to a uniform value (m_ambient_temperature
223// or the ambient mass fraction m_Y0[k]) wherever no explicit initial-profile
224// table was supplied, so a channel with no Tg_profile/Te_profile/Y0_profile
225// simply starts uniform.
226double PlasmaChannel1D::initialValue(std::size_t n, std::size_t j)
227{
228 double r = z(j);
229 if (n == 0) return m_initTg.empty() ? m_ambient_temperature : m_initTg.eval(r);
230 if (n == 1) return m_initTe.empty() ? m_ambient_temperature : m_initTe.eval(r);
231 std::size_t k = n - 2;
232 return m_initY.empty() ? m_Y0[k] : m_initY[k].eval(r);
233}
234
235// Clamp a trial state back onto the physically valid manifold after a failed
236// Newton/BDF step produces something the thermo layer can't evaluate: floor
237// both temperatures at 200 K (Cantera's thermo/kinetics routines are not
238// guaranteed valid, and can throw, below their fit range or at/below 0 K) and
239// clip every mass fraction into [0, 1] (a valid composition; Newton overshoots
240// can otherwise drive Y_k slightly negative or above 1). This does not
241// conserve mass Sum(Y_k)=1 -- it is a last-resort rescue to get an evaluable
242// state for the next retry, not a physical correction.
244{
245 auto x = xg.subspan(loc(), size());
246 for (std::size_t j = 0; j < m_points; j++) {
247 x[index(0, j)] = std::max(x[index(0, j)], 200.0);
248 x[index(1, j)] = std::max(x[index(1, j)], 200.0);
249 for (std::size_t k = 0; k < m_n_species; k++) {
250 x[index(2 + k, j)] = std::min(std::max(x[index(2 + k, j)], 0.0), 1.0);
251 }
252 }
253}
254
255// Thin pass-through to the shared 0D physics (ReactorRHS), so every caller
256// (eval()'s residual, and TransientPlasmaChannelSolver's history recording)
257// computes n_e the same way the reaction source terms themselves see it.
258double PlasmaChannel1D::electronDensity(double Tg, double Te, const double* Y) const
259{
260 return m_reactor_rhs->electronDensity(Tg, Te, Y, m_rho);
261}
262
263// Cantera's Jacobian coloring calls eval() once per perturbed global point
264// `jg` (finite-difference Jacobian: only that point's residual needs
265// recomputing) plus once with jg==npos (a full residual evaluation, e.g. for
266// the initial guess or a converged-solution check). This domain's stencil
267// couples each node only to its immediate east/west neighbours (see fvDiv
268// below), so perturbing point `jg` can only change the residual at jg-1, jg,
269// and jg+1 -- hence the narrow [jmin, jmax] window computed below instead of
270// recomputing every node on every perturbation.
273 double rdt)
274{
275 // Points outside this domain's own range (jg is a *global* index across
276 // all domains in the Sim1D) can't affect our residual at all; skip early.
277 if (jg != Cantera::npos
278 && (jg + 1 < firstPoint() || jg > lastPoint() + 1)) {
279 return;
280 }
281 auto x = xg.subspan(loc(), size());
282 auto rsd = rg.subspan(loc(), size());
283 auto diag = maskg.subspan(loc(), size());
284
285 std::size_t jmin, jmax;
286 if (jg == Cantera::npos) {
287 // Full evaluation: every node's residual needs (re)computing.
288 jmin = 0;
289 jmax = m_points - 1;
290 } else {
291 // Narrow window: only the perturbed point and its two neighbours.
292 std::size_t local_point = (jg == 0) ? 0 : jg - firstPoint();
293 jmin = std::max<std::size_t>(local_point, 1) - 1;
294 jmax = std::min(local_point + 1, m_points - 1);
295 }
296
297 // Finite-volume cylindrical divergence (1/r) d/dr(r * coef * dphi/dr) at
298 // node `node` for component `component`, face coefficient from coef(phi).
299 // The axis face (node==0) has zero area (symmetry). The outer wall is
300 // imposed as a Dirichlet ghost node held at `ghost`, a half-interval
301 // beyond the last node -- this keeps every node transient (well scaled),
302 // avoiding an ill-conditioned algebraic boundary row.
303 // Units below: radius/spacing/volume are always [m]/[m]/[m^2] (geometry is
304 // component-independent); `value_*`/`coef_*`/`flux_*`/the return value take
305 // whatever unit `component` has -- [K] and [W/m/K] for the Tg/Te conduction
306 // calls, [-] and [kg/m/s] for the species-diffusion call below (see the two
307 // call sites: coef is kappa/kappa_e or rho*D respectively).
308 // Lambda parameters (a plain, non-Doxygen comment: this lambda has no
309 // separate declaration for \param to attach to, so a doc-comment here
310 // would be misattached to the enclosing eval()):
311 // component Component index being diffused: 0=Tg, 1=Te, or 2+k for
312 // species k [-]
313 // node Radial grid node index [-]
314 // ghost Dirichlet ghost value beyond the wall, in `component`'s
315 // unit ([K] for Tg/Te, [-] for Y_k)
316 // coef Face coefficient functor value->coef(value): kappa/kappa_e
317 // [W/m/K] for Tg/Te, or rho*D [kg/m/s] for species
318 auto fvDiv = [&](std::size_t component, std::size_t node, double ghost,
319 auto coef) -> double {
320 double value_here = x[index(component, node)];
321 double radius_east, spacing_east, value_east; // [m], [m], component's unit
322 if (node < m_points - 1) {
323 value_east = x[index(component, node + 1)];
324 radius_east = 0.5 * (z(node) + z(node + 1));
325 spacing_east = z(node + 1) - z(node);
326 } else { // last node: ghost at r = z + spacing_west, value = ambient
327 // [m]
328 double last_spacing =
329 (m_points >= 2) ? z(node) - z(node - 1) : m_outer_radius;
330 value_east = ghost;
331 radius_east = z(node) + 0.5 * last_spacing;
332 spacing_east = last_spacing;
333 }
334 double coef_east = 0.5 * (coef(value_here) + coef(value_east));
335 double flux_east = numeric::faceFlux(radius_east, coef_east, value_here,
336 value_east, spacing_east);
337 double flux_west = 0.0, radius_west = 0.0; // [m]
338 if (node > 0) {
339 double value_west = x[index(component, node - 1)];
340 radius_west = 0.5 * (z(node - 1) + z(node));
341 double spacing_west = z(node) - z(node - 1); // [m]
342 double coef_west = 0.5 * (coef(value_west) + coef(value_here));
343 flux_west = numeric::faceFlux(radius_west, coef_west, value_west,
344 value_here, spacing_west);
345 }
346 // [m^2]
347 double volume = numeric::annularVolume(radius_east, radius_west);
348 return (flux_east - flux_west) / volume;
349 };
350
351 // Node-centered radial gradient d(phi)/dr at `node` for `component`,
352 // used by the species-diffusion enthalpy-transport term below (a mixed-
353 // gradient PRODUCT term, not a flux divergence, so fvDiv() itself does
354 // not apply here). Obtained as the arithmetic mean of the same one-sided
355 // east/west face differences fvDiv() already forms internally, so the
356 // gradient used here stays consistent with the divergence stencil
357 // discretizing the rest of the transport terms. At the axis (node==0)
358 // this returns 0 exactly -- symmetry makes every component an even
359 // function of r there, so its radial derivative vanishes -- rather than
360 // a one-sided difference extrapolated across the (zero-area) axis face.
361 // At the wall (node==m_points-1) the east-side difference uses the same
362 // Dirichlet ghost value `ghost` a half-cell beyond the last node that
363 // fvDiv()'s own wall treatment uses.
364 // component Component index: 0=Tg, 1=Te, or 2+k for species k [-]
365 // node Radial grid node index [-]
366 // ghost Dirichlet ghost value beyond the wall, in `component`'s
367 // unit ([K] for Tg/Te, [-] for Y_k)
368 auto centeredGradient = [&](std::size_t component, std::size_t node,
369 double ghost) -> double {
370 if (node == 0) return 0.0; // axis symmetry: d(phi)/dr(r=0) = 0
371 double value_here = x[index(component, node)];
372 double value_west = x[index(component, node - 1)];
373 double spacing_west = z(node) - z(node - 1); // [m]
374 double gradient_west = (value_here - value_west) / spacing_west;
375
376 double gradient_east;
377 if (node < m_points - 1) {
378 double value_east = x[index(component, node + 1)];
379 double spacing_east = z(node + 1) - z(node); // [m]
380 gradient_east = (value_east - value_here) / spacing_east;
381 } else { // wall: ghost a half-cell beyond the last node
382 double last_spacing =
383 (m_points >= 2) ? z(node) - z(node - 1) : m_outer_radius;
384 gradient_east = (ghost - value_here) / last_spacing;
385 }
386 return 0.5 * (gradient_west + gradient_east);
387 };
388
389 // Molecular weights W_k [kg/kmol], node-independent; fetched once and
390 // reused every node by the species-diffusion compressibility term below.
391 const std::vector<double>& molecular_weights = m_reactor_rhs->molecularWeights();
392
393 std::vector<double> dYdt(m_n_species); // dY_k/dt [1/s], one entry per species
394 std::vector<double> raw_Y_divergence(m_n_species); // see the species-diffusion
395 // block below [1/m^2]
396 for (std::size_t j = jmin; j <= jmax; j++) {
397 double Tg = x[index(0, j)]; // gas temperature [K]
398 double Te = x[index(1, j)]; // electron temperature [K]
399 const double* Y = &x[index(2, j)]; // species mass fractions [-]
400
401 // lhsTg/lhsTe are the effective heat capacities per unit volume
402 // [J/m^3/K] that ReactorRHS::rates already divided the chemistry/
403 // Joule/exchange source terms by, so dTg/dTe below are already true
404 // dT/dt [K/s] for those terms. The conduction term computed via
405 // fvDiv() is a bare flux divergence, not yet divided by capacitance,
406 // so it must be divided by the SAME lhsTg/lhsTe here to add
407 // consistently onto dTg/dTe (see the class-level header comment for
408 // the governing PDEs, rho*cv_h*dTg/dt = div(...) + ...).
409 // [K/s], [K/s], [J/m^3/K], [J/m^3/K]
410 double dTg = 0.0, dTe = 0.0, lhsTg = 1.0, lhsTe = 1.0;
411 std::fill(dYdt.begin(), dYdt.end(), 0.0);
412 m_reactor_rhs->rates(Tg, Te, Y, m_rho, m_electric_field, dTg, dTe,
413 dYdt.data(), lhsTg, lhsTe);
414
415 if (m_has_transport) {
416 if (!m_kappa.empty()) {
417 dTg += fvDiv(0, j, m_ambient_temperature,
418 [&](double T) { return m_kappa.eval(T); }) / lhsTg;
419 }
420 if (!m_kappa_e.empty()) {
421 dTe += fvDiv(1, j, m_ambient_temperature,
422 [&](double T) { return m_kappa_e.eval(T); }) / lhsTe;
423 }
424 if (m_species_diffusivity > 0.0) {
425 // Raw (unit-coefficient) divergence div(grad(Y_k)) =
426 // (1/r) d/dr(r dY_k/dr) at this node, for every species k --
427 // computed once per species and reused below for the
428 // ordinary Fickian diffusion term (Term A) AND the species-
429 // diffusion compressibility term (Term B). Term A's actual
430 // face coefficient rho*D is a spatially uniform constant
431 // (does not depend on Y_k), so fvDiv(coef=rho*D) equals
432 // rho*D*raw_Y_divergence[k] exactly -- no need to call
433 // fvDiv() twice per species with two different constant
434 // coefficients.
435 for (std::size_t k = 0; k < m_n_species; k++) {
436 raw_Y_divergence[k] =
437 fvDiv(2 + k, j, m_Y0[k], [](double) { return 1.0; });
438 }
439
440 // --- Term A: ordinary Fickian species diffusion,
441 // div(rho*D*grad(Y_k)), converted to dY_k/dt by /rho ==
442 // D*raw_Y_divergence[k] (see above; rho assumed spatially
443 // uniform -- this model has no continuity equation).
444 // Matches the mass-fraction species equation in the
445 // class-level header comment.
446 for (std::size_t k = 0; k < m_n_species; k++) {
447 dYdt[k] += m_species_diffusivity * raw_Y_divergence[k];
448 }
449
450 // --- Term B: species-diffusion compressibility. Expanding
451 // DP/Dt for the two-temperature ideal-gas EOS under Fick's
452 // law contributes an extra energy-equation source (see
453 // ARCHITECTURE.md, "species-diffusion compressibility"):
454 // electron: +rho*D*(R*Te/We) * (1/r) d/dr(r dYe/dr)
455 // gas: +rho*D*R*Tg * sum_{k!=e} (1/Wk) * (1/r) d/dr(r dYk/dr)
456 // (summed over heavy species only -- they share one Tg).
457 const double We = molecular_weights[m_electron_index];
458 dTe += m_rho * m_species_diffusivity * (GasConstant * Te / We)
459 * raw_Y_divergence[m_electron_index] / lhsTe;
460
461 double gas_compressibility_sum = 0.0;
462 for (std::size_t k = 0; k < m_n_species; k++) {
463 if (k == m_electron_index) continue;
464 gas_compressibility_sum +=
465 raw_Y_divergence[k] / molecular_weights[k];
466 }
467 dTg += m_rho * m_species_diffusivity * GasConstant * Tg
468 * gas_compressibility_sum / lhsTg;
469
470 // --- Term C: enthalpy diffusion (species heat transport).
471 // The full multicomponent energy equation's heat flux
472 // includes +sum_k h_k*j_k (species carry their own sensible
473 // enthalpy as they diffuse); with Fick's law j_k=-rho*D*
474 // grad(Y_k), its divergence -div(sum_k h_k*j_k) reduces to a
475 // mixed-gradient PRODUCT term rather than a divergence (see
476 // ARCHITECTURE.md, "enthalpy diffusion"):
477 // electron: +rho*D*cp_e * (dYe/dr)(dTe/dr)
478 // gas: +rho*D*(dTg/dr) * sum_{k!=e} cp_k * (dYk/dr)
479 const std::vector<double>& species_heat_capacity =
480 m_reactor_rhs->speciesCp();
481 const double Tg_gradient =
482 centeredGradient(0, j, m_ambient_temperature);
483 const double Te_gradient =
484 centeredGradient(1, j, m_ambient_temperature);
485 const double Ye_gradient = centeredGradient(
486 2 + m_electron_index, j, m_Y0[m_electron_index]);
487 dTe += m_rho * m_species_diffusivity
488 * species_heat_capacity[m_electron_index] * Ye_gradient
489 * Te_gradient / lhsTe;
490
491 double gas_enthalpy_diffusion_sum = 0.0;
492 for (std::size_t k = 0; k < m_n_species; k++) {
493 if (k == m_electron_index) continue;
494 double Yk_gradient = centeredGradient(2 + k, j, m_Y0[k]);
495 gas_enthalpy_diffusion_sum +=
496 species_heat_capacity[k] * Yk_gradient;
497 }
498 dTg += m_rho * m_species_diffusivity * Tg_gradient
499 * gas_enthalpy_diffusion_sum / lhsTg;
500 }
501 }
502
503 // Steady residual = source+transport rate; the -rdt*(phi-phi_prev) term
504 // is Cantera's pseudo-transient damping (rdt = 1/dt during a time step,
505 // 0 during a steady solve or when computing a bare method-of-lines RHS
506 // -- see ChannelRHS::eval in TransientPlasmaChannelSolver.cpp). diag=1
507 // marks every component here as a true (non-algebraic) unknown, so
508 // Cantera includes the transient term for all of them; there is no
509 // Dirichlet/algebraic row in this domain (the wall is a ghost-cell
510 // flux, not a fixed unknown).
511 rsd[index(0, j)] = dTg - rdt * (Tg - prevSoln(0, j));
512 rsd[index(1, j)] = dTe - rdt * (Te - prevSoln(1, j));
513 diag[index(0, j)] = 1;
514 diag[index(1, j)] = 1;
515 for (std::size_t k = 0; k < m_n_species; k++) {
516 rsd[index(2 + k, j)] = dYdt[k] - rdt * (Y[k] - prevSoln(2 + k, j));
517 diag[index(2 + k, j)] = 1;
518 }
519 }
520}
521
522} // namespace rizer
Pure-arithmetic primitives for a 1D cylindrical (radial) finite-volume discretization of the operator...
size_t lastPoint() const
size_t size() const
shared_ptr< Solution > phase() const
shared_ptr< vector< double > > m_state
vector< double > values(const string &component) const
virtual void resize(size_t nv, size_t np)
double z(size_t jlocal) const
double prevSoln(size_t n, size_t j) const
size_t firstPoint() const
Domain1D(size_t nv=1, size_t points=1, double time=0.0)
size_t index(size_t n, size_t j) const
virtual size_t loc(size_t j=0) const
double initialValue(std::size_t n, std::size_t j) override
Initial value of component n at grid point j ([K] for Tg/Te, [-] for a species mass fraction); seeds ...
void eval(std::size_t jg, Cantera::span< const double > xg, Cantera::span< double > rg, Cantera::span< int > maskg, double rdt) override
Residual + transient-mask assembly at global point jg (or every point if jg == Cantera::npos); see th...
void getValues(const std::string &component, Cantera::span< double > values) const override
Radial profile of one component, read from the domain's own solution buffer (units match that compone...
std::size_t nSpecies() const
Number of species K [-] in this domain's mechanism.
bool hasComponent(const std::string &name, bool checkAlias=true) const override
Whether name is "Tg", "Te", or a species of this domain's mechanism.
void resetBadValues(Cantera::span< double > xg) override
Clamp a trial state back onto a physically evaluable range (Tg, Te >= 200 K; every Y_k in [0,...
std::string componentName(std::size_t n) const override
Component n -> name ("Tg", "Te", or a species name).
double electronDensity(double Tg, double Te, const double *Y) const
Electron number density [1/m^3] for a node state (X_e * P / (k_B * Tmean)), matching the n_e used int...
PlasmaChannel1D(const PlasmaChannelConfig &cfg)
Build the domain (mesh, thermo/kinetics, property tables) from cfg.
std::size_t componentIndex(const std::string &name, bool checkAlias=true) const override
Component name -> index (inverse of componentName()); throws if unknown.
shared_ptr< Solution > newSolution(const string &infile, const string &name="", const string &transport="default", const vector< shared_ptr< Solution > > &adjacent={})
const size_t npos
double faceFlux(double r_face, double coef_face, double value_inner, double value_outer, double spacing)
Conductive/diffusive flux across one radial cell face: r_face * coef_face * (value_outer - value_inne...
double annularVolume(double r_east, double r_west)
Annular cell volume per unit axial length between two face radii, 0.5 * (r_east^2 - r_west^2) [m^2].
constexpr double R_kmol
Ideal gas constant [J/(kmol K)] (matches Cantera's GasConstant).
Definition units.h:60
constexpr double GasConstant
void registerPlasmaRates()
Register the custom plasma reaction rates with Cantera's ReactionRateFactory.
std::vector< double > initY_v
std::vector< double > sigma_v
std::vector< double > species_h0k
std::vector< double > sigma_T
std::vector< double > initY_r
std::vector< double > nu_m_T
std::vector< CollisionModel::SpeciesSpec > specs
std::vector< double > nu_m_v
std::vector< double > grid
bool reacting
Include finite-rate chemistry source terms (true) or freeze composition (false) [-].
Definition ReactorRHS.h:63
std::vector< double > sigma_v
sigma(Te) table: electrical conductivity values at sigma_T [S/m]
Definition ReactorRHS.h:72
CollisionModel collision
Composition-resolved sigma/nu_E (preferred).
Definition ReactorRHS.h:65
std::vector< double > nu_m_T
nu_m(Te) table: electron-temperature grid points [K], strictly increasing
Definition ReactorRHS.h:75
std::vector< double > species_h0k
Per-species standard enthalpy of formation at 0 K [J/kmol], length nSpecies(), in mechanism species-i...
Definition ReactorRHS.h:94
double nu_m
Scalar electron momentum-transfer frequency, used when nu_m_T/nu_m_v are empty [1/s].
Definition ReactorRHS.h:81
std::vector< double > nu_m_v
nu_m(Te) table: electron momentum-transfer frequency values at nu_m_T [1/s]
Definition ReactorRHS.h:78
double nu_E
Scalar elastic electron-heavy energy-exchange frequency, used when collision is empty [1/s].
Definition ReactorRHS.h:84
std::vector< double > sigma_T
sigma(Te) table: electron-temperature grid points [K], strictly increasing
Definition ReactorRHS.h:69
Physical constants and unit conversions, in SI by default.