72 m.doc() =
"rizer Cantera 1D plasma extension (ThermalPlasmaColumn1D)";
75 "Version string of the linked libcantera.");
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.");
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());
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]).");
114 py::class_<rizer::Plasma0DReactor>(m,
"Reactor0D")
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) {
153 cfg.
Te_n =
static_cast<std::size_t
>(std::max(2, Te_n));
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>>());
170 return std::make_unique<rizer::Plasma0DReactor>(cfg);
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))
187 py::array_t<double> Y,
double rho) {
191 py::arg(
"Tg"), py::arg(
"Te"), py::arg(
"Y"), py::arg(
"rho"),
192 "Electrical conductivity [S/m] at (Tg, Te, Y, rho).")
200 .def(
"electron_density",
202 py::array_t<double> Y,
double rho) {
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).")
223 py::array_t<double> Y,
double E,
double mass,
double p_ext,
224 double polytropic_index) {
226 std::vector<double> dydt(3 + self.
nSpecies(), 0.0);
227 self.
rhs(Tg, Te, V, y.data(), E, mass, p_ext, polytropic_index,
229 return py::array_t<double>(dydt.size(), dydt.data());
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",
241 .def_property_readonly(
"P_elastic",
243 .def_property_readonly(
"P_inelastic",
245 .def_property_readonly(
"P_chemical",
247 .def_property_readonly(
"P_chemical_e",
249 .def_property_readonly(
"P_Joule",
251 .def_property_readonly(
"nu_eH",
253 .def_property_readonly(
"nu_ee",
255 .def_property_readonly(
"nu_eI",
257 .def_property_readonly(
"nu_eH_mass_weighted",
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,
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>>());
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);
332 for (py::ssize_t i = 0; i < ita.shape(0); i++) {
333 opts.
init_T[
static_cast<std::size_t
>(i)] = ita(i);
341 py::gil_scoped_release release;
343 sigma, kappa, prad, opts);
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());
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"
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"
388 "dict with keys r, T, sigma, kappa (numpy arrays) and\n"
389 "current [A], electric_field [V/m], n_points (scalars).");
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)
498 auto optVec = [](py::object obj) -> std::vector<double> {
499 if (obj.is_none())
return {};
500 return toVec(obj.cast<py::array_t<double>>());
504 auto& cfg = options.
cfg;
507 cfg.n_points = n_points;
510 cfg.electric_field = electric_field;
512 cfg.D_species = D_species;
513 cfg.reacting = reacting;
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);
524 cfg.initTg_v = initTg_v.is_none() ? std::vector<double>{Tg0, Tg0}
526 cfg.initTg_r = initTg_r.is_none() ? std::vector<double>{0.0, R_max}
528 cfg.initTe_v = initTe_v.is_none() ? std::vector<double>{Te0, Te0}
530 cfg.initTe_r = initTe_r.is_none() ? std::vector<double>{0.0, R_max}
534 cfg.nu_m_T = optVec(nu_m_T);
535 cfg.nu_m_v = optVec(nu_m_v);
537 cfg.grid = optVec(grid);
539 cfg.Et_t = optVec(Efield_t);
540 cfg.Et_v = optVec(Efield_v);
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>();
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++) {
551 static_cast<std::size_t
>(i * initYView.shape(1) + k)] =
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 "
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>>());
592 cfg.Te_n =
static_cast<std::size_t
>(Te_n);
593 cfg.spitzer = spitzer;
607 py::gil_scoped_release release;
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());
615 out[
"Tg"] = py::array_t<double>(
617 out[
"Te"] = py::array_t<double>(
619 out[
"ne"] = py::array_t<double>(
621 out[
"Y"] = py::array_t<double>(
623 out[
"species"] = history.
species;
624 out[
"nframes"] = history.
nframes;
625 out[
"npts"] = history.
npts;
626 out[
"nsp"] = history.
nsp;
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"
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) "
673 "D_species : constant scalar species mass diffusivity [m^2/s] "
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 "
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\" "
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 [-]");