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
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 // species_h0k Per-species standard enthalpy of formation at 0 K
141 // [J/kmol], length nsp; required when `reacting` and
142 // the mechanism has an electron-impact reaction (see
143 // rizer.kinetics.electron_reactions)
144 .def(py::init([](const std::string& mech, const std::string& phase,
145 bool reacting, py::list xsec_energy_J,
146 py::list xsec_sigma_m2, py::array_t<double> radius_m,
147 py::array_t<int> ion_Z, double Te_min, double Te_max,
148 int Te_n, bool spitzer, double gap,
149 py::array_t<double> species_h0k) {
151 cfg.mech = mech; cfg.phase = phase; cfg.reacting = reacting;
152 cfg.Te_min = Te_min; cfg.Te_max = Te_max;
153 cfg.Te_n = static_cast<std::size_t>(std::max(2, Te_n));
154 cfg.spitzer = spitzer; cfg.gap = gap;
155 cfg.species_h0k = toVec(species_h0k);
156 const std::size_t nsp = static_cast<std::size_t>(xsec_energy_J.size());
157 cfg.specs.resize(nsp);
158 auto r = radius_m.unchecked<1>();
159 auto z = ion_Z.unchecked<1>();
160 for (std::size_t k = 0; k < nsp; k++) {
161 cfg.specs[k].radius = r(static_cast<py::ssize_t>(k));
162 cfg.specs[k].Z = z(static_cast<py::ssize_t>(k));
163 py::object eo = xsec_energy_J[k];
164 py::object so = xsec_sigma_m2[k];
165 if (!eo.is_none() && !so.is_none()) {
166 cfg.specs[k].energy_J = toVec(eo.cast<py::array_t<double>>());
167 cfg.specs[k].sigma_m2 = toVec(so.cast<py::array_t<double>>());
168 }
169 }
170 return std::make_unique<rizer::Plasma0DReactor>(cfg);
171 }),
172 py::arg("mech"), py::arg("phase"), py::arg("reacting") = true,
173 py::arg("xsec_energy_J"), py::arg("xsec_sigma_m2"),
174 py::arg("radius_m"), py::arg("ion_Z"),
175 py::arg("Te_min") = 300.0, py::arg("Te_max") = 1.0e5,
176 py::arg("Te_n") = 400, py::arg("spitzer") = true, py::arg("gap") = 0.0,
177 py::arg("species_h0k") = py::array_t<double>(0))
178 // Electrical conductivity [S/m] at (Tg, Te, Y, rho). Lambda parameters
179 // (plain comment, see the note at mean_cross_section above):
180 // self The bound Reactor0D instance
181 // Tg Gas (heavy-species) temperature [K]
182 // Te Electron temperature [K]
183 // Y Species mass fractions [-], length nsp
184 // rho Mass density [kg/m^3]
185 .def("conductivity",
186 [](rizer::Plasma0DReactor& self, double Tg, double Te,
187 py::array_t<double> Y, double rho) {
188 auto y = toVec(Y);
189 return self.conductivity(Tg, Te, y.data(), rho);
190 },
191 py::arg("Tg"), py::arg("Te"), py::arg("Y"), py::arg("rho"),
192 "Electrical conductivity [S/m] at (Tg, Te, Y, rho).")
193 // Electron number density [1/m^3] at (Tg, Te, Y, rho). Lambda
194 // parameters (plain comment, see the note at mean_cross_section above):
195 // self The bound Reactor0D instance
196 // Tg Gas (heavy-species) temperature [K]
197 // Te Electron temperature [K]
198 // Y Species mass fractions [-], length nsp
199 // rho Mass density [kg/m^3]
200 .def("electron_density",
201 [](rizer::Plasma0DReactor& self, double Tg, double Te,
202 py::array_t<double> Y, double rho) {
203 auto y = toVec(Y);
204 return self.electronDensity(Tg, Te, y.data(), rho);
205 },
206 py::arg("Tg"), py::arg("Te"), py::arg("Y"), py::arg("rho"),
207 "Electron number density [1/m^3] at (Tg, Te, Y, rho).")
208 // 0D RHS dydt = [dTg, dTe, dV, dY...] for state [Tg, Te, V, Y...];
209 // rho = mass/V; polytropic_index=inf -> constant volume. Lambda
210 // parameters (plain comment, see the note at mean_cross_section above):
211 // self The bound Reactor0D instance
212 // Tg Gas (heavy-species) temperature [K]
213 // Te Electron temperature [K]
214 // V Volume [m^3]
215 // Y Species mass fractions [-], length nsp
216 // E Applied electric field [V/m]
217 // mass Fixed total mass [kg] (rho = mass/V)
218 // p_ext External pressure [Pa] (drives the polytropic
219 // volume equation)
220 // polytropic_index Polytropic index [-] (inf = constant volume)
221 .def("rhs",
222 [](rizer::Plasma0DReactor& self, double Tg, double Te, double V,
223 py::array_t<double> Y, double E, double mass, double p_ext,
224 double polytropic_index) {
225 auto y = toVec(Y);
226 std::vector<double> dydt(3 + self.nSpecies(), 0.0);
227 self.rhs(Tg, Te, V, y.data(), E, mass, p_ext, polytropic_index,
228 dydt.data());
229 return py::array_t<double>(dydt.size(), dydt.data());
230 },
231 py::arg("Tg"), py::arg("Te"), py::arg("V"), py::arg("Y"), py::arg("E"),
232 py::arg("mass"), py::arg("p_ext"), py::arg("polytropic_index"),
233 "0D RHS dydt = [dTg, dTe, dV, dY...] for state [Tg, Te, V, Y...]; "
234 "rho = mass/V; polytropic_index=inf -> constant volume.")
235 .def_property_readonly("species",
236 [](rizer::Plasma0DReactor& self) { return self.speciesNames(); })
237 // Power / Maxwellian-condition breakdown from the most recent rhs()
238 // call (rizer.models.nrp.isomass_2T_volume_reactor_cpp.Isomass2TVolumeReactor's
239 // stored P_elastic/P_inelastic/P_chemical/P_chemical_e/P_Joule/
240 // nu_eH/nu_ee/nu_eI/nu_eH_mass_weighted attributes, natively).
241 .def_property_readonly("P_elastic",
242 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_elastic; })
243 .def_property_readonly("P_inelastic",
244 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_inelastic; })
245 .def_property_readonly("P_chemical",
246 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_chemical; })
247 .def_property_readonly("P_chemical_e",
248 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_chemical_e; })
249 .def_property_readonly("P_Joule",
250 [](rizer::Plasma0DReactor& self) { return self.diagnostics().P_Joule; })
251 .def_property_readonly("nu_eH",
252 [](rizer::Plasma0DReactor& self) { return self.diagnostics().nu_eH; })
253 .def_property_readonly("nu_ee",
254 [](rizer::Plasma0DReactor& self) { return self.diagnostics().nu_ee; })
255 .def_property_readonly("nu_eI",
256 [](rizer::Plasma0DReactor& self) { return self.diagnostics().nu_eI; })
257 .def_property_readonly("nu_eH_mass_weighted",
258 [](rizer::Plasma0DReactor& self) { return self.diagnostics().nu_eH_mass_weighted; });
259
260 // Define the solve_column() binding.
261 // The lambda converts the Python inputs into C++ types, calls solveColumn(),
262 // and converts the result back into a Python dict of numpy arrays and scalars.
263 // Lambda parameters (plain comment, see the note at mean_cross_section
264 // above -- this lambda has no separate declaration for \param to attach
265 // to):
266 // R Outer wall radius [m]
267 // electric_field Axial E field [V/m]
268 // T_wall Wall temperature [K]
269 // sigma_T Temperature values [K], 1-D array, strictly increasing
270 // sigma_v Corresponding sigma values [S/m], 1-D array
271 // kappa_T Temperature values [K], 1-D array, strictly increasing
272 // kappa_v Corresponding kappa values [W/(m*K)], 1-D array
273 // prad_T Temperature values [K], 1-D array, strictly increasing,
274 // or None
275 // prad_v Corresponding P_rad values [W/m^3], 1-D array, or None
276 // n_points Initial uniform grid count
277 // T_center_guess Parabolic-seed centerline temperature [K]
278 // rho_cp rho*cp [J/m^3/K] for pseudo-transient scaling
279 // loglevel Sim1D verbosity (0 = silent)
280 // refine_grid Enable Cantera adaptive grid refinement
281 // refine_ratio Refiner: max ratio of adjacent cell widths
282 // refine_slope Refiner: max fractional slope change per cell
283 // refine_curve Refiner: max fractional curvature per cell
284 // refine_prune Refiner: remove points below this threshold
285 // init_r Optional seed radii [m], strictly increasing, 0..R, or
286 // None
287 // init_T Optional seed temperatures [K] at init_r, or None
288 m.def("solve_column",
289 [](double R, double electric_field, double T_wall, py::array_t<double> sigma_T,
290 py::array_t<double> sigma_v, py::array_t<double> kappa_T,
291 py::array_t<double> kappa_v, py::object prad_T, py::object prad_v,
292 std::size_t n_points, double T_center_guess, double rho_cp, int loglevel,
293 bool refine_grid, double refine_ratio, double refine_slope,
294 double refine_curve, double refine_prune, py::object init_r,
295 py::object init_T)
296 {
297 // Convert the input numpy arrays into PropertyTable objects.
298 rizer::PropertyTable sigma = makeTable(sigma_T, sigma_v);
299 rizer::PropertyTable kappa = makeTable(kappa_T, kappa_v);
300 // For the optional radiation property, check if the inputs are None.
301 // If either is None, create a constant PropertyTable with value 0.0.
302 // Otherwise, create a PropertyTable from the provided arrays.
304 (prad_T.is_none() || prad_v.is_none())
306 : makeTable(prad_T.cast<py::array_t<double>>(),
307 prad_v.cast<py::array_t<double>>());
308 // Set up the solver options from the provided parameters.
310 opts.n_points = n_points;
311 opts.T_center_guess = T_center_guess;
312 opts.rho_cp = rho_cp;
313 opts.loglevel = loglevel;
314 opts.refine_grid = refine_grid;
315 opts.refine_ratio = refine_ratio;
316 opts.refine_slope = refine_slope;
317 opts.refine_curve = refine_curve;
318 opts.refine_prune = refine_prune;
319
320 // If the user provided an initial seed profile,
321 // convert the numpy arrays into std::vectors.
322 if (!init_r.is_none() && !init_T.is_none()) {
323 auto ir = init_r.cast<py::array_t<double>>();
324 auto it = init_T.cast<py::array_t<double>>();
325 auto ira = ir.unchecked<1>();
326 auto ita = it.unchecked<1>();
327 opts.init_r.resize(static_cast<std::size_t>(ira.shape(0)));
328 opts.init_T.resize(static_cast<std::size_t>(ita.shape(0)));
329 for (py::ssize_t i = 0; i < ira.shape(0); i++) {
330 opts.init_r[static_cast<std::size_t>(i)] = ira(i);
331 }
332 for (py::ssize_t i = 0; i < ita.shape(0); i++) {
333 opts.init_T[static_cast<std::size_t>(i)] = ita(i);
334 }
335 }
336
337 // Call the C++ solver function, releasing the GIL since it is pure C++.
338 // The result is a ColumnResult struct containing the solution.
340 {
341 py::gil_scoped_release release; // solve is pure C++
342 res = rizer::solveColumn(R, electric_field, T_wall,
343 sigma, kappa, prad, opts);
344 }
345
346 // Convert the result into a Python dict of numpy arrays and scalars.
347 py::dict out;
348 out["r"] = py::array_t<double>(res.r.size(), res.r.data());
349 out["T"] = py::array_t<double>(res.T.size(), res.T.data());
350 out["sigma"] = py::array_t<double>(res.sigma.size(), res.sigma.data());
351 out["kappa"] = py::array_t<double>(res.kappa.size(), res.kappa.data());
352 out["current"] = res.current;
353 out["electric_field"] = res.electric_field;
354 out["n_points"] = res.n_points;
355 return out;
356 },
357 py::arg("R"), py::arg("electric_field"), py::arg("T_wall"),
358 py::arg("sigma_T"), py::arg("sigma_v"),
359 py::arg("kappa_T"), py::arg("kappa_v"),
360 py::arg("prad_T") = py::none(), py::arg("prad_v") = py::none(),
361 py::arg("n_points") = 41, py::arg("T_center_guess") = 10000.0,
362 py::arg("rho_cp") = 1.0e3, py::arg("loglevel") = 0,
363 py::arg("refine_grid") = true, py::arg("refine_ratio") = 4.0,
364 py::arg("refine_slope") = 0.05, py::arg("refine_curve") = 0.10,
365 py::arg("refine_prune") = 0.0,
366 py::arg("init_r") = py::none(), py::arg("init_T") = py::none(),
367 "Solve the steady radial Elenbaas-Heller column.\n\n"
368 "Assembles [Empty1D | ThermalPlasmaColumn1D | Empty1D] in a Cantera\n"
369 "Sim1D, solves with Newton + pseudo-transient fallback, and optionally\n"
370 "refines the radial grid adaptively.\n\n"
371 "Parameters\n"
372 "----------\n"
373 "R : outer wall radius [m]\n"
374 "electric_field: axial E field [V/m]\n"
375 "T_wall : wall temperature [K]\n"
376 "sigma_T/v : tabulated sigma(T) [S/m] — 1-D arrays of equal length\n"
377 "kappa_T/v : tabulated kappa(T) [W/m/K]\n"
378 "prad_T/v : tabulated P_rad(T) [W/m^3], or None for no radiation\n"
379 "n_points : initial uniform grid count\n"
380 "T_center_guess: parabolic-seed centerline temperature [K]\n"
381 "rho_cp : rho*cp [J/m^3/K] for pseudo-transient scaling\n"
382 "loglevel : Sim1D verbosity (0 = silent)\n"
383 "refine_grid : enable adaptive grid refinement\n"
384 "refine_ratio/slope/curve/prune: Refiner parameters\n"
385 "init_r/T : optional seed profile T(r); overrides the parabola\n\n"
386 "Returns\n"
387 "-------\n"
388 "dict with keys r, T, sigma, kappa (numpy arrays) and\n"
389 "current [A], electric_field [V/m], n_points (scalars).");
390
391 // Advance the transient, two-temperature, finite-rate reacting 1-D radial
392 // plasma channel (PlasmaChannel1D) from t=0 to t = dt*n_steps. Lambda
393 // parameters (plain comment, see the note at mean_cross_section above --
394 // this lambda has no separate declaration for \param to attach to):
395 // mech Cantera mechanism (YAML) path
396 // phase Cantera mechanism (YAML) phase
397 // n_points Radial grid points [-] (ignored if `grid` is given)
398 // R_max Outer (wall) radius [m]
399 // rho Fixed mass density [kg/m^3] (constant-volume model)
400 // Tg0 Initial/ambient gas temperature [K] (uniform default
401 // if initTg_r/v not given)
402 // Te0 Initial/ambient electron temperature [K] (uniform
403 // default if initTe_r/v not given)
404 // Y0 Initial + ambient mass fractions [-], length nsp
405 // sigma_T Legacy sigma(Te) table: temperature grid [K], or None
406 // sigma_v Legacy sigma(Te) table: conductivity values [S/m], or
407 // None
408 // nu_E Legacy constant electron-heavy elastic exchange
409 // frequency [1/s]
410 // electric_field Constant applied axial field E [V/m] (superseded by
411 // Efield_t/v)
412 // kappa_T Gas thermal conductivity table: temperature grid [K],
413 // or None
414 // kappa_v Gas thermal conductivity table: kappa(Tg) values
415 // [W/m/K], or None
416 // kappa_e_T Electron thermal conductivity table: temperature grid
417 // [K], or None
418 // kappa_e_v Electron thermal conductivity table: kappa_e(Te)
419 // values [W/m/K], or None
420 // D_species Constant scalar species mass diffusivity [m^2/s]
421 // (0 = frozen transport)
422 // reacting true: include finite-rate chemistry source terms;
423 // false: frozen composition
424 // T_amb Wall/ambient temperature [K] (Dirichlet BC)
425 // initTg_r Optional initial Tg(r) profile: radii [m], or None
426 // initTg_v Optional initial Tg(r) profile: temperatures [K], or
427 // None
428 // initTe_r Optional initial Te(r) profile: radii [m], or None
429 // initTe_v Optional initial Te(r) profile: temperatures [K], or
430 // None
431 // grid Optional explicit radial mesh [m] (overrides
432 // n_points/R_max), or None
433 // nu_m Legacy constant electron momentum-transfer frequency
434 // [1/s] (>0 activates sigma = n_e e^2/(m_e nu_m))
435 // initY_r Optional initial composition profile: radii [m],
436 // length nr, or None
437 // initY_v Optional initial composition profile: mass fractions
438 // [-], row-major shape [nr, nsp], or None
439 // Efield_t Optional field-vs-time schedule: time grid [s]
440 // (overrides electric_field), or None
441 // Efield_v Optional field-vs-time schedule: field values [V/m],
442 // or None
443 // nu_m_T Optional Te-dependent nu_m(Te) table: temperature
444 // grid [K], or None (empty -> scalar nu_m)
445 // nu_m_v Optional Te-dependent nu_m(Te) table: frequency
446 // values [1/s], or None
447 // xsec_energy_J Composition-resolved collision model: per-species
448 // cross-section energy grids [J] (list of 1-D arrays
449 // or None per species), or None to disable
450 // xsec_sigma_m2 Composition-resolved collision model: per-species
451 // cross-section values [m^2] (list of 1-D arrays or
452 // None per species), or None to disable
453 // radius_m Composition-resolved collision model: per-species
454 // hard-sphere effective radius [m] (NaN if unused), or
455 // None to disable
456 // ion_Z Composition-resolved collision model: per-species
457 // ion charge number [-] (0 for neutrals), or None to
458 // disable
459 // Te_min Collision-model Te grid lower bound [K] (used only
460 // if the collision model is enabled)
461 // Te_max Collision-model Te grid upper bound [K] (used only
462 // if the collision model is enabled)
463 // Te_n Collision-model Te grid point count [-] (log-spaced;
464 // used only if the collision model is enabled)
465 // spitzer true: apply the Spitzer e-e correction to the
466 // collision model
467 // species_h0k Per-species standard enthalpy of formation at 0 K
468 // [J/kmol], length nsp, or None; required when
469 // `reacting` and the mechanism has an electron-impact
470 // reaction (see rizer.kinetics.electron_reactions)
471 // dt Output cadence / initial step size [s]
472 // n_steps Number of output steps [-] (t_total = dt*n_steps)
473 // record_every Record a frame every N output steps [-]
474 // loglevel Diagnostic verbosity (0 = silent)
475 // integrator "newton" (adaptive Backward-Euler) or "bdf" (stiff
476 // CVODE)
477 m.def("solve_channel_transient",
478 [](const std::string& mech, const std::string& phase, std::size_t n_points,
479 double R_max, double rho, double Tg0, double Te0, py::array_t<double> Y0,
480 py::object sigma_T, py::object sigma_v, double nu_E, double electric_field,
481 py::object kappa_T, py::object kappa_v, py::object kappa_e_T,
482 py::object kappa_e_v, double D_species, bool reacting, double T_amb,
483 py::object initTg_r, py::object initTg_v, py::object initTe_r,
484 py::object initTe_v, py::object grid, double nu_m, py::object initY_r,
485 py::object initY_v, py::object Efield_t, py::object Efield_v,
486 py::object nu_m_T, py::object nu_m_v, py::object xsec_energy_J,
487 py::object xsec_sigma_m2, py::object radius_m, py::object ion_Z,
488 double Te_min, double Te_max, int Te_n, bool spitzer,
489 py::object species_h0k, double dt,
490 std::size_t n_steps, std::size_t record_every, int loglevel,
491 const std::string& integrator)
492 {
493 // Every optional property/profile table below (sigma, kappa, kappa_e,
494 // nu_m, grid, Et) is passed from Python as `None` when disabled;
495 // ChannelOptions/PlasmaChannelConfig instead use empty vectors as
496 // their "disabled" sentinel (see makeTable() in PlasmaChannel1D.cpp),
497 // so this converts the Python convention to the C++ one in one place.
498 auto optVec = [](py::object obj) -> std::vector<double> {
499 if (obj.is_none()) return {};
500 return toVec(obj.cast<py::array_t<double>>());
501 };
502
503 rizer::ChannelOptions options;
504 auto& cfg = options.cfg;
505 cfg.mech = mech;
506 cfg.phase = phase;
507 cfg.n_points = n_points;
508 cfg.R_max = R_max;
509 cfg.rho = rho;
510 cfg.electric_field = electric_field;
511 cfg.nu_E = nu_E;
512 cfg.D_species = D_species;
513 cfg.reacting = reacting;
514 cfg.T_amb = T_amb;
515 cfg.Y0 = toVec(Y0);
516 cfg.species_h0k = optVec(species_h0k);
517 cfg.sigma_v = optVec(sigma_v);
518 cfg.sigma_T = optVec(sigma_T);
519 cfg.kappa_v = optVec(kappa_v);
520 cfg.kappa_T = optVec(kappa_T);
521 cfg.kappa_e_v = optVec(kappa_e_v);
522 cfg.kappa_e_T = optVec(kappa_e_T);
523 // Initial profiles: explicit Tg(r)/Te(r) if given, else uniform Tg0/Te0.
524 cfg.initTg_v = initTg_v.is_none() ? std::vector<double>{Tg0, Tg0}
525 : optVec(initTg_v);
526 cfg.initTg_r = initTg_r.is_none() ? std::vector<double>{0.0, R_max}
527 : optVec(initTg_r);
528 cfg.initTe_v = initTe_v.is_none() ? std::vector<double>{Te0, Te0}
529 : optVec(initTe_v);
530 cfg.initTe_r = initTe_r.is_none() ? std::vector<double>{0.0, R_max}
531 : optVec(initTe_r);
532 cfg.nu_m = nu_m;
533 // Te-dependent nu_m(Te) table (empty -> scalar nu_m).
534 cfg.nu_m_T = optVec(nu_m_T);
535 cfg.nu_m_v = optVec(nu_m_v);
536 // Explicit radial mesh (empty -> uniform).
537 cfg.grid = optVec(grid);
538 // Field-vs-time table (empty -> constant).
539 cfg.Et_t = optVec(Efield_t);
540 cfg.Et_v = optVec(Efield_v);
541 // Initial composition profile: initY_r (nr) + initY_v (2D [nr, nsp]).
542 if (!initY_r.is_none() && !initY_v.is_none()) {
543 cfg.initY_r = toVec(initY_r.cast<py::array_t<double>>());
544 auto initYArray = initY_v.cast<py::array_t<double>>();
545 auto initYView = initYArray.unchecked<2>();
546 cfg.initY_v.resize(
547 static_cast<std::size_t>(initYView.shape(0) * initYView.shape(1)));
548 for (py::ssize_t i = 0; i < initYView.shape(0); i++) {
549 for (py::ssize_t k = 0; k < initYView.shape(1); k++) {
550 cfg.initY_v[
551 static_cast<std::size_t>(i * initYView.shape(1) + k)] =
552 initYView(i, k);
553 }
554 }
555 }
556 // Composition-resolved collision model (empty -> legacy sigma/nu_m/nu_E
557 // closure above). xsec_energy_J/xsec_sigma_m2/radius_m/ion_Z mirror the
558 // Reactor0D binding exactly, one entry per species: all four must be
559 // given together (or all omitted) since a per-species spec needs its
560 // radius/Z slot even when it carries no tabulated cross-section.
561 const bool any_collision_arg = !xsec_energy_J.is_none()
562 || !xsec_sigma_m2.is_none() || !radius_m.is_none() || !ion_Z.is_none();
563 if (any_collision_arg) {
564 if (xsec_energy_J.is_none() || xsec_sigma_m2.is_none()
565 || radius_m.is_none() || ion_Z.is_none()) {
566 throw std::invalid_argument(
567 "solve_channel_transient: xsec_energy_J, xsec_sigma_m2, "
568 "radius_m, and ion_Z must all be given together (or all "
569 "omitted) to enable the composition-resolved collision "
570 "model.");
571 }
572 py::list e_list = xsec_energy_J.cast<py::list>();
573 py::list s_list = xsec_sigma_m2.cast<py::list>();
574 auto radius_arr = radius_m.cast<py::array_t<double>>();
575 auto ion_z_arr = ion_Z.cast<py::array_t<int>>();
576 const std::size_t nsp_specs = static_cast<std::size_t>(e_list.size());
577 cfg.specs.resize(nsp_specs);
578 auto r = radius_arr.unchecked<1>();
579 auto z = ion_z_arr.unchecked<1>();
580 for (std::size_t k = 0; k < nsp_specs; k++) {
581 cfg.specs[k].radius = r(static_cast<py::ssize_t>(k));
582 cfg.specs[k].Z = z(static_cast<py::ssize_t>(k));
583 py::object eo = e_list[k];
584 py::object so = s_list[k];
585 if (!eo.is_none() && !so.is_none()) {
586 cfg.specs[k].energy_J = toVec(eo.cast<py::array_t<double>>());
587 cfg.specs[k].sigma_m2 = toVec(so.cast<py::array_t<double>>());
588 }
589 }
590 cfg.Te_min = Te_min;
591 cfg.Te_max = Te_max;
592 cfg.Te_n = static_cast<std::size_t>(Te_n);
593 cfg.spitzer = spitzer;
594 }
595
596 options.dt = dt;
597 options.n_steps = n_steps;
598 options.record_every = record_every;
599 options.loglevel = loglevel;
600 options.integrator = integrator;
601
602 // A transient BDF/Newton solve over many steps can run for seconds
603 // to minutes; release the GIL for its duration so other Python
604 // threads (and, e.g., a Ctrl-C from the interpreter) aren't blocked.
605 rizer::ChannelHistory history;
606 {
607 py::gil_scoped_release release;
608 history = rizer::solveChannelTransient(options);
609 }
610
611 py::dict out;
612 out["t"] = py::array_t<double>(history.t.size(), history.t.data());
613 out["r"] = py::array_t<double>(history.r.size(), history.r.data());
614 // Shaped [nframes, npts] and [nframes, npts, nsp].
615 out["Tg"] = py::array_t<double>(
616 {history.nframes, history.npts}, history.Tg.data());
617 out["Te"] = py::array_t<double>(
618 {history.nframes, history.npts}, history.Te.data());
619 out["ne"] = py::array_t<double>(
620 {history.nframes, history.npts}, history.ne.data());
621 out["Y"] = py::array_t<double>(
622 {history.nframes, history.npts, history.nsp}, history.Y.data());
623 out["species"] = history.species;
624 out["nframes"] = history.nframes;
625 out["npts"] = history.npts;
626 out["nsp"] = history.nsp;
627 return out;
628 },
629 py::arg("mech"), py::arg("phase"), py::arg("n_points"),
630 py::arg("R_max"), py::arg("rho"), py::arg("Tg0"), py::arg("Te0"),
631 py::arg("Y0"), py::arg("sigma_T") = py::none(),
632 py::arg("sigma_v") = py::none(), py::arg("nu_E") = 0.0,
633 py::arg("electric_field") = 0.0,
634 py::arg("kappa_T") = py::none(), py::arg("kappa_v") = py::none(),
635 py::arg("kappa_e_T") = py::none(), py::arg("kappa_e_v") = py::none(),
636 py::arg("D_species") = 0.0, py::arg("reacting") = true,
637 py::arg("T_amb") = 300.0,
638 py::arg("initTg_r") = py::none(), py::arg("initTg_v") = py::none(),
639 py::arg("initTe_r") = py::none(), py::arg("initTe_v") = py::none(),
640 py::arg("grid") = py::none(), py::arg("nu_m") = 0.0,
641 py::arg("initY_r") = py::none(), py::arg("initY_v") = py::none(),
642 py::arg("Efield_t") = py::none(), py::arg("Efield_v") = py::none(),
643 py::arg("nu_m_T") = py::none(), py::arg("nu_m_v") = py::none(),
644 py::arg("xsec_energy_J") = py::none(), py::arg("xsec_sigma_m2") = py::none(),
645 py::arg("radius_m") = py::none(), py::arg("ion_Z") = py::none(),
646 py::arg("Te_min") = 300.0, py::arg("Te_max") = 1.0e5,
647 py::arg("Te_n") = 400, py::arg("spitzer") = true,
648 py::arg("species_h0k") = py::none(),
649 py::arg("dt") = 1.0e-9,
650 py::arg("n_steps") = 1000, py::arg("record_every") = 1,
651 py::arg("loglevel") = 0, py::arg("integrator") = "newton",
652 "Advance the transient, two-temperature, finite-rate reacting 1-D radial\n"
653 "plasma channel (PlasmaChannel1D) from t=0 to t=dt*n_steps.\n\n"
654 "Parameters\n"
655 "----------\n"
656 "mech, phase : Cantera mechanism (YAML) path and phase name\n"
657 "n_points : radial grid points [-] (ignored if `grid` is given)\n"
658 "R_max : outer (wall) radius [m]\n"
659 "rho : fixed mass density [kg/m^3] (constant-volume model)\n"
660 "Tg0, Te0 : initial gas/electron temperature [K] (uniform default\n"
661 " if initTg_r/v or initTe_r/v are not given)\n"
662 "Y0 : initial + ambient mass fractions [-], length nsp\n"
663 "sigma_T/v, nu_E, nu_m, nu_m_T/v : legacy Joule/exchange closure -- sigma\n"
664 " from a sigma(Te) table, or n_e e^2/(m_e nu_m) from the\n"
665 " local n_e if nu_m>0 or a nu_m(Te) table is given, plus a\n"
666 " constant elastic frequency nu_E. Mutually exclusive with\n"
667 " xsec_energy_J/xsec_sigma_m2/radius_m/ion_Z below -- an\n"
668 " error is raised if both are supplied.\n"
669 "electric_field: constant applied field E [V/m] (superseded by Efield_t/v)\n"
670 "kappa_T/v : tabulated gas thermal conductivity kappa(Tg) [K],[W/m/K]\n"
671 "kappa_e_T/v : tabulated electron thermal conductivity kappa_e(Te) "
672 "[K],[W/m/K]\n"
673 "D_species : constant scalar species mass diffusivity [m^2/s] "
674 "(0 = frozen)\n"
675 "reacting : include finite-rate chemistry source terms\n"
676 "T_amb : wall/ambient temperature [K] (Dirichlet BC)\n"
677 "initTg_r/v : initial radial profile Tg(r) [m],[K]\n"
678 "initTe_r/v : initial radial profile Te(r) [m],[K]\n"
679 "grid : explicit radial mesh [m] (overrides n_points/R_max)\n"
680 "initY_r/v : initial radial composition profile: radii [m] (length nr)\n"
681 " and row-major mass fractions [-] (shape [nr, nsp])\n"
682 "Efield_t/v : field-vs-time table [s],[V/m] (overrides electric_field)\n"
683 "xsec_energy_J, xsec_sigma_m2, radius_m, ion_Z :\n"
684 " composition-resolved collision model (preferred over the\n"
685 " legacy closure above): per-species lists/arrays, in\n"
686 " species-index order (length nsp), exactly matching the\n"
687 " Reactor0D binding. xsec_energy_J/xsec_sigma_m2 are lists\n"
688 " of 1D arrays (or None per species) giving a tabulated\n"
689 " cross-section Q(E) [J],[m^2]; radius_m is a hard-sphere\n"
690 " effective radius [m] (NaN if not used); ion_Z is the ion\n"
691 " charge number (0 for neutrals). All four must be given\n"
692 " together (or all omitted).\n"
693 "Te_min/Te_max/Te_n : collision-model Te grid [K],[K],[-] (log-spaced)\n"
694 "spitzer : apply the Spitzer e-e correction to the collision model\n"
695 "species_h0k : per-species standard enthalpy of formation at 0 K "
696 "[J/kmol],\n"
697 " length nsp, or None; required when `reacting` and the\n"
698 " mechanism has an electron-impact reaction (see\n"
699 " rizer.kinetics.electron_reactions)\n"
700 "dt : output cadence / initial step [s]\n"
701 "n_steps : number of output steps [-] (dt*n_steps = total time [s])\n"
702 "record_every : record a frame every N output steps [-]\n"
703 "loglevel : diagnostic verbosity (0 = silent)\n"
704 "integrator : \"newton\" (adaptive Backward-Euler) or \"bdf\" "
705 "(stiff CVODE)\n\n"
706 "Returns\n"
707 "-------\n"
708 "dict with keys:\n"
709 " t : recorded times [s], shape [nframes]\n"
710 " r : radial grid [m], shape [npts]\n"
711 " Tg : gas temperature [K], shape [nframes, npts]\n"
712 " Te : electron temperature [K], shape [nframes, npts]\n"
713 " ne : electron number density [1/m^3], shape [nframes, npts]\n"
714 " Y : species mass fractions [-], shape [nframes, npts, nsp]\n"
715 " species : species names, length nsp (order matches the Y axis)\n"
716 " nframes, npts, nsp : array-shape scalars [-]");
717}
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.
std::vector< double > species_h0k
Per-species standard enthalpy of formation at 0 K [J/kmol]; forwarded to ReactorRHS::Config::species_...
double Te_min
Lower bound of the tabulated electron-temperature grid [K].
double P_Joule
Joule heating power [W/m^3].
Definition ReactorRHS.h:48
double nu_eH
Electron-heavy momentum-transfer collision frequency [1/s] (Mitchner II-13.3).
Definition ReactorRHS.h:49
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 nu_eI
Electron-ion momentum-transfer collision frequency [1/s].
Definition ReactorRHS.h:51
double nu_ee
Electron-electron collision frequency [1/s] (Mitchner II-8.11e).
Definition ReactorRHS.h:50
double P_elastic
Elastic electron-heavy exchange power [W/m^3].
Definition ReactorRHS.h:44
double nu_eH_mass_weighted
sum_h nu_eh/m_h [1/kg/s] (Mitchner VIII-3.8, term 2)
Definition ReactorRHS.h:52