rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
bindings.cpp
Go to the documentation of this file.
1// pybind11 bindings for the rizer Cantera 1D plasma extension (_plasma1d).
2//
3// This extension is a pure C++ implementation of the Elenbaas-Heller column
4// problem, using Cantera's 1D simulation framework (Sim1D) to solve the
5// steady-state radial energy balance with a user-specified axial electric field.
6// This file contains the pybind11 bindings, which are a thin wrapper around the
7// C++ implementation in PlasmaColumnSolver.h/cpp.
8//
9// Design rule: "flat numerics only" across the Python boundary.
10// - No Cantera C++ type (Solution, Domain1D, Sim1D, ...) is ever exposed to
11// Python. This sidesteps all ABI-sharing concerns with the stock `cantera`
12// Python package; both can be loaded in the same process without conflicts.
13// - Properties enter as parallel (T, value) numpy arrays; they are converted
14// to PropertyTable objects inside the binding lambda.
15// - The converged profile leaves as a plain Python dict of numpy arrays and
16// Python scalars — no custom C++ types cross the boundary.
17//
18// Exposed API:
19// cantera_version() -> str Version of the linked libcantera
20// solve_column(...) -> dict Solve and return (r, T, sigma, kappa,
21// current, electric_field, n_points)
22
23#ifndef NOMINMAX
24#define NOMINMAX
25#endif
26#include <pybind11/pybind11.h>
27#include <pybind11/stl.h>
28#include <pybind11/numpy.h>
29
30#include "cantera/base/global.h"
31
32#include "PropertyTable.h"
33#include "CollisionModel.h"
34#include "Plasma0DReactor.h"
35#include "PlasmaColumnSolver.h"
36#include "PlasmaRates.h"
38
39#include <vector>
40
41namespace py = pybind11;
42
43namespace {
44
47std::vector<double> toVec(const py::array_t<double>& a)
48{
49 // Re-wrapping as c_style|forcecast forces a contiguous double array (copying
50 // only if `a` isn't already one), so the raw-pointer range below is always
51 // valid -- unlike raw pointer arithmetic, this does not assume `a` itself is
52 // contiguous (e.g. a strided view).
53 const py::array_t<double, py::array::c_style | py::array::forcecast> c(a);
54 return std::vector<double>(c.data(), c.data() + c.size());
55}
56
62rizer::PropertyTable makeTable(const py::array_t<double>& T,
63 const py::array_t<double>& values)
64{
65 return rizer::PropertyTable(toVec(T), toVec(values));
66}
67
68} // namespace
69
70PYBIND11_MODULE(_plasma1d, m)
71{
72 m.doc() = "rizer Cantera 1D plasma extension (ThermalPlasmaColumn1D)";
73
74 m.def("cantera_version", []() { return Cantera::version(); },
75 "Version string of the linked libcantera.");
76
77 m.def("register_plasma_rates", &rizer::registerPlasmaRates,
78 "(Re-)register the native C++ plasma reaction rates "
79 "(janev-dissociative-recombination-C2Hy, Druyvesteyn, "
80 "reverse-two-temperature-plasma) with Cantera's ReactionRateFactory. "
81 "Idempotent; replaces the Python ExtensibleRate registrations of the "
82 "same names, so mechanisms load and evaluate natively afterwards.");
83
84 // Maxwellian momentum-transfer kernel Qbar(Te) (Mitchner II-6.30), exposed for
85 // validation against the Python TabulatedSpeciesCrossSection integral. Vectorized
86 // over Te. energy_J/sigma_m2 are the tabulated cross-section (zero outside range).
87 // Lambda parameters (a plain, non-Doxygen comment: this lambda has no
88 // separate declaration for \param to attach to, so a doc-comment here
89 // would be misattached to the enclosing PYBIND11_MODULE function):
90 // energy_J Tabulated collision energy grid [J], strictly increasing
91 // sigma_m2 Tabulated cross section Q(energy_J) [m^2], same length as
92 // energy_J
93 // Te Electron temperatures to evaluate Qbar(Te) at [K]
94 // x_max Upper limit of the reduced-energy (E/kTe) integration [-]
95 // N Number of quadrature points for the integration [-]
96 m.def("mean_cross_section",
97 [](py::array_t<double> energy_J, py::array_t<double> sigma_m2,
98 py::array_t<double> Te, double x_max, int N) {
99 std::vector<double> e = toVec(energy_J);
100 std::vector<double> s = toVec(sigma_m2);
101 std::vector<double> t = toVec(Te);
102 std::vector<double> q =
104 return py::array_t<double>(q.size(), q.data());
105 },
106 py::arg("energy_J"), py::arg("sigma_m2"), py::arg("Te"),
107 py::arg("x_max") = 20.0, py::arg("N") = 1000,
108 "Maxwellian-averaged momentum-transfer cross section Qbar(Te) [m^2] "
109 "(Mitchner II-6.30), interpolating Q from (energy_J [J], sigma_m2 [m^2]).");
110
111 // Native C++ two-temperature 0D plasma reactor: a persistent evaluator that the
112 // Python driver (scipy + NRP circuit) calls per RHS. Flat numerics only; the
113 // per-species cross-section model is passed once at construction as ragged lists.
114 py::class_<rizer::Plasma0DReactor>(m, "Reactor0D")
115 // Construct a Reactor0D from a Cantera mechanism and an optional
116 // per-species collision model. Lambda parameters (plain comment --
117 // see the note at mean_cross_section above for why this isn't `//!`):
118 // mech Cantera mechanism (YAML) path
119 // phase Cantera mechanism (YAML) phase
120 // reacting true: include finite-rate chemistry source terms;
121 // false: frozen composition
122 // xsec_energy_J Per-species tabulated cross-section energy grids
123 // [J] (list of 1-D arrays, or None per species),
124 // length nsp
125 // xsec_sigma_m2 Per-species tabulated cross-section values [m^2]
126 // (list of 1-D arrays, or None per species),
127 // length nsp
128 // radius_m Per-species hard-sphere effective radius [m]
129 // (NaN if not used), length nsp
130 // ion_Z Per-species ion charge number [-] (0 for
131 // neutrals), length nsp
132 // Te_min Collision-model Te grid lower bound [K]
133 // Te_max Collision-model Te grid upper bound [K]
134 // Te_n Collision-model Te grid point count [-]
135 // (log-spaced)
136 // spitzer true: apply the Spitzer e-e correction to the
137 // collision model
138 // gap Inter-electrode gap [m] (optional; used in the
139 // polytropic volume equation)
140 .def(py::init([](const std::string& mech, const std::string& phase,
141 bool reacting, py::list xsec_energy_J,
142 py::list xsec_sigma_m2, py::array_t<double> radius_m,
143 py::array_t<int> ion_Z, double Te_min, double Te_max,
144 int Te_n, bool spitzer, double gap) {
146 cfg.mech = mech; cfg.phase = phase; cfg.reacting = reacting;
147 cfg.Te_min = Te_min; cfg.Te_max = Te_max;
148 cfg.Te_n = static_cast<std::size_t>(std::max(2, Te_n));
149 cfg.spitzer = spitzer; cfg.gap = gap;
150 const std::size_t nsp = static_cast<std::size_t>(xsec_energy_J.size());
151 cfg.specs.resize(nsp);
152 auto r = radius_m.unchecked<1>();
153 auto z = ion_Z.unchecked<1>();
154 for (std::size_t k = 0; k < nsp; k++) {
155 cfg.specs[k].radius = r(static_cast<py::ssize_t>(k));
156 cfg.specs[k].Z = z(static_cast<py::ssize_t>(k));
157 py::object eo = xsec_energy_J[k];
158 py::object so = xsec_sigma_m2[k];
159 if (!eo.is_none() && !so.is_none()) {
160 cfg.specs[k].energy_J = toVec(eo.cast<py::array_t<double>>());
161 cfg.specs[k].sigma_m2 = toVec(so.cast<py::array_t<double>>());
162 }
163 }
164 return std::make_unique<rizer::Plasma0DReactor>(cfg);
165 }),
166 py::arg("mech"), py::arg("phase"), py::arg("reacting") = true,
167 py::arg("xsec_energy_J"), py::arg("xsec_sigma_m2"),
168 py::arg("radius_m"), py::arg("ion_Z"),
169 py::arg("Te_min") = 300.0, py::arg("Te_max") = 1.0e5,
170 py::arg("Te_n") = 400, py::arg("spitzer") = true, py::arg("gap") = 0.0)
171 // Electrical conductivity [S/m] at (Tg, Te, Y, rho). Lambda parameters
172 // (plain comment, see the note at mean_cross_section above):
173 // self The bound Reactor0D instance
174 // Tg Gas (heavy-species) temperature [K]
175 // Te Electron temperature [K]
176 // Y Species mass fractions [-], length nsp
177 // rho Mass density [kg/m^3]
178 .def("conductivity",
179 [](rizer::Plasma0DReactor& self, double Tg, double Te,
180 py::array_t<double> Y, double rho) {
181 auto y = toVec(Y);
182 return self.conductivity(Tg, Te, y.data(), rho);
183 },
184 py::arg("Tg"), py::arg("Te"), py::arg("Y"), py::arg("rho"),
185 "Electrical conductivity [S/m] at (Tg, Te, Y, rho).")
186 // Electron number density [1/m^3] at (Tg, Te, Y, rho). Lambda
187 // parameters (plain comment, see the note at mean_cross_section above):
188 // self The bound Reactor0D instance
189 // Tg Gas (heavy-species) temperature [K]
190 // Te Electron temperature [K]
191 // Y Species mass fractions [-], length nsp
192 // rho Mass density [kg/m^3]
193 .def("electron_density",
194 [](rizer::Plasma0DReactor& self, double Tg, double Te,
195 py::array_t<double> Y, double rho) {
196 auto y = toVec(Y);
197 return self.electronDensity(Tg, Te, y.data(), rho);
198 },
199 py::arg("Tg"), py::arg("Te"), py::arg("Y"), py::arg("rho"),
200 "Electron number density [1/m^3] at (Tg, Te, Y, rho).")
201 // 0D RHS dydt = [dTg, dTe, dV, dY...] for state [Tg, Te, V, Y...];
202 // rho = mass/V; polytropic_index=inf -> constant volume. Lambda
203 // parameters (plain comment, see the note at mean_cross_section above):
204 // self The bound Reactor0D instance
205 // Tg Gas (heavy-species) temperature [K]
206 // Te Electron temperature [K]
207 // V Volume [m^3]
208 // Y Species mass fractions [-], length nsp
209 // E Applied electric field [V/m]
210 // mass Fixed total mass [kg] (rho = mass/V)
211 // p_ext External pressure [Pa] (drives the polytropic
212 // volume equation)
213 // polytropic_index Polytropic index [-] (inf = constant volume)
214 .def("rhs",
215 [](rizer::Plasma0DReactor& self, double Tg, double Te, double V,
216 py::array_t<double> Y, double E, double mass, double p_ext,
217 double polytropic_index) {
218 auto y = toVec(Y);
219 std::vector<double> dydt(3 + self.nSpecies(), 0.0);
220 self.rhs(Tg, Te, V, y.data(), E, mass, p_ext, polytropic_index,
221 dydt.data());
222 return py::array_t<double>(dydt.size(), dydt.data());
223 },
224 py::arg("Tg"), py::arg("Te"), py::arg("V"), py::arg("Y"), py::arg("E"),
225 py::arg("mass"), py::arg("p_ext"), py::arg("polytropic_index"),
226 "0D RHS dydt = [dTg, dTe, dV, dY...] for state [Tg, Te, V, Y...]; "
227 "rho = mass/V; polytropic_index=inf -> constant volume.")
228 .def_property_readonly("species",
229 [](rizer::Plasma0DReactor& self) { return self.speciesNames(); })
230 // Power / Maxwellian-condition breakdown from the most recent rhs()
231 // call (rizer.plasma.constant_mass_reactor.ConstantMassPlasmaReactorOde's
232 // stored P_elastic/P_inelastic/P_chemical/P_chemical_e/P_Joule/
233 // cond_maxwell_1/cond_maxwell_2 attributes, natively).
234 .def_property_readonly("P_elastic",
235 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_elastic; })
236 .def_property_readonly("P_inelastic",
237 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_inelastic; })
238 .def_property_readonly("P_chemical",
239 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_chemical; })
240 .def_property_readonly("P_chemical_e",
241 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_chemical_e; })
242 .def_property_readonly("P_Joule",
243 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_Joule; })
244 .def_property_readonly("cond_maxwell_1",
245 [](rizer::Plasma0DReactor& self) { return self.diagnostics().cond_maxwell_1; })
246 .def_property_readonly("cond_maxwell_2",
247 [](rizer::Plasma0DReactor& self) { return self.diagnostics().cond_maxwell_2; });
248
249 // Define the solve_column() binding.
250 // The lambda converts the Python inputs into C++ types, calls solveColumn(),
251 // and converts the result back into a Python dict of numpy arrays and scalars.
252 // Lambda parameters (plain comment, see the note at mean_cross_section
253 // above -- this lambda has no separate declaration for \param to attach
254 // to):
255 // R Outer wall radius [m]
256 // electric_field Axial E field [V/m]
257 // T_wall Wall temperature [K]
258 // sigma_T Temperature values [K], 1-D array, strictly increasing
259 // sigma_v Corresponding sigma values [S/m], 1-D array
260 // kappa_T Temperature values [K], 1-D array, strictly increasing
261 // kappa_v Corresponding kappa values [W/(m*K)], 1-D array
262 // prad_T Temperature values [K], 1-D array, strictly increasing,
263 // or None
264 // prad_v Corresponding P_rad values [W/m^3], 1-D array, or None
265 // n_points Initial uniform grid count
266 // T_center_guess Parabolic-seed centerline temperature [K]
267 // rho_cp rho*cp [J/m^3/K] for pseudo-transient scaling
268 // loglevel Sim1D verbosity (0 = silent)
269 // refine_grid Enable Cantera adaptive grid refinement
270 // refine_ratio Refiner: max ratio of adjacent cell widths
271 // refine_slope Refiner: max fractional slope change per cell
272 // refine_curve Refiner: max fractional curvature per cell
273 // refine_prune Refiner: remove points below this threshold
274 // init_r Optional seed radii [m], strictly increasing, 0..R, or
275 // None
276 // init_T Optional seed temperatures [K] at init_r, or None
277 m.def("solve_column",
278 [](double R, double electric_field, double T_wall, py::array_t<double> sigma_T,
279 py::array_t<double> sigma_v, py::array_t<double> kappa_T,
280 py::array_t<double> kappa_v, py::object prad_T, py::object prad_v,
281 std::size_t n_points, double T_center_guess, double rho_cp, int loglevel,
282 bool refine_grid, double refine_ratio, double refine_slope,
283 double refine_curve, double refine_prune, py::object init_r,
284 py::object init_T)
285 {
286 // Convert the input numpy arrays into PropertyTable objects.
287 rizer::PropertyTable sigma = makeTable(sigma_T, sigma_v);
288 rizer::PropertyTable kappa = makeTable(kappa_T, kappa_v);
289 // For the optional radiation property, check if the inputs are None.
290 // If either is None, create a constant PropertyTable with value 0.0.
291 // Otherwise, create a PropertyTable from the provided arrays.
293 (prad_T.is_none() || prad_v.is_none())
295 : makeTable(prad_T.cast<py::array_t<double>>(),
296 prad_v.cast<py::array_t<double>>());
297 // Set up the solver options from the provided parameters.
299 opts.n_points = n_points;
300 opts.T_center_guess = T_center_guess;
301 opts.rho_cp = rho_cp;
302 opts.loglevel = loglevel;
303 opts.refine_grid = refine_grid;
304 opts.refine_ratio = refine_ratio;
305 opts.refine_slope = refine_slope;
306 opts.refine_curve = refine_curve;
307 opts.refine_prune = refine_prune;
308
309 // If the user provided an initial seed profile,
310 // convert the numpy arrays into std::vectors.
311 if (!init_r.is_none() && !init_T.is_none()) {
312 auto ir = init_r.cast<py::array_t<double>>();
313 auto it = init_T.cast<py::array_t<double>>();
314 auto ira = ir.unchecked<1>();
315 auto ita = it.unchecked<1>();
316 opts.init_r.resize(static_cast<std::size_t>(ira.shape(0)));
317 opts.init_T.resize(static_cast<std::size_t>(ita.shape(0)));
318 for (py::ssize_t i = 0; i < ira.shape(0); i++) {
319 opts.init_r[static_cast<std::size_t>(i)] = ira(i);
320 }
321 for (py::ssize_t i = 0; i < ita.shape(0); i++) {
322 opts.init_T[static_cast<std::size_t>(i)] = ita(i);
323 }
324 }
325
326 // Call the C++ solver function, releasing the GIL since it is pure C++.
327 // The result is a ColumnResult struct containing the solution.
329 {
330 py::gil_scoped_release release; // solve is pure C++
331 res = rizer::solveColumn(R, electric_field, T_wall,
332 sigma, kappa, prad, opts);
333 }
334
335 // Convert the result into a Python dict of numpy arrays and scalars.
336 py::dict out;
337 out["r"] = py::array_t<double>(res.r.size(), res.r.data());
338 out["T"] = py::array_t<double>(res.T.size(), res.T.data());
339 out["sigma"] = py::array_t<double>(res.sigma.size(), res.sigma.data());
340 out["kappa"] = py::array_t<double>(res.kappa.size(), res.kappa.data());
341 out["current"] = res.current;
342 out["electric_field"] = res.electric_field;
343 out["n_points"] = res.n_points;
344 return out;
345 },
346 py::arg("R"), py::arg("electric_field"), py::arg("T_wall"),
347 py::arg("sigma_T"), py::arg("sigma_v"),
348 py::arg("kappa_T"), py::arg("kappa_v"),
349 py::arg("prad_T") = py::none(), py::arg("prad_v") = py::none(),
350 py::arg("n_points") = 41, py::arg("T_center_guess") = 10000.0,
351 py::arg("rho_cp") = 1.0e3, py::arg("loglevel") = 0,
352 py::arg("refine_grid") = true, py::arg("refine_ratio") = 4.0,
353 py::arg("refine_slope") = 0.05, py::arg("refine_curve") = 0.10,
354 py::arg("refine_prune") = 0.0,
355 py::arg("init_r") = py::none(), py::arg("init_T") = py::none(),
356 "Solve the steady radial Elenbaas-Heller column.\n\n"
357 "Assembles [Empty1D | ThermalPlasmaColumn1D | Empty1D] in a Cantera\n"
358 "Sim1D, solves with Newton + pseudo-transient fallback, and optionally\n"
359 "refines the radial grid adaptively.\n\n"
360 "Parameters\n"
361 "----------\n"
362 "R : outer wall radius [m]\n"
363 "electric_field: axial E field [V/m]\n"
364 "T_wall : wall temperature [K]\n"
365 "sigma_T/v : tabulated sigma(T) [S/m] — 1-D arrays of equal length\n"
366 "kappa_T/v : tabulated kappa(T) [W/m/K]\n"
367 "prad_T/v : tabulated P_rad(T) [W/m^3], or None for no radiation\n"
368 "n_points : initial uniform grid count\n"
369 "T_center_guess: parabolic-seed centerline temperature [K]\n"
370 "rho_cp : rho*cp [J/m^3/K] for pseudo-transient scaling\n"
371 "loglevel : Sim1D verbosity (0 = silent)\n"
372 "refine_grid : enable adaptive grid refinement\n"
373 "refine_ratio/slope/curve/prune: Refiner parameters\n"
374 "init_r/T : optional seed profile T(r); overrides the parabola\n\n"
375 "Returns\n"
376 "-------\n"
377 "dict with keys r, T, sigma, kappa (numpy arrays) and\n"
378 "current [A], electric_field [V/m], n_points (scalars).");
379
380 // Advance the transient, two-temperature, finite-rate reacting 1-D radial
381 // plasma channel (PlasmaChannel1D) from t=0 to t = dt*n_steps. Lambda
382 // parameters (plain comment, see the note at mean_cross_section above --
383 // this lambda has no separate declaration for \param to attach to):
384 // mech Cantera mechanism (YAML) path
385 // phase Cantera mechanism (YAML) phase
386 // n_points Radial grid points [-] (ignored if `grid` is given)
387 // R_max Outer (wall) radius [m]
388 // rho Fixed mass density [kg/m^3] (constant-volume model)
389 // Tg0 Initial/ambient gas temperature [K] (uniform default
390 // if initTg_r/v not given)
391 // Te0 Initial/ambient electron temperature [K] (uniform
392 // default if initTe_r/v not given)
393 // Y0 Initial + ambient mass fractions [-], length nsp
394 // sigma_T Legacy sigma(Te) table: temperature grid [K], or None
395 // sigma_v Legacy sigma(Te) table: conductivity values [S/m], or
396 // None
397 // nu_E Legacy constant electron-heavy elastic exchange
398 // frequency [1/s]
399 // electric_field Constant applied axial field E [V/m] (superseded by
400 // Efield_t/v)
401 // kappa_T Gas thermal conductivity table: temperature grid [K],
402 // or None
403 // kappa_v Gas thermal conductivity table: kappa(Tg) values
404 // [W/m/K], or None
405 // kappa_e_T Electron thermal conductivity table: temperature grid
406 // [K], or None
407 // kappa_e_v Electron thermal conductivity table: kappa_e(Te)
408 // values [W/m/K], or None
409 // D_species Constant scalar species mass diffusivity [m^2/s]
410 // (0 = frozen transport)
411 // reacting true: include finite-rate chemistry source terms;
412 // false: frozen composition
413 // T_amb Wall/ambient temperature [K] (Dirichlet BC)
414 // initTg_r Optional initial Tg(r) profile: radii [m], or None
415 // initTg_v Optional initial Tg(r) profile: temperatures [K], or
416 // None
417 // initTe_r Optional initial Te(r) profile: radii [m], or None
418 // initTe_v Optional initial Te(r) profile: temperatures [K], or
419 // None
420 // grid Optional explicit radial mesh [m] (overrides
421 // n_points/R_max), or None
422 // nu_m Legacy constant electron momentum-transfer frequency
423 // [1/s] (>0 activates sigma = n_e e^2/(m_e nu_m))
424 // initY_r Optional initial composition profile: radii [m],
425 // length nr, or None
426 // initY_v Optional initial composition profile: mass fractions
427 // [-], row-major shape [nr, nsp], or None
428 // Efield_t Optional field-vs-time schedule: time grid [s]
429 // (overrides electric_field), or None
430 // Efield_v Optional field-vs-time schedule: field values [V/m],
431 // or None
432 // nu_m_T Optional Te-dependent nu_m(Te) table: temperature
433 // grid [K], or None (empty -> scalar nu_m)
434 // nu_m_v Optional Te-dependent nu_m(Te) table: frequency
435 // values [1/s], or None
436 // xsec_energy_J Composition-resolved collision model: per-species
437 // cross-section energy grids [J] (list of 1-D arrays
438 // or None per species), or None to disable
439 // xsec_sigma_m2 Composition-resolved collision model: per-species
440 // cross-section values [m^2] (list of 1-D arrays or
441 // None per species), or None to disable
442 // radius_m Composition-resolved collision model: per-species
443 // hard-sphere effective radius [m] (NaN if unused), or
444 // None to disable
445 // ion_Z Composition-resolved collision model: per-species
446 // ion charge number [-] (0 for neutrals), or None to
447 // disable
448 // Te_min Collision-model Te grid lower bound [K] (used only
449 // if the collision model is enabled)
450 // Te_max Collision-model Te grid upper bound [K] (used only
451 // if the collision model is enabled)
452 // Te_n Collision-model Te grid point count [-] (log-spaced;
453 // used only if the collision model is enabled)
454 // spitzer true: apply the Spitzer e-e correction to the
455 // collision model
456 // dt Output cadence / initial step size [s]
457 // n_steps Number of output steps [-] (t_total = dt*n_steps)
458 // record_every Record a frame every N output steps [-]
459 // loglevel Diagnostic verbosity (0 = silent)
460 // integrator "newton" (adaptive Backward-Euler) or "bdf" (stiff
461 // CVODE)
462 m.def("solve_channel_transient",
463 [](const std::string& mech, const std::string& phase, std::size_t n_points,
464 double R_max, double rho, double Tg0, double Te0, py::array_t<double> Y0,
465 py::object sigma_T, py::object sigma_v, double nu_E, double electric_field,
466 py::object kappa_T, py::object kappa_v, py::object kappa_e_T,
467 py::object kappa_e_v, double D_species, bool reacting, double T_amb,
468 py::object initTg_r, py::object initTg_v, py::object initTe_r,
469 py::object initTe_v, py::object grid, double nu_m, py::object initY_r,
470 py::object initY_v, py::object Efield_t, py::object Efield_v,
471 py::object nu_m_T, py::object nu_m_v, py::object xsec_energy_J,
472 py::object xsec_sigma_m2, py::object radius_m, py::object ion_Z,
473 double Te_min, double Te_max, int Te_n, bool spitzer, double dt,
474 std::size_t n_steps, std::size_t record_every, int loglevel,
475 const std::string& integrator)
476 {
477 // Every optional property/profile table below (sigma, kappa, kappa_e,
478 // nu_m, grid, Et) is passed from Python as `None` when disabled;
479 // ChannelOptions/PlasmaChannelConfig instead use empty vectors as
480 // their "disabled" sentinel (see makeTable() in PlasmaChannel1D.cpp),
481 // so this converts the Python convention to the C++ one in one place.
482 auto optVec = [](py::object obj) -> std::vector<double> {
483 if (obj.is_none()) return {};
484 return toVec(obj.cast<py::array_t<double>>());
485 };
486
487 rizer::ChannelOptions options;
488 auto& cfg = options.cfg;
489 cfg.mech = mech;
490 cfg.phase = phase;
491 cfg.n_points = n_points;
492 cfg.R_max = R_max;
493 cfg.rho = rho;
494 cfg.electric_field = electric_field;
495 cfg.nu_E = nu_E;
496 cfg.D_species = D_species;
497 cfg.reacting = reacting;
498 cfg.T_amb = T_amb;
499 cfg.Y0 = toVec(Y0);
500 cfg.sigma_v = optVec(sigma_v);
501 cfg.sigma_T = optVec(sigma_T);
502 cfg.kappa_v = optVec(kappa_v);
503 cfg.kappa_T = optVec(kappa_T);
504 cfg.kappa_e_v = optVec(kappa_e_v);
505 cfg.kappa_e_T = optVec(kappa_e_T);
506 // Initial profiles: explicit Tg(r)/Te(r) if given, else uniform Tg0/Te0.
507 cfg.initTg_v = initTg_v.is_none() ? std::vector<double>{Tg0, Tg0}
508 : optVec(initTg_v);
509 cfg.initTg_r = initTg_r.is_none() ? std::vector<double>{0.0, R_max}
510 : optVec(initTg_r);
511 cfg.initTe_v = initTe_v.is_none() ? std::vector<double>{Te0, Te0}
512 : optVec(initTe_v);
513 cfg.initTe_r = initTe_r.is_none() ? std::vector<double>{0.0, R_max}
514 : optVec(initTe_r);
515 cfg.nu_m = nu_m;
516 // Te-dependent nu_m(Te) table (empty -> scalar nu_m).
517 cfg.nu_m_T = optVec(nu_m_T);
518 cfg.nu_m_v = optVec(nu_m_v);
519 // Explicit radial mesh (empty -> uniform).
520 cfg.grid = optVec(grid);
521 // Field-vs-time table (empty -> constant).
522 cfg.Et_t = optVec(Efield_t);
523 cfg.Et_v = optVec(Efield_v);
524 // Initial composition profile: initY_r (nr) + initY_v (2D [nr, nsp]).
525 if (!initY_r.is_none() && !initY_v.is_none()) {
526 cfg.initY_r = toVec(initY_r.cast<py::array_t<double>>());
527 auto initYArray = initY_v.cast<py::array_t<double>>();
528 auto initYView = initYArray.unchecked<2>();
529 cfg.initY_v.resize(
530 static_cast<std::size_t>(initYView.shape(0) * initYView.shape(1)));
531 for (py::ssize_t i = 0; i < initYView.shape(0); i++) {
532 for (py::ssize_t k = 0; k < initYView.shape(1); k++) {
533 cfg.initY_v[
534 static_cast<std::size_t>(i * initYView.shape(1) + k)] =
535 initYView(i, k);
536 }
537 }
538 }
539 // Composition-resolved collision model (empty -> legacy sigma/nu_m/nu_E
540 // closure above). xsec_energy_J/xsec_sigma_m2/radius_m/ion_Z mirror the
541 // Reactor0D binding exactly, one entry per species: all four must be
542 // given together (or all omitted) since a per-species spec needs its
543 // radius/Z slot even when it carries no tabulated cross-section.
544 const bool any_collision_arg = !xsec_energy_J.is_none()
545 || !xsec_sigma_m2.is_none() || !radius_m.is_none() || !ion_Z.is_none();
546 if (any_collision_arg) {
547 if (xsec_energy_J.is_none() || xsec_sigma_m2.is_none()
548 || radius_m.is_none() || ion_Z.is_none()) {
549 throw std::invalid_argument(
550 "solve_channel_transient: xsec_energy_J, xsec_sigma_m2, "
551 "radius_m, and ion_Z must all be given together (or all "
552 "omitted) to enable the composition-resolved collision "
553 "model.");
554 }
555 py::list e_list = xsec_energy_J.cast<py::list>();
556 py::list s_list = xsec_sigma_m2.cast<py::list>();
557 auto radius_arr = radius_m.cast<py::array_t<double>>();
558 auto ion_z_arr = ion_Z.cast<py::array_t<int>>();
559 const std::size_t nsp_specs = static_cast<std::size_t>(e_list.size());
560 cfg.specs.resize(nsp_specs);
561 auto r = radius_arr.unchecked<1>();
562 auto z = ion_z_arr.unchecked<1>();
563 for (std::size_t k = 0; k < nsp_specs; k++) {
564 cfg.specs[k].radius = r(static_cast<py::ssize_t>(k));
565 cfg.specs[k].Z = z(static_cast<py::ssize_t>(k));
566 py::object eo = e_list[k];
567 py::object so = s_list[k];
568 if (!eo.is_none() && !so.is_none()) {
569 cfg.specs[k].energy_J = toVec(eo.cast<py::array_t<double>>());
570 cfg.specs[k].sigma_m2 = toVec(so.cast<py::array_t<double>>());
571 }
572 }
573 cfg.Te_min = Te_min;
574 cfg.Te_max = Te_max;
575 cfg.Te_n = static_cast<std::size_t>(Te_n);
576 cfg.spitzer = spitzer;
577 }
578
579 options.dt = dt;
580 options.n_steps = n_steps;
581 options.record_every = record_every;
582 options.loglevel = loglevel;
583 options.integrator = integrator;
584
585 // A transient BDF/Newton solve over many steps can run for seconds
586 // to minutes; release the GIL for its duration so other Python
587 // threads (and, e.g., a Ctrl-C from the interpreter) aren't blocked.
588 rizer::ChannelHistory history;
589 {
590 py::gil_scoped_release release;
591 history = rizer::solveChannelTransient(options);
592 }
593
594 py::dict out;
595 out["t"] = py::array_t<double>(history.t.size(), history.t.data());
596 out["r"] = py::array_t<double>(history.r.size(), history.r.data());
597 // Shaped [nframes, npts] and [nframes, npts, nsp].
598 out["Tg"] = py::array_t<double>(
599 {history.nframes, history.npts}, history.Tg.data());
600 out["Te"] = py::array_t<double>(
601 {history.nframes, history.npts}, history.Te.data());
602 out["ne"] = py::array_t<double>(
603 {history.nframes, history.npts}, history.ne.data());
604 out["Y"] = py::array_t<double>(
605 {history.nframes, history.npts, history.nsp}, history.Y.data());
606 out["species"] = history.species;
607 out["nframes"] = history.nframes;
608 out["npts"] = history.npts;
609 out["nsp"] = history.nsp;
610 return out;
611 },
612 py::arg("mech"), py::arg("phase"), py::arg("n_points"),
613 py::arg("R_max"), py::arg("rho"), py::arg("Tg0"), py::arg("Te0"),
614 py::arg("Y0"), py::arg("sigma_T") = py::none(),
615 py::arg("sigma_v") = py::none(), py::arg("nu_E") = 0.0,
616 py::arg("electric_field") = 0.0,
617 py::arg("kappa_T") = py::none(), py::arg("kappa_v") = py::none(),
618 py::arg("kappa_e_T") = py::none(), py::arg("kappa_e_v") = py::none(),
619 py::arg("D_species") = 0.0, py::arg("reacting") = true,
620 py::arg("T_amb") = 300.0,
621 py::arg("initTg_r") = py::none(), py::arg("initTg_v") = py::none(),
622 py::arg("initTe_r") = py::none(), py::arg("initTe_v") = py::none(),
623 py::arg("grid") = py::none(), py::arg("nu_m") = 0.0,
624 py::arg("initY_r") = py::none(), py::arg("initY_v") = py::none(),
625 py::arg("Efield_t") = py::none(), py::arg("Efield_v") = py::none(),
626 py::arg("nu_m_T") = py::none(), py::arg("nu_m_v") = py::none(),
627 py::arg("xsec_energy_J") = py::none(), py::arg("xsec_sigma_m2") = py::none(),
628 py::arg("radius_m") = py::none(), py::arg("ion_Z") = py::none(),
629 py::arg("Te_min") = 300.0, py::arg("Te_max") = 1.0e5,
630 py::arg("Te_n") = 400, py::arg("spitzer") = true,
631 py::arg("dt") = 1.0e-9,
632 py::arg("n_steps") = 1000, py::arg("record_every") = 1,
633 py::arg("loglevel") = 0, py::arg("integrator") = "newton",
634 "Advance the transient, two-temperature, finite-rate reacting 1-D radial\n"
635 "plasma channel (PlasmaChannel1D) from t=0 to t=dt*n_steps.\n\n"
636 "Parameters\n"
637 "----------\n"
638 "mech, phase : Cantera mechanism (YAML) path and phase name\n"
639 "n_points : radial grid points [-] (ignored if `grid` is given)\n"
640 "R_max : outer (wall) radius [m]\n"
641 "rho : fixed mass density [kg/m^3] (constant-volume model)\n"
642 "Tg0, Te0 : initial gas/electron temperature [K] (uniform default\n"
643 " if initTg_r/v or initTe_r/v are not given)\n"
644 "Y0 : initial + ambient mass fractions [-], length nsp\n"
645 "sigma_T/v, nu_E, nu_m, nu_m_T/v : legacy Joule/exchange closure -- sigma\n"
646 " from a sigma(Te) table, or n_e e^2/(m_e nu_m) from the\n"
647 " local n_e if nu_m>0 or a nu_m(Te) table is given, plus a\n"
648 " constant elastic frequency nu_E. Mutually exclusive with\n"
649 " xsec_energy_J/xsec_sigma_m2/radius_m/ion_Z below -- an\n"
650 " error is raised if both are supplied.\n"
651 "electric_field: constant applied field E [V/m] (superseded by Efield_t/v)\n"
652 "kappa_T/v : tabulated gas thermal conductivity kappa(Tg) [K],[W/m/K]\n"
653 "kappa_e_T/v : tabulated electron thermal conductivity kappa_e(Te) "
654 "[K],[W/m/K]\n"
655 "D_species : constant scalar species mass diffusivity [m^2/s] "
656 "(0 = frozen)\n"
657 "reacting : include finite-rate chemistry source terms\n"
658 "T_amb : wall/ambient temperature [K] (Dirichlet BC)\n"
659 "initTg_r/v : initial radial profile Tg(r) [m],[K]\n"
660 "initTe_r/v : initial radial profile Te(r) [m],[K]\n"
661 "grid : explicit radial mesh [m] (overrides n_points/R_max)\n"
662 "initY_r/v : initial radial composition profile: radii [m] (length nr)\n"
663 " and row-major mass fractions [-] (shape [nr, nsp])\n"
664 "Efield_t/v : field-vs-time table [s],[V/m] (overrides electric_field)\n"
665 "xsec_energy_J, xsec_sigma_m2, radius_m, ion_Z :\n"
666 " composition-resolved collision model (preferred over the\n"
667 " legacy closure above): per-species lists/arrays, in\n"
668 " species-index order (length nsp), exactly matching the\n"
669 " Reactor0D binding. xsec_energy_J/xsec_sigma_m2 are lists\n"
670 " of 1D arrays (or None per species) giving a tabulated\n"
671 " cross-section Q(E) [J],[m^2]; radius_m is a hard-sphere\n"
672 " effective radius [m] (NaN if not used); ion_Z is the ion\n"
673 " charge number (0 for neutrals). All four must be given\n"
674 " together (or all omitted).\n"
675 "Te_min/Te_max/Te_n : collision-model Te grid [K],[K],[-] (log-spaced)\n"
676 "spitzer : apply the Spitzer e-e correction to the collision model\n"
677 "dt : output cadence / initial step [s]\n"
678 "n_steps : number of output steps [-] (dt*n_steps = total time [s])\n"
679 "record_every : record a frame every N output steps [-]\n"
680 "loglevel : diagnostic verbosity (0 = silent)\n"
681 "integrator : \"newton\" (adaptive Backward-Euler) or \"bdf\" "
682 "(stiff CVODE)\n\n"
683 "Returns\n"
684 "-------\n"
685 "dict with keys:\n"
686 " t : recorded times [s], shape [nframes]\n"
687 " r : radial grid [m], shape [npts]\n"
688 " Tg : gas temperature [K], shape [nframes, npts]\n"
689 " Te : electron temperature [K], shape [nframes, npts]\n"
690 " ne : electron number density [1/m^3], shape [nframes, npts]\n"
691 " Y : species mass fractions [-], shape [nframes, npts, nsp]\n"
692 " species : species names, length nsp (order matches the Y axis)\n"
693 " nframes, npts, nsp : array-shape scalars [-]");
694}
PYBIND11_MODULE(_plasma1d, m)
Definition bindings.cpp:70
static std::vector< double > meanCrossSectionTable(const std::vector< double > &energy_J, const std::vector< double > &sigma_m2, const std::vector< double > &Te, double x_max=20.0, int N=1000)
Tabulated variant of meanCrossSection, evaluated at each Te in the input vector.
Native C++ two-temperature 0D plasma reactor (stateless per-eval evaluator).
std::size_t nSpecies() const
double electronDensity(double Tg, double Te, const double *Y, double rho) const
Electron number density [1/m^3].
std::vector< std::string > speciesNames() const
void rhs(double Tg, double Te, double V, const double *Y, double E, double mass, double p_ext, double polytropic_index, double *dydt) const
Full 0D RHS.
const ReactorRHS::Diagnostics & diagnostics() const
Power / Maxwellian-condition breakdown from the most recent rhs() call (see ReactorRHS::Diagnostics) ...
double conductivity(double Tg, double Te, const double *Y, double rho) const
Electrical conductivity [S/m] at state .
static PropertyTable constant(double value)
A two-point constant table returning value for any T.
string version()
ChannelHistory solveChannelTransient(const ChannelOptions &opts)
Build a PlasmaChannel1D from opts.cfg, advance it from t=0 to [s] with the requested backend (opts....
void registerPlasmaRates()
Register the custom plasma reaction rates with Cantera's ReactionRateFactory.
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.
std::vector< double > ne
Electron number density [1/m^3], row-major [nframes, npts].
std::size_t npts
Number of radial grid points [-].
std::vector< double > t
Recorded times [s], length nframes.
std::vector< std::string > species
Species names, length nsp (order matches the Y axis).
std::size_t nsp
Number of species [-].
std::vector< double > Y
Species mass fractions [-], row-major [nframes, npts, nsp].
std::vector< double > r
Radial grid [m], length npts.
std::size_t nframes
Number of recorded time frames [-].
std::vector< double > Tg
Gas temperature [K], row-major [nframes, npts].
std::vector< double > Te
Electron temperature [K], row-major [nframes, npts].
int loglevel
Diagnostic verbosity [-] (0 = silent; >0 prints progress).
std::string integrator
Time-integration backend:
std::size_t n_steps
Number of output steps [-] (dt*n_steps = total time [s]).
PlasmaChannelConfig cfg
Domain physics + initial/boundary state.
double dt
Output cadence / initial step [s].
std::size_t record_every
Record a frame every N output steps [-].
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.
bool spitzer
Use Spitzer (Coulomb) conductivity contribution when true.
double gap
[m], for the polytropic volume equation (optional)
std::size_t Te_n
Number of points in the tabulated Te grid [-].
double Te_max
Upper bound of the tabulated electron-temperature grid [K].
bool reacting
If false, disables chemistry source terms in the RHS.
std::vector< CollisionModel::SpeciesSpec > specs
Per-species cross-section model (cross-section data / radius / ion Z).
std::string phase
Mechanism file/URI, and phase name within it.
double Te_min
Lower bound of the tabulated electron-temperature grid [K].
double cond_maxwell_1
nu_ee/m_e [1/kg/s] (Mitchner VIII-3.8, term 1)
Definition ReactorRHS.h:49
double P_Joule
Joule heating power [W/m^3].
Definition ReactorRHS.h:48
double P_chemical_e
Electron chemical power [W/m^3].
Definition ReactorRHS.h:47
double P_chemical
Heavy-species chemical power [W/m^3].
Definition ReactorRHS.h:46
double P_inelastic
Inelastic electron-impact-reaction power [W/m^3].
Definition ReactorRHS.h:45
double P_elastic
Elastic electron-heavy exchange power [W/m^3].
Definition ReactorRHS.h:44
double cond_maxwell_2
sum_h nu_eh/m_h [1/kg/s] (Mitchner VIII-3.8, term 2)
Definition ReactorRHS.h:50