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