rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
ThermalPlasmaColumn1D.cpp
Go to the documentation of this file.
3
4#include <algorithm>
5#include <cmath>
6#include <stdexcept>
7#include <string>
8
9namespace rizer {
10
11// ---------------------------------------------------------------------------------- //
12// Constructor //
13// ---------------------------------------------------------------------------------- //
14
16 double electric_field,
17 double T_wall, PropertyTable sigma,
18 PropertyTable kappa,
19 PropertyTable p_rad,
20 double T_center_guess, double rho_cp,
21 PropertyTable init_profile)
22 : Cantera::Domain1D(/*nv=*/1, /*points=*/npoints) // 1 solution component: T(r)
23 , m_R(R)
24 , m_E(electric_field)
25 , m_Twall(T_wall)
26 , m_Tcenter(T_center_guess)
27 , m_rho_cp(rho_cp)
28 , m_sigma(std::move(sigma))
29 , m_kappa(std::move(kappa))
30 , m_prad(std::move(p_rad))
31 , m_init(std::move(init_profile))
32{
33 if (R <= 0.0 || npoints < 3) {
34 throw std::invalid_argument(
35 "ThermalPlasmaColumn1D: require R > 0 and at least 3 grid points "
36 "(got R=" + std::to_string(R) + ", n=" + std::to_string(npoints)
37 + ").");
38 }
39 // Uniform initial radial grid r in [0, R]; PlasmaColumnSolver will call
40 // sim.solve() which refines it adaptively via the Refiner criteria.
41 setupUniformGrid(npoints, R, 0.0);
42
43 setComponentName(0, "T");
44 setBounds(/*n=*/0, /*lower=*/0.0, /*upper=*/1.0e5); // [K]
45 setSteadyTolerances(/*rtol=*/1.0e-4, /*atol=*/1.0e-9);
46 setTransientTolerances(/*rtol=*/1.0e-4, /*atol=*/1.0e-11);
47}
48
49// ---------------------------------------------------------------------------------- //
50// Public methods //
51// ---------------------------------------------------------------------------------- //
52
55 double rdt)
56{
57 // Cantera calls eval() both for a full residual sweep (jg == npos) and for
58 // Jacobian columns (jg = global index of the perturbed variable), once per
59 // column during finite-difference Jacobian assembly. In the latter case,
60 // perturbing node jg only changes the residuals at jg itself and its two
61 // immediate neighbours (the 3-point conduction stencil below), so a
62 // cheap early-out here skips domains/points the perturbation cannot
63 // possibly affect, saving redundant PropertyTable evaluations.
64 if (jg != Cantera::npos
65 && (jg + 1 < firstPoint() || jg > lastPoint() + 1)) {
66 return;
67 }
68
69 // Slice the global arrays down to this domain's local range.
70 auto x = xg.subspan(loc(), size()); // local solution vector
71 // (T at each grid point)
72 auto rsd = rg.subspan(loc(), size()); // local residual vector
73 // (dT/dt at each grid point)
74 auto diag = maskg.subspan(loc(), size()); // local mask vector (1 if
75 // dT/dt exists, 0 if algebraic)
76
77 // Determine the range of local indices to update.
78 std::size_t jmin, jmax;
79 if (jg == Cantera::npos) { // full sweep
80 jmin = 0;
81 jmax = m_points - 1;
82 } else { // single Jacobian column — 3-point stencil
83 std::size_t jpt = (jg == 0) ? 0 : jg - firstPoint();
84 jmin = std::max<std::size_t>(jpt, 1) - 1;
85 jmax = std::min(jpt + 1, m_points - 1);
86 }
87
88 const double E2 = m_E * m_E;
89
90 for (std::size_t j = jmin; j <= jmax; j++) {
91 const double Tj = x[index(0, j)];
92
93 // ---- Dirichlet wall BC (algebraic row, diag=0 tells the solver it
94 // has no time derivative) ------------------------------------
95 // The residual is simply @f$ T(r=R) - T_\text{wall} @f$, which
96 // Newton drives to zero by forcing the last node's temperature to
97 // the prescribed wall value; there is no flux balance at this row
98 // (the last interior cell's east flux uses this Dirichlet value as
99 // its neighbour, see flux_e above for j = m_points - 2).
100 if (j == m_points - 1) {
101 rsd[index(0, j)] = Tj - m_Twall;
102 diag[index(0, j)] = 0;
103 continue;
104 }
105
106 // ---- Conductive flux on the east face (between j and j+1) -------
107 // The finite-volume divergence is
108 // @f$ \frac{1}{r}\frac{d}{dr}\!\left(r \kappa \frac{dT}{dr}\right) @f$.
109 // The east face is at @f$ r_e = 0.5(r_j + r_{j+1}) @f$, with
110 // spacing @f$ dr_e = r_{j+1} - r_j @f$.
111 // The thermal conductivity at the face is the arithmetic average
112 // of the two nodes.
113 //
114 // zj : Radius of node j (i.e. z(0)=0, z(npts-1)=R)
115 // r_e : Mid-point radius of the east face
116 // (i.e. r_e(0)=0.5*(z(0)+z(1)=0.5*z(1)))
117 // dr_e : Node spacing to the east
118 // Tjp1 : Temperature at node j+1
119 // kap_e : Arithmetic-average thermal conductivity at the face
120 // flux_e : @f$ r_e \kappa_e \left.\frac{dT}{dr}\right|_e
121 // = r_e \kappa_e (T_{j+1} - T_j) / dr_e @f$ (units: W/m)
122 const double zj = z(j);
123 const double re = 0.5 * (zj + z(j + 1));
124 const double dre = z(j + 1) - zj;
125 const double Tjp1 = x[index(0, j + 1)];
126 const double kap_e = 0.5 * (m_kappa.eval(Tj) + m_kappa.eval(Tjp1));
127 const double flux_e = numeric::faceFlux(re, kap_e, Tj, Tjp1, dre);
128
129 // ---- Conductive flux on the west face (between j-1 and j) -------
130 // At the axis (j=0) there is no j-1 neighbour; the r=0 symmetry BC
131 // (@f$ dT/dr = 0 @f$ at @f$ r = 0 @f$, i.e. no heat flows "through"
132 // the centreline) is imposed geometrically rather than via an
133 // explicit equation: the west face radius r_w is 0 there, and
134 // since flux ~ r_w * ..., the west flux is forced to exactly zero
135 // regardless of dT/dr.
136 // No ghost node or extra BC row is needed for this case.
137 double flux_w, rw;
138 if (j == 0) {
139 rw = 0.0;
140 flux_w = 0.0;
141 } else {
142 rw = 0.5 * (z(j - 1) + zj);
143 const double Tjm1 = x[index(0, j - 1)];
144 const double drw = zj - z(j - 1);
145 const double kap_w = 0.5 * (m_kappa.eval(Tjm1) + m_kappa.eval(Tj));
146 flux_w = numeric::faceFlux(rw, kap_w, Tjm1, Tj, drw);
147 }
148
149 // ---- Finite-volume divergence ------------------------------------
150 // @f$ \text{vol} = \int r\,dr @f$ over the cell
151 // @f$ = 0.5(r_e^2 - r_w^2) @f$.
152 // div_flux approximates
153 // @f$ \frac{1}{r}\frac{d}{dr}\!\left(r \kappa \frac{dT}{dr}\right) @f$
154 // at node j.
155 const double vol = numeric::annularVolume(re, rw);
156 const double div_flux = (flux_e - flux_w) / vol;
157
158 // ---- Source term: Joule heating minus radiative loss -------------
159 // Unlike kappa (looked up at the two face-adjacent nodes and
160 // averaged, since it multiplies a flux between nodes), sigma and
161 // P_rad are cell-centred quantities and are looked up once at this
162 // node's own temperature Tj via PropertyTable::eval() (linear
163 // interpolation on the tabulated grid; m_prad.eval() returns 0 if
164 // no radiation table was supplied).
165 const double source = m_sigma.eval(Tj) * E2 - m_prad.eval(Tj);
166
167 // ---- Residual (scaled by rho*cp so Newton updates are in K) ------
168 // The pseudo-transient term rdt*(T - T_prev) adds a diagonal shift
169 // during Cantera's time-stepping fallback and vanishes at steady
170 // state (rdt -> 0 once Newton converges).
171 rsd[index(0, j)] = (div_flux + source) / m_rho_cp
172 - rdt * (Tj - prevSoln(0, j));
173 diag[index(0, j)] = 1;
174 }
175}
176
177
178std::size_t ThermalPlasmaColumn1D::componentIndex(const std::string& name,
179 bool /*checkAlias*/) const
180{
181 if (name == "T") {
182 return 0;
183 }
184 throw std::invalid_argument(
185 "ThermalPlasmaColumn1D: no component named '" + name + "'.");
186}
187
188
189bool ThermalPlasmaColumn1D::hasComponent(const std::string& name,
190 bool /*checkAlias*/) const
191{
192 return name == "T";
193}
194
195
196void ThermalPlasmaColumn1D::getValues(const std::string& component,
198{
199 if (component != "T") {
200 throw std::invalid_argument(
201 "ThermalPlasmaColumn1D: no component named '" + component + "'.");
202 }
203 if (!m_state) {
204 throw std::runtime_error(
205 "ThermalPlasmaColumn1D::getValues: domain not installed in a container.");
206 }
207 const double* soln = m_state->data() + loc();
208 for (std::size_t j = 0; j < m_points; j++) {
209 values[j] = soln[index(0, j)];
210 }
211}
212
213
214double ThermalPlasmaColumn1D::initialValue(std::size_t /*n*/, std::size_t j)
215{
216 double r = z(j);
217 if (!m_init.empty()) {
218 // Seeded guess (e.g. the analytical EH profile), interpolated in r.
219 return m_init.eval(r);
220 }
221 // Default parabola: T_center at the axis, T_wall at @f$ r = R @f$.
222 double s = r / m_R;
223 return m_Twall + (m_Tcenter - m_Twall) * (1.0 - s * s);
224}
225
226
228{
229 auto x = xg.subspan(loc(), size());
230 double lo = lowerBound(0);
231 double hi = upperBound(0);
232 for (std::size_t j = 0; j < m_points; j++) {
233 double T = x[index(0, j)];
234 x[index(0, j)] = std::min(std::max(T, lo), hi);
235 }
236}
237
238} // namespace rizer
Pure-arithmetic primitives for a 1D cylindrical (radial) finite-volume discretization of the diffusiv...
void setTransientTolerances(double rtol, double atol, size_t n=npos)
size_t lastPoint() const
void setComponentName(size_t n, const string &name)
void setupUniformGrid(size_t points, double length, double start=0.)
size_t size() const
double lowerBound(size_t n) const
shared_ptr< vector< double > > m_state
vector< double > values(const string &component) const
double z(size_t jlocal) const
double upperBound(size_t n) const
void setSteadyTolerances(double rtol, double atol, size_t n=npos)
void setBounds(size_t n, double lower, double upper)
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
std::size_t componentIndex(const std::string &name, bool checkAlias=true) const override
Return the index of the named solution component.
double initialValue(std::size_t n, std::size_t j) override
Initial guess for component n at grid point j.
void getValues(const std::string &component, Cantera::span< double > values) const override
Copy converged temperatures from the shared global solution vector.
bool hasComponent(const std::string &name, bool checkAlias=true) const override
Test whether a component name exists in this domain.
void eval(std::size_t jg, Cantera::span< const double > xg, Cantera::span< double > rg, Cantera::span< int > maskg, double rdt) override
Evaluate the residual function at point jg.
ThermalPlasmaColumn1D(double R, std::size_t npoints, double electric_field, double T_wall, PropertyTable sigma, PropertyTable kappa, PropertyTable p_rad, double T_center_guess, double rho_cp, PropertyTable init_profile=PropertyTable())
Construct a radial LTE arc-column domain with tabulated property closures.
void resetBadValues(Cantera::span< double > xg) override
Clamp any out-of-bound temperatures after a failed Newton step to keep the solve from wandering into ...
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].