rizer.cantera_ext
C++ Cantera 1-D plasma extension (custom Domain1D models and solvers)
Loading...
Searching...
No Matches
CanteraThermo.cpp
Go to the documentation of this file.
2
4
7
8#include <algorithm>
9#include <cmath>
10#include <stdexcept>
11#include <string>
12
13namespace rizer
14{
15
16 // ------------------------------------------------------------------------------ //
17 // Constructor //
18 // ------------------------------------------------------------------------------ //
19
20 CanteraThermo::CanteraThermo(const std::string &mech, const std::string &phase)
21 {
22 m_phase = Cantera::newThermo(mech, phase);
23 m_thermo = m_phase.get();
24 // Resolved once, by dynamic_cast: every two-temperature call below is a
25 // PlasmaPhase member, and a plain IdealGasPhase would carry no electron
26 // temperature at all. A static_cast would reach them anyway and read a
27 // phase that does not have them.
28 m_plasma = dynamic_cast<Cantera::PlasmaPhase *>(m_thermo);
29 if (m_plasma == nullptr)
30 {
31 throw std::invalid_argument(
32 "CanteraThermo: phase \"" + phase + "\" in \"" + mech +
33 "\" is a " + m_thermo->type() +
34 ", not a PlasmaPhase, so it carries no electron temperature. "
35 "Declare it with \"thermo: plasma\" in the mechanism.");
36 }
37 m_nsp = m_thermo->nSpecies();
38 m_ie = m_plasma->electronSpeciesIndex();
39 m_W_electron = m_thermo->molecularWeight(m_ie);
40 m_Yc.assign(m_nsp, 0.0);
41 m_work.assign(m_nsp, 0.0);
42 }
43
44 // ------------------------------------------------------------------------------ //
45 // Public methods //
46 // ------------------------------------------------------------------------------ //
47
48 void CanteraThermo::pressures(double /*r*/, double /*t*/, double rho, double Tg,
49 double Te, const double *Y, double &p_heavy,
50 double &p_electron) const
51 {
52 setState(rho, Tg, Te, Y);
53 // electronPressure() is n_e k_B T_e and pressure() is n_tot k_B T_mean
54 // with T_mean the mole-fraction-weighted mean, so their difference is
55 // n_H k_B T_H exactly: the split of PHYSICS.md (G6), taken from the
56 // phase rather than recomputed from it.
57 const TwoTemperatureState eos = readTwoTemperatureState(*m_plasma);
58 p_electron = eos.electron_pressure;
59 p_heavy = eos.heavy_pressure;
60 }
61
62 double CanteraThermo::soundSpeed(double /*r*/, double /*t*/, double rho,
63 double Tg, double Te, const double *Y) const
64 {
65 // PHYSICS.md appendix A.4, frozen two-pool speed: the pools' isentropic
66 // bulk moduli add, so c = sqrt((gamma_H p_H + gamma_e p_e)/rho) with the
67 // same partial-pressure split pressures() uses.
68 //
69 // gamma_H is summed over the heavy species alone rather than taken as
70 // the mixture's cp_mass/cv_mass. The electron's heat capacity per kg of
71 // ELECTRONS is enormous (1.5 R / W_e), so even at Y_e ~ 1e-7 it carries
72 // a percent of the mixture cp and the mixture ratio is not the heavy
73 // pool's. The electron pool enters through ElectronGamma instead.
74 double p_heavy = 0.0;
75 double p_electron = 0.0;
76 pressures(0.0, 0.0, rho, Tg, Te, Y, p_heavy, p_electron);
77 return soundSpeedAtSetState(p_heavy, p_electron, Tg);
78 }
79
80 double CanteraThermo::heavyEnergy(double Tg, const double *Y) const
81 {
82 // An ideal gas's internal energy is set by temperature and composition
83 // alone, so the density only has to keep the state valid, and the
84 // electron temperature cancels: PlasmaPhase evaluates the electron term
85 // at whatever Te is set and the subtraction below takes the same term
86 // back out, which is why Tg is passed for both.
87 setState(/*rho=*/1.0, Tg, /*Te=*/Tg, Y);
88 return heavyEnergyAtSetState();
89 }
90
92 {
93 return monatomicElectronCv(m_W_electron);
94 }
95
96 double CanteraThermo::densityFromPressure(double pressure, double Tg,
97 double Te, const double *Y) const
98 {
99 // PlasmaPhase::setPressure() is the inverse of the pressure() that
100 // pressures() splits: it sets rho = p Wbar / (R T_mean) with the same
101 // mean temperature, so there is nothing here to invert by hand.
102 setState(/*rho=*/1.0, Tg, Te, Y);
103 m_thermo->setPressure(pressure);
104 const double density = m_thermo->density();
105 if (!(density > 0.0))
106 {
107 throw std::invalid_argument(
108 "CanteraThermo::densityFromPressure: a target pressure of " +
109 std::to_string(pressure) + " Pa gives a density of " +
110 std::to_string(density) +
111 " kg/m^3; the pressure, the temperatures or the mass fractions "
112 "do not describe a gas.");
113 }
114 return density;
115 }
116
117 double CanteraThermo::heavyTemperature(double /*rho*/, const double *Y,
118 double heavy_internal_energy_target,
119 double Tg_seed) const
120 {
121 // Newton on e_H(Tg) = target. e_H is monotone in Tg (every c_v > 0), so
122 // the only way to leave the bracket is a bad step; damping to the bracket
123 // rather than trusting the step keeps a stale seed from diverging.
124 constexpr double T_low = TemperatureFloor;
125 constexpr double T_high = TemperatureCeiling;
126 constexpr int max_iterations = 100;
127 constexpr double tolerance = 1.0e-10;
128
129 if (!std::isfinite(Tg_seed))
130 {
131 throw std::runtime_error(
132 "CanteraThermo::heavyTemperature: the Tg seed is " +
133 std::to_string(Tg_seed) + "; a non-finite seed means the state "
134 "handed in is already corrupted.");
135 }
136 double Tg = std::min(std::max(Tg_seed, T_low), T_high);
137
138 for (int iteration = 0; iteration < max_iterations; iteration++)
139 {
140 double internal_energy, slope; // [J/kg of mixture], [J/kg/K]
141 heavyEnergyAndCv(Tg, Y, internal_energy, slope);
142 const double residual = internal_energy - heavy_internal_energy_target;
143 if (!(slope > 0.0))
144 {
145 throw std::runtime_error(
146 "CanteraThermo::heavyTemperature: non-positive heavy heat "
147 "capacity at Tg = " +
148 std::to_string(Tg) + " K.");
149 }
150 const double step = residual / slope;
151 const double Tg_next = std::min(std::max(Tg - step, T_low), T_high);
152 if (std::abs(Tg_next - Tg) <= tolerance * std::max(1.0, Tg_next))
153 {
154 // A step that stops moving is only convergence if the residual went
155 // with it. Sitting against a bracket edge also stops moving -- the
156 // clamp returns the same value every iteration - and reporting that
157 // as a converged temperature would hand the caller the floor
158 // dressed up as an answer.
159 const double scale =
160 std::max(std::abs(heavy_internal_energy_target), 1.0);
161 const double final_residual =
162 heavyEnergy(Tg_next, Y) - heavy_internal_energy_target;
163 if (std::abs(final_residual) > 1.0e-6 * scale)
164 {
165 throw std::runtime_error(
166 "CanteraThermo::heavyTemperature: stalled at Tg = " +
167 std::to_string(Tg_next) + " K with a relative energy residual of " +
168 std::to_string(std::abs(final_residual) / scale) +
169 ". The target internal energy of " +
170 std::to_string(heavy_internal_energy_target) +
171 " J/kg is outside the bracket [" + std::to_string(T_low) + ", " +
172 std::to_string(T_high) + "] K for this composition.");
173 }
174 return Tg_next;
175 }
176 Tg = Tg_next;
177 }
178 throw std::runtime_error(
179 "CanteraThermo::heavyTemperature: Newton did not converge in " +
180 std::to_string(max_iterations) + " iterations from seed " +
181 std::to_string(Tg_seed) + " K.");
182 }
183
185 double rho, double Tg, double Te,
186 const double *Y) const
187 {
188 setState(rho, Tg, Te, Y);
189 const TwoTemperatureState eos = readTwoTemperatureState(*m_plasma);
190 StateProperties properties;
191 properties.p_heavy = eos.heavy_pressure;
192 properties.p_electron = eos.electron_pressure;
193 properties.sound_speed =
194 soundSpeedAtSetState(eos.heavy_pressure, eos.electron_pressure, Tg);
195 properties.heavy_energy = heavyEnergyAtSetState();
196 return properties;
197 }
198
199 // ------------------------------------------------------------------------------ //
200 // Private methods //
201 // ------------------------------------------------------------------------------ //
202
203 void CanteraThermo::setState(double rho, double Tg, double Te,
204 const double *Y) const
205 {
206 // Rejected rather than floored. Cantera's NASA polynomials are fitted
207 // from 200 K up and a hydrodynamic state that arrives colder is broken,
208 // not merely cold: flooring it would hand every caller below a pressure
209 // and an energy for a state the solver never asked about. The one place
210 // a sub-floor temperature is legitimate is the heavy-pool inversion,
211 // and heavyTemperature() keeps its own iterates inside the bracket.
212 if (!(Tg >= TemperatureFloor) || !(Te >= TemperatureFloor) ||
213 !(Tg <= TemperatureCeiling))
214 {
215 throw std::runtime_error(
216 "CanteraThermo::setState: Tg = " + std::to_string(Tg) +
217 " K, Te = " + std::to_string(Te) + " K; Tg must lie within [" +
218 std::to_string(TemperatureFloor) + ", " +
219 std::to_string(TemperatureCeiling) +
220 "] K and Te must be at least " + std::to_string(TemperatureFloor) +
221 " K, where the mechanism's thermo fits hold.");
222 }
223 // Negative mass fractions are clipped, as PlasmaCell::setState clips
224 // them (see there for why, and for the measured sizes): the hydro step
225 // reads Y after the reaction sub-step, and measured inputs reach -3.5e-8
226 // (C2H6+) on the Python split solver. A NaN is a broken state, not
227 // round-off, so it raises instead of being clipped to zero.
228 for (std::size_t k = 0; k < m_nsp; k++)
229 {
230 if (std::isnan(Y[k]))
231 {
232 throw std::runtime_error(
233 "CanteraThermo::setState: the mass fraction of species " +
234 m_thermo->speciesName(k) + " is NaN at Tg = " +
235 std::to_string(Tg) + " K, Te = " + std::to_string(Te) + " K.");
236 }
237 m_Yc[k] = (Y[k] > 0.0) ? Y[k] : 0.0;
238 }
239 m_thermo->setMassFractions_NoNorm(
240 Cantera::span<const double>(m_Yc.data(), m_nsp));
241 m_plasma->setElectronTemperature(Te);
242 m_thermo->setState_TD(Tg, rho);
243 }
244
245 void CanteraThermo::heavyEnergyAndCv(double Tg, const double *Y,
246 double &internal_energy,
247 double &cv) const
248 {
249 // heavyEnergy() leaves the state it set on the phase, which is the state
250 // heavyCpAndCv() then reads: one state-setting for both, and the slope
251 // is taken at the same state as the energy it is the slope of.
252 internal_energy = heavyEnergy(Tg, Y);
253 double cp_heavy = 0.0; // [J/kg of mixture/K], not needed here
254 heavyCpAndCv(cp_heavy, cv);
255 }
256
257 void CanteraThermo::heavyCpAndCv(double &cp, double &cv) const
258 {
259 // Mayer per species, c_v = (c_p^molar - R) / W, summed over the heavy
260 // species alone. Cantera offers no heavy-pool-only capacity: cp_mass()
261 // carries the electron at its own temperature.
262 m_thermo->getPartialMolarCp(Cantera::span<double>(m_work.data(), m_nsp));
263 cp = 0.0;
264 cv = 0.0;
265 for (std::size_t k = 0; k < m_nsp; k++)
266 {
267 if (k == m_ie)
268 continue;
269 const double moles =
270 m_thermo->massFraction(k) / m_thermo->molecularWeight(k);
271 cp += moles * m_work[k];
272 cv += moles * (m_work[k] - Cantera::GasConstant);
273 }
274 }
275
276 double CanteraThermo::soundSpeedAtSetState(double p_heavy, double p_electron,
277 double Tg) const
278 {
279 double cp_heavy = 0.0; // [J/kg of mixture/K]
280 double cv_heavy = 0.0; // [J/kg of mixture/K]
281 heavyCpAndCv(cp_heavy, cv_heavy);
282 if (!(cv_heavy > 0.0))
283 {
284 throw std::runtime_error(
285 "CanteraThermo::soundSpeed: non-positive heavy heat capacity at "
286 "Tg = " +
287 std::to_string(Tg) + " K; the mixture has no heavy pool to carry "
288 "an acoustic wave.");
289 }
290 return std::sqrt(((cp_heavy / cv_heavy) * p_heavy +
291 ElectronGamma * p_electron) /
292 m_thermo->density());
293 }
294
295 double CanteraThermo::heavyEnergyAtSetState() const
296 {
297 // intEnergy_mass() is sum_k Y_k u_k / W_k over the whole mixture, with
298 // PlasmaPhase evaluating the electron's u at Te and every heavy
299 // species' at Tg. Removing the electron term leaves the heavy pool on
300 // the per kg of MIXTURE basis that PHYSICS.md (4b) conserves.
301 m_thermo->getPartialMolarIntEnergies(
302 Cantera::span<double>(m_work.data(), m_nsp));
303 return m_thermo->intEnergy_mass() -
304 m_thermo->massFraction(m_ie) * m_work[m_ie] / m_W_electron;
305 }
306
307} // namespace rizer
The production ThermoAccessor for the hydro sub-step: Cantera's two-temperature PlasmaPhase behind Hy...
The two-temperature quantities every model in this extension reads back from a PlasmaPhase,...
size_t nSpecies() const
string speciesName(size_t k) const
CanteraThermo(const std::string &mech, const std::string &phase)
Loads the phase alone, never its reactions: the accessor evaluates thermo only, and a mechanism's rea...
double electronCv() const override
exactly: no thermo lookup and no state, so it is the same number at every node and every step.
double heavyEnergy(double Tg, const double *Y) const override
Cantera's mixture internal energy less the electron's contribution, on the per kg of MIXTURE basis th...
double soundSpeed(double r, double t, double rho, double Tg, double Te, const double *Y) const override
summed over the heavy species alone, from the fixed ElectronGamma, over the same partial-pressure sp...
StateProperties stateProperties(double r, double t, double rho, double Tg, double Te, const double *Y) const override
The four state functions from one state-setting, rather than the three the default composition perfor...
double densityFromPressure(double pressure, double Tg, double Te, const double *Y) const override
PlasmaPhase::setPressure() read back through density().
double heavyTemperature(double rho, const double *Y, double heavy_internal_energy_target, double Tg_seed) const override
Damped Newton on = target, seeded with Tg_seed and confined to [200 K, 1e5 K].
void pressures(double r, double t, double rho, double Tg, double Te, const double *Y, double &p_heavy, double &p_electron) const override
PlasmaPhase::electronPressure(), and the total less that.
const double GasConstant
shared_ptr< ThermoPhase > newThermo(const AnyMap &phaseNode, const AnyMap &rootNode=AnyMap())
constexpr double ElectronGamma
Ratio of specific heats of the electron pool [-].
constexpr double TemperatureFloor
Lower limit of the mechanism's NASA polynomials [K].
Definition PlasmaState.h:34
double monatomicElectronCv(double electron_molecular_weight)
Electron specific heat at constant volume, per kg of ELECTRONS, .
Definition PlasmaState.h:64
constexpr double TemperatureCeiling
Upper limit of the physically meaningful range [K].
Definition PlasmaState.h:43
TwoTemperatureState readTwoTemperatureState(const Cantera::PlasmaPhase &phase)
Read the two-temperature EOS back from a phase whose state is set.
Definition PlasmaState.h:84
Every state function hydroStep() reads off one primitive state when it builds the conservative variab...
double heavy_energy
Heavy-pool energy [J/kg of mixture].
double p_heavy
Heavy-pool partial pressure [Pa].
double p_electron
Electron partial pressure [Pa].
double sound_speed
Frozen two-pool sound speed [m/s].
The two-temperature equation of state at the state currently set on a phase, PHYSICS....
Definition PlasmaState.h:72