33 m_n_species(m_solution->thermo()->
nSpecies()),
34 m_electron_index(m_solution->thermo()->speciesIndex(
"e-")),
35 m_outer_radius(cfg.R_max),
36 m_electric_field(cfg.electric_field),
37 m_species_diffusivity(cfg.D_species),
38 m_reacting(cfg.reacting),
40 m_ambient_temperature(cfg.T_amb),
43 cfg.kappa_H_T, cfg.kappa_H_v,
"kappa_H_T/kappa_H_v")),
45 cfg.kappa_e_T, cfg.kappa_e_v,
"kappa_e_T/kappa_e_v")),
47 cfg.initTg_r, cfg.initTg_v,
"initTg_r/initTg_v")),
49 cfg.initTe_r, cfg.initTe_v,
"initTe_r/initTe_v"))
63 const bool explicit_grid = cfg.
grid.size() >= 2;
64 const std::size_t n_points = explicit_grid ? cfg.
grid.size() : cfg.
n_points;
69 if (cfg.
grid.front() != 0.0)
71 throw std::invalid_argument(
72 "PlasmaChannel1D: an explicit grid must start on the axis, "
73 "grid[0] = 0, but grid[0] = " +
74 std::to_string(cfg.
grid.front()) +
75 " m. The first face is pinned at r = 0, so the first cell "
76 "would span the whole disc inside that node.");
78 m_outer_radius = cfg.
grid.back();
82 throw std::invalid_argument(
"PlasmaChannel1D: need R_max>0, n_points>=1 "
83 "(or supply an explicit grid of >=2 points).");
87 throw std::invalid_argument(
"PlasmaChannel1D: mechanism has no 'e-' species.");
94 throw std::invalid_argument(
95 "PlasmaChannel1D: Y_amb is required. It is the composition "
96 "OUTSIDE the domain, which the wall ghost and the hydro inflow "
97 "read; it is not derived from the initial composition. Pass the "
98 "ambient composition explicitly, repeating the initial one only "
99 "if that is genuinely what the boundary holds.");
101 if (m_Y_amb.size() != m_n_species)
103 throw std::invalid_argument(
104 "PlasmaChannel1D: Y_amb has " +
105 std::to_string(m_Y_amb.size()) +
106 " entries, but the mechanism has " +
107 std::to_string(m_n_species) +
113 const double ambient_sum =
114 std::accumulate(m_Y_amb.begin(), m_Y_amb.end(), 0.0);
115 if (!(std::abs(ambient_sum - 1.0) <= 1.0e-10))
117 throw std::invalid_argument(
118 "PlasmaChannel1D: Y_amb, the composition of the outer boundary, "
120 std::to_string(ambient_sum) +
", not 1 to within 1e-10.");
143 const bool have_collision = !cfg.
specs.empty();
144 const bool have_legacy_closure = !cfg.
sigma_T.empty() || !cfg.
sigma_v.empty() ||
147 if (have_collision && have_legacy_closure)
149 throw std::invalid_argument(
150 "PlasmaChannel1D: cfg.specs (composition-resolved collision model) "
151 "cannot be combined with sigma_T/sigma_v, nu_m, nu_m_T/nu_m_v, or "
152 "nu_E (the legacy closure) -- pick one.");
169 if (cfg.
specs.size() != m_n_species)
171 throw std::invalid_argument(
172 "PlasmaChannel1D: got collision specs for " +
173 std::to_string(cfg.
specs.size()) +
174 " species, but the mechanism has " +
175 std::to_string(m_n_species) +
176 " species. Give one spec per mechanism species, in its "
179 std::vector<CollisionModel::SpeciesSpec> specs = cfg.
specs;
180 const auto &molecular_weights = m_solution->thermo()->molecularWeights();
181 for (std::size_t k = 0; k < m_n_species; k++)
184 specs[k].molar_mass = molecular_weights[k] * 1.0e-3;
185 specs[k].is_electron = (k == m_electron_index);
201 m_reactor_config = reactor_config;
205 std::make_unique<PlasmaCell>(m_solution, std::move(reactor_config));
215 throw std::invalid_argument(
216 "PlasmaChannel1D: initY_r needs at least 2 radii and initY_v "
217 "exactly initY_r.size() * nSpecies entries, row-major; got " +
218 std::to_string(cfg.initY_r.size()) +
" and " +
219 std::to_string(cfg.initY_v.size()) +
" for " +
220 std::to_string(m_n_species) +
" species.");
223 const std::size_t n_radii = cfg.initY_r.size();
224 m_initY.reserve(m_n_species);
225 for (std::size_t k = 0; k < m_n_species; k++)
227 std::vector<double> column(n_radii);
228 for (std::size_t i = 0; i < n_radii; i++)
230 column[i] = cfg.initY_v[i * m_n_species + k];
232 m_initY.push_back(PropertyTable::fromTableOrScalar(
233 cfg.initY_r, column,
"initY_r/initY_v"));
237 !m_kappa_H.empty() || !m_kappa_e.empty() || m_species_diffusivity > 0.0;
245 else if (cfg.n_points >= 2)
247 setupUniformGrid(cfg.n_points, cfg.R_max, 0.0);
254 const double outer_node = z(n_points - 1);
255 auto requireSpansGrid = [&](
const PropertyTable &table,
const std::string &name)
257 if (!table.empty() && !(table.covers(0.0) && table.covers(outer_node)))
259 throw std::invalid_argument(
260 "PlasmaChannel1D: the initial profile " + name +
" covers [" +
262 "] m but the grid spans [0, " +
significant(outer_node) +
"] m.");
265 requireSpansGrid(m_initTg,
"initTg_r/initTg_v");
266 requireSpansGrid(m_initTe,
"initTe_r/initTe_v");
269 requireSpansGrid(table,
"initY_r/initY_v");
279 for (std::size_t j = 0; j < n_points; j++)
281 const double Y_electron = m_initY[m_electron_index].eval(z(j));
282 if (!(Y_electron > 0.0))
284 std::ostringstream message;
285 message <<
"PlasmaChannel1D: the initial electron mass fraction is "
286 << Y_electron <<
" at node " << j <<
" (r=" << z(j)
287 <<
" m). The electron temperature is undefined where "
288 "there are no electrons; seed a small electron "
289 "fraction at every node, with a matching positive "
290 "ion to keep the charge balance.";
291 throw std::invalid_argument(message.str());
299 const bool want_density_profile =
300 !cfg.init_rho_r.empty() || !cfg.init_rho_v.empty();
301 if (want_density_profile && (cfg.init_rho_r.size() < 2 ||
302 cfg.init_rho_r.size() != cfg.init_rho_v.size()))
304 throw std::invalid_argument(
305 "PlasmaChannel1D: init_rho_r and init_rho_v must be the same "
306 "length and have at least 2 points. For a density that is the "
307 "same everywhere, leave both empty and set the scalar `rho`.");
314 "init_rho_r/init_rho_v");
315 requireSpansGrid(init_rho,
"init_rho_r/init_rho_v");
316 m_rho.assign(n_points, cfg.rho);
317 if (!init_rho.empty())
319 for (std::size_t j = 0; j < n_points; j++)
321 m_rho[j] = init_rho.eval(z(j));
324 for (std::size_t j = 0; j < n_points; j++)
326 if (!(m_rho[j] > 0.0))
328 std::ostringstream message;
330 <<
"PlasmaChannel1D: density at node "
331 << j <<
" (r=" << z(j) <<
" m) is " << m_rho[j]
332 <<
" kg/m^3. The species equation divides by it, so a zero or "
333 "negative density is not a state this domain can evaluate.";
334 throw std::invalid_argument(message.str());
348 if (!m_hydrodynamics)
351 auto name = [&unused](
const std::string &field)
353 unused += unused.empty() ? field :
", " + field;
355 if (!cfg.init_ur_r.empty() || !cfg.init_ur_v.empty())
357 name(
"init_ur_r/init_ur_v");
359 if (cfg.p_amb >= 0.0)
361 if (cfg.outer_bc_mode !=
"ambient_ghost")
362 name(
"outer_bc_mode");
365 throw std::invalid_argument(
366 "PlasmaChannel1D: " +
368 " is read only by the hydrodynamic sub-step, which this "
369 "config leaves off. Set hydrodynamics=true, or drop it.");
377 if (!(cfg.p_amb > 0.0))
379 throw std::invalid_argument(
380 "PlasmaChannel1D: hydrodynamics needs p_amb > 0 Pa, the "
381 "ambient pressure its outer boundary imposes.");
383 if (!(cfg.cfl > 0.0) || cfg.cfl > 1.0)
385 throw std::invalid_argument(
386 "PlasmaChannel1D: hydrodynamics needs 0 < cfl <= 1.");
390 throw std::invalid_argument(
391 "PlasmaChannel1D: hydrodynamics needs at least 2 radial "
392 "nodes; a single node has no face to carry a flux.");
409 m_wall_ghost_density = m_rho.back();
412 auto thermo = m_solution->thermo();
413 thermo->setMassFractions_NoNorm(
414 Cantera::span<const double>(m_Y_amb.data(), m_Y_amb.size()));
421 thermo->setElectronTemperature(cfg.T_amb);
422 thermo->setState_TP(cfg.T_amb, cfg.p_amb);
423 m_wall_ghost_density = thermo->density();
428 "init_ur_r/init_ur_v");
429 requireSpansGrid(init_ur,
"init_ur_r/init_ur_v");
430 m_u_r.assign(n_points, 0.0);
431 if (!init_ur.empty())
433 for (std::size_t j = 0; j < n_points; j++)
435 m_u_r[j] = init_ur.eval(z(j));
439 setComponentName(0,
"Tg");
440 setComponentName(1,
"Te");
441 for (std::size_t k = 0; k < m_n_species; k++)
443 setComponentName(2 + k, m_solution->thermo()->speciesName(k));
451 setSteadyTolerances(1.0e-4, 1.0e-10);
452 setTransientTolerances(1.0e-4, 1.0e-12);
485 auto x = xg.subspan(
loc(),
size());
486 auto rsd = rg.subspan(
loc(),
size());
487 auto diag = maskg.subspan(
loc(),
size());
490 std::size_t jmin, jmax;
500 std::size_t local_point = (jg == 0) ? 0 : jg -
firstPoint();
501 jmin = std::max<std::size_t>(local_point, 1) - 1;
502 jmax = std::min(local_point + 1,
m_points - 1);
510 std::vector<double> dYdt(m_n_species);
513 std::vector<double> species_flux_divergence(m_n_species);
516 for (std::size_t j = jmin; j <= jmax; j++)
518 double Tg = x[
index(0, j)];
519 double Te = x[
index(1, j)];
520 const double *Y = &x[
index(2, j)];
532 double dTg = 0.0, dTe = 0.0, lhsTg = 1.0, lhsTe = 1.0;
533 std::fill(dYdt.begin(), dYdt.end(), 0.0);
534 m_cell->sourceTerms(Tg, Te, Y, m_rho[j], m_electric_field,
535 dTg, dTe, dYdt.data(), lhsTg, lhsTe);
540 dYdt.data(), species_flux_divergence.data());
554 diag[
index(0, j)] = 1;
555 diag[
index(1, j)] = 1;
556 for (std::size_t k = 0; k < m_n_species; k++)
558 rsd[
index(2 + k, j)] = dYdt[k] - rdt * (Y[k] -
prevSoln(2 + k, j));
559 diag[
index(2 + k, j)] = 1;
574 return m_solution->thermo()->speciesName(n - 2);
586 std::size_t k = m_solution->thermo()->speciesIndex(name);
589 throw std::invalid_argument(
"PlasmaChannel1D: no component '" + name +
"'.");
594 return name ==
"Tg" ||
609 throw std::runtime_error(
"PlasmaChannel1D::getValues: not installed.");
613 for (std::size_t j = 0; j <
m_points; j++)
628 return m_initTg.empty() ? m_ambient_temperature : m_initTg.eval(r);
630 return m_initTe.empty() ? m_ambient_temperature : m_initTe.eval(r);
631 std::size_t k = n - 2;
632 return m_initY[k].eval(r);
638 const std::vector<double> &u_r)
642 throw std::invalid_argument(
643 "PlasmaChannel1D::setHydroState: rho and u_r must each have one "
644 "entry per radial node.");
646 for (std::size_t j = 0; j <
m_points; j++)
650 std::ostringstream message;
651 message <<
"PlasmaChannel1D::setHydroState: density at node " << j
652 <<
" (r=" <<
z(j) <<
" m) is " << rho[j]
653 <<
" kg/m^3. The hydro sub-step has driven it to zero or "
654 "below; lower cfl or refine the grid.";
655 throw std::invalid_argument(message.str());
669 std::size_t j,
double &pressure,
670 double &electron_density)
const
672 m_cell->pressureAndElectronDensity(Tg, Te, Y, m_rho[j], pressure,
677 const double *Y,
double electric_field,
682 throw std::runtime_error(
683 "PlasmaChannel1D::nodeRates: this domain has conduction or "
684 "species diffusion enabled, so a node's rates depend on its "
685 "neighbours and cannot be evaluated one node at a time. Use "
693 double lhs_Tg = 1.0, lhs_Te = 1.0;
698 const std::size_t slot =
static_cast<std::size_t
>(omp_get_thread_num());
707 throw std::runtime_error(
708 "PlasmaChannel1D::nodeRates: thread " +
709 std::to_string(slot) +
710 " has no evaluator of its own; the pool holds " +
712 ". Call setEvaluatorPoolSize() with the number of threads that "
713 "will enter this function before starting the parallel region.");
716 const std::size_t slot = 0;
719 (slot == 0) ? *m_cell : *m_extra_rhs[slot - 1];
720 reactor.
sourceTerms(Tg, Te, Y, m_rho[j], electric_field,
721 ydot[0], ydot[1], ydot + 2, lhs_Tg, lhs_Te);
728 return m_cell->electricalConductivity(Tg, Te, Y, m_rho[j]);
732 double &conduction_out,
733 double &diffusion_out,
734 std::vector<double> &species_mass_out)
const
736 conduction_out = 0.0;
738 species_mass_out.assign(m_n_species, 0.0);
739 if (!m_has_transport)
743 const std::size_t wall_node =
m_points - 1;
744 std::vector<double> species_enthalpy(m_n_species);
745 m_cell->speciesEnthalpies(x[
index(0, wall_node)], x[
index(1, wall_node)],
746 &x[
index(2, wall_node)], m_rho[wall_node],
747 species_enthalpy.data());
749 *
this, x, diffusionCoefficients(), species_enthalpy.data());
766 return 0.5 * (
z(j - 1) +
z(j));
777 return z(j) -
z(j - 1);
786 throw std::runtime_error(
787 "PlasmaChannel1D::setEvaluatorPoolSize: the pool must hold at "
788 "least the evaluator built at construction.");
790 m_extra_rhs.resize(slots - 1);
791 for (
auto &evaluator : m_extra_rhs)
795 evaluator = std::make_unique<PlasmaCell>(
808 diffusion.
kappa = &m_kappa_H;
809 diffusion.
kappa_e = &m_kappa_e;
813 diffusion.
Y_amb = &m_Y_amb;
Pure-arithmetic primitives for a 1D cylindrical (radial) finite-volume discretization of the diffusiv...
The transient two-temperature radial channel as a Cantera Domain1D: per-node source terms from Plasma...
The two-temperature quantities every model in this extension reads back from a PlasmaPhase,...
The parabolic half of PlasmaChannel1D::eval(): conduction in both pools, Fickian species diffusion,...
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
Shared single-cell two-temperature reacting-plasma RHS (one physics source).
void sourceTerms(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).
void wallEnergyOutflow(Cantera::span< const double > x, double &conduction_out, double &diffusion_out, std::vector< double > &species_mass_out) const
Energy conduction and species diffusion move out through the wall ghost face at the state x,...
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 setEvaluatorPoolSize(std::size_t slots)
Give nodeRates() slots independent evaluators so that many threads can call it at once.
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.
void setHydroState(const std::vector< double > &rho, const std::vector< double > &u_r)
Replace the fields the hydro sub-step owns.
void nodeDiagnostics(double Tg, double Te, const double *Y, std::size_t j, double &pressure, double &electron_density) const
Total pressure [Pa] and electron number density [1/m^3] for a node state, both from the two-temperatu...
bool hasComponent(const std::string &name, bool checkAlias=true) const override
Whether name is "Tg", "Te", or a species of this domain's mechanism.
std::string componentName(std::size_t n) const override
Component n -> name ("Tg", "Te", or a species name).
void nodeRates(std::size_t j, double Tg, double Te, const double *Y, double electric_field, double *ydot) const
One node's time derivatives with no transport stencil: exactly what eval() computes for node j when h...
bool hydrodynamics() const
Whether the radial hydrodynamic sub-step is active [-].
double nodeElectricalConductivity(double Tg, double Te, const double *Y, std::size_t j) const
Electrical conductivity [S/m] at one node's state, from the same composition-resolved closure the sou...
double faceRadius(std::size_t j) const
Radius of face j, the single definition of where a cell boundary sits (NUMERICS.md section 2).
std::size_t evaluatorPoolSize() const
Number of independent evaluators nodeRates() can be called on at once [-]; 1 unless setEvaluatorPoolS...
PlasmaChannel1D(const PlasmaChannelConfig &cfg)
Build the domain (mesh, thermo/kinetics, property tables) from cfg.
double faceSpacing(std::size_t j) const
Node-to-node spacing across face j [m], the distance a gradient at that face is taken over.
std::size_t componentIndex(const std::string &name, bool checkAlias=true) const override
Component name -> index (inverse of componentName()); throws if unknown.
static PropertyTable fromTableOrScalar(const std::vector< double > &x, const std::vector< double > &v, const char *name="property table")
Build a table from a grid/value pair, a single scalar, or neither.
OuterBc parseOuterBc(const std::string &mode)
Map an outer_bc_mode config string onto the enum.
std::shared_ptr< Cantera::Solution > loadMechanism(const std::string &mech, const std::string &phase)
Load a mechanism with rizer's custom rate types registered first.
void addRadialDiffusion(const PlasmaChannel1D &domain, Cantera::span< const double > x, const RadialDiffusionCoefficients &c, std::size_t j, double lhsTg, double lhsTe, double &dTg, double &dTe, double *dYdt, double *scratch)
Add one node's diffusive contributions to the rates the caller holds.
std::string significant(double value)
value to 6 significant digits, for exception messages.
WallEnergyOutflow wallEnergyOutflow(const PlasmaChannel1D &domain, Cantera::span< const double > x, const RadialDiffusionCoefficients &c, const double *species_enthalpy)
What conduction and species diffusion move through the wall face at the state x.
double nu_m
Scalar electron momentum-transfer frequency, used when nu_m_T/nu_m_v are empty [1/s].
CollisionModel collision
Composition-resolved sigma/nu_E (preferred).
std::vector< double > sigma_v
sigma(Te) table: electrical conductivity values at sigma_T [S/m]
bool reacting
Include finite-rate chemistry source terms (true) or freeze composition (false) [-].
std::vector< double > species_h0k
Per-species standard enthalpy of formation at 0 K [J/kmol], length nSpecies(), in mechanism species-i...
std::vector< double > nu_m_v
nu_m(Te) table: electron momentum-transfer frequency values at nu_m_T [1/s]
double nu_E
Scalar elastic electron-heavy energy-exchange frequency, used when collision is empty [1/s].
std::vector< double > nu_m_T
nu_m(Te) table: electron-temperature grid points [K], strictly increasing
std::vector< double > sigma_T
sigma(Te) table: electron-temperature grid points [K], strictly increasing
Everything PlasmaChannel1D needs to build itself.
std::vector< double > initY_v
std::string mech
Cantera mechanism (YAML) file path.
std::vector< double > sigma_v
double nu_m
Electron momentum-transfer frequency nu_m [1/s].
std::vector< double > species_h0k
Per-species standard enthalpy of formation at 0 K [J/kmol], forwarded to PlasmaCellConfig::species_h0...
std::size_t n_points
Number of radial grid points [-]. Ignored when grid is given.
double nu_E
Elastic electron-heavy energy-exchange frequency nu_E [1/s].
std::size_t Te_n
Collision-model Te grid point count [-]. Read only with specs.
double R_max
Outer (wall) radius R [m]. Ignored when grid is given.
double Te_min
Collision-model Te grid lower bound [K].
std::vector< double > sigma_T
Electrical conductivity table sigma(Te) [K], [S/m].
bool reacting
Include finite-rate chemistry source terms [-].
std::vector< double > initY_r
Y(r) at t=0.
std::vector< double > nu_m_T
Te-dependent nu_m(Te) table [K], [1/s].
double Te_max
Collision-model Te grid upper bound [K].
std::vector< CollisionModel::SpeciesSpec > specs
Composition-resolved collision model, preferred over the legacy sigma/nu_m/nu_E closures above and mu...
std::vector< double > nu_m_v
std::vector< double > grid
Explicit radial grid [m], increasing from 0.
std::string phase
Phase name within mech; must be a PlasmaPhase carrying "e-".
Everything the diffusion block needs that the grid and the state do not carry: the transport coeffici...
std::size_t electron_index
Index of the electron species [-].
const PropertyTable * kappa_e
Electron conductivity [W/m/K]; an empty table switches electron conduction off.
const std::vector< double > * Y_amb
Ambient mass fractions [-], length K: the species ghost.
double ambient_temperature
Wall/ambient temperature [K], the Dirichlet ghost both conduction calls sit against.
double species_diffusivity
Scalar species mass diffusivity [m^2/s]; zero switches the species block, and with it both energy te...
std::size_t n_species
Number of species K [-].
const PropertyTable * kappa
Heavy-pool conductivity [W/m/K]; an empty table switches heavy conduction off.
const std::vector< double > * species_cp
Per-species [J/kg/K], length K.
const std::vector< double > * molecular_weights
Per-species molecular weight [kg/kmol], length K.
double wall_ghost_density
Mass density of the wall ghost half-cell [kg/m^3], which closes the face coefficient of the outermost...
Energy leaving through the wall ghost face per unit time, per unit axial length and per radian [W/m] ...
std::vector< double > species_mass
Mass of each species diffusion carries through the ghost face [kg/m/s], length K; empty when species ...
double diffusion
Enthalpy species diffusion carries through the ghost face [W/m].
double conduction
Conduction of both pools against the ghost temperature [W/m].
Physical constants and unit conversions, in SI by default.