rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
PlasmaColumnSolver.cpp
Go to the documentation of this file.
3#include "units.h"
4#include "numeric/Format.h"
5
9
10#include <algorithm>
11#include <cmath>
12#include <memory>
13#include <stdexcept>
14#include <string>
15
16namespace rizer {
17
18// ---------------------------------------------------------------------------------- //
19// Free functions //
20// ---------------------------------------------------------------------------------- //
21
22ColumnResult solveColumn(double R, double electric_field, double T_wall,
23 const PropertyTable& sigma, const PropertyTable& kappa,
24 const PropertyTable& p_rad, const ColumnOptions& opts)
25{
26 using std::make_shared;
27
28 // Optional initial guess T(r), folded into the domain so it is applied at
29 // construction (via initialValue) without the deprecated Sim1D::setProfile.
30 // Both empty leaves init_profile default-constructed, and the domain builds
31 // its own parabolic seed from T_wall and T_center_guess. Any other pair that
32 // is not >= 2 matching points is refused: falling back to the parabolic seed
33 // would discard the caller's guess without a word.
34 PropertyTable init_profile;
35 if (!opts.init_r.empty() || !opts.init_T.empty()) {
36 if (opts.init_r.size() < 2 || opts.init_r.size() != opts.init_T.size()) {
37 throw std::invalid_argument(
38 "solveColumn: init_r and init_T must be given together with the "
39 "same length of at least 2 points, or both omitted for the "
40 "parabolic seed; got " + std::to_string(opts.init_r.size()) +
41 " and " + std::to_string(opts.init_T.size()) + ".");
42 }
43 init_profile = PropertyTable(opts.init_r, opts.init_T);
44 }
45
46 // The interior domain owns the actual Elenbaas-Heller physics: the radial
47 // conduction/Joule-heating/radiation balance is assembled as residuals on
48 // this domain's grid points; sigma/kappa/p_rad are consulted at each
49 // Newton iterate via their tabulated PropertyTable lookups.
50 auto column = make_shared<ThermalPlasmaColumn1D>(
51 R, opts.n_points, electric_field, T_wall,
52 sigma, kappa, p_rad, opts.T_center_guess, opts.rho_cp, init_profile);
53
54 // Dummy no-op terminators (no Solution required), matching the pattern of
55 // Cantera's BoundaryValueProblem sample. The column owns its own BCs.
56 auto left = make_shared<Cantera::Empty1D>();
57 auto right = make_shared<Cantera::Empty1D>();
58
59 // Assemble the three-domain Sim1D stack [Empty1D | ThermalPlasmaColumn1D |
60 // Empty1D]. Sim1D indexes domains in this vector order, so the column is
61 // always domain index 1 regardless of how many points it has.
62 std::vector<std::shared_ptr<Cantera::Domain1D>> domains{left, column, right};
63 Cantera::Sim1D sim(domains);
64
65 // Refinement applies to the column (domain index 1). These thresholds
66 // (ratio/slope/curve/prune) come straight from ColumnOptions and control
67 // how aggressively Sim1D inserts/removes grid points between Newton
68 // solves when refine_grid is enabled below.
69 sim.setRefineCriteria(/*dom=*/1, /*ratio=*/opts.refine_ratio,
70 /*slope=*/opts.refine_slope,
71 /*curve=*/opts.refine_curve,
72 /*prune=*/opts.refine_prune);
73
74 // Drive the steady solve. Internally, Sim1D::solve() first attempts a
75 // direct Newton solve on the current grid; if Newton fails to converge it
76 // automatically falls back to pseudo-transient time-stepping (using
77 // rho_cp baked into the column's residuals as the effective transient
78 // time constant) to move the iterate closer to the solution before
79 // retrying Newton. When refine_grid is true, this Newton/time-stepping
80 // cycle is repeated on progressively refined grids (per the criteria set
81 // above) until no further refinement is needed.
82 sim.solve(/*loglevel=*/opts.loglevel, /*refine_grid=*/opts.refine_grid);
83
84 // Extract the converged profile from the (possibly refined) grid. Note
85 // nPoints()/z(j) reflect the final refined grid, which may differ in size
86 // and spacing from the initial opts.n_points uniform grid.
87 const std::size_t np = column->nPoints();
88 ColumnResult res;
89 res.n_points = np;
90 res.electric_field = electric_field;
91 res.r.reserve(np);
92 res.T.reserve(np);
93 res.sigma.reserve(np);
94 res.kappa.reserve(np);
95
96 // Re-evaluate sigma(T)/kappa(T) at the converged temperatures (rather than
97 // caching values from the last Newton iterate) so the returned arrays are
98 // exactly consistent with the final T profile.
99 std::vector<double> Tvals = column->values("T");
100 // Newton iterates may leave the tables mid-solve (resetBadValues brings them
101 // back), so the tables are checked against the converged column instead.
102 // The tolerance absorbs the round-off of a wall held at a table's first
103 // point.
104 auto requireCovered = [&](const PropertyTable &table, const char *name)
105 {
106 for (double T : Tvals)
107 {
108 const double tolerance = 1.0e-6 * std::max(1.0, std::abs(T));
109 if (!table.empty() &&
110 !(T >= table.first() - tolerance && T <= table.last() + tolerance))
111 {
112 throw std::runtime_error(
113 std::string("solveColumn: the converged column reaches T = ") +
114 significant(T) + " K, outside the " + name + " table [" +
115 significant(table.first()) + ", " +
116 significant(table.last()) +
117 "] K, where the property is held at its end value.");
118 }
119 }
120 };
121 requireCovered(sigma, "sigma(T)");
122 requireCovered(kappa, "kappa(T)");
123 requireCovered(p_rad, "P_rad(T)");
124 for (std::size_t j = 0; j < np; j++) {
125 double r = column->z(j);
126 double T = Tvals[j];
127 res.r.push_back(r);
128 res.T.push_back(T);
129 res.sigma.push_back(column->sigmaAt(T));
130 res.kappa.push_back(column->kappaAt(T));
131 }
132
133 // Integrated current:
134 // @f$I = \int_0^R \sigma(T)\, E\, 2\pi r\, dr@f$ (trapezoidal).
135 double I = 0.0;
136 for (std::size_t j = 0; j + 1 < np; j++) {
137 // Trapezoidal rule:
138 // @f$\int_a^b f(r)\, dr \approx \tfrac{1}{2}(f(a)+f(b))(b-a)@f$
139 double f0 = res.sigma[j] * res.r[j];
140 double f1 = res.sigma[j + 1] * res.r[j + 1];
141 double dr = res.r[j + 1] - res.r[j];
142 I += 0.5 * (f0 + f1) * dr;
143 }
144 // E is uniform along the column (fixed axial field), so it can be pulled
145 // out of the radial integral: @f$I = 2\pi E \int \sigma(T)\, r\, dr@f$.
146 res.current = 2.0 * units::pi * electric_field * I;
147
148 return res;
149}
150
151} // namespace rizer
Number formatting for exception messages.
void solve(int loglevel=0, bool refine_grid=true)
void setRefineCriteria(int dom=-1, double ratio=10.0, double slope=0.8, double curve=0.8, double prune=-0.1)
double first() const
First abscissa of a non-empty table, in the table's own units.
double last() const
Last abscissa of a non-empty table, in the table's own units.
bool empty() const
True for a default-constructed (empty) table; eval() returns 0.0.
constexpr double pi
pi (matches numpy's np.pi used in units.py).
Definition units.h:51
std::string significant(double value)
value to 6 significant digits, for exception messages.
Definition Format.h:18
ColumnResult solveColumn(double R, double electric_field, double T_wall, const PropertyTable &sigma, const PropertyTable &kappa, const PropertyTable &p_rad, const ColumnOptions &opts)
Build, solve, and extract the radial Elenbaas-Heller column.
Solver configuration. All fields have defaults matching a typical H2 arc.
std::vector< double > init_r
Optional seed profile for the initial guess.
int loglevel
Sim1D solver verbosity [-] (0 = silent, 1 = progress, higher = more detail).
double refine_ratio
Refiner: max allowed ratio of adjacent cell widths [-].
double refine_curve
Refiner: max allowed fractional curvature of T per cell [-].
double rho_cp
Volumetric heat capacity [J/m^3/K] — scales the pseudo-transient term used as a Newton fallback.
double refine_slope
Refiner: max allowed fractional change in T slope between adjacent cells [-].
double T_center_guess
Parabolic-seed centerline temperature [K], used to build the default initial guess.
double refine_prune
Refiner: remove grid points whose contribution falls below this threshold [-].
std::vector< double > init_T
Optional seed temperatures [K] at each init_r location.
std::size_t n_points
Initial uniform grid point count [-] (refined during solve if refine_grid).
bool refine_grid
Enable Cantera adaptive grid refinement [-] (true/false) during the solve.
Output of a completed column solve.
double electric_field
Axial E field [V/m] used for this solve (echo of the input).
std::size_t n_points
Number of grid points after adaptive refinement.
std::vector< double > T
Converged temperature [K] at each grid point.
double current
Total arc current [A], trapezoidal quadrature.
std::vector< double > sigma
Electrical conductivity [S/m] evaluated at T.
std::vector< double > kappa
Thermal conductivity [W/m/K] evaluated at T.
std::vector< double > r
Radial grid [m] after refinement, r[0]=0, r[N-1]=R.
Physical constants and unit conversions, in SI by default.