rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
PlasmaRates.cpp
Go to the documentation of this file.
1// PlasmaRates.cpp — see PlasmaRates.h for the design rationale.
2
3#include "PlasmaRates.h"
4#include "units.h"
5
15
16#include <cmath>
17
18using namespace Cantera;
19
20namespace rizer {
21
22// ===========================================================================
23// Registration
24// ===========================================================================
25
27{
29 // `reg` replaces any existing entry silently, so this is idempotent.
30 f->reg("janev-dissociative-recombination-C2Hy",
31 // Lambda parameters (a plain, non-Doxygen comment: this lambda has no
32 // separate declaration for \param to attach to, so a doc-comment
33 // here would be misattached to the enclosing registerPlasmaRates()):
34 // node YAML/AnyMap node for this reaction's rate block
35 // (holds "A").
36 // units Unit context used to convert "A" into Cantera's
37 // internal rate-coeff units.
38 [](const AnyMap& node, const UnitStack& units)
39 {
41 });
42 f->reg("Druyvesteyn",
43 // Lambda parameters (plain comment, see the note above):
44 // node YAML/AnyMap node for this reaction's rate block
45 // (holds "A", "b", "Ea").
46 // units Unit context used to convert "A" into Cantera's
47 // internal rate-coeff units.
48 [](const AnyMap& node, const UnitStack& units)
49 {
50 return new DruyvesteynRate(node, units);
51 });
52 f->reg("reverse-two-temperature-plasma",
53 // Lambda parameters (plain comment, see the note above):
54 // node YAML/AnyMap node for this reaction's rate block
55 // (holds "forward_equation", "T").
56 // units Unit context passed through to
57 // ReactionRate::setParameters.
58 [](const AnyMap& node, const UnitStack& units)
59 {
60 return new ReverseTwoTemperaturePlasmaRate(node, units);
61 });
62}
63
64
65// ===========================================================================
66// janev-dissociative-recombination-C2Hy
67// ===========================================================================
68
69bool ElectronTemperatureData::update(const ThermoPhase& phase, const Kinetics& /*kin*/)
70{
71 auto plasma = dynamic_cast<const PlasmaPhase*>(&phase);
72 double Te = plasma ? plasma->electronTemperature() : phase.temperature();
73 if (Te != electronTemp) {
74 electronTemp = Te;
75 return true;
76 }
77 return false;
78}
79
85
86std::unique_ptr<MultiRateBase>
92
94 const AnyMap& node, const UnitStack& rate_units)
95{
96 ReactionRate::setParameters(node, rate_units); // sets units + m_input
97 if (!node.hasKey("A")) {
98 m_A = NAN;
99 return;
100 }
101 // A has the same units as the rate constant (the Te-dependent factor is
102 // dimensionless), so it converts like any rate coefficient (e.g.
103 // cm^3/mol/s or m^3/kmol/s, per the reaction's declared `units:` block,
104 // both into Cantera's internal kmol/m/s system).
105 m_A = node.units().convertRateCoeff(node["A"], conversionUnits());
106 m_valid = true;
107}
108
110{
111 if (std::isnan(m_A)) {
112 return;
113 }
114 node["A"].setQuantity(m_A, conversionUnits());
115}
116
117// Compiled counterpart of the Python `JanevDissociativeRecombinationC2Hy`
118// ct.ExtensibleRate (rizer/kin/extensible_rate.py), which implements
119// Janev2004 Eq. 70 for dissociative recombination: a closed-form rate that
120// depends on the electron temperature Te only (no Tg, no reduced field).
122 const ElectronTemperatureData& shared_data) const
123{
124 // Convert Te from K to eV: @f$ T_{e,\text{eV}} = T_e k_B / e ==
125 // T_e \cdot \text{K\_to\_eV} @f$ (units.h mirrors units.py's
126 // `K_to_eV` exactly).
127 const double Te_eV = shared_data.electronTemp * rizer::units::K_to_eV;
128 // @f$ k(T_e) = A / ( \sqrt{T_{e,\text{eV}}} (1 + 0.27
129 // T_{e,\text{eV}}^{0.55}) ) @f$; the bracketed factor is dimensionless,
130 // so the result carries the same units as A (a Cantera rate
131 // coefficient in kmol/m/s units).
132 return m_A / (std::sqrt(Te_eV) * (1.0 + 0.27 * std::pow(Te_eV, 0.55)));
133}
134
135
136// ===========================================================================
137// Druyvesteyn
138// ===========================================================================
139
141 const Cantera::UnitStack& rate_units)
142{
143 setParameters(node, rate_units);
144}
145
146std::unique_ptr<MultiRateBase> DruyvesteynRate::newMultiRate() const
147{
148 return std::make_unique<MultiRate<DruyvesteynRate, ElectronTemperatureData>>();
149}
150
151void DruyvesteynRate::setParameters(const AnyMap& node, const UnitStack& rate_units)
152{
153 ReactionRate::setParameters(node, rate_units); // sets units + m_input
154 if (!node.hasKey("A")) {
155 m_A = m_b = m_Ea_K = NAN;
156 return;
157 }
158 // A carries the rate-constant units (Te^b's contribution is treated as
159 // dimensionless, exactly like the Python reference's convert_rate_coeff);
160 // Ea is expressed in Kelvin so (Ea/Te)^2 is dimensionless.
161 m_A = node.units().convertRateCoeff(node["A"], conversionUnits());
162 m_b = node["b"].asDouble();
163 m_Ea_K = node.units().convertActivationEnergy(node["Ea"], "K");
164 m_valid = true;
165}
166
167void DruyvesteynRate::validate(const std::string& equation,
168 const Cantera::Kinetics& /*kin*/)
169{
170 // Mirrors the Python rate's validate. Throw std::, not
171 // CanteraError("...{}...") — the fmt instantiation rule (CLAUDE.md).
172 // NaN (missing "A" in the YAML rate node, see setParameters) is caught
173 // separately: NaN < 0 is false, so the negative-A check below cannot
174 // catch it on its own.
175 if (std::isnan(m_A)) {
176 throw std::domain_error(
177 "DruyvesteynRate: missing pre-exponential 'A' for reaction '"
178 + equation + "'.");
179 }
180 if (m_A < 0) {
181 throw std::domain_error(
182 "DruyvesteynRate: negative pre-exponential 'A' for reaction '"
183 + equation + "'.");
184 }
185}
186
188{
189 if (std::isnan(m_A)) {
190 return;
191 }
192 node["A"].setQuantity(m_A, conversionUnits());
193 node["b"] = m_b;
194 node["Ea"].setQuantity(m_Ea_K, "K", true);
195}
196
197// Compiled counterpart of the Python `DruyvesteynRate` ct.ExtensibleRate
198// (rizer/kin/extensible_rate.py): a modified Arrhenius in the electron
199// temperature whose exponential uses the squared temperature ratio of a
200// Druyvesteyn EEDF. Depends on Te only (no Tg, no reduced field).
202 const ElectronTemperatureData& shared_data) const
203{
204 const double Te = shared_data.electronTemp;
205 const double ratio = m_Ea_K / Te;
206 // @f$ k(T_e) = A \, T_e^{b} \, e^{-(E_a/T_e)^2} @f$; the exponential and
207 // the power factor are dimensionless, so the result carries A's units (a
208 // Cantera rate coefficient in kmol/m/s units).
209 return m_A * std::pow(Te, m_b) * std::exp(-ratio * ratio);
210}
211
212
213// ===========================================================================
214// reverse-two-temperature-plasma
215// ===========================================================================
216
218{
219 // Thermo: a single-temperature IDEAL-GAS copy of the main phase (electron is
220 // a normal ideal-gas species), mirroring rizer's Python shadow
221 // (ct.Solution(thermo="ideal-gas", species=plasma.species())). The reverse
222 // rate uses detailed balance at one temperature (Tg or Te), NOT a 2-T K_eq.
223 auto thermo = std::make_shared<IdealGasPhase>();
224 // Auto-add elements (incl. "E") as species are added.
225 thermo->addUndefinedElements();
226 for (size_t k = 0; k < phase.nSpecies(); k++) {
227 thermo->addSpecies(phase.species(k));
228 }
229 thermo->initThermo();
230
231 // Kinetics: every "forward" reaction (i.e. everything EXCEPT the custom
232 // reverse-two-temperature-plasma rates). We keep the janev and
233 // two-temperature-plasma forwards because some reverse reactions name a
234 // janev forward in their `forward_equation`, so its k_fwd/K_eq must be
235 // computable here. Excluding only the reverse rates is what stops the
236 // shadow from recursing back into this code.
237 auto skin = newKinetics("bulk");
238 skin->addThermo(thermo);
239 // Size species arrays from the phase (factory does this before
240 // addReaction).
241 skin->init();
242 // Reaction::rate() is non-const; we only read reactions, so take a non-const
243 // view of the (const) source kinetics. The reaction objects are SHARED with
244 // the main kinetics (as rizer's Python shadow phase does): the native rates'
245 // setContext is stateless (TwoTempPlasmaRate only checks reversibility) and
246 // MultiRate evaluators key off their own internal indices, not the shared
247 // rate's index, so sharing does not corrupt the main solution. Sharing also
248 // avoids a parameters()->newReaction round-trip, which fails because A is
249 // serialized as a units-bearing Quantity that convert() cannot re-read.
250 auto& mkin = const_cast<Kinetics&>(kin);
251 for (size_t i = 0; i < mkin.nReactions(); i++) {
252 auto r = mkin.reaction(i);
253 std::string t = r->rate() ? r->rate()->type() : std::string();
254 if (t == "reverse-two-temperature-plasma") {
255 continue;
256 }
257 skin->addReaction(r, false);
258 fwdIndex[r->equation()] = skin->nReactions() - 1;
259 }
260 skin->resizeReactions();
261
263 shadow->setThermo(thermo);
264 shadow->setKinetics(skin);
265 const std::size_t nR = skin->nReactions();
266 kf_Tg.assign(nR, 0.0); Keq_Tg.assign(nR, 0.0);
267 kf_Te.assign(nR, 0.0); Keq_Te.assign(nR, 0.0);
268}
269
270bool ReverseTwoTempData::update(const ThermoPhase& phase, const Kinetics& kin)
271{
272 if (!shadow) {
273 buildShadow(phase, kin);
274 }
275
276 auto plasma = dynamic_cast<const PlasmaPhase*>(&phase);
277 const double Tg = phase.temperature();
278 const double Te = plasma ? plasma->electronTemperature() : Tg;
279 const double P = phase.pressure();
280 if (Tg == m_Tg && Te == m_Te && P == m_P) {
281 return false;
282 }
283 m_Tg = Tg;
284 m_Te = Te;
285 m_P = P;
286
287 // Push the composition onto the ideal-gas shadow, then read the forward rate
288 // constants and (concentration) equilibrium constants at BOTH Tg and Te. A
289 // two-temperature-plasma forward rate in this single-T phase sees Tg=Te=T, so
290 // evaluating at Te gives kf(Te) and at Tg gives kf(Tg) -- exactly what the
291 // Python reference's ideal-gas shadow does (gas.TP = T, P).
292 auto sth = shadow->thermo();
293 auto skin = shadow->kinetics();
294 std::vector<double> Y(phase.nSpecies());
295 for (size_t k = 0; k < phase.nSpecies(); k++) {
296 Y[k] = phase.massFraction(k);
297 }
298 sth->setMassFractions(Y);
299 sth->setState_TP(Tg, P);
300 skin->getFwdRateConstants(kf_Tg);
301 skin->getEquilibriumConstants(Keq_Tg);
302 sth->setState_TP(Te, P);
303 skin->getFwdRateConstants(kf_Te);
304 skin->getEquilibriumConstants(Keq_Te);
305 return true;
306}
307
313
314std::unique_ptr<MultiRateBase>
320
322 const AnyMap& node, const UnitStack& rate_units)
323{
324 ReactionRate::setParameters(node, rate_units);
325 m_forward_equation = node.getString("forward_equation", "");
326 m_T = node.getString("T", "Tg");
327 m_idx = npos;
328 m_valid = !m_forward_equation.empty();
329}
330
332{
333 node["forward_equation"] = m_forward_equation;
334 node["T"] = m_T;
335}
336
337// Compiled counterpart of the Python `ReverseTwoTemperaturePlasma`
338// ct.ExtensibleRate (rizer/kin/extensible_rate.py). Implements detailed
339// balance at a single (Tg- or Te-) temperature for the reverse of a forward
340// two-temperature-plasma-family reaction:
341// @f[
342// k_{rev}(T) = \frac{k_{fwd}(T)}{K_{eq}(T)}
343// @f]
344// with T = Te for electron-impact reactions and T = Tg for heavy-species
345// reactions (per the reaction's `T:` tag). k_fwd and K_eq are both looked up
346// from the shadow ideal-gas solution built/cached in ReverseTwoTempData.
348 const ReverseTwoTempData& shared_data) const
349{
350 if (m_idx == npos) {
351 auto it = shared_data.fwdIndex.find(m_forward_equation);
352 if (it == shared_data.fwdIndex.end()) {
353 throw CanteraError("ReverseTwoTemperaturePlasmaRate::evalFromStruct",
354 "Forward reaction '{}' not found in the forward (shadow) mechanism.",
356 }
357 m_idx = it->second;
358 }
359 // Detailed balance at the tagged temperature:
360 // @f$ k_{rev} = k_f(T) / K_c(T) @f$.
361 const bool useTe = (m_T == "Te");
362 const std::vector<double>& kf = useTe ? shared_data.kf_Te : shared_data.kf_Tg;
363 const std::vector<double>& Keq = useTe ? shared_data.Keq_Te : shared_data.Keq_Tg;
364 const double K = Keq[m_idx];
365 return (K > 0.0) ? kf[m_idx] / K : 0.0;
366}
367
368} // namespace rizer
bool hasKey(const string &key) const
const UnitSystem & units() const
const string & getString(const string &key, const string &default_) const
virtual void resizeReactions()
double massFraction(size_t k) const
size_t nSpecies() const
double temperature() const
shared_ptr< Species > species(const string &name) const
virtual double pressure() const
double electronTemperature() const override
static ReactionRateFactory * factory()
virtual void setParameters(const AnyMap &node, const UnitStack &units)
const Units & conversionUnits() const
static shared_ptr< Solution > create()
double convertRateCoeff(const AnyValue &val, const Units &dest) const
double convertActivationEnergy(double value, const string &src, const string &dest) const
void setParameters(const Cantera::AnyMap &node, const Cantera::UnitStack &rate_units) override
Set the rate parameters from a YAML/AnyMap rate node.
std::unique_ptr< Cantera::MultiRateBase > newMultiRate() const override
double m_b
Te exponent [-].
double m_A
Pre-factor in Cantera's internal (kmol, m, s) rate-coefficient units [order-dependent].
void getParameters(Cantera::AnyMap &node) const override
void validate(const std::string &equation, const Cantera::Kinetics &kin) override
Reject a missing or negative pre-factor (mirrors the Python rate's validate; missing "A" leaves m_A a...
double evalFromStruct(const ElectronTemperatureData &shared_data) const
Evaluate the Druyvesteyn rate .
double m_Ea_K
Activation energy expressed in Kelvin [K].
std::unique_ptr< Cantera::MultiRateBase > newMultiRate() const override
void setParameters(const Cantera::AnyMap &node, const Cantera::UnitStack &rate_units) override
Set the rate parameters from a YAML/AnyMap rate node.
double evalFromStruct(const ElectronTemperatureData &shared_data) const
Evaluate the Janev dissociative-recombination rate .
double m_A
Rate pre-factor, in Cantera's internal (kmol, m, s) rate-coefficient units [kmol/m^3/s units,...
void getParameters(Cantera::AnyMap &node) const override
Reverse of a forward two-temperature-plasma reaction:
std::unique_ptr< Cantera::MultiRateBase > newMultiRate() const override
double evalFromStruct(const ReverseTwoTempData &shared_data) const
Evaluate the reverse rate at the tagged temperature.
void getParameters(Cantera::AnyMap &node) const override
std::string m_forward_equation
Equation string of the forward reaction this rate reverses [-].
size_t m_idx
Cached index of the forward reaction in the shadow kinetics' reaction list [-].
void setParameters(const Cantera::AnyMap &node, const Cantera::UnitStack &rate_units) override
Set the rate parameters from a YAML/AnyMap rate node.
std::string m_T
Which temperature tags this reaction ("Te" or "Tg"); informational, kept for YAML round-trip [-].
shared_ptr< Kinetics > newKinetics(const string &model)
const size_t npos
constexpr double K_to_eV
K -> eV.
Definition units.h:71
void registerPlasmaRates()
Register the custom plasma reaction rates with Cantera's ReactionRateFactory.
Shared data carrying the electron temperature (mirrors Cantera's own TwoTempPlasmaData but only needs...
Definition PlasmaRates.h:49
double electronTemp
electron temperature [K]
Definition PlasmaRates.h:61
bool update(const Cantera::ThermoPhase &phase, const Cantera::Kinetics &kin) override
Read the electron temperature from the phase.
Shared data for all reverse-two-temperature-plasma reactions.
bool update(const Cantera::ThermoPhase &phase, const Cantera::Kinetics &kin) override
Refresh the cached Tg, Te, P and the shadow-solution rate/equilibrium constants.
std::shared_ptr< Cantera::Solution > shadow
Forward-only ideal-gas shadow solution [-].
std::vector< double > Keq_Tg
std::vector< double > kf_Te
Per-reaction forward rate constant [kmol/m^3/s units, order-dependent] and dimensionless Kc,...
std::vector< double > Keq_Te
std::unordered_map< std::string, size_t > fwdIndex
Forward reaction equation string -> index into the shadow kinetics' reaction list [-].
void buildShadow(const Cantera::ThermoPhase &phase, const Cantera::Kinetics &kin)
Lazily build the ideal-gas shadow Solution from the main phase + kinetics.
std::vector< double > kf_Tg
Per-reaction forward rate constant [kmol/m^3/s units, order-dependent] and dimensionless Kc,...
Physical constants and unit conversions, in SI by default.