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 R, std::size_t npoints, double electric_field, double T_wall,
12 PropertyTable sigma, PropertyTable kappa, PropertyTable p_rad,
13 double T_center_guess, double rho_cp, PropertyTable init_profile)
14 : Cantera::Domain1D(/*nv=*/1, /*points=*/npoints) // 1 solution component: T(r)
15 , m_R(R)
16 , m_E(electric_field)
17 , m_Twall(T_wall)
18 , m_Tcenter(T_center_guess)
19 , m_rho_cp(rho_cp)
20 , m_sigma(std::move(sigma))
21 , m_kappa(std::move(kappa))
22 , m_prad(std::move(p_rad))
23 , m_init(std::move(init_profile))
24{
25 if (R <= 0.0 || npoints < 3) {
26 throw std::invalid_argument(
27 "ThermalPlasmaColumn1D: require R > 0 and at least 3 grid points "
28 "(got R=" + std::to_string(R) + ", n=" + std::to_string(npoints)
29 + ").");
30 }
31 // Uniform initial radial grid r in [0, R]; PlasmaColumnSolver will call
32 // sim.solve() which refines it adaptively via the Refiner criteria.
33 setupUniformGrid(npoints, R, 0.0);
34
35 setComponentName(0, "T");
36 setBounds(0, 0.0, 1.0e5); // physical temperature bounds [K]
37 setSteadyTolerances(1.0e-4, 1.0e-9); // relative, absolute
38 setTransientTolerances(1.0e-4, 1.0e-11);
39}
40
42 const std::string& name, bool /*checkAlias*/) const
43{
44 if (name == "T") {
45 return 0;
46 }
47 throw std::invalid_argument(
48 "ThermalPlasmaColumn1D: no component named '" + name + "'.");
49}
50
52 const std::string& name, bool /*checkAlias*/) const
53{
54 return name == "T";
55}
56
58 const std::string& component, Cantera::span<double> values) const
59{
60 if (component != "T") {
61 throw std::invalid_argument(
62 "ThermalPlasmaColumn1D: no component named '" + component + "'.");
63 }
64 if (!m_state) {
65 throw std::runtime_error(
66 "ThermalPlasmaColumn1D::getValues: domain not installed in a container.");
67 }
68 const double* soln = m_state->data() + loc();
69 for (std::size_t j = 0; j < m_points; j++) {
70 values[j] = soln[index(0, j)];
71 }
72}
73
74double ThermalPlasmaColumn1D::initialValue(std::size_t /*n*/, std::size_t j)
75{
76 double r = z(j);
77 if (!m_init.empty()) {
78 // Seeded guess (e.g. the analytical EH profile), interpolated in r.
79 return m_init.eval(r);
80 }
81 // Default parabola: T_center at the axis, T_wall at r = R.
82 double s = r / m_R;
83 return m_Twall + (m_Tcenter - m_Twall) * (1.0 - s * s);
84}
85
87{
88 auto x = xg.subspan(loc(), size());
89 double lo = lowerBound(0);
90 double hi = upperBound(0);
91 for (std::size_t j = 0; j < m_points; j++) {
92 double T = x[index(0, j)];
93 x[index(0, j)] = std::min(std::max(T, lo), hi);
94 }
95}
96
99 Cantera::span<int> maskg, double rdt)
100{
101 // Cantera calls eval() both for a full residual sweep (jg == npos) and for
102 // Jacobian columns (jg = global index of the perturbed variable). In the
103 // latter case we only need to update the three points that the stencil of
104 // node jg touches; skip everything else to avoid redundant work.
105 if (jg != Cantera::npos
106 && (jg + 1 < firstPoint() || jg > lastPoint() + 1)) {
107 return;
108 }
109
110 // Slice the global arrays down to this domain's local range.
111 auto x = xg.subspan(loc(), size()); // local solution vector (T at each grid point)
112 auto rsd = rg.subspan(loc(), size()); // local residual vector (dT/dt at each grid point)
113 auto diag = maskg.subspan(loc(), size()); // local mask vector (1 if dT/dt exists, 0 if algebraic)
114
115 // Determine the range of local indices to update.
116 std::size_t jmin, jmax;
117 if (jg == Cantera::npos) { // full sweep
118 jmin = 0;
119 jmax = m_points - 1;
120 } else { // single Jacobian column — 3-point stencil
121 std::size_t jpt = (jg == 0) ? 0 : jg - firstPoint();
122 jmin = std::max<std::size_t>(jpt, 1) - 1;
123 jmax = std::min(jpt + 1, m_points - 1);
124 }
125
126 const double E2 = m_E * m_E;
127
128 for (std::size_t j = jmin; j <= jmax; j++) {
129 const double Tj = x[index(0, j)];
130
131 // ---- Dirichlet wall BC (algebraic row, diag=0 tells the solver it
132 // has no time derivative) ------------------------------------
133 if (j == m_points - 1) {
134 rsd[index(0, j)] = Tj - m_Twall;
135 diag[index(0, j)] = 0;
136 continue;
137 }
138
139 // ---- Conductive flux on the east face (between j and j+1) -------
140 // The finite-volume divergence is (1/r) d/dr(r * kappa * dT/dr).
141 // The east face is at r_e = 0.5*(r_j + r_{j+1}), with spacing dr_e = r_{j+1} - r_j.
142 // The thermal conductivity at the face is the arithmetic average of the two nodes.
143 //
144 // zj : radius of node j (i.e. z(0)=0, z(npts-1)=R)
145 // r_e : mid-point radius of the east face (i.e. r_e(0)=0.5*(z(0)+z(1)=0.5*z(1)) )
146 // dr_e : node spacing to the east
147 // Tjp1 : temperature at node j+1
148 // kap_e : arithmetic-average thermal conductivity at the face
149 // flux_e : r_e * kap_e * dT/dr|_e = r_e * kap_e * (T(j+1) - Tj) / dr_e (units: W/m)
150 const double zj = z(j);
151 const double re = 0.5 * (zj + z(j + 1));
152 const double dre = z(j + 1) - zj;
153 const double Tjp1 = x[index(0, j + 1)];
154 const double kap_e = 0.5 * (m_kappa.eval(Tj) + m_kappa.eval(Tjp1));
155 const double flux_e = re * kap_e * (Tjp1 - Tj) / dre;
156
157 // ---- Conductive flux on the west face (between j-1 and j) -------
158 // At the axis (j=0) the face radius r_w = 0, so the flux vanishes
159 // naturally — this is the symmetry BC (dT/dr = 0 at r = 0).
160 double flux_w, rw;
161 if (j == 0) {
162 rw = 0.0;
163 flux_w = 0.0;
164 } else {
165 rw = 0.5 * (z(j - 1) + zj);
166 const double Tjm1 = x[index(0, j - 1)];
167 const double drw = zj - z(j - 1);
168 const double kap_w = 0.5 * (m_kappa.eval(Tjm1) + m_kappa.eval(Tj));
169 flux_w = rw * kap_w * (Tj - Tjm1) / drw;
170 }
171
172 // ---- Finite-volume divergence ------------------------------------
173 // vol = integral of r dr over the cell = 0.5*(r_e^2 - r_w^2)
174 // div_flux approximates (1/r) d/dr(r * kappa * dT/dr) at node j.
175 const double vol = 0.5 * (re * re - rw * rw);
176 const double div_flux = (flux_e - flux_w) / vol;
177
178 // ---- Source term: Joule heating minus radiative loss -------------
179 const double source = m_sigma.eval(Tj) * E2 - m_prad.eval(Tj);
180
181 // ---- Residual (scaled by rho*cp so Newton updates are in K) ------
182 // The pseudo-transient term rdt*(T - T_prev) adds a diagonal shift
183 // during Cantera's time-stepping fallback and vanishes at steady
184 // state (rdt -> 0 once Newton converges).
185 rsd[index(0, j)] = (div_flux + source) / m_rho_cp
186 - rdt * (Tj - prevSoln(0, j));
187 diag[index(0, j)] = 1;
188 }
189}
190
191} // 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
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
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())
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