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")
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) {
148 cfg.
Te_n =
static_cast<std::size_t
>(std::max(2, Te_n));
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>>());
164 return std::make_unique<rizer::Plasma0DReactor>(cfg);
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)
180 py::array_t<double> Y,
double rho) {
184 py::arg(
"Tg"), py::arg(
"Te"), py::arg(
"Y"), py::arg(
"rho"),
185 "Electrical conductivity [S/m] at (Tg, Te, Y, rho).")
193 .def(
"electron_density",
195 py::array_t<double> Y,
double rho) {
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).")
216 py::array_t<double> Y,
double E,
double mass,
double p_ext,
217 double polytropic_index) {
219 std::vector<double> dydt(3 + self.
nSpecies(), 0.0);
220 self.
rhs(Tg, Te, V, y.data(), E, mass, p_ext, polytropic_index,
222 return py::array_t<double>(dydt.size(), dydt.data());
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",
234 .def_property_readonly(
"P_elastic",
236 .def_property_readonly(
"P_inelastic",
238 .def_property_readonly(
"P_chemical",
240 .def_property_readonly(
"P_chemical_e",
242 .def_property_readonly(
"P_Joule",
244 .def_property_readonly(
"cond_maxwell_1",
246 .def_property_readonly(
"cond_maxwell_2",
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,
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>>());
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);
321 for (py::ssize_t i = 0; i < ita.shape(0); i++) {
322 opts.
init_T[
static_cast<std::size_t
>(i)] = ita(i);
330 py::gil_scoped_release release;
332 sigma, kappa, prad, opts);
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());
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"
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"
377 "dict with keys r, T, sigma, kappa (numpy arrays) and\n"
378 "current [A], electric_field [V/m], n_points (scalars).");
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)
482 auto optVec = [](py::object obj) -> std::vector<double> {
483 if (obj.is_none())
return {};
484 return toVec(obj.cast<py::array_t<double>>());
488 auto& cfg = options.
cfg;
491 cfg.n_points = n_points;
494 cfg.electric_field = electric_field;
496 cfg.D_species = D_species;
497 cfg.reacting = reacting;
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);
507 cfg.initTg_v = initTg_v.is_none() ? std::vector<double>{Tg0, Tg0}
509 cfg.initTg_r = initTg_r.is_none() ? std::vector<double>{0.0, R_max}
511 cfg.initTe_v = initTe_v.is_none() ? std::vector<double>{Te0, Te0}
513 cfg.initTe_r = initTe_r.is_none() ? std::vector<double>{0.0, R_max}
517 cfg.nu_m_T = optVec(nu_m_T);
518 cfg.nu_m_v = optVec(nu_m_v);
520 cfg.grid = optVec(grid);
522 cfg.Et_t = optVec(Efield_t);
523 cfg.Et_v = optVec(Efield_v);
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>();
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++) {
534 static_cast<std::size_t
>(i * initYView.shape(1) + k)] =
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 "
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>>());
575 cfg.Te_n =
static_cast<std::size_t
>(Te_n);
576 cfg.spitzer = spitzer;
590 py::gil_scoped_release release;
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());
598 out[
"Tg"] = py::array_t<double>(
600 out[
"Te"] = py::array_t<double>(
602 out[
"ne"] = py::array_t<double>(
604 out[
"Y"] = py::array_t<double>(
606 out[
"species"] = history.
species;
607 out[
"nframes"] = history.
nframes;
608 out[
"npts"] = history.
npts;
609 out[
"nsp"] = history.
nsp;
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"
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) "
655 "D_species : constant scalar species mass diffusivity [m^2/s] "
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\" "
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 [-]");