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