From 3afda84aa95318d7b7e571b0267fba28810d7531 Mon Sep 17 00:00:00 2001 From: Quentin Male Date: Sun, 11 May 2025 14:35:26 -0400 Subject: [PATCH 01/29] Initial implementation of two-term approximation EEDF solver Co-authored-by: Nicolas Barleon --- .../kinetics/ElectronCollisionPlasmaRate.h | 63 +- .../cantera/kinetics/ElectronCrossSection.h | 61 ++ include/cantera/kinetics/Kinetics.h | 4 + include/cantera/kinetics/KineticsFactory.h | 20 + .../cantera/thermo/EEDFTwoTermApproximation.h | 271 ++++++ include/cantera/thermo/PlasmaPhase.h | 267 ++++-- include/cantera/thermo/ThermoPhase.h | 11 +- interfaces/cython/cantera/thermo.pxd | 7 +- interfaces/cython/cantera/thermo.pyx | 44 +- samples/python/thermo/plasmatest.py | 55 ++ src/kinetics/ElectronCollisionPlasmaRate.cpp | 70 +- src/kinetics/ElectronCrossSection.cpp | 95 +++ src/kinetics/KineticsFactory.cpp | 81 +- src/thermo/EEDFTwoTermApproximation.cpp | 777 ++++++++++++++++++ src/thermo/PlasmaPhase.cpp | 598 +++++++++----- 15 files changed, 2098 insertions(+), 326 deletions(-) create mode 100644 include/cantera/kinetics/ElectronCrossSection.h create mode 100644 include/cantera/thermo/EEDFTwoTermApproximation.h create mode 100644 samples/python/thermo/plasmatest.py create mode 100644 src/kinetics/ElectronCrossSection.cpp create mode 100644 src/thermo/EEDFTwoTermApproximation.cpp diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index e627998e841..19c47f9feae 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -32,7 +32,6 @@ struct ElectronCollisionPlasmaData : public ReactionData energyLevels.resize(0); distribution.resize(0); m_dist_number = -1; - m_level_number = -1; } vector energyLevels; //!< electron energy levels @@ -103,7 +102,6 @@ struct ElectronCollisionPlasmaData : public ReactionData class ElectronCollisionPlasmaRate : public ReactionRate { public: - ElectronCollisionPlasmaRate() = default; ElectronCollisionPlasmaRate(const AnyMap& node, @@ -143,6 +141,47 @@ class ElectronCollisionPlasmaRate : public ReactionRate throw NotImplementedError("ElectronCollisionPlasmaRate::ddTScaledFromStruct"); } + //! The kind of the process + const string& kind() const { + return m_kind; + } + + //! The target of the process + const string& target() const { + return m_target; + } + + //! The product of the process + const string& product() const { + return m_product; + } + + + //! Set the value of #m_energyLevels [eV] + void set_energyLevels(vector epsilon) { + m_energyLevels = epsilon; + } + + //! Set the value of #m_crossSections [eV] + void set_crossSections(vector sigma) { + m_crossSections = sigma; + } + + //! Set the value of m_threshold [eV] + void set_threshold(double threshold) + { + m_threshold = threshold; + } + + void set_cs_ok() { + cs_ok = true; + } + + const bool get_cs_ok() const { + return cs_ok; + } + + //! The value of #m_energyLevels [eV] const vector& energyLevels() const { return m_energyLevels; @@ -158,10 +197,30 @@ class ElectronCollisionPlasmaRate : public ReactionRate return m_crossSectionsInterpolated; } + //! Set the value of #m_crossSectionsInterpolated + void setCrossSectionInterpolated(vector& cs) { + m_crossSectionsInterpolated = cs; + } + //! Update the value of #m_crossSectionsInterpolated [m2] void updateInterpolatedCrossSection(const vector&); private: + //! The name of the kind of electron collision + string m_kind; + + //! The name of the target of electron collision + string m_target; + + //! The product of electron collision + string m_product; + + //! The energy threshold of electron collision + double m_threshold; + + //! Check if a cross-section is define for this rate + bool cs_ok = false; + //! electron energy levels [eV] vector m_energyLevels; diff --git a/include/cantera/kinetics/ElectronCrossSection.h b/include/cantera/kinetics/ElectronCrossSection.h new file mode 100644 index 00000000000..334c450a257 --- /dev/null +++ b/include/cantera/kinetics/ElectronCrossSection.h @@ -0,0 +1,61 @@ +//! @file ElectronCrossSection.h Declaration for class Cantera::ElectronCrossSection. + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://cantera.org/license.txt for license and copyright information. + +#ifndef CT_ELECTRONCROSSSECTION_H +#define CT_ELECTRONCROSSSECTION_H + +#include "cantera/base/ct_defs.h" +#include "cantera/base/AnyMap.h" + +namespace Cantera +{ + +//! Contains data about the cross sections of electron collision +class ElectronCrossSection +{ +public: + ElectronCrossSection(); + + //! ElectronCrossSection objects are not copyable or assignable + ElectronCrossSection(const ElectronCrossSection&) = delete; + ElectronCrossSection& operator=(const ElectronCrossSection& other) = delete; + ~ElectronCrossSection(); + + //! Validate the cross-section data. + // void validate(); + + //! The name of the kind of electron collision + string kind; + + //! The name of the target of electron collision + string target; + + //! The product of electron collision + string product; + + vector products; + + //! Data of cross section. [m^2] + vector crossSection; + + //! The energy level corresponding to the cross section. [eV] + vector energyLevel; + + //! The threshold of a process in [eV] + double threshold; + + //! Extra data used for specific models + // AnyMap extra; +}; + +//! create an ElectronCrossSection object to store data. +unique_ptr newElectronCrossSection(const AnyMap& node); + +//! Get a vector of ElectronCrossSection objects to access the data. +// std::vector> getElectronCrossSections(const AnyValue& items); + +} + +#endif diff --git a/include/cantera/kinetics/Kinetics.h b/include/cantera/kinetics/Kinetics.h index ffcf1827df0..144ef3df049 100644 --- a/include/cantera/kinetics/Kinetics.h +++ b/include/cantera/kinetics/Kinetics.h @@ -1408,6 +1408,10 @@ class Kinetics return m_root.lock(); } + bool ready() const { + return m_ready; + } + //! Register a function to be called if reaction is added. //! @param id A unique ID corresponding to the object affected by the callback. //! Typically, this is a pointer to an object that also holds a reference to the diff --git a/include/cantera/kinetics/KineticsFactory.h b/include/cantera/kinetics/KineticsFactory.h index 4f40287d486..7793ce6915b 100644 --- a/include/cantera/kinetics/KineticsFactory.h +++ b/include/cantera/kinetics/KineticsFactory.h @@ -85,6 +85,26 @@ shared_ptr newKinetics(const vector>& phases, void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); +/** + * Add reactions to a Kinetics object. + * + * @param kin The Kinetics object to be initialized + * @param rxnList The list of Reaction objects + */ +void addReactions(Kinetics& kin, vector> rxnList); + +/** + * Get the list of reactions in AnyMap. + * + * @param kin The Kinetics object to be initialized + * @param phaseNode Phase entry for the phase where the reactions occur. This + * phase definition is used to determine the source of the reactions added + * to the Kinetics object. + * @param rootNode The root node of the file containing the phase definition, + * which will be treated as the default source for reactions + */ +vector reactionsAnyMapList(Kinetics& kin, const AnyMap& phaseNode, + const AnyMap& rootNode=AnyMap()); //! @} } diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h new file mode 100644 index 00000000000..8180f86987f --- /dev/null +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -0,0 +1,271 @@ +/** + * @file EEDFTwoTermApproximation.h EEDF Two-Term approximation solver. + */ + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://cantera.org/license.txt for license and copyright information. + +#ifndef CT_EEDF_TWO_TERM_APPROXIMATION_H +#define CT_EEDF_TWO_TERM_APPROXIMATION_H + +#include "cantera/base/ct_defs.h" +#include "cantera/numerics/eigen_sparse.h" +#include "cantera/numerics/funcs.h" +#include "cantera/base/global.h" + +namespace Cantera +{ + +class PlasmaPhase; + +typedef Eigen::SparseMatrix SparseMat_fp; +typedef Eigen::Triplet Triplet_fp; +typedef vector vector_fp; + + +/** + * EEDF solver options. Used internally by class EEDFTwoTermApproximation. + */ +class TwoTermOpt +{ +public: + TwoTermOpt() = default; + + double m_delta0 = 1e14; //!< Initial value of the iteration parameter + size_t m_maxn = 200; //!< Maximum number of iterations + double m_factorM = 4.0; //!< Reduction factor of error + size_t m_points = 150; //!< Number of points for energy grid + double m_rtol = 1e-5; //!< Relative tolerance of EEDF for solving Boltzmann equation + string m_growth = "temporal"; //!< String for the growth model (none, temporal or spatial) + double m_moleFractionThreshold = 0.01; //!< Threshold for species not considered in the Boltzmann solver but present in the mixture + string m_firstguess = "maxwell"; //!< String for EEDF first guess + double m_init_kTe = 2.0; //!< Initial electron mean energy + +}; // end of class TwoTermOpt + +class EEDFTwoTermApproximation +{ +public: + EEDFTwoTermApproximation() = default; + + + //! Constructor combined with the initialization function + /*! + * This constructor initializes the EEDFTwoTermApproximation object with everything + * it needs to start solving EEDF. + * + * @param s PlasmaPhase object that will be used in the solver calls. + */ + EEDFTwoTermApproximation(PlasmaPhase& s); + + virtual ~EEDFTwoTermApproximation() = default; + + // compute the EEDF given an electric field + // CQM The solver will take the species to consider and the set of cross-sections + // from the PlasmaPhase object. + // It will write the EEDF and its grid into the PlasmaPhase object. + // Successful returns are indicated by a return value of 0. + int calculateDistributionFunction(); + + void setLinearGrid(double& kTe_max, size_t& ncell); + + /** + * Options controlling how the calculation is carried out. + * @see TwoTermOpt + */ + TwoTermOpt options; + + vector getGridEdge() const { + return m_gridEdge; + } + + vector getEEDFEdge() const { + return m_f0_edge; + } + + double getElectronMobility() const { + return m_electronMobility; + } + +protected: + /** + * Prepare for EEDF calculations. + * @param s object representing the solution phase. + */ + void initialize(PlasmaPhase& s); + + //! Pointer to the PlasmaPhase object used to initialize this object. + /*! + * This PlasmaPhase object must be compatible with the PlasmaPhase objects + * input from the compute function. Currently, this means that the 2 + * PlasmaPhases have to have consist of the same species and elements. + */ + PlasmaPhase* m_phase; + + //! Iterate f0 (EEDF) until convergence + void converge(Eigen::VectorXd& f0); + + //! An iteration of solving electron energy distribution function + Eigen::VectorXd iterate(const Eigen::VectorXd& f0, double delta); + + //! The integral in [a, b] of \f$x u(x) \exp[g (x_0 - x)]\f$ + //! assuming that u is linear with u(a) = u0 and u(b) = u1 + double integralPQ(double a, double b, double u0, double u1, + double g, double x0); + + //! Vector g is used by matrix_P() and matrix_Q(). + /** + * \f[ + * g_i = \frac{1}{\epsilon_{i+1} - \epsilon_{i-1}} \ln(\frac{F_{0, i+1}}{F_{0, i-1}}) + * \f] + */ + vector_fp vector_g(const Eigen::VectorXd& f0); + + //! The matrix of scattering-out. + /** + * \f[ + * P_{i,k} = \gamma \int_{\epsilon_i - 1/2}^{\epsilon_i + 1/2} + * \epsilon \sigma_k exp[(\epsilon_i - \epsilon)g_i] d \epsilon + * \f] + */ + SparseMat_fp matrix_P(const vector_fp& g, size_t k); + + //! The matrix of scattering-in + /** + * \f[ + * Q_{i,j,k} = \gamma \int_{\epsilon_1}^{\epsilon_2} + * \epsilon \sigma_k exp[(\epsilon_j - \epsilon)g_j] d \epsilon + * \f] + */ + //! where the interval \f$[\epsilon_1, \epsilon_2]\f$ is the overlap of cell j, + //! and cell i shifted by the threshold energy: + /** + * \f[ + * \epsilon_1 = \min(\max(\epsilon_{i-1/2}+u_k, \epsilon_{j-1/2}),\epsilon_{j+1/2}), + * \f] + * \f[ + * \epsilon_2 = \min(\max(\epsilon_{i+1/2}+u_k, \epsilon_{j-1/2}),\epsilon_{j+1/2}) + * \f] + */ + SparseMat_fp matrix_Q(const vector_fp& g, size_t k); + + //! Matrix A (Ax = b) of the equation of EEDF, which is discretized by the exponential scheme + //! of Scharfetter and Gummel, + /** + * \f[ + * \left[ \tilde{W} F_0 - \tilde{D} \frac{d F_0}{\epsilon} \right]_{i+1/2} = + * \frac{\tilde{W}_{i+1/2} F_{0,i}}{1 - \exp[-z_{i+1/2}]} + + * \frac{\tilde{W}_{i+1/2} F_{0,i+1}}{1 - \exp[z_{i+1/2}]} + * \f] + * where \f$ z_{i+1/2} = \tilde{w}_{i+1/2} / \tilde{D}_{i+1/2} \f$ (Peclet number). + */ + SparseMat_fp matrix_A(const Eigen::VectorXd& f0); + + //! Reduced net production frequency. Equation (10) of ref. [1] + //! divided by N. + //! @param f0 EEDF + double netProductionFreq(const Eigen::VectorXd& f0); + + //! Diffusivity + double electronDiffusivity(const Eigen::VectorXd& f0); + + //! Mobility + double electronMobility(const Eigen::VectorXd& f0); + + void initSpeciesIndexCS(); + + void checkSpeciesNoCrossSection(); + + void updateCS(); + + void update_mole_fractions(); + + void calculateTotalElasticCrossSection(); + + void calculateTotalCrossSection(); + + void setGridCache(); + + double norm(const Eigen::VectorXd& f, const Eigen::VectorXd& grid); + + double m_electronMobility; + + //! Grid of electron energy (cell center) [eV] + Eigen::VectorXd m_gridCenter; + + //! Grid of electron energy (cell boundary i-1/2) [eV] + vector_fp m_gridEdge; + + //! Location of cell j for grid cache + vector> m_j; + + //! Location of cell i for grid cache + vector> m_i; + + //! Cross section at the boundaries of the overlap of cell i and j + vector> m_sigma; + + //! The energy boundaries of the overlap of cell i and j + vector> m_eps; + + //! The cross section at the center of a cell + std::vector m_sigma_offset; + + //! normalized electron energy distribution function + Eigen::VectorXd m_f0; + + //! EEDF at grid edges (cell boundaries) + vector_fp m_f0_edge; + + //! Total electron cross section on the cell center of energy grid + vector_fp m_totalCrossSectionCenter; + + //! Total electron cross section on the cell boundary (i-1/2) of + //! energy grid + vector_fp m_totalCrossSectionEdge; + + //! vector of total elastic cross section weighted with mass ratio + vector_fp m_sigmaElastic; + + //! list of target species indices in global Cantera numbering (1 index per cs) + vector m_kTargets; + + //! list of target species indices in local X EEDF numbering (1 index per cs) + vector m_klocTargets; + + //! Indices of species which has no cross-section data + vector m_kOthers; + + //! Local to global indices + vector m_k_lg_Targets; + + //! Mole fraction of targets + vector_fp m_X_targets; + + //! Previous mole fraction of targets used to compute eedf + vector_fp m_X_targets_prev; + + double m_gamma; + + //! boolean for the electron-electron collisions + bool m_eeCol = false; + + //! Compute electron-electron collision integrals + void eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, + double& a, size_t nPoints); + + //! flag of having an EEDF + bool m_has_EEDF; + + //! First call to calculateDistributionFunction + bool m_first_call; + +private: + + + +}; // end of class EEDFTwoTermApproximation + +} // end of namespace Cantera + +#endif \ No newline at end of file diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index 266fbfe6466..acdbeab0a64 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -10,6 +10,8 @@ #define CT_PLASMAPHASE_H #include "cantera/thermo/IdealGasPhase.h" +#include "cantera/kinetics/ElectronCrossSection.h" +#include "cantera/thermo/EEDFTwoTermApproximation.h" #include "cantera/numerics/eigen_sparse.h" namespace Cantera @@ -96,21 +98,22 @@ class PlasmaPhase: public IdealGasPhase */ explicit PlasmaPhase(const string& inputFile="", const string& id=""); - ~PlasmaPhase(); - string type() const override { return "plasma"; } void initThermo() override; + //! Overload to signal updating electron energy density function. + virtual void setTemperature(const double temp) override; + + bool addElectronCrossSection(shared_ptr ecs); + //! Set electron energy levels. //! @param levels The vector of electron energy levels (eV). //! Length: #m_nPoints. //! @param length The length of the @c levels. - //! @param updateEnergyDist update electron energy distribution - void setElectronEnergyLevels(const double* levels, size_t length, - bool updateEnergyDist=true); + void setElectronEnergyLevels(const double* levels, size_t length); //! Get electron energy levels. //! @param levels The vector of electron energy levels (eV). Length: #m_nPoints @@ -128,6 +131,13 @@ class PlasmaPhase: public IdealGasPhase const double* distrb, size_t length); + //! Set discretized electron energy distribution. + //! @param distrb The vector of electron energy distribution. + //! Length: #m_nPoints. + //! @param length The length of the vectors, which equals #m_nPoints. + void setDiscretizedElectronEnergyDist(const double* distrb, + size_t length); + //! Get electron energy distribution. //! @param distrb The vector of electron energy distribution. //! Length: #m_nPoints. @@ -220,11 +230,6 @@ class PlasmaPhase: public IdealGasPhase return m_nPoints; } - //! Number of collisions - size_t nCollisions() const { - return m_collisions.size(); - } - //! Electron Species Index size_t electronSpeciesIndex() const { return m_electronSpeciesIndex; @@ -283,6 +288,9 @@ class PlasmaPhase: public IdealGasPhase void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()) override; + //! Update electron energy distribution. + void updateElectronEnergyDistribution(); + //! Electron species name string electronSpeciesName() const { return speciesName(m_electronSpeciesIndex); @@ -298,22 +306,109 @@ class PlasmaPhase: public IdealGasPhase return m_levelNum; } - virtual void setSolution(std::weak_ptr soln) override; + vector kInelastic() const { + return m_kInelastic; + } - /** - * The elastic power loss (J/s/m³) - * @f[ - * P_k = N_A N_A C_e e \sum_k C_k K_k, - * @f] - * where @f$ C_k @f$ and @f$ C_e @f$ are the concentration (kmol/m³) of the - * target species and electrons, respectively. @f$ K_k @f$ is the elastic - * electron energy loss coefficient (eV-m³/s). - */ - double elasticPowerLoss(); + // number of cross section dataset + size_t nElectronCrossSections() const { + return m_ncs; + } + + // target of a specific process + string target(size_t k) { + return m_ecss[k]->target; + } + + // product of a specific process + string product(size_t k) { + return m_ecss[k]->product; + } + + const std::vector& products(size_t k) const { + return m_ecss[k]->products; // Directly retrieve the stored product list + } + + // kind of a specific process + string kind(size_t k) { + return m_ecss[k]->kind; + } + + // threshold of a specific process + double threshold(size_t k) { + return m_ecss[k]->threshold; + } + + vector shiftFactor() const { + return m_shiftFactor; + } + + vector inFactor() const { + return m_inFactor; + } + + // Gas number density [m^-3] + double N() const { + return molarDensity() * Avogadro; + } + + double F() const { + return m_F; + } + + double E() const { + return m_E; + } + + double ionDegree() const { + return m_ionDegree; + } + + double kT() const { + return m_kT; + } + + double EN() const { + return m_EN; + } + + vector> crossSections() const { + return m_crossSections; + } + + vector> energyLevels() const { + return m_energyLevels; + } + + vector kElastic() const { + return m_kElastic; + } + + //! Set reduced electric field given in [V.m2] + void setReducedElectricField(double EN) { + m_EN = EN; // [V.m2] + m_E = m_EN * molarDensity() * Avogadro; // [V/m] + } + //! Get elastic electron energy loss rate (eV/s) + double elasticElectronEnergyLossRate() { + return concentration(m_electronSpeciesIndex) * + normalizedElasticElectronEnergyLossRate(); + } + + //! Get normalized elastic electron energy loss rate (eV-m3/kmol/s) + double normalizedElasticElectronEnergyLossRate(); protected: + + void initialize(); + void updateThermo() const override; + //! update interpolated cross sections + //! This function needs to be called when the EEDF is updated or + //! when the cross sections are updated + void updateInterpolatedCrossSections(); + //! When electron energy distribution changed, plasma properties such as //! electron-collision reaction rates need to be re-evaluated. void electronEnergyDistributionChanged(); @@ -344,9 +439,6 @@ class PlasmaPhase: public IdealGasPhase */ void checkElectronEnergyDistribution() const; - //! Update electron energy distribution. - void updateElectronEnergyDistribution(); - //! Set isotropic electron energy distribution void setIsotropicElectronEnergyDistribution(); @@ -357,14 +449,8 @@ class PlasmaPhase: public IdealGasPhase //! Electron energy distribution norm void normalizeElectronEnergyDistribution(); - //! Update interpolated cross section of a collision - bool updateInterpolatedCrossSection(size_t k); - - //! Update electron energy distribution difference - void updateElectronEnergyDistDifference(); - // Electron energy order in the exponential term - double m_isotropicShapeFactor = 1.0; + double m_isotropicShapeFactor = 2.0; //! Number of points of electron energy levels size_t m_nPoints = 1001; @@ -382,6 +468,9 @@ class PlasmaPhase: public IdealGasPhase //! Electron temperature [K] double m_electronTemp; + //! Gas number density + //double m_N; + //! Electron energy distribution type string m_distributionType = "isotropic"; @@ -391,40 +480,76 @@ class PlasmaPhase: public IdealGasPhase //! Flag of normalizing electron energy distribution bool m_do_normalizeElectronEnergyDist = true; + //! Indices of inelastic collisions in m_crossSections + vector m_kInelastic; + + //! electric field [V/m] + double m_E; + + //! reduced electric field [V.m2] + double m_EN; + + //! reduced electric field [Td] + //double m_EN_Td; + + //! electric field freq [Hz] + double m_F; + + //! normalized electron energy distribution function + Eigen::VectorXd m_f0; + + //! Mole fraction of targets + //vector m_X_targets; + + //! list of target species indices in local X EEDF numbering (1 index per cs) + //std::vector m_klocTargets; + + //! number of cross section sets + size_t m_ncs; + + //! array of cross-section object + vector> m_ecss; + + //! Cross section data. m_crossSections[i][j], where i is the specific process, + //! j is the index of vector. Unit: [m^2] + std::vector> m_crossSections; + + //! Electron energy levels correpsonding to the cross section data. m_energyLevels[i][j], + //! where i is the specific process, j is the index of vector. Unit: [eV] + std::vector> m_energyLevels; + + //! shift factor. This is used for calculating the collision term. + std::vector m_shiftFactor; + + //! in factor. This is used for calculating the Q matrix of + //! scattering-in processes. + std::vector m_inFactor; + + //! Indices of elastic collisions in m_crossSections + std::vector m_kElastic; + + //! flag of electron energy distribution function + bool m_f0_ok; + + //! ionization degree for the electron-electron collisions (tmp is the previous one) + double m_ionDegree; + + //! Boltzmann constant times gas temperature [eV] + double m_kT; //! Data for initiate reaction AnyMap m_root; - //! Electron energy distribution Difference dF/dε (V^-5/2) - Eigen::ArrayXd m_electronEnergyDistDiff; - - //! Elastic electron energy loss coefficients (eV m3/s) - /*! The elastic electron energy loss coefficient for species k is, - * @f[ - * K_k = \frac{2 m_e}{m_k} \sqrt{\frac{2 e}{m_e}} \int_0^{\infty} \sigma_k - * \epsilon^2 \left( F_0 + \frac{k_B T}{e} - * \frac{\partial F_0}{\partial \epsilon} \right) d \epsilon, - * @f] - * where @f$ m_e @f$ [kg] is the electron mass, @f$ \epsilon @f$ [V] is the - * electron energy, @f$ \sigma_k @f$ [m2] is the reaction collision cross section, - * @f$ F_0 @f$ [V^(-3/2)] is the normalized electron energy distribution function. - */ - vector m_elasticElectronEnergyLossCoefficients; - - //! Updates the elastic electron energy loss coefficient for collision index i - /*! Calculates the elastic energy loss coefficient using the current electron - energy distribution and cross sections. - */ - void updateElasticElectronEnergyLossCoefficient(size_t i); - - //! Update elastic electron energy loss coefficients - /*! Used by elasticPowerLoss() and other plasma property calculations that - depends on #m_elasticElectronEnergyLossCoefficients. This function calls - updateInterpolatedCrossSection() before calling - updateElasticElectronEnergyLossCoefficient() - */ - void updateElasticElectronEnergyLossCoefficients(); + //! get the target species index + size_t targetSpeciesIndex(shared_ptr R); + + //! get cross section interpolated + vector crossSection(shared_ptr reaction); private: + + //! pointer to EEDF solver + unique_ptr ptrEEDFSolver = nullptr; + //! Electron energy distribution change variable. Whenever //! #m_electronEnergyDist changes, this int is incremented. int m_distNum = -1; @@ -436,25 +561,11 @@ class PlasmaPhase: public IdealGasPhase //! The list of shared pointers of plasma collision reactions vector> m_collisions; - //! The list of shared pointers of collision rates - vector> m_collisionRates; - - //! The collision-target species indices of #m_collisions - vector m_targetSpeciesIndices; - - //! Interpolated cross sections. This is used for storing - //! interpolated cross sections temporarily. - vector m_interp_cs; - - //! The list of whether the interpolated cross sections is ready - vector m_interp_cs_ready; - - //! Set collisions. This function sets the list of collisions and - //! the list of target species using #addCollision. - void setCollisions(); + //! Indices of elastic collisions + vector m_elasticCollisionIndices; - //! Add a collision and record the target species - void addCollision(std::shared_ptr collision); + //! Collision cross section + vector m_interpolatedCrossSections; }; diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index ec3952983dc..af5c049a233 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -389,7 +389,7 @@ enum class ThermoBasis * * @ingroup thermoprops */ -class ThermoPhase : public Phase +class ThermoPhase : public Phase, public std::enable_shared_from_this { public: //! Constructor. Note that ThermoPhase is meant to be used as a base class, @@ -2031,6 +2031,10 @@ class ThermoPhase : public Phase m_soln = soln; } + shared_ptr kinetics() { + return m_kinetics; + } + protected: //! Store the parameters of a ThermoPhase object such that an identical //! one could be reconstructed using the newThermo(AnyMap&) function. This @@ -2070,6 +2074,11 @@ class ThermoPhase : public Phase //! reference to Solution std::weak_ptr m_soln; + + //! The kinetics object associates with ThermoPhase + //! Some phase requires Kinetics to perform calculation + //! such as PlasmaPhase + shared_ptr m_kinetics; }; } diff --git a/interfaces/cython/cantera/thermo.pxd b/interfaces/cython/cantera/thermo.pxd index a22beca57f0..b04a18ede4d 100644 --- a/interfaces/cython/cantera/thermo.pxd +++ b/interfaces/cython/cantera/thermo.pxd @@ -193,6 +193,7 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": cdef cppclass CxxPlasmaPhase "Cantera::PlasmaPhase" (CxxThermoPhase): CxxPlasmaPhase() void setElectronTemperature(double) except +translate_exception + void setReducedElectricField(double) except +translate_exception void setElectronEnergyLevels(double*, size_t) except +translate_exception void getElectronEnergyLevels(double*) void setDiscretizedElectronEnergyDist(double*, double*, size_t) except +translate_exception @@ -210,7 +211,11 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": size_t nElectronEnergyLevels() double electronPressure() string electronSpeciesName() - double elasticPowerLoss() except +translate_exception + double EN() + void updateElectronEnergyDistribution() + double elasticElectronEnergyLossRate() + double normalizedElasticElectronEnergyLossRate() + #double elasticPowerLoss() except +translate_exception cdef extern from "cantera/cython/thermo_utils.h": diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index b091f708829..49ecb9b1e01 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -1783,6 +1783,18 @@ cdef class ThermoPhase(_SolutionBase): raise ThermoModelMethodError(self.thermo_model) return self.plasma.electronPressure() + property EN: + """Get/Set EN [V.m2].""" + def __get__(self): + if not self._enable_plasma: + raise ThermoModelMethodError(self.thermo_model) + return self.plasma.EN() + + def __set__(self, value): + if not self._enable_plasma: + raise ThermoModelMethodError(self.thermo_model) + self.plasma.setReducedElectricField(value) + def set_discretized_electron_energy_distribution(self, levels, distribution): """ Set electron energy distribution. When this method is used, electron @@ -1809,6 +1821,12 @@ cdef class ThermoPhase(_SolutionBase): &data_dist[0], len(levels)) + def update_EEDF(self): + if not self._enable_plasma: + raise TypeError('This method is invalid for ' + f'thermo model: {self.thermo_model}.') + self.plasma.updateElectronEnergyDistribution() + property n_electron_energy_levels: """ Number of electron energy levels """ def __get__(self): @@ -1904,15 +1922,33 @@ cdef class ThermoPhase(_SolutionBase): raise ThermoModelMethodError(self.thermo_model) return pystr(self.plasma.electronSpeciesName()) - property elastic_power_loss: + property elastic_electron_energy_loss_rate: + """ Elastic electron energy loss rate """ + def __get__(self): + if not self._enable_plasma: + raise ThermoModelMethodError(self.thermo_model) + return self.plasma.elasticElectronEnergyLossRate() + + property normalized_elastic_electron_energy_loss_rate: """ - Elastic power loss (J/s/m3) - .. versionadded:: 3.2 + Normalized elastic electron energy loss rate + The elastic electron energy loss rate is normalized + by dividing the concentration of electron. """ def __get__(self): if not self._enable_plasma: raise ThermoModelMethodError(self.thermo_model) - return self.plasma.elasticPowerLoss() + return self.plasma.normalizedElasticElectronEnergyLossRate() + + #property elastic_power_loss: + # """ + # Elastic power loss (J/s/m3) + # .. versionadded:: 3.2 + # """ + # def __get__(self): + # if not self._enable_plasma: + # raise ThermoModelMethodError(self.thermo_model) + # return self.plasma.elasticPowerLoss() cdef class InterfacePhase(ThermoPhase): """ A class representing a surface, edge phase """ diff --git a/samples/python/thermo/plasmatest.py b/samples/python/thermo/plasmatest.py new file mode 100644 index 00000000000..94d54875396 --- /dev/null +++ b/samples/python/thermo/plasmatest.py @@ -0,0 +1,55 @@ +""" +EEDF calculation +============== +Compute EEDF with two term approximation solver at constant E/N. +Compare with results from BOLOS. + +Requires: cantera >= XX. + +.. tags:: Python, plasma +""" + + +import matplotlib.pyplot as plt +import cantera as ct + +gas = ct.Solution('example_data/air-plasma_Phelps.yaml') +gas.TPX = 300., 101325., 'N2:0.79, O2:0.21, N2+:1E-10, Electron:1E-10' +gas.EN = 200.0 * 1e-21 # Reduced electric field [V.m^2] +gas.update_EEDF() + +grid = gas.electron_energy_levels +eedf = gas.electron_energy_distribution + +# results from BOLOS +cgrid = [6.000e-02, 6.908e-02, 7.954e-02, 9.158e-02, 1.054e-01, 1.214e-01, 1.398e-01, + 1.609e-01, 1.853e-01, 2.133e-01, 2.456e-01, 2.828e-01, 3.256e-01, 3.749e-01, + 4.317e-01, 4.970e-01, 5.723e-01, 6.589e-01, 7.586e-01, 8.735e-01, 1.006e+00, + 1.158e+00, 1.333e+00, 1.535e+00, 1.767e+00, 2.035e+00, 2.343e+00, 2.698e+00, + 3.106e+00, 3.576e+00, 4.117e+00, 4.741e+00, 5.458e+00, 6.284e+00, 7.236e+00, + 8.331e+00, 9.592e+00, 1.104e+01, 1.272e+01, 1.464e+01, 1.686e+01, 1.941e+01, + 2.235e+01, 2.573e+01, 2.962e+01, 3.411e+01, 3.927e+01, 4.522e+01, 5.206e+01, + 5.994e+01] +cf0 = [1.445e-01, 1.445e-01, 1.445e-01, 1.445e-01, 1.445e-01, 1.445e-01, 1.445e-01, + 1.445e-01, 1.445e-01, 1.444e-01, 1.444e-01, 1.444e-01, 1.443e-01, 1.442e-01, + 1.441e-01, 1.439e-01, 1.436e-01, 1.431e-01, 1.422e-01, 1.408e-01, 1.389e-01, + 1.360e-01, 1.318e-01, 1.256e-01, 1.161e-01, 9.910e-02, 7.723e-02, 6.190e-02, + 5.368e-02, 4.878e-02, 4.461e-02, 4.041e-02, 3.588e-02, 3.094e-02, 2.564e-02, + 2.009e-02, 1.446e-02, 9.423e-03, 5.364e-03, 2.571e-03, 1.085e-03, 3.935e-04, + 1.172e-04, 2.766e-05, 4.955e-06, 6.462e-07, 5.744e-08, 3.272e-09, 1.149e-10, + 4.822e-12] + +fig, ax = plt.subplots() + +ax.plot(grid, eedf, c='k', label='CANTERA') +ax.plot(cgrid, cf0, ls='None', mfc='None', mec='k', marker='o', label='BOLOS') + +ax.set_xscale('log') +ax.set_yscale('log') + +ax.set_xlim(1e-2, 1e2) +ax.set_ylim(1e-10, 1e4) + +ax.legend() + +plt.show() diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index 68f7253199d..2815e075ede 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -36,31 +36,61 @@ bool ElectronCollisionPlasmaData::update(const ThermoPhase& phase, const Kinetic pp.getElectronEnergyDistribution(distribution.data()); // Update energy levels - levelChanged = pp.levelNumber() != m_level_number; - if (levelChanged) { + // levelChanged = pp.levelNumber() != m_level_number; + // if (levelChanged) { m_level_number = pp.levelNumber(); energyLevels.resize(pp.nElectronEnergyLevels()); pp.getElectronEnergyLevels(energyLevels.data()); - } + // } return true; } -void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitStack& rate_units) +void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitStack& rate_units) { ReactionRate::setParameters(node, rate_units); - if (!node.hasKey("energy-levels") && !node.hasKey("cross-sections")) { - return; - } - if (node.hasKey("energy-levels")) { + + // **Extract kind, target, and product from reaction node** + if (node.hasKey("kind")) { + m_kind = node["kind"].asString(); + } /*else { + throw CanteraError("ElectronCollisionPlasmaRate::setParameters", + "Missing `kind` field in electron-collision-plasma reaction."); + }*/ + + if (node.hasKey("target")) { + m_target = node["target"].asString(); + } /*else { + throw CanteraError("ElectronCollisionPlasmaRate::setParameters", + "Missing `target` field in electron-collision-plasma reaction."); + }*/ + + if (node.hasKey("product")) { + m_product = node["product"].asString(); + } /*else { + throw CanteraError("ElectronCollisionPlasmaRate::setParameters", + "Missing `product` field in electron-collision-plasma reaction."); + }*/ + + // **First, check if cross-sections are embedded in the reaction itself** + if (node.hasKey("energy-levels") && node.hasKey("cross-sections")) { + //writelog("Using embedded cross-section data from reaction definition.\n"); + m_energyLevels = node["energy-levels"].asVector(); - } - if (node.hasKey("cross-sections")) { m_crossSections = node["cross-sections"].asVector(); - } - if (m_energyLevels.size() != m_crossSections.size()) { - throw CanteraError("ElectronCollisionPlasmaRate::setParameters", - "Energy levels and cross section must have the same length."); + + if (m_energyLevels.size() != m_crossSections.size()) { + throw CanteraError("ElectronCollisionPlasmaRate::setParameters", + "Mismatch: `energy-levels` and `cross-sections` must have the same length."); + } + + cs_ok = true; // Mark as valid cross-section data + } + + // **If no cross-section data was found, defer to PlasmaPhase (old format)** + else { + //writelog("No cross-section data found in reaction, relying on PlasmaPhase initialization.\n"); + cs_ok = false; // This will be handled later in `PlasmaPhase::initThermo()` } } @@ -84,9 +114,15 @@ double ElectronCollisionPlasmaRate::evalFromStruct( const ElectronCollisionPlasmaData& shared_data) { // Interpolate cross-sections data to the energy levels of - // the electron energy distribution function - if (shared_data.levelChanged) { - updateInterpolatedCrossSection(shared_data.energyLevels); + // the electron energy distribution function when the interpolated + // cross section is empty + // Note that the PlasmaPhase should handle the interpolated cross sections + // for all ElectronCollisionPlasmaRate reactions + if (m_crossSectionsInterpolated.size() == 0) { + for (double level : shared_data.energyLevels) { + m_crossSectionsInterpolated.push_back(linearInterp(level, + m_energyLevels, m_crossSections)); + } } // Map cross sections to Eigen::ArrayXd diff --git a/src/kinetics/ElectronCrossSection.cpp b/src/kinetics/ElectronCrossSection.cpp new file mode 100644 index 00000000000..023e36cb6ee --- /dev/null +++ b/src/kinetics/ElectronCrossSection.cpp @@ -0,0 +1,95 @@ +/** + * @file ElectronCrossSection.cpp Definition file for class ElectronCrossSection. + */ +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://cantera.org/license.txt for license and copyright information. + +#include "cantera/kinetics/ElectronCrossSection.h" +#include "cantera/base/global.h" + +namespace Cantera { + +ElectronCrossSection::ElectronCrossSection() + : threshold(0.0) +{ +} + +ElectronCrossSection::~ElectronCrossSection() +{ +} + +// void ElectronCrossSection::validate() +// { +// if (kind == "ionization" || kind == "attachment" || kind == "excitation") { +// if (threshold < 0.0) { +// throw CanteraError("ElectronCrossSection::validate", +// "The threshold of the process", +// "(kind = '{}', target = '{}', product = '{}')", +// "cannot be negative", kind, target, product); +// } +// } else if (kind != "effective" && kind != "elastic") { +// throw CanteraError("ElectronCrossSection::validate", +// "'{}' is an unknown type of cross section data.", kind); +// } +// } + +unique_ptr newElectronCrossSection(const AnyMap& node) +{ + unique_ptr ecs(new ElectronCrossSection()); + + ecs->kind = node["kind"].asString(); + ecs->target = node["target"].asString(); + + auto& data = node["data"].asVector>(); + for (size_t i = 0; i < data.size(); i++) { + ecs->energyLevel.push_back(data[i][0]); + ecs->crossSection.push_back(data[i][1]); + } + + if (node.hasKey("threshold")){ + ecs->threshold = node["threshold"].asDouble(); //std::stof(node.attrib("threshold")); + } else { + ecs->threshold = 0.0; + + if (ecs->kind == "excitation" || ecs->kind == "ionization" || ecs->kind == "attachment") { + for (size_t i = 0; i < ecs->energyLevel.size(); i++) { + if (ecs->energyLevel[i] > 0.0) { // Look for first non-zero cross-section + ecs->threshold = ecs->energyLevel[i]; + break; + } + } + } + + } + + if (node.hasKey("products")) { + ecs->products = node["products"].asVector(); // Store all products + ecs->product = ecs->products.empty() ? ecs->target : ecs->products[0]; // Keep first product for compatibility + } else if (node.hasKey("product")) { + ecs->product = node["product"].asString(); + ecs->products.push_back(ecs->product); // Ensure products list is always populated + } else { + ecs->product = ecs->target; + ecs->products.push_back(ecs->product); + } + + /*if (node.hasKey("product")) { + ecs->product = node["product"].asString(); + } else { + ecs->product = ecs->target; + }*/ + + // Some writelog to check the datas loaded concerning the cross section + //writelog("Kind : {:s}\n",ecs->kind); + //writelog("Target : {:s}\n",ecs->target); + //writelog("Product : {:s}\n",ecs->product); + //writelog("Threshold : {:14.5g} eV\n",ecs->threshold); + //writelog("Energy : \n"); + //for (size_t i = 0; i < ecs->energyLevel.size(); i++){ + // writelog("{:9.4g} {:9.4g} \n",ecs->energyLevel[i], ecs->crossSection[i]); + //} + + return ecs; +} + +} \ No newline at end of file diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index 585a1314ebd..f87fe082035 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -83,15 +83,26 @@ shared_ptr newKinetics(const vector>& phases, } } - shared_ptr kin(KineticsFactory::factory()->newKinetics(kinType)); + shared_ptr kin; + if (soln && soln->thermo() && soln->thermo()->kinetics()) { + // If kinetics was initiated in thermo already, use it directly + kin = soln->thermo()->kinetics(); + } else { + // Otherwise, create a new kinetics + kin = std::shared_ptr(KineticsFactory::factory()->newKinetics(kinType)); + } + if (soln) { soln->setKinetics(kin); } for (auto& phase : phases) { kin->addThermo(phase); } - kin->init(); - addReactions(*kin, phaseNode, rootNode); + + if (!kin->ready()) { + kin->init(); + addReactions(*kin, phaseNode, rootNode); + } return kin; } @@ -104,7 +115,8 @@ shared_ptr newKinetics(const vector>& phases, return newKinetics(phases, phaseNode, root); } -void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode) +vector reactionsAnyMapList(Kinetics& kin, const AnyMap& phaseNode, + const AnyMap& rootNode) { kin.skipUndeclaredThirdBodies( phaseNode.getBool("skip-undeclared-third-bodies", false)); @@ -163,6 +175,7 @@ void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode // Add reactions from each section fmt::memory_buffer add_rxn_err; + vector reactionsList; for (size_t i = 0; i < sections.size(); i++) { if (rules[i] == "all") { kin.skipUndeclaredSpecies(false); @@ -183,32 +196,56 @@ void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode AnyMap reactions = AnyMap::fromYamlFile(fileName, rootNode.getString("__file__", "")); loadExtensions(reactions); + for (const auto& R : reactions[node].asVector()) { - #ifdef NDEBUG - try { - kin.addReaction(newReaction(R, kin), false); - } catch (CanteraError& err) { - fmt_append(add_rxn_err, "{}", err.what()); - } - #else - kin.addReaction(newReaction(R, kin), false); - #endif + reactionsList.push_back(R); } } else { // specified section is in the current file for (const auto& R : rootNode.at(sections[i]).asVector()) { - #ifdef NDEBUG - try { - kin.addReaction(newReaction(R, kin), false); - } catch (CanteraError& err) { - fmt_append(add_rxn_err, "{}", err.what()); - } - #else - kin.addReaction(newReaction(R, kin), false); - #endif + reactionsList.push_back(R); } } } + return reactionsList; +} + +void addReactions(Kinetics& kin, vector> rxnList) +{ + fmt::memory_buffer add_rxn_err; + for (shared_ptr rxn : rxnList) { + #ifdef NDEBUG + try { + kin.addReaction(rxn, false); + } catch (CanteraError& err) { + fmt_append(add_rxn_err, "{}", err.what()); + } + #else + kin.addReaction(rxn, false); + #endif + } + + if (add_rxn_err.size()) { + throw CanteraError("addReactions", to_string(add_rxn_err)); + } + kin.checkDuplicates(); + kin.resizeReactions(); +} + +void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode) +{ + fmt::memory_buffer add_rxn_err; + for (AnyMap R : reactionsAnyMapList(kin, phaseNode, rootNode)) { + #ifdef NDEBUG + try { + kin.addReaction(newReaction(R, kin), false); + } catch (CanteraError& err) { + fmt_append(add_rxn_err, "{}", err.what()); + } + #else + kin.addReaction(newReaction(R, kin), false); + #endif + } if (add_rxn_err.size()) { throw CanteraError("addReactions", to_string(add_rxn_err)); diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp new file mode 100644 index 00000000000..b85f758034b --- /dev/null +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -0,0 +1,777 @@ +/** + * @file EEDFTwoTermApproximation.cpp + * EEDF Two-Term approximation solver. Implementation file for class + * EEDFTwoTermApproximation. + */ + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://cantera.org/license.txt for license and copyright information. + +#include "cantera/thermo/EEDFTwoTermApproximation.h" +#include "cantera/base/ctexceptions.h" +#include "cantera/thermo/PlasmaPhase.h" +#include + +namespace Cantera +{ + +EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase& s) +{ + initialize(s); +} + +void EEDFTwoTermApproximation::initialize(PlasmaPhase& s) +{ + // store a pointer to s. + m_phase = &s; + m_first_call = true; + m_has_EEDF = false; + m_gamma = pow(2.0 * ElectronCharge / ElectronMass, 0.5); +} + +void EEDFTwoTermApproximation::setLinearGrid(double& kTe_max, size_t& ncell) +{ + options.m_points = ncell; + m_gridCenter.resize(options.m_points); + m_gridEdge.resize(options.m_points + 1); + m_f0.resize(options.m_points); + m_f0_edge.resize(options.m_points + 1); + for (size_t j = 0; j < options.m_points; j++) { + m_gridCenter[j] = kTe_max * ( j + 0.5 ) / options.m_points; + m_gridEdge[j] = kTe_max * j / options.m_points; + } + m_gridEdge[options.m_points] = kTe_max; + setGridCache(); +} + +int EEDFTwoTermApproximation::calculateDistributionFunction() +{ + if (m_first_call) + { + + for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { + + std::string target = m_phase->target(k); + std::vector products = m_phase->products(k); + + // Print all identified products + std::string productListStr = "{ "; + for (const auto& p : products) { + productListStr += p + " "; + } + productListStr += "}"; + + initSpeciesIndexCS(); + m_first_call = false; + } + } else { + + } + + update_mole_fractions(); + checkSpeciesNoCrossSection(); + updateCS(); + + if (!m_has_EEDF) { + writelog("No existing EEDF. Using first guess method: {}\n", options.m_firstguess); + if (options.m_firstguess == "maxwell") { + writelog("First guess EEDF maxwell\n"); + for (size_t j = 0; j < options.m_points; j++) { + m_f0(j) = 2.0 * pow(1.0 / Pi, 0.5) * pow(options.m_init_kTe, -3. / 2.) * + exp(-m_gridCenter[j] / options.m_init_kTe); + } + } else { + throw CanteraError("EEDFTwoTermApproximation::calculateDistributionFunction", + " unknown EEDF first guess"); + } + } + + converge(m_f0); + + // write the EEDF at grid edges + vector f(m_f0.data(), m_f0.data() + m_f0.rows() * m_f0.cols()); + vector x(m_gridCenter.data(), m_gridCenter.data() + m_gridCenter.rows() * m_gridCenter.cols()); + for (size_t i = 0; i < options.m_points + 1; i++) { + m_f0_edge[i] = linearInterp(m_gridEdge[i], x, f); + } + + m_has_EEDF = true; + + // update electron mobility + m_electronMobility = electronMobility(m_f0); + + return 0; + +} + +void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) +{ + double err0 = 0.0; + double err1 = 0.0; + double delta = options.m_delta0; + + if (options.m_maxn == 0) { + throw CanteraError("EEDFTwoTermApproximation::converge", + "options.m_maxn is zero; no iterations will occur."); + } + if (options.m_points == 0) { + throw CanteraError("EEDFTwoTermApproximation::converge", + "options.m_points is zero; the EEDF grid is empty."); + } + if (std::isnan(delta) || delta == 0.0) { + throw CanteraError("EEDFTwoTermApproximation::converge", + "options.m_delta0 is NaN or zero; solver cannot update."); + } + + for (size_t n = 0; n < options.m_maxn; n++) { + if (0.0 < err1 && err1 < err0) { + delta *= log(options.m_factorM) / (log(err0) - log(err1)); + } + + Eigen::VectorXd f0_old = f0; + f0 = iterate(f0_old, delta); + + err0 = err1; + Eigen::VectorXd Df0(options.m_points); + for (size_t i = 0; i < options.m_points; i++) { + Df0(i) = abs(f0_old(i) - f0(i)); + } + err1 = norm(Df0, m_gridCenter); + + if ((f0.array() != f0.array()).any()) { + throw CanteraError("EEDFTwoTermApproximation::converge", + "NaN detected in EEDF solution."); + } + if ((f0.array().abs() > 1e300).any()) { + throw CanteraError("EEDFTwoTermApproximation::converge", + "Inf detected in EEDF solution."); + } + + if (err1 < options.m_rtol) { + break; + } else if (n == options.m_maxn - 1) { + throw CanteraError("WeaklyIonizedGas::converge", "Convergence failed"); + } + } +} + +Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, double delta) +{ + // CQM multiple call to vector_* and matrix_* + // probably extremely ineficient + // must be refactored!! + + if ((f0.array() != f0.array()).any()) { + throw CanteraError("EEDFTwoTermApproximation::iterate", + "NaN detected in input f0."); + } + + SparseMat_fp PQ(options.m_points, options.m_points); + vector_fp g = vector_g(f0); + + for (size_t k : m_phase->kInelastic()) { + std::vector products = m_phase->products(k); // Retrieve all products + + // Format product list as a string + std::string productListStr = "{ "; + for (const auto& p : products) { + productListStr += p + " "; + } + productListStr += "}"; + + SparseMat_fp Q_k = matrix_Q(g, k); + SparseMat_fp P_k = matrix_P(g, k); + double mole_fraction = m_X_targets[m_klocTargets[k]]; + + PQ += (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; + } + + std::vector> pq_values; + int count = 0; + for (int j = 0; j < PQ.outerSize(); ++j) { + for (Eigen::SparseMatrix::InnerIterator it(PQ, j); it; ++it) { + if (count < 5) { + pq_values.push_back({it.row(), it.col(), it.value()}); + } + count++; + } + } + + SparseMat_fp A = matrix_A(f0); + SparseMat_fp I(options.m_points, options.m_points); + for (size_t i = 0; i < options.m_points; i++) { + I.insert(i,i) = 1.0; + } + A -= PQ; + A *= delta; + A += I; + + if (A.rows() == 0 || A.cols() == 0) { + throw CanteraError("EEDFTwoTermApproximation::iterate", + "Matrix A has zero rows/columns."); + } + + // SparseLU : + Eigen::SparseLU solver(A); + if (solver.info() == Eigen::NumericalIssue) { + throw CanteraError("EEDFTwoTermApproximation::iterate", + "Error SparseLU solver: NumericalIssue"); + } else if (solver.info() == Eigen::InvalidInput) { + throw CanteraError("EEDFTwoTermApproximation::iterate", + "Error SparseLU solver: InvalidInput"); + } + if (solver.info() != Eigen::Success) { + throw CanteraError("EEDFTwoTermApproximation::iterate", + "Error SparseLU solver", "Decomposition failed"); + return f0; + } + + // solve f0 + Eigen::VectorXd f1 = solver.solve(f0); + if(solver.info() != Eigen::Success) { + throw CanteraError("EEDFTwoTermApproximation::iterate", "Solving failed"); + return f0; + } + + if ((f1.array() != f1.array()).any()) { + throw CanteraError("EEDFTwoTermApproximation::iterate", + "NaN detected in computed f1."); + } + + f1 /= norm(f1, m_gridCenter); + + return f1; +} + +double EEDFTwoTermApproximation::integralPQ(double a, double b, double u0, double u1, + double g, double x0) +{ + double A1; + double A2; + if (g != 0.0) { + double expm1a = expm1(g * (-a + x0)); + double expm1b = expm1(g * (-b + x0)); + double ag = a * g; + double ag1 = ag + 1; + double bg = b * g; + double bg1 = bg + 1; + A1 = (expm1a * ag1 + ag - expm1b * bg1 - bg) / (g*g); + A2 = (expm1a * (2 * ag1 + ag * ag) + ag * (ag + 2) - + expm1b * (2 * bg1 + bg * bg) - bg * (bg + 2)) / (g*g*g); + } else { + A1 = 0.5 * (b*b - a*a); + A2 = 1.0 / 3.0 * (b*b*b - a*a*a); + } + + // The interpolation formula of u(x) = c0 + c1 * x + double c0 = (a * u1 - b * u0) / (a - b); + double c1 = (u0 - u1) / (a - b); + + return c0 * A1 + c1 * A2; +} + +vector_fp EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) +{ + vector_fp g(options.m_points, 0.0); + const double f_min = 1e-300; // Smallest safe floating-point value + + // Handle first point (i = 0) + double f1 = std::max(f0(1), f_min); + double f0_ = std::max(f0(0), f_min); + g[0] = log(f1 / f0_) / (m_gridCenter[1] - m_gridCenter[0]); + + // Handle last point (i = N) + size_t N = options.m_points - 1; + double fN = std::max(f0(N), f_min); + double fNm1 = std::max(f0(N - 1), f_min); + g[N] = log(fN / fNm1) / (m_gridCenter[N] - m_gridCenter[N - 1]); + + // Handle interior points + for (size_t i = 1; i < N; ++i) { + double f_up = std::max(f0(i + 1), f_min); + double f_down = std::max(f0(i - 1), f_min); + g[i] = log(f_up / f_down) / (m_gridCenter[i + 1] - m_gridCenter[i - 1]); + } + + return g; +} + +SparseMat_fp EEDFTwoTermApproximation::matrix_P(const vector_fp& g, size_t k) +{ + vector tripletList; + for (size_t n = 0; n < m_eps[k].size(); n++) { + double eps_a = m_eps[k][n][0]; + double eps_b = m_eps[k][n][1]; + double sigma_a = m_sigma[k][n][0]; + double sigma_b = m_sigma[k][n][1]; + size_t j = m_j[k][n]; + double r = integralPQ(eps_a, eps_b, sigma_a, sigma_b, g[j], m_gridCenter[j]); + double p = m_gamma * r; + + tripletList.push_back(Triplet_fp(j, j, p)); + } + SparseMat_fp P(options.m_points, options.m_points); + P.setFromTriplets(tripletList.begin(), tripletList.end()); + return P; +} + +SparseMat_fp EEDFTwoTermApproximation::matrix_Q(const vector_fp& g, size_t k) +{ + vector tripletList; + for (size_t n = 0; n < m_eps[k].size(); n++) { + double eps_a = m_eps[k][n][0]; + double eps_b = m_eps[k][n][1]; + double sigma_a = m_sigma[k][n][0]; + double sigma_b = m_sigma[k][n][1]; + size_t i = m_i[k][n]; + size_t j = m_j[k][n]; + double r = integralPQ(eps_a, eps_b, sigma_a, sigma_b, g[j], m_gridCenter[j]); + double q = m_phase->inFactor()[k] * m_gamma * r; + + tripletList.push_back(Triplet_fp(i, j, q)); + } + SparseMat_fp Q(options.m_points, options.m_points); + Q.setFromTriplets(tripletList.begin(), tripletList.end()); + return Q; +} + +SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) +{ + vector_fp a0(options.m_points + 1); + vector_fp a1(options.m_points + 1); + size_t N = options.m_points - 1; + // Scharfetter-Gummel scheme + double nu = netProductionFreq(f0); + a0[0] = NAN; + a1[0] = NAN; + a0[N+1] = NAN; + a1[N+1] = NAN; + + // Electron-electron collisions declarations + double a; + vector_fp A1, A2, A3; + if (m_eeCol) { + eeColIntegrals(A1, A2, A3, a, options.m_points); + } + + double alpha; + if (options.m_growth == "spatial") { + double mu = electronMobility(f0); + double D = electronDiffusivity(f0); + alpha = (mu * m_phase->E() - sqrt(pow(mu * m_phase->E(), 2) - 4 * D * nu * m_phase->N())) / 2.0 / D / m_phase->N(); + } else { + alpha = 0.0; + } + + double sigma_tilde; + double omega = 2 * Pi * m_phase->F(); + for (size_t j = 1; j < options.m_points; j++) { + if (options.m_growth == "temporal") { + sigma_tilde = m_totalCrossSectionEdge[j] + nu / pow(m_gridEdge[j], 0.5) / m_gamma; + } + else { + sigma_tilde = m_totalCrossSectionEdge[j]; + } + double q = omega / (m_phase->N() * m_gamma * pow(m_gridEdge[j], 0.5)); + double W = -m_gamma * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; + double F = sigma_tilde * sigma_tilde / (sigma_tilde * sigma_tilde + q * q); + double DA = m_gamma / 3.0 * pow(m_phase->E() / m_phase->N(), 2.0) * m_gridEdge[j]; + double DB = m_gamma * m_phase->kT() * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; + double D = DA / sigma_tilde * F + DB; + if (m_eeCol) { + W -= 3 * a * m_phase->ionDegree() * A1[j]; + D += 2 * a * m_phase->ionDegree() * (A2[j] + pow(m_gridEdge[j], 1.5) * A3[j]); + } + if (options.m_growth == "spatial") { + W -= m_gamma / 3.0 * 2 * alpha * m_phase->E() / m_phase->N() * m_gridEdge[j] / sigma_tilde; + } + + double z = W * (m_gridCenter[j] - m_gridCenter[j-1]) / D; + if (!std::isfinite(z)) { + throw CanteraError("matrix_A", "Non-finite Peclet number encountered"); + } + if (std::abs(z) > 500) { + writelog("Warning: Large Peclet number z = {:.3e} at j = {}. W = {:.3e}, D = {:.3e}, E/N = {:.3e}\n", + z, j, W, D, m_phase->E() / m_phase->N()); + } + a0[j] = W / (1 - std::exp(-z)); + a1[j] = W / (1 - std::exp(z)); + } + + std::vector tripletList; + // center diagonal + // zero flux b.c. at energy = 0 + tripletList.push_back(Triplet_fp(0, 0, a0[1])); + + for (size_t j = 1; j < options.m_points - 1; j++) { + tripletList.push_back(Triplet_fp(j, j, a0[j+1] - a1[j])); + } + + // upper diagonal + for (size_t j = 0; j < options.m_points - 1; j++) { + tripletList.push_back(Triplet_fp(j, j+1, a1[j+1])); + } + + // lower diagonal + for (size_t j = 1; j < options.m_points; j++) { + tripletList.push_back(Triplet_fp(j, j-1, -a0[j])); + } + + // zero flux b.c. + tripletList.push_back(Triplet_fp(N, N, -a1[N])); + + SparseMat_fp A(options.m_points, options.m_points); + A.setFromTriplets(tripletList.begin(), tripletList.end()); + + //plus G + SparseMat_fp G(options.m_points, options.m_points); + if (options.m_growth == "temporal") { + for (size_t i = 0; i < options.m_points; i++) { + G.insert(i, i) = 2.0 / 3.0 * (pow(m_gridEdge[i+1], 1.5) - pow(m_gridEdge[i], 1.5)) * nu; + } + } + else if (options.m_growth == "spatial") { + for (size_t i = 0; i < options.m_points; i++) { + double sigma_c = 0.5 * (m_totalCrossSectionEdge[i] + m_totalCrossSectionEdge[i + 1]); + G.insert(i, i) = - alpha * m_gamma / 3 * (alpha * (pow(m_gridEdge[i + 1], 2) - pow(m_gridEdge[i], 2)) / sigma_c / 2 + - m_phase->E() / m_phase->N() * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i])); + } + } + + return A + G; +} + +double EEDFTwoTermApproximation::netProductionFreq(const Eigen::VectorXd& f0) +{ + double nu = 0.0; + vector_fp g = vector_g(f0); + + for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { + if (m_phase->kind(k) == "ionization" || + m_phase->kind(k) == "attachment") { + SparseMat_fp PQ = (matrix_Q(g, k) - matrix_P(g, k)) * + m_X_targets[m_klocTargets[k]]; + Eigen::VectorXd s = PQ * f0; + for (size_t i = 0; i < options.m_points; i++) { + nu += s[i]; + if (!std::isfinite(s[i])) { + writelog("NaN in netProductionFreq at s[{}] for k = {}\n", i, k); + break; + } + } + } + } + return nu; +} + +double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) +{ + vector_fp y(options.m_points, 0.0); + double nu = netProductionFreq(f0); + for (size_t i = 0; i < options.m_points; i++) { + if (m_gridCenter[i] != 0.0) { + y[i] = m_gridCenter[i] * f0(i) / + (m_totalCrossSectionCenter[i] + nu / m_gamma / pow(m_gridCenter[i], 0.5)); + } + } + auto f = Eigen::Map(y.data(), y.size()); + auto x = Eigen::Map(m_gridCenter.data(), m_gridCenter.size()); + return 1./3. * m_gamma * simpson(f, x) / m_phase->N(); +} + +double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) +{ + double nu = netProductionFreq(f0); + vector_fp y(options.m_points + 1, 0.0); + for (size_t i = 1; i < options.m_points; i++) { + // calculate df0 at i-1/2 + double df0 = (f0(i) - f0(i-1)) / (m_gridCenter[i] - m_gridCenter[i-1]); + if (m_gridEdge[i] != 0.0) { + y[i] = m_gridEdge[i] * df0 / + (m_totalCrossSectionEdge[i] + nu / m_gamma / pow(m_gridEdge[i], 0.5)); + } + } + auto f = Eigen::Map(y.data(), y.size()); + auto x = Eigen::Map(m_gridEdge.data(), m_gridEdge.size()); + return -1./3. * m_gamma * simpson(f, x) / m_phase->N(); +} + +void EEDFTwoTermApproximation::initSpeciesIndexCS() +{ + // set up target index + m_kTargets.resize(m_phase->nElectronCrossSections()); + m_klocTargets.resize(m_phase->nElectronCrossSections()); + for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) + { + + m_kTargets[k] = m_phase->speciesIndex(m_phase->target(k)); + if (m_kTargets[k] == string::npos) { + throw CanteraError("EEDFTwoTermApproximation::initSpeciesIndexCS" + " species not found!", + m_phase->target(k)); + } + // Check if it is a new target or not : + auto it = find(m_k_lg_Targets.begin(), m_k_lg_Targets.end(), m_kTargets[k]); + + if (it == m_k_lg_Targets.end()){ + m_k_lg_Targets.push_back(m_kTargets[k]); + m_klocTargets[k] = m_k_lg_Targets.size() - 1; + } else { + m_klocTargets[k] = distance(m_k_lg_Targets.begin(), it); + } + } + + m_X_targets.resize(m_k_lg_Targets.size()); + m_X_targets_prev.resize(m_k_lg_Targets.size()); + for (size_t k = 0; k < m_X_targets.size(); k++) + { + size_t k_glob = m_k_lg_Targets[k]; + m_X_targets[k] = m_phase->moleFraction(k_glob); + m_X_targets_prev[k] = m_phase->moleFraction(k_glob); + } + + // set up indices of species which has no cross-section data + for (size_t k = 0; k < m_phase->nSpecies(); k++) + { + auto it = std::find(m_kTargets.begin(), m_kTargets.end(), k); + if (it == m_kTargets.end()) { + m_kOthers.push_back(k); + } + } +} + +void EEDFTwoTermApproximation::checkSpeciesNoCrossSection() +{ + // warn that a specific species needs cross-section data. + for (size_t k : m_kOthers) { + if (m_phase->moleFraction(k) > options.m_moleFractionThreshold) { + writelog("EEDFTwoTermApproximation:checkSpeciesNoCrossSection\n"); + writelog("Warning:The mole fraction of species {} is more than 0.01 (X = {:.3g}) but it has no cross-section data\n", m_phase->speciesName(k), m_phase->moleFraction(k)); + } + } +} + +void EEDFTwoTermApproximation::updateCS() +{ + // Compute sigma_m and sigma_\epsilon + calculateTotalCrossSection(); + calculateTotalElasticCrossSection(); +} + +// Update the species mole fractions used for EEDF computation +void EEDFTwoTermApproximation::update_mole_fractions() +{ + + double tmp_sum = 0.0; + for (size_t k = 0; k < m_X_targets.size(); k++) + { + m_X_targets[k] = m_phase->moleFraction(m_k_lg_Targets[k]); + tmp_sum = tmp_sum + m_phase->moleFraction(m_k_lg_Targets[k]); + } + + // Normalize the mole fractions to unity: + for (size_t k = 0; k < m_X_targets.size(); k++) + { + m_X_targets[k] = m_X_targets[k] / tmp_sum; + } + +} + +void EEDFTwoTermApproximation::calculateTotalCrossSection() +{ + + m_totalCrossSectionCenter.assign(options.m_points, 0.0); + m_totalCrossSectionEdge.assign(options.m_points + 1, 0.0); + for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { + vector_fp x = m_phase->energyLevels()[k]; + vector_fp y = m_phase->crossSections()[k]; + + std::vector products = m_phase->products(k); // Retrieve all products + + // Format product list as a string + std::string productListStr = "{ "; + for (const auto& p : products) { + productListStr += p + " "; + } + productListStr += "}"; + + for (size_t i = 0; i < options.m_points; i++) { + double cs_value = linearInterp(m_gridCenter[i], x, y); + m_totalCrossSectionCenter[i] += m_X_targets[m_klocTargets[k]] * + linearInterp(m_gridCenter[i], x, y); + + } + for (size_t i = 0; i < options.m_points + 1; i++) { + m_totalCrossSectionEdge[i] += m_X_targets[m_klocTargets[k]] * + linearInterp(m_gridEdge[i], x, y); + } + } +} + +void EEDFTwoTermApproximation::calculateTotalElasticCrossSection() +{ + m_sigmaElastic.clear(); + m_sigmaElastic.resize(options.m_points, 0.0); + for (size_t k : m_phase->kElastic()) { + vector_fp x = m_phase->energyLevels()[k]; + vector_fp y = m_phase->crossSections()[k]; + // Note: + // moleFraction(m_kTargets[k]) <=> m_X_targets[m_klocTargets[k]] + double mass_ratio = ElectronMass / (m_phase->molecularWeight(m_kTargets[k]) / Avogadro); + for (size_t i = 0; i < options.m_points; i++) { + m_sigmaElastic[i] += 2.0 * mass_ratio * m_X_targets[m_klocTargets[k]] * + linearInterp(m_gridEdge[i], x, y); + } + } +} + +void EEDFTwoTermApproximation::setGridCache() +{ + m_sigma.clear(); + m_sigma.resize(m_phase->nElectronCrossSections()); + m_sigma_offset.clear(); + m_sigma_offset.resize(m_phase->nElectronCrossSections()); + m_eps.clear(); + m_eps.resize(m_phase->nElectronCrossSections()); + m_j.clear(); + m_j.resize(m_phase->nElectronCrossSections()); + m_i.clear(); + m_i.resize(m_phase->nElectronCrossSections()); + for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { + auto x = m_phase->energyLevels()[k]; + auto y = m_phase->crossSections()[k]; + vector_fp eps1(options.m_points + 1); + for (size_t i = 0; i < options.m_points + 1; i++) { + eps1[i] = clip(m_phase->shiftFactor()[k] * m_gridEdge[i] + m_phase->threshold(k), + m_gridEdge[0] + 1e-9, m_gridEdge[options.m_points] - 1e-9); + } + vector_fp nodes = eps1; + for (size_t i = 0; i < options.m_points + 1; i++) { + if (m_gridEdge[i] >= eps1[0] && m_gridEdge[i] <= eps1[options.m_points]) { + nodes.push_back(m_gridEdge[i]); + } + } + for (size_t i = 0; i < x.size(); i++) { + if (x[i] >= eps1[0] && x[i] <= eps1[options.m_points]) { + nodes.push_back(x[i]); + } + } + + std::sort(nodes.begin(), nodes.end()); + + auto last = std::unique(nodes.begin(), nodes.end()); + nodes.resize(std::distance(nodes.begin(), last)); + vector_fp sigma0(nodes.size()); + for (size_t i = 0; i < nodes.size(); i++) { + sigma0[i] = linearInterp(nodes[i], x, y); + } + + // search position of cell j + for (size_t i = 1; i < nodes.size(); i++) { + auto low = std::lower_bound(m_gridEdge.begin(), m_gridEdge.end(), nodes[i]); + m_j[k].push_back(low - m_gridEdge.begin() - 1); + } + + // search position of cell i + for (size_t i = 1; i < nodes.size(); i++) { + auto low = std::lower_bound(eps1.begin(), eps1.end(), nodes[i]); + m_i[k].push_back(low - eps1.begin() - 1); + } + + // construct sigma + for (size_t i = 0; i < nodes.size() - 1; i++) { + vector_fp sigma{sigma0[i], sigma0[i+1]}; + m_sigma[k].push_back(sigma); + } + + // construct eps + for (size_t i = 0; i < nodes.size() - 1; i++) { + vector_fp eps{nodes[i], nodes[i+1]}; + m_eps[k].push_back(eps); + } + + // construct sigma_offset + auto x_offset = m_phase->energyLevels()[k]; + for (auto& element : x_offset) { + element -= m_phase->threshold(k); + } + for (size_t i = 0; i < options.m_points; i++) { + m_sigma_offset[k].push_back(linearInterp(m_gridCenter[i], x_offset, y)); + } + } +} + +double EEDFTwoTermApproximation::norm(const Eigen::VectorXd& f, const Eigen::VectorXd& grid) +{ + string m_quadratureMethod = "simpson"; + Eigen::VectorXd p(f.size()); + for (int i = 0; i < f.size(); i++) { + p[i] = f(i) * pow(grid[i], 0.5); + } + return numericalQuadrature(m_quadratureMethod, p, grid); +} + + +void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, + double& a, size_t nPoints) +{ + // Ensure vectors are initialized + A1.assign(nPoints, 0.0); + A2.assign(nPoints, 0.0); + A3.assign(nPoints, 0.0); + + // Compute net production frequency + double nu = netProductionFreq(m_f0); + // simulations with repeated calls to update EEDF will produce numerical instability here + double nu_floor = 1e-40; // adjust as needed for stability + if (nu < nu_floor) { + writelog("eeColIntegrals: nu = {:.3e} too small, applying floor\n", nu); + nu = nu_floor; + } + + // Compute effective cross-section term + double sigma_tilde; + for (size_t j = 1; j < nPoints; j++) { + sigma_tilde = m_totalCrossSectionCenter[j] + nu / pow(m_gridEdge[j], 0.5) / m_gamma; + } + + // Compute Coulomb logarithm + double lnLambda; + if (nu > 0.0) { + lnLambda = log(sigma_tilde / nu); + } else { + lnLambda = log(4.0 * Pi * pow(m_gridEdge.back(), 3) / 3.0); + } + + // Compute e-e collision prefactor + a = 4.0 * Pi * ElectronCharge * ElectronCharge * lnLambda / (m_gamma * pow(nu, 2)); + + // Compute integral terms A1, A2, A3 + for (size_t j = 1; j < nPoints; j++) { + double eps_j = m_gridCenter[j]; // Electron energy level + double f0_j = m_f0[j]; // EEDF at energy level j + + double integral_A1 = 0.0; + double integral_A2 = 0.0; + double integral_A3 = 0.0; + + for (size_t i = 1; i < nPoints; i++) { + double eps_i = m_gridCenter[i]; + double f0_i = m_f0[i]; + + double weight = f0_i * pow(eps_i, 0.5) * exp(-abs(eps_i - eps_j) / eps_i); + + integral_A1 += weight * pow(eps_i, 1.5); + integral_A2 += weight * pow(eps_i, 0.5); + integral_A3 += weight; + } + + // Store computed values + A1[j] = integral_A1; + A2[j] = integral_A2; + A3[j] = integral_A3; + } + +} + +} diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 7bc60e04f9d..68fc7258fe3 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -4,12 +4,15 @@ // at https://cantera.org/license.txt for license and copyright information. #include "cantera/thermo/PlasmaPhase.h" +#include "cantera/thermo/EEDFTwoTermApproximation.h" #include #include "cantera/thermo/Species.h" #include "cantera/base/global.h" #include "cantera/numerics/funcs.h" #include "cantera/kinetics/Kinetics.h" +#include "cantera/kinetics/KineticsFactory.h" #include "cantera/kinetics/Reaction.h" +#include #include "cantera/kinetics/ElectronCollisionPlasmaRate.h" namespace Cantera { @@ -20,28 +23,31 @@ namespace { PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) { + initialize(); + initThermoFile(inputFile, id_); // initial grid m_electronEnergyLevels = Eigen::ArrayXd::LinSpaced(m_nPoints, 0.0, 1.0); // initial electron temperature - m_electronTemp = temperature(); + setElectronTemperature(temperature()); +} - // resize vectors - m_interp_cs.resize(m_nPoints); +void PlasmaPhase::initialize() +{ + m_ncs = 0; + m_f0_ok = false; + m_EN = 0.0; + m_E = 0.0; + m_F = 0.0; + m_ionDegree = 0.0; } -PlasmaPhase::~PlasmaPhase() +void PlasmaPhase::setTemperature(const double temp) { - if (shared_ptr soln = m_soln.lock()) { - soln->removeChangedCallback(this); - soln->kinetics()->removeReactionAddedCallback(this); - } - for (size_t k = 0; k < nCollisions(); k++) { - // remove callback - m_collisions[k]->removeSetRateCallback(this); - } + Phase::setTemperature(temp); + m_kT = Boltzmann * temp / ElectronCharge; } void PlasmaPhase::updateElectronEnergyDistribution() @@ -51,9 +57,21 @@ void PlasmaPhase::updateElectronEnergyDistribution() "Invalid for discretized electron energy distribution."); } else if (m_distributionType == "isotropic") { setIsotropicElectronEnergyDistribution(); + } else if (m_distributionType == "TwoTermApproximation") { + auto ierr = ptrEEDFSolver->calculateDistributionFunction(); + if (ierr == 0) { + auto x = ptrEEDFSolver->getGridEdge(); + auto y = ptrEEDFSolver->getEEDFEdge(); + m_nPoints = x.size(); + m_electronEnergyLevels = Eigen::Map(x.data(), m_nPoints); + m_electronEnergyDist = Eigen::Map(y.data(), m_nPoints); + } else { + throw CanteraError("PlasmaPhase::updateElectronEnergyDistribution", + "Call to calculateDistributionFunction failed."); + } } - updateElectronEnergyDistDifference(); electronEnergyDistributionChanged(); + updateElectronTemperatureFromEnergyDist(); } void PlasmaPhase::normalizeElectronEnergyDistribution() { @@ -71,7 +89,8 @@ void PlasmaPhase::normalizeElectronEnergyDistribution() { void PlasmaPhase::setElectronEnergyDistributionType(const string& type) { if (type == "discretized" || - type == "isotropic") { + type == "isotropic" || + type == "TwoTermApproximation") { m_distributionType = type; } else { throw CanteraError("PlasmaPhase::setElectronEnergyDistributionType", @@ -83,12 +102,13 @@ void PlasmaPhase::setIsotropicElectronEnergyDistribution() { m_electronEnergyDist.resize(m_nPoints); double x = m_isotropicShapeFactor; - double gamma1 = boost::math::tgamma(3.0 / 2.0 / x); - double gamma2 = boost::math::tgamma(5.0 / 2.0 / x); + double gamma1 = boost::math::tgamma(3.0 / 2.0 * x); + double gamma2 = boost::math::tgamma(5.0 / 2.0 * x); double c1 = x * std::pow(gamma2, 1.5) / std::pow(gamma1, 2.5); - double c2 = std::pow(gamma2 / gamma1, x); + double c2 = x * std::pow(gamma2 / gamma1, x); m_electronEnergyDist = - c1 / std::pow(meanElectronEnergy(), 1.5) * + c1 * m_electronEnergyLevels.sqrt() / + std::pow(meanElectronEnergy(), 1.5) * (-c2 * (m_electronEnergyLevels / meanElectronEnergy()).pow(x)).exp(); checkElectronEnergyDistribution(); @@ -104,22 +124,13 @@ void PlasmaPhase::setMeanElectronEnergy(double energy) { updateElectronEnergyDistribution(); } -void PlasmaPhase::setElectronEnergyLevels(const double* levels, size_t length, - bool updateEnergyDist) +void PlasmaPhase::setElectronEnergyLevels(const double* levels, size_t length) { m_nPoints = length; m_electronEnergyLevels = Eigen::Map(levels, length); checkElectronEnergyLevels(); electronEnergyLevelChanged(); - if (updateEnergyDist) updateElectronEnergyDistribution(); - m_interp_cs.resize(m_nPoints); - // The cross sections are interpolated on the energy levels - if (nCollisions() > 0) { - for (size_t i = 0; i < m_collisions.size(); i++) { - m_interp_cs_ready[i] = false; - updateInterpolatedCrossSection(i); - } - } + updateElectronEnergyDistribution(); } void PlasmaPhase::electronEnergyDistributionChanged() @@ -129,6 +140,10 @@ void PlasmaPhase::electronEnergyDistributionChanged() void PlasmaPhase::electronEnergyLevelChanged() { + // Cross sections are interpolated on the energy levels + if (m_collisions.size() > 0) { + updateInterpolatedCrossSections(); + } m_levelNum++; } @@ -165,7 +180,9 @@ void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* levels, size_t length) { m_distributionType = "discretized"; - setElectronEnergyLevels(levels, length, false); + m_nPoints = length; + m_electronEnergyLevels = + Eigen::Map(levels, length); m_electronEnergyDist = Eigen::Map(dist, length); checkElectronEnergyLevels(); @@ -173,35 +190,39 @@ void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* levels, normalizeElectronEnergyDistribution(); } checkElectronEnergyDistribution(); - updateElectronEnergyDistDifference(); updateElectronTemperatureFromEnergyDist(); electronEnergyLevelChanged(); electronEnergyDistributionChanged(); } +void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* dist, + size_t length) +{ + m_distributionType = "discretized"; + m_nPoints = length; + m_electronEnergyDist = + Eigen::Map(dist, length); + checkElectronEnergyLevels(); + if (m_do_normalizeElectronEnergyDist) { + normalizeElectronEnergyDistribution(); + } + checkElectronEnergyDistribution(); + updateElectronTemperatureFromEnergyDist(); + electronEnergyDistributionChanged(); +} + void PlasmaPhase::updateElectronTemperatureFromEnergyDist() { // calculate mean electron energy and electron temperature Eigen::ArrayXd eps52 = m_electronEnergyLevels.pow(5./2.); double epsilon_m = 2.0 / 5.0 * numericalQuadrature(m_quadratureMethod, m_electronEnergyDist, eps52); - if (epsilon_m < 0.0 && m_quadratureMethod == "simpson") { - // try trapezoidal method - epsilon_m = 2.0 / 5.0 * numericalQuadrature( - "trapezoidal", m_electronEnergyDist, eps52); - } - - if (epsilon_m < 0.0) { - throw CanteraError("PlasmaPhase::updateElectronTemperatureFromEnergyDist", - "The electron energy distribution produces negative electron temperature."); - } - m_electronTemp = 2.0 / 3.0 * epsilon_m * ElectronCharge / Boltzmann; } void PlasmaPhase::setIsotropicShapeFactor(double x) { m_isotropicShapeFactor = x; - updateElectronEnergyDistribution(); + setIsotropicElectronEnergyDistribution(); } void PlasmaPhase::getParameters(AnyMap& phaseNode) const @@ -224,34 +245,38 @@ void PlasmaPhase::getParameters(AnyMap& phaseNode) const phaseNode["electron-energy-distribution"] = std::move(eedf); } + void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) { IdealGasPhase::setParameters(phaseNode, rootNode); m_root = rootNode; + if (phaseNode.hasKey("electron-energy-distribution")) { const AnyMap eedf = phaseNode["electron-energy-distribution"].as(); m_distributionType = eedf["type"].asString(); + if (m_distributionType == "isotropic") { if (eedf.hasKey("shape-factor")) { - m_isotropicShapeFactor = eedf["shape-factor"].asDouble(); + setIsotropicShapeFactor(eedf["shape-factor"].asDouble()); } else { throw CanteraError("PlasmaPhase::setParameters", "isotropic type requires shape-factor key."); } - if (eedf.hasKey("energy-levels")) { - setElectronEnergyLevels(eedf["energy-levels"].asVector().data(), - eedf["energy-levels"].asVector().size(), - false); - } if (eedf.hasKey("mean-electron-energy")) { double energy = eedf.convert("mean-electron-energy", "eV"); - // setMeanElectronEnergy() calls updateElectronEnergyDistribution() setMeanElectronEnergy(energy); } else { throw CanteraError("PlasmaPhase::setParameters", "isotropic type requires electron-temperature key."); } - } else if (m_distributionType == "discretized") { + if (eedf.hasKey("energy-levels")) { + setElectronEnergyLevels(eedf["energy-levels"].asVector().data(), + eedf["energy-levels"].asVector().size()); + } + setIsotropicElectronEnergyDistribution(); + } + + else if (m_distributionType == "discretized") { if (!eedf.hasKey("energy-levels")) { throw CanteraError("PlasmaPhase::setParameters", "Cannot find key energy-levels."); @@ -266,18 +291,216 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) setDiscretizedElectronEnergyDist(eedf["energy-levels"].asVector().data(), eedf["distribution"].asVector().data(), eedf["energy-levels"].asVector().size()); + } + + else if (m_distributionType == "TwoTermApproximation") { + bool foundCrossSections = false; + + // Check for 'cross-sections' block (Old Format) + if (rootNode.hasKey("cross-sections")) { + writelog("Using explicit `cross-sections` block.\n"); + for (const auto& item : rootNode["cross-sections"].asVector()) { + addElectronCrossSection(newElectronCrossSection(item)); + } + writelog("m_ncs = {:3d}\n", m_ncs); + foundCrossSections = true; + } + + // Check for 'electron-collision-plasma' reactions (New Format) + if (rootNode.hasKey("reactions")) { + for (const auto& reactionItem : rootNode["reactions"].asVector()) { + if (reactionItem.hasKey("type") && + reactionItem["type"].asString() == "electron-collision-plasma" && + reactionItem.hasKey("energy-levels") && + reactionItem.hasKey("cross-sections")) { + + // Convert reaction into cross-section format + AnyMap newCrossSection; + newCrossSection["target"] = reactionItem.getString("target", "unknown"); + newCrossSection["product"] = reactionItem.getString("product", "unknown"); + newCrossSection["kind"] = reactionItem.getString("kind", "unknown"); + newCrossSection["energy-levels"] = reactionItem["energy-levels"].asVector(); + newCrossSection["cross-sections"] = reactionItem["cross-sections"].asVector(); + + addElectronCrossSection(newElectronCrossSection(newCrossSection)); + foundCrossSections = true; + } + } + } + + if (rootNode.hasKey("collisions")) { + for (const auto& collisionItem : rootNode["collisions"].asVector()) { + if (collisionItem.hasKey("type") && + collisionItem["type"].asString() == "electron-collision-plasma" && + collisionItem.hasKey("energy-levels") && + collisionItem.hasKey("cross-sections")) { + + AnyMap newCrossSection; + + // Extract target from equation if not explicitly defined + std::string equation = collisionItem.getString("equation", ""); + std::string targetSpecies = "unknown"; + std::vector productSpeciesList; + + if (!equation.empty()) { + size_t arrowPos = equation.find("=>"); + if (arrowPos != std::string::npos) { + // Extract reactants + std::string reactantSide = equation.substr(0, arrowPos); + std::vector reactants; + std::stringstream ssReactants(reactantSide); + std::string reactant; + while (ssReactants >> reactant) { + if (reactant != "+" && reactant != "e") { // Ignore '+' and 'e' + reactants.push_back(reactant); + } + } + + // First reactant (non-electron) is the target species + if (!reactants.empty()) { + targetSpecies = reactants[0]; + } + + // Extract products + std::string productSide = equation.substr(arrowPos + 2); + std::stringstream ssProducts(productSide); + std::string product; + while (ssProducts >> product) { + if (product != "+" && product != "e") { + productSpeciesList.push_back(product); + } + } + + } + } + + std::string productListStr = "{ "; + for (const auto& p : productSpeciesList) { + productListStr += p + " "; + } + productListStr += "}"; + + std::string kind = "excitation"; // Default type + if (productSpeciesList.size() == 1 && productSpeciesList[0] == targetSpecies) { + kind = "effective"; // Elastic collision (momentum transfer) + } else { + for (const auto& p : productSpeciesList) { + if (p.back() == '+') { + kind = "ionization"; + break; + } + if (p.back() == '-') { + kind = "attachment"; + break; + } + } + } + + // store the correctly identified data + newCrossSection["target"] = targetSpecies; + newCrossSection["products"] = productSpeciesList; + newCrossSection["product"] = productSpeciesList.empty() ? "unknown" : productSpeciesList[0]; + newCrossSection["kind"] = kind; + newCrossSection["energy-levels"] = collisionItem["energy-levels"].asVector(); + newCrossSection["cross-sections"] = collisionItem["cross-sections"].asVector(); + + // Convert 'energy-levels' and 'cross-sections' into 'data' format + std::vector> dataPairs; + std::vector energyLevels = collisionItem["energy-levels"].asVector(); + std::vector crossSections = collisionItem["cross-sections"].asVector(); + + if (energyLevels.size() != crossSections.size()) { + throw CanteraError("PlasmaPhase::setParameters", + "Mismatch: `energy-levels` and `cross-sections` must have the same length."); + } + + // Properly format 'data' field + for (size_t i = 0; i < energyLevels.size(); i++) { + dataPairs.push_back({energyLevels[i], crossSections[i]}); + } + + newCrossSection["data"] = dataPairs; + + addElectronCrossSection(newElectronCrossSection(newCrossSection)); + foundCrossSections = true; + } + } + } + + // Throw error if no valid cross-section data is found + if (!foundCrossSections) { + throw CanteraError("PlasmaPhase::setParameters", + "No valid electron collision cross-section data found."); + } + + // Initialize the Boltzmann Solver + ptrEEDFSolver = make_unique(*this); + + // Set Energy Grid (Hardcoded Defaults for Now) + double kTe_max = 60; + size_t nGridCells = 301; + m_nPoints = nGridCells + 1; + ptrEEDFSolver->setLinearGrid(kTe_max, nGridCells); } } } + +bool PlasmaPhase::addElectronCrossSection(shared_ptr ecs) +{ + // ecs->validate(); + m_ecss.push_back(ecs); + + m_energyLevels.push_back(ecs->energyLevel); + m_crossSections.push_back(ecs->crossSection); + + // shift factor + if (ecs->kind == "ionization") { + m_shiftFactor.push_back(2); + } else { + m_shiftFactor.push_back(1); + } + + // scattering-in factor + if (ecs->kind == "ionization") { + m_inFactor.push_back(2); + } else if (ecs->kind == "attachment") { + m_inFactor.push_back(0); + } else { + m_inFactor.push_back(1); + } + + if (ecs->kind == "effective" || ecs->kind == "elastic") { + for (size_t k = 0; k < m_ncs; k++) { + if (target(k) == ecs->target) + if (kind(k) == "elastic" || kind(k) == "effective") { + throw CanteraError("PlasmaPhase::addElectronCrossSection" + "Already contains a data of effective/ELASTIC cross section for '{}'.", + ecs->target); + } + } + m_kElastic.push_back(m_ncs); + } else { + m_kInelastic.push_back(m_ncs); + } + + // add one to number of cross sections + m_ncs++; + + m_f0_ok = false; + + return true; +} + bool PlasmaPhase::addSpecies(shared_ptr spec) { bool added = IdealGasPhase::addSpecies(spec); size_t k = m_kk - 1; - if (spec->composition.find("E") != spec->composition.end() && - spec->composition.size() == 1 && - spec->composition["E"] == 1) { + if ((spec->name == "e" || spec->name == "Electron") || + (spec->composition.find("E") != spec->composition.end() && + spec->composition.size() == 1 && + spec->composition["E"] == 1)) { if (m_electronSpeciesIndex == npos) { m_electronSpeciesIndex = k; } else { @@ -292,191 +515,163 @@ bool PlasmaPhase::addSpecies(shared_ptr spec) void PlasmaPhase::initThermo() { IdealGasPhase::initThermo(); - // check electron species + + // Check electron species if (m_electronSpeciesIndex == npos) { throw CanteraError("PlasmaPhase::initThermo", "No electron species found."); } -} -void PlasmaPhase::setSolution(std::weak_ptr soln) { - ThermoPhase::setSolution(soln); - // register callback function to be executed - // when the thermo or kinetics object changed - if (shared_ptr soln = m_soln.lock()) { - soln->registerChangedCallback(this, [&]() { - setCollisions(); - }); - } -} + // Initialize kinetics + m_kinetics = newKinetics("bulk"); + m_kinetics->addThermo(shared_from_this()); -void PlasmaPhase::setCollisions() -{ - m_collisions.clear(); - m_collisionRates.clear(); - m_targetSpeciesIndices.clear(); + vector> reactions; + for (AnyMap R : reactionsAnyMapList(*m_kinetics, m_input, m_root)) { + shared_ptr reaction = newReaction(R, *m_kinetics); - if (shared_ptr soln = m_soln.lock()) { - shared_ptr kin = soln->kinetics(); - if (!kin) { - return; - } + // Check if this is an 'electron-collision-plasma' reaction + if (reaction->type() == "electron-collision-plasma") { + auto rate = std::dynamic_pointer_cast(reaction->rate()); - // add collision from the initial list of reactions - for (size_t i = 0; i < kin->nReactions(); i++) { - std::shared_ptr R = kin->reaction(i); - if (R->rate()->type() != "electron-collision-plasma") { - continue; + // If the reaction already has 'energy-levels' and 'cross-sections', use them + if (R.hasKey("energy-levels") && R.hasKey("cross-sections")) { + writelog("Using embedded cross-section data in reaction.\n"); + rate->set_energyLevels(R["energy-levels"].asVector()); + rate->set_crossSections(R["cross-sections"].asVector()); + rate->set_threshold(R.getDouble("threshold", 0.0)); + rate->set_cs_ok(); // Mark as valid cross-section data + + } else { + // Try to match with preloaded cross-sections in 'm_ecss[]' (old format) + for (size_t k = 0; k < m_ncs; k++) { + if (rate->target() == m_ecss[k]->target && + rate->product() == m_ecss[k]->product && + rate->kind() == m_ecss[k]->kind) { + + rate->set_crossSections(m_ecss[k]->crossSection); + rate->set_energyLevels(m_ecss[k]->energyLevel); + rate->set_threshold(m_ecss[k]->threshold); + rate->set_cs_ok(); + } + } } - addCollision(R); - } - // register callback when reaction is added later - // Modifying collision reactions is not supported - kin->registerReactionAddedCallback(this, [this, kin]() { - size_t i = kin->nReactions() - 1; - if (kin->reaction(i)->type() == "electron-collision-plasma") { - addCollision(kin->reaction(i)); + // Check if cross-section data exists + if (!(rate->get_cs_ok())) { + throw CanteraError("PlasmaPhase::initThermo", + "Energy levels and cross-sections are undefined for an electron-collision-plasma reaction."); } - }); - } -} + } -void PlasmaPhase::addCollision(std::shared_ptr collision) -{ - size_t i = nCollisions(); + reactions.push_back(reaction); + } - // setup callback to signal updating the cross-section-related - // parameters - collision->registerSetRateCallback(this, [this, i, collision]() { - m_interp_cs_ready[i] = false; - m_collisionRates[i] = - std::dynamic_pointer_cast(collision->rate()); - }); + // Add reactions to the kinetics object + addReactions(*m_kinetics, reactions); - // Identify target species for electron-collision reactions - for (const auto& [name, _] : collision->reactants) { - // Reactants are expected to be electrons and the target species - if (name != electronSpeciesName()) { - m_targetSpeciesIndices.emplace_back(speciesIndex(name)); - break; + // Initialize 'm_collisions' and identify elastic collisions + m_collisions.clear(); + size_t i = 0; + for (shared_ptr reaction : reactions) { + if (reaction->type() == "electron-collision-plasma") { + m_collisions.push_back(reaction); + + // Check if the reaction is elastic (reactants = products) + if (reaction->reactants == reaction->products) { + m_elasticCollisionIndices.push_back(i); + } + i++; // Count only 'electron-collision-plasma' reactions } } - m_collisions.emplace_back(collision); - m_collisionRates.emplace_back( - std::dynamic_pointer_cast(collision->rate())); - m_interp_cs_ready.emplace_back(false); - - // resize parameters - m_elasticElectronEnergyLossCoefficients.resize(nCollisions()); + // Interpolate cross-sections + writelog("Final electron species index: " + std::to_string(m_electronSpeciesIndex) + "\n"); + updateInterpolatedCrossSections(); } -bool PlasmaPhase::updateInterpolatedCrossSection(size_t i) +void PlasmaPhase::updateInterpolatedCrossSections() { - if (m_interp_cs_ready[i]) { - return false; - } - vector levels(m_nPoints); - Eigen::Map(levels.data(), m_nPoints) = m_electronEnergyLevels; - m_collisionRates[i]->updateInterpolatedCrossSection(levels); - m_interp_cs_ready[i] = true; - return true; -} + for (shared_ptr collision : m_collisions) { + auto rate = boost::polymorphic_pointer_downcast + (collision->rate()); -void PlasmaPhase::updateElectronEnergyDistDifference() -{ - m_electronEnergyDistDiff.resize(nElectronEnergyLevels()); - // Forward difference for the first point - m_electronEnergyDistDiff[0] = - (m_electronEnergyDist[1] - m_electronEnergyDist[0]) / - (m_electronEnergyLevels[1] - m_electronEnergyLevels[0]); + vector cs_interp; + for (double level : m_electronEnergyLevels) { + cs_interp.push_back(linearInterp(level, + rate->energyLevels(), rate->crossSections())); + } - // Central difference for the middle points - for (size_t i = 1; i < m_nPoints - 1; i++) { - double h1 = m_electronEnergyLevels[i+1] - m_electronEnergyLevels[i]; - double h0 = m_electronEnergyLevels[i] - m_electronEnergyLevels[i-1]; - m_electronEnergyDistDiff[i] = (h0 * h0 * m_electronEnergyDist[i+1] + - (h1 * h1 - h0 * h0) * m_electronEnergyDist[i] - - h1 * h1 * m_electronEnergyDist[i-1]) / - (h1 * h0) / (h1 + h0); + // Set interpolated cross-section + rate->setCrossSectionInterpolated(cs_interp); } - - // Backward difference for the last point - m_electronEnergyDistDiff[m_nPoints-1] = - (m_electronEnergyDist[m_nPoints-1] - - m_electronEnergyDist[m_nPoints-2]) / - (m_electronEnergyLevels[m_nPoints-1] - - m_electronEnergyLevels[m_nPoints-2]); } -void PlasmaPhase::updateElasticElectronEnergyLossCoefficients() +size_t PlasmaPhase::targetSpeciesIndex(shared_ptr R) { - // cache of cross section plus distribution plus energy-level number - static const int cacheId = m_cache.getId(); - CachedScalar last_stateNum = m_cache.getScalar(cacheId); - - // combine the distribution and energy level number - int stateNum = m_distNum + m_levelNum; - - vector interpChanged(m_collisions.size()); - for (size_t i = 0; i < m_collisions.size(); i++) { - interpChanged[i] = updateInterpolatedCrossSection(i); + if (R->type() != "electron-collision-plasma") { + throw CanteraError("PlasmaPhase::targetSpeciesIndex", + "Invalid reaction type. Type electron-collision-plasma is needed."); } - - if (last_stateNum.validate(temperature(), stateNum)) { - // check each cross section, and only update coefficients that - // the interpolated cross sections change - for (size_t i = 0; i < m_collisions.size(); i++) { - if (interpChanged[i]) { - updateElasticElectronEnergyLossCoefficient(i); - } - } - } else { - // update every coefficient if distribution, temperature, - // or energy levels change. - for (size_t i = 0; i < m_collisions.size(); i++) { - updateElasticElectronEnergyLossCoefficient(i); + for (const auto& [name, stoich] : R->reactants) { + if (name != electronSpeciesName()) { + return speciesIndex(name); } } + throw CanteraError("PlasmaPhase::targetSpeciesIndex", + "No target found. Target cannot be electron."); } -void PlasmaPhase::updateElasticElectronEnergyLossCoefficient(size_t i) +vector PlasmaPhase::crossSection(shared_ptr reaction) { - // @todo exclude attachment collisions - size_t k = m_targetSpeciesIndices[i]; - - // Map cross sections to Eigen::ArrayXd - auto cs_array = Eigen::Map( - m_collisionRates[i]->crossSectionInterpolated().data(), - m_collisionRates[i]->crossSectionInterpolated().size() - ); - - // Mass ratio calculation - double mass_ratio = ElectronMass / molecularWeight(k) * Avogadro; - - // Calculate the rate using Simpson's rule or trapezoidal rule - Eigen::ArrayXd f0_plus = m_electronEnergyDist + Boltzmann * temperature() / - ElectronCharge * m_electronEnergyDistDiff; - m_elasticElectronEnergyLossCoefficients[i] = 2.0 * mass_ratio * gamma * - numericalQuadrature( - m_quadratureMethod, 1.0 / 3.0 * f0_plus.cwiseProduct(cs_array), - m_electronEnergyLevels.pow(3.0)); + if (reaction->type() != "electron-collision-plasma") { + throw CanteraError("PlasmaPhase::crossSection", + "Invalid reaction type. Type electron-collision-plasma is needed."); + } else { + auto rate = boost::polymorphic_pointer_downcast + (reaction->rate()); + std::vector cs_interp; + for (double level : m_electronEnergyLevels) { + cs_interp.push_back(linearInterp(level, + rate->energyLevels(), + rate->crossSections())); + } + return cs_interp; + } } -double PlasmaPhase::elasticPowerLoss() +double PlasmaPhase::normalizedElasticElectronEnergyLossRate() { - updateElasticElectronEnergyLossCoefficients(); - // The elastic power loss includes the contributions from inelastic - // collisions (inelastic recoil effects). double rate = 0.0; - for (size_t i = 0; i < nCollisions(); i++) { - rate += concentration(m_targetSpeciesIndices[i]) * - m_elasticElectronEnergyLossCoefficients[i]; + // calculate dF/depsilon (forward difference) + Eigen::ArrayXd dF(nElectronEnergyLevels()); + for (size_t i = 0; i < nElectronEnergyLevels() - 1; i++) { + dF[i] = (m_electronEnergyDist[i+1] - m_electronEnergyDist[i]) / + (m_electronEnergyLevels[i+1] - m_electronEnergyLevels[i]); + } + dF[nElectronEnergyLevels()-1] = dF[nElectronEnergyLevels()-2]; + + for (size_t i : m_elasticCollisionIndices) { + size_t k = targetSpeciesIndex(m_collisions[i]); + // get the interpolated cross sections + auto collision = boost::polymorphic_pointer_downcast + (m_collisions[i]->rate()); + // Map cross sections to Eigen::ArrayXd + auto cs_array = Eigen::Map( + collision->crossSectionInterpolated().data(), + collision->crossSectionInterpolated().size() + ); + + double mass_ratio = ElectronMass / molecularWeight(k) * Avogadro; + rate += mass_ratio * Avogadro * concentration(k) * ( + simpson(1.0 / 3.0 * m_electronEnergyDist.cwiseProduct( + cs_array), m_electronEnergyLevels.pow(3.0)) + + simpson(Boltzmann * temperature() / ElectronCharge * + cs_array.cwiseProduct(dF), m_electronEnergyLevels)); } + double gamma = sqrt(2 * ElectronCharge / ElectronMass); - return Avogadro * Avogadro * ElectronCharge * - concentration(m_electronSpeciesIndex) * rate; + return 2.0 * gamma * rate; } void PlasmaPhase::updateThermo() const @@ -497,6 +692,7 @@ void PlasmaPhase::updateThermo() const } // update the species Gibbs functions m_g0_RT[k] = m_h0_RT[k] - m_s0_R[k]; + // update the nDensity array } double PlasmaPhase::enthalpy_mole() const { From e46eb2a175645a1644b9314596ed36b8d40b5325 Mon Sep 17 00:00:00 2001 From: Bang-Shiuh Chen Date: Mon, 12 May 2025 21:47:34 -0400 Subject: [PATCH 02/29] Restore elastic power loss after adding EEDF solver --- include/cantera/thermo/PlasmaPhase.h | 76 +++++++++++ interfaces/cython/cantera/thermo.pxd | 2 +- interfaces/cython/cantera/thermo.pyx | 18 +-- src/thermo/PlasmaPhase.cpp | 192 +++++++++++++++++++++++++++ 4 files changed, 278 insertions(+), 10 deletions(-) diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index acdbeab0a64..07a6649d477 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -98,6 +98,8 @@ class PlasmaPhase: public IdealGasPhase */ explicit PlasmaPhase(const string& inputFile="", const string& id=""); + ~PlasmaPhase(); + string type() const override { return "plasma"; } @@ -230,6 +232,11 @@ class PlasmaPhase: public IdealGasPhase return m_nPoints; } + //! Number of collisions + size_t nCollisions() const { + return m_collisions.size(); + } + //! Electron Species Index size_t electronSpeciesIndex() const { return m_electronSpeciesIndex; @@ -398,6 +405,19 @@ class PlasmaPhase: public IdealGasPhase //! Get normalized elastic electron energy loss rate (eV-m3/kmol/s) double normalizedElasticElectronEnergyLossRate(); + virtual void setSolution(std::weak_ptr soln) override; + + /** + * The elastic power loss (J/s/m³) + * @f[ + * P_k = N_A N_A C_e e \sum_k C_k K_k, + * @f] + * where @f$ C_k @f$ and @f$ C_e @f$ are the concentration (kmol/m³) of the + * target species and electrons, respectively. @f$ K_k @f$ is the elastic + * electron energy loss coefficient (eV-m³/s). + */ + double elasticPowerLoss(); + protected: void initialize(); @@ -449,6 +469,12 @@ class PlasmaPhase: public IdealGasPhase //! Electron energy distribution norm void normalizeElectronEnergyDistribution(); + //! Update interpolated cross section of a collision + bool updateInterpolatedCrossSection(size_t k); + + //! Update electron energy distribution difference + void updateElectronEnergyDistDifference(); + // Electron energy order in the exponential term double m_isotropicShapeFactor = 2.0; @@ -545,6 +571,36 @@ class PlasmaPhase: public IdealGasPhase //! get cross section interpolated vector crossSection(shared_ptr reaction); + //! Electron energy distribution Difference dF/dε (V^-5/2) + Eigen::ArrayXd m_electronEnergyDistDiff; + + //! Elastic electron energy loss coefficients (eV m3/s) + /*! The elastic electron energy loss coefficient for species k is, + * @f[ + * K_k = \frac{2 m_e}{m_k} \sqrt{\frac{2 e}{m_e}} \int_0^{\infty} \sigma_k + * \epsilon^2 \left( F_0 + \frac{k_B T}{e} + * \frac{\partial F_0}{\partial \epsilon} \right) d \epsilon, + * @f] + * where @f$ m_e @f$ [kg] is the electron mass, @f$ \epsilon @f$ [V] is the + * electron energy, @f$ \sigma_k @f$ [m2] is the reaction collision cross section, + * @f$ F_0 @f$ [V^(-3/2)] is the normalized electron energy distribution function. + */ + vector m_elasticElectronEnergyLossCoefficients; + + //! Updates the elastic electron energy loss coefficient for collision index i + /*! Calculates the elastic energy loss coefficient using the current electron + energy distribution and cross sections. + */ + void updateElasticElectronEnergyLossCoefficient(size_t i); + + //! Update elastic electron energy loss coefficients + /*! Used by elasticPowerLoss() and other plasma property calculations that + depends on #m_elasticElectronEnergyLossCoefficients. This function calls + updateInterpolatedCrossSection() before calling + updateElasticElectronEnergyLossCoefficient() + */ + void updateElasticElectronEnergyLossCoefficients(); + private: //! pointer to EEDF solver @@ -561,6 +617,26 @@ class PlasmaPhase: public IdealGasPhase //! The list of shared pointers of plasma collision reactions vector> m_collisions; + //! The list of shared pointers of collision rates + vector> m_collisionRates; + + //! The collision-target species indices of #m_collisions + vector m_targetSpeciesIndices; + + //! Interpolated cross sections. This is used for storing + //! interpolated cross sections temporarily. + vector m_interp_cs; + + //! The list of whether the interpolated cross sections is ready + vector m_interp_cs_ready; + + //! Set collisions. This function sets the list of collisions and + //! the list of target species using #addCollision. + void setCollisions(); + + //! Add a collision and record the target species + void addCollision(std::shared_ptr collision); + //! Indices of elastic collisions vector m_elasticCollisionIndices; diff --git a/interfaces/cython/cantera/thermo.pxd b/interfaces/cython/cantera/thermo.pxd index b04a18ede4d..f5b1e682451 100644 --- a/interfaces/cython/cantera/thermo.pxd +++ b/interfaces/cython/cantera/thermo.pxd @@ -215,7 +215,7 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": void updateElectronEnergyDistribution() double elasticElectronEnergyLossRate() double normalizedElasticElectronEnergyLossRate() - #double elasticPowerLoss() except +translate_exception + double elasticPowerLoss() except +translate_exception cdef extern from "cantera/cython/thermo_utils.h": diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 49ecb9b1e01..153706d7a5e 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -1940,15 +1940,15 @@ cdef class ThermoPhase(_SolutionBase): raise ThermoModelMethodError(self.thermo_model) return self.plasma.normalizedElasticElectronEnergyLossRate() - #property elastic_power_loss: - # """ - # Elastic power loss (J/s/m3) - # .. versionadded:: 3.2 - # """ - # def __get__(self): - # if not self._enable_plasma: - # raise ThermoModelMethodError(self.thermo_model) - # return self.plasma.elasticPowerLoss() + property elastic_power_loss: + """ + Elastic power loss (J/s/m3) + .. versionadded:: 3.2 + """ + def __get__(self): + if not self._enable_plasma: + raise ThermoModelMethodError(self.thermo_model) + return self.plasma.elasticPowerLoss() cdef class InterfacePhase(ThermoPhase): """ A class representing a surface, edge phase """ diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 68fc7258fe3..88c26084a84 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -44,6 +44,18 @@ void PlasmaPhase::initialize() m_ionDegree = 0.0; } +PlasmaPhase::~PlasmaPhase() +{ + if (shared_ptr soln = m_soln.lock()) { + soln->removeChangedCallback(this); + soln->kinetics()->removeReactionAddedCallback(this); + } + for (size_t k = 0; k < nCollisions(); k++) { + // remove callback + m_collisions[k]->removeSetRateCallback(this); + } +} + void PlasmaPhase::setTemperature(const double temp) { Phase::setTemperature(temp); @@ -590,6 +602,91 @@ void PlasmaPhase::initThermo() updateInterpolatedCrossSections(); } +void PlasmaPhase::setSolution(std::weak_ptr soln) { + ThermoPhase::setSolution(soln); + // register callback function to be executed + // when the thermo or kinetics object changed + if (shared_ptr soln = m_soln.lock()) { + soln->registerChangedCallback(this, [&]() { + setCollisions(); + }); + } +} + +void PlasmaPhase::setCollisions() +{ + m_collisions.clear(); + m_collisionRates.clear(); + m_targetSpeciesIndices.clear(); + + if (shared_ptr soln = m_soln.lock()) { + shared_ptr kin = soln->kinetics(); + if (!kin) { + return; + } + + // add collision from the initial list of reactions + for (size_t i = 0; i < kin->nReactions(); i++) { + std::shared_ptr R = kin->reaction(i); + if (R->rate()->type() != "electron-collision-plasma") { + continue; + } + addCollision(R); + } + + // register callback when reaction is added later + // Modifying collision reactions is not supported + kin->registerReactionAddedCallback(this, [this, kin]() { + size_t i = kin->nReactions() - 1; + if (kin->reaction(i)->type() == "electron-collision-plasma") { + addCollision(kin->reaction(i)); + } + }); + } +} + +void PlasmaPhase::addCollision(std::shared_ptr collision) +{ + size_t i = nCollisions(); + + // setup callback to signal updating the cross-section-related + // parameters + collision->registerSetRateCallback(this, [this, i, collision]() { + m_interp_cs_ready[i] = false; + m_collisionRates[i] = + std::dynamic_pointer_cast(collision->rate()); + }); + + // Identify target species for electron-collision reactions + for (const auto& [name, _] : collision->reactants) { + // Reactants are expected to be electrons and the target species + if (name != electronSpeciesName()) { + m_targetSpeciesIndices.emplace_back(speciesIndex(name)); + break; + } + } + + m_collisions.emplace_back(collision); + m_collisionRates.emplace_back( + std::dynamic_pointer_cast(collision->rate())); + m_interp_cs_ready.emplace_back(false); + + // resize parameters + m_elasticElectronEnergyLossCoefficients.resize(nCollisions()); +} + +bool PlasmaPhase::updateInterpolatedCrossSection(size_t i) +{ + if (m_interp_cs_ready[i]) { + return false; + } + vector levels(m_nPoints); + Eigen::Map(levels.data(), m_nPoints) = m_electronEnergyLevels; + m_collisionRates[i]->updateInterpolatedCrossSection(levels); + m_interp_cs_ready[i] = true; + return true; +} + void PlasmaPhase::updateInterpolatedCrossSections() { for (shared_ptr collision : m_collisions) { @@ -674,6 +771,101 @@ double PlasmaPhase::normalizedElasticElectronEnergyLossRate() return 2.0 * gamma * rate; } +void PlasmaPhase::updateElectronEnergyDistDifference() +{ + m_electronEnergyDistDiff.resize(nElectronEnergyLevels()); + // Forward difference for the first point + m_electronEnergyDistDiff[0] = + (m_electronEnergyDist[1] - m_electronEnergyDist[0]) / + (m_electronEnergyLevels[1] - m_electronEnergyLevels[0]); + + // Central difference for the middle points + for (size_t i = 1; i < m_nPoints - 1; i++) { + double h1 = m_electronEnergyLevels[i+1] - m_electronEnergyLevels[i]; + double h0 = m_electronEnergyLevels[i] - m_electronEnergyLevels[i-1]; + m_electronEnergyDistDiff[i] = (h0 * h0 * m_electronEnergyDist[i+1] + + (h1 * h1 - h0 * h0) * m_electronEnergyDist[i] - + h1 * h1 * m_electronEnergyDist[i-1]) / + (h1 * h0) / (h1 + h0); + } + + // Backward difference for the last point + m_electronEnergyDistDiff[m_nPoints-1] = + (m_electronEnergyDist[m_nPoints-1] - + m_electronEnergyDist[m_nPoints-2]) / + (m_electronEnergyLevels[m_nPoints-1] - + m_electronEnergyLevels[m_nPoints-2]); +} + +void PlasmaPhase::updateElasticElectronEnergyLossCoefficients() +{ + // cache of cross section plus distribution plus energy-level number + static const int cacheId = m_cache.getId(); + CachedScalar last_stateNum = m_cache.getScalar(cacheId); + + // combine the distribution and energy level number + int stateNum = m_distNum + m_levelNum; + + vector interpChanged(m_collisions.size()); + for (size_t i = 0; i < m_collisions.size(); i++) { + interpChanged[i] = updateInterpolatedCrossSection(i); + } + + if (last_stateNum.validate(temperature(), stateNum)) { + // check each cross section, and only update coefficients that + // the interpolated cross sections change + for (size_t i = 0; i < m_collisions.size(); i++) { + if (interpChanged[i]) { + updateElasticElectronEnergyLossCoefficient(i); + } + } + } else { + // update every coefficient if distribution, temperature, + // or energy levels change. + for (size_t i = 0; i < m_collisions.size(); i++) { + updateElasticElectronEnergyLossCoefficient(i); + } + } +} + +void PlasmaPhase::updateElasticElectronEnergyLossCoefficient(size_t i) +{ + // @todo exclude attachment collisions + size_t k = m_targetSpeciesIndices[i]; + + // Map cross sections to Eigen::ArrayXd + auto cs_array = Eigen::Map( + m_collisionRates[i]->crossSectionInterpolated().data(), + m_collisionRates[i]->crossSectionInterpolated().size() + ); + + // Mass ratio calculation + double mass_ratio = ElectronMass / molecularWeight(k) * Avogadro; + + // Calculate the rate using Simpson's rule or trapezoidal rule + Eigen::ArrayXd f0_plus = m_electronEnergyDist + Boltzmann * temperature() / + ElectronCharge * m_electronEnergyDistDiff; + m_elasticElectronEnergyLossCoefficients[i] = 2.0 * mass_ratio * gamma * + numericalQuadrature( + m_quadratureMethod, 1.0 / 3.0 * f0_plus.cwiseProduct(cs_array), + m_electronEnergyLevels.pow(3.0)); +} + +double PlasmaPhase::elasticPowerLoss() +{ + updateElasticElectronEnergyLossCoefficients(); + // The elastic power loss includes the contributions from inelastic + // collisions (inelastic recoil effects). + double rate = 0.0; + for (size_t i = 0; i < nCollisions(); i++) { + rate += concentration(m_targetSpeciesIndices[i]) * + m_elasticElectronEnergyLossCoefficients[i]; + } + + return Avogadro * Avogadro * ElectronCharge * + concentration(m_electronSpeciesIndex) * rate; +} + void PlasmaPhase::updateThermo() const { IdealGasPhase::updateThermo(); From 7eb60d27db8ecad601b731274e8edd181639beb8 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Tue, 13 May 2025 00:42:36 -0400 Subject: [PATCH 03/29] Updates to EEDF solver --- src/thermo/PlasmaPhase.cpp | 27 ++++++++++++++++++++++++++- 1 file changed, 26 insertions(+), 1 deletion(-) diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 88c26084a84..295d58bed2f 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -31,7 +31,7 @@ PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) m_electronEnergyLevels = Eigen::ArrayXd::LinSpaced(m_nPoints, 0.0, 1.0); // initial electron temperature - setElectronTemperature(temperature()); + m_electronTemp = temperature(); } void PlasmaPhase::initialize() @@ -82,6 +82,7 @@ void PlasmaPhase::updateElectronEnergyDistribution() "Call to calculateDistributionFunction failed."); } } + updateElectronEnergyDistDifference(); electronEnergyDistributionChanged(); updateElectronTemperatureFromEnergyDist(); } @@ -143,6 +144,14 @@ void PlasmaPhase::setElectronEnergyLevels(const double* levels, size_t length) checkElectronEnergyLevels(); electronEnergyLevelChanged(); updateElectronEnergyDistribution(); + m_interp_cs.resize(m_nPoints); + // The cross sections are interpolated on the energy levels + if (nCollisions() > 0) { + for (size_t i = 0; i < m_collisions.size(); i++) { + m_interp_cs_ready[i] = false; + updateInterpolatedCrossSection(i); + } + } } void PlasmaPhase::electronEnergyDistributionChanged() @@ -202,6 +211,7 @@ void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* levels, normalizeElectronEnergyDistribution(); } checkElectronEnergyDistribution(); + updateElectronEnergyDistDifference(); updateElectronTemperatureFromEnergyDist(); electronEnergyLevelChanged(); electronEnergyDistributionChanged(); @@ -842,9 +852,24 @@ void PlasmaPhase::updateElasticElectronEnergyLossCoefficient(size_t i) // Mass ratio calculation double mass_ratio = ElectronMass / molecularWeight(k) * Avogadro; + if (m_electronEnergyDist.size() != m_electronEnergyDistDiff.size()) { + throw CanteraError("updateElasticElectronEnergyLossCoefficient", + "EEDF and EEDF gradient sizes do not match."); + } + + if (m_electronEnergyDist.size() != cs_array.size()) { + throw CanteraError("updateElasticElectronEnergyLossCoefficient", + "EEDF and cross-section sizes do not match."); + } + + if (m_electronEnergyDist.size() != m_electronEnergyLevels.size()) { + throw CanteraError("updateElasticElectronEnergyLossCoefficient", + "EEDF and energy level sizes do not match."); + } // Calculate the rate using Simpson's rule or trapezoidal rule Eigen::ArrayXd f0_plus = m_electronEnergyDist + Boltzmann * temperature() / ElectronCharge * m_electronEnergyDistDiff; + m_elasticElectronEnergyLossCoefficients[i] = 2.0 * mass_ratio * gamma * numericalQuadrature( m_quadratureMethod, 1.0 / 3.0 * f0_plus.cwiseProduct(cs_array), From 08d6b9a6e70fe1782b19c1e2a924435c5fdfdae6 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Tue, 13 May 2025 02:03:46 -0400 Subject: [PATCH 04/29] Delete unnecessary setDiscretizedElectronEnergyDist method updateElectronTemperatureFromEDist update --- include/cantera/thermo/PlasmaPhase.h | 7 ------ src/thermo/PlasmaPhase.cpp | 35 +++++++++------------------- 2 files changed, 11 insertions(+), 31 deletions(-) diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index 07a6649d477..6d0189b9998 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -133,13 +133,6 @@ class PlasmaPhase: public IdealGasPhase const double* distrb, size_t length); - //! Set discretized electron energy distribution. - //! @param distrb The vector of electron energy distribution. - //! Length: #m_nPoints. - //! @param length The length of the vectors, which equals #m_nPoints. - void setDiscretizedElectronEnergyDist(const double* distrb, - size_t length); - //! Get electron energy distribution. //! @param distrb The vector of electron energy distribution. //! Length: #m_nPoints. diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 295d58bed2f..74d01a745a8 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -144,14 +144,6 @@ void PlasmaPhase::setElectronEnergyLevels(const double* levels, size_t length) checkElectronEnergyLevels(); electronEnergyLevelChanged(); updateElectronEnergyDistribution(); - m_interp_cs.resize(m_nPoints); - // The cross sections are interpolated on the energy levels - if (nCollisions() > 0) { - for (size_t i = 0; i < m_collisions.size(); i++) { - m_interp_cs_ready[i] = false; - updateInterpolatedCrossSection(i); - } - } } void PlasmaPhase::electronEnergyDistributionChanged() @@ -217,28 +209,23 @@ void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* levels, electronEnergyDistributionChanged(); } -void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* dist, - size_t length) -{ - m_distributionType = "discretized"; - m_nPoints = length; - m_electronEnergyDist = - Eigen::Map(dist, length); - checkElectronEnergyLevels(); - if (m_do_normalizeElectronEnergyDist) { - normalizeElectronEnergyDistribution(); - } - checkElectronEnergyDistribution(); - updateElectronTemperatureFromEnergyDist(); - electronEnergyDistributionChanged(); -} - void PlasmaPhase::updateElectronTemperatureFromEnergyDist() { // calculate mean electron energy and electron temperature Eigen::ArrayXd eps52 = m_electronEnergyLevels.pow(5./2.); double epsilon_m = 2.0 / 5.0 * numericalQuadrature(m_quadratureMethod, m_electronEnergyDist, eps52); + if (epsilon_m < 0.0 && m_quadratureMethod == "simpson") { + // try trapezoidal method + epsilon_m = 2.0 / 5.0 * numericalQuadrature( + "trapezoidal", m_electronEnergyDist, eps52); + } + + if (epsilon_m < 0.0) { + throw CanteraError("PlasmaPhase::updateElectronTemperatureFromEnergyDist", + "The electron energy distribution produces negative electron temperature."); + } + m_electronTemp = 2.0 / 3.0 * epsilon_m * ElectronCharge / Boltzmann; } From 1eec1bad0fc93ef5d7f4a7b25cee5c37139571d4 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Tue, 13 May 2025 03:30:51 -0400 Subject: [PATCH 05/29] only updateElectronTemperatureFromEDist in twoTerm when EEDF is valid fixes nans on Elastic power loss and Te initialization --- src/thermo/PlasmaPhase.cpp | 15 ++++++++++++++- 1 file changed, 14 insertions(+), 1 deletion(-) diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 74d01a745a8..d975ba46a95 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -32,6 +32,7 @@ PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) // initial electron temperature m_electronTemp = temperature(); + writelog("PlasmaPhase ctor: Te initialized to {}\n", m_electronTemp); } void PlasmaPhase::initialize() @@ -81,10 +82,22 @@ void PlasmaPhase::updateElectronEnergyDistribution() throw CanteraError("PlasmaPhase::updateElectronEnergyDistribution", "Call to calculateDistributionFunction failed."); } + bool validEEDF = ( + m_electronEnergyDist.size() == m_nPoints && + m_electronEnergyDist.allFinite() && + m_electronEnergyDist.maxCoeff() > 0.0 && + m_electronEnergyDist.sum() > 0.0 + ); + + if (validEEDF) { + updateElectronTemperatureFromEnergyDist(); + } else { + writelog("Skipping Te update: EEDF is empty, non-finite, or unnormalized.\n"); + } } updateElectronEnergyDistDifference(); electronEnergyDistributionChanged(); - updateElectronTemperatureFromEnergyDist(); + } void PlasmaPhase::normalizeElectronEnergyDistribution() { From 839c9af0397ec9a02fe5b72594ec91720c4ab430 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Tue, 13 May 2025 03:45:28 -0400 Subject: [PATCH 06/29] Removed redundant normalized elastic electron energy loss rate calcs --- include/cantera/thermo/PlasmaPhase.h | 8 ------- interfaces/cython/cantera/thermo.pxd | 2 -- interfaces/cython/cantera/thermo.pyx | 18 -------------- src/thermo/PlasmaPhase.cpp | 36 +--------------------------- 4 files changed, 1 insertion(+), 63 deletions(-) diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index 6d0189b9998..ff2e1f7c8f8 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -389,14 +389,6 @@ class PlasmaPhase: public IdealGasPhase m_EN = EN; // [V.m2] m_E = m_EN * molarDensity() * Avogadro; // [V/m] } - //! Get elastic electron energy loss rate (eV/s) - double elasticElectronEnergyLossRate() { - return concentration(m_electronSpeciesIndex) * - normalizedElasticElectronEnergyLossRate(); - } - - //! Get normalized elastic electron energy loss rate (eV-m3/kmol/s) - double normalizedElasticElectronEnergyLossRate(); virtual void setSolution(std::weak_ptr soln) override; diff --git a/interfaces/cython/cantera/thermo.pxd b/interfaces/cython/cantera/thermo.pxd index f5b1e682451..877445a58eb 100644 --- a/interfaces/cython/cantera/thermo.pxd +++ b/interfaces/cython/cantera/thermo.pxd @@ -213,8 +213,6 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": string electronSpeciesName() double EN() void updateElectronEnergyDistribution() - double elasticElectronEnergyLossRate() - double normalizedElasticElectronEnergyLossRate() double elasticPowerLoss() except +translate_exception diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 153706d7a5e..e259dca51a8 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -1922,24 +1922,6 @@ cdef class ThermoPhase(_SolutionBase): raise ThermoModelMethodError(self.thermo_model) return pystr(self.plasma.electronSpeciesName()) - property elastic_electron_energy_loss_rate: - """ Elastic electron energy loss rate """ - def __get__(self): - if not self._enable_plasma: - raise ThermoModelMethodError(self.thermo_model) - return self.plasma.elasticElectronEnergyLossRate() - - property normalized_elastic_electron_energy_loss_rate: - """ - Normalized elastic electron energy loss rate - The elastic electron energy loss rate is normalized - by dividing the concentration of electron. - """ - def __get__(self): - if not self._enable_plasma: - raise ThermoModelMethodError(self.thermo_model) - return self.plasma.normalizedElasticElectronEnergyLossRate() - property elastic_power_loss: """ Elastic power loss (J/s/m3) diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index d975ba46a95..15c2b64541a 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -97,7 +97,7 @@ void PlasmaPhase::updateElectronEnergyDistribution() } updateElectronEnergyDistDifference(); electronEnergyDistributionChanged(); - + } void PlasmaPhase::normalizeElectronEnergyDistribution() { @@ -747,40 +747,6 @@ vector PlasmaPhase::crossSection(shared_ptr reaction) } } -double PlasmaPhase::normalizedElasticElectronEnergyLossRate() -{ - double rate = 0.0; - // calculate dF/depsilon (forward difference) - Eigen::ArrayXd dF(nElectronEnergyLevels()); - for (size_t i = 0; i < nElectronEnergyLevels() - 1; i++) { - dF[i] = (m_electronEnergyDist[i+1] - m_electronEnergyDist[i]) / - (m_electronEnergyLevels[i+1] - m_electronEnergyLevels[i]); - } - dF[nElectronEnergyLevels()-1] = dF[nElectronEnergyLevels()-2]; - - for (size_t i : m_elasticCollisionIndices) { - size_t k = targetSpeciesIndex(m_collisions[i]); - // get the interpolated cross sections - auto collision = boost::polymorphic_pointer_downcast - (m_collisions[i]->rate()); - // Map cross sections to Eigen::ArrayXd - auto cs_array = Eigen::Map( - collision->crossSectionInterpolated().data(), - collision->crossSectionInterpolated().size() - ); - - double mass_ratio = ElectronMass / molecularWeight(k) * Avogadro; - rate += mass_ratio * Avogadro * concentration(k) * ( - simpson(1.0 / 3.0 * m_electronEnergyDist.cwiseProduct( - cs_array), m_electronEnergyLevels.pow(3.0)) + - simpson(Boltzmann * temperature() / ElectronCharge * - cs_array.cwiseProduct(dF), m_electronEnergyLevels)); - } - double gamma = sqrt(2 * ElectronCharge / ElectronMass); - - return 2.0 * gamma * rate; -} - void PlasmaPhase::updateElectronEnergyDistDifference() { m_electronEnergyDistDiff.resize(nElectronEnergyLevels()); From 1cd1c5c7877b3efe68debb3a625b9660de3b0c96 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Tue, 13 May 2025 04:43:40 -0400 Subject: [PATCH 07/29] All tests passed, and reactor and solver still work Removed debugging writelogs --- include/cantera/thermo/PlasmaPhase.h | 2 +- src/thermo/PlasmaPhase.cpp | 30 +++++++--------------------- test/data/consistency-cases.yaml | 6 ++++++ 3 files changed, 14 insertions(+), 24 deletions(-) diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index ff2e1f7c8f8..6b5b2f724ec 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -461,7 +461,7 @@ class PlasmaPhase: public IdealGasPhase void updateElectronEnergyDistDifference(); // Electron energy order in the exponential term - double m_isotropicShapeFactor = 2.0; + double m_isotropicShapeFactor = 1.0; //! Number of points of electron energy levels size_t m_nPoints = 1001; diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 15c2b64541a..9fca2667d57 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -32,7 +32,6 @@ PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) // initial electron temperature m_electronTemp = temperature(); - writelog("PlasmaPhase ctor: Te initialized to {}\n", m_electronTemp); } void PlasmaPhase::initialize() @@ -128,13 +127,12 @@ void PlasmaPhase::setIsotropicElectronEnergyDistribution() { m_electronEnergyDist.resize(m_nPoints); double x = m_isotropicShapeFactor; - double gamma1 = boost::math::tgamma(3.0 / 2.0 * x); - double gamma2 = boost::math::tgamma(5.0 / 2.0 * x); + double gamma1 = boost::math::tgamma(3.0 / 2.0 / x); + double gamma2 = boost::math::tgamma(5.0 / 2.0 / x); double c1 = x * std::pow(gamma2, 1.5) / std::pow(gamma1, 2.5); - double c2 = x * std::pow(gamma2 / gamma1, x); + double c2 = std::pow(gamma2 / gamma1, x); m_electronEnergyDist = - c1 * m_electronEnergyLevels.sqrt() / - std::pow(meanElectronEnergy(), 1.5) * + c1 / std::pow(meanElectronEnergy(), 1.5) * (-c2 * (m_electronEnergyLevels / meanElectronEnergy()).pow(x)).exp(); checkElectronEnergyDistribution(); @@ -244,7 +242,8 @@ void PlasmaPhase::updateElectronTemperatureFromEnergyDist() void PlasmaPhase::setIsotropicShapeFactor(double x) { m_isotropicShapeFactor = x; - setIsotropicElectronEnergyDistribution(); + updateElectronEnergyDistribution(); + //setIsotropicElectronEnergyDistribution(); } void PlasmaPhase::getParameters(AnyMap& phaseNode) const @@ -608,7 +607,7 @@ void PlasmaPhase::initThermo() } // Interpolate cross-sections - writelog("Final electron species index: " + std::to_string(m_electronSpeciesIndex) + "\n"); + //writelog("Final electron species index: " + std::to_string(m_electronSpeciesIndex) + "\n"); updateInterpolatedCrossSections(); } @@ -818,24 +817,9 @@ void PlasmaPhase::updateElasticElectronEnergyLossCoefficient(size_t i) // Mass ratio calculation double mass_ratio = ElectronMass / molecularWeight(k) * Avogadro; - if (m_electronEnergyDist.size() != m_electronEnergyDistDiff.size()) { - throw CanteraError("updateElasticElectronEnergyLossCoefficient", - "EEDF and EEDF gradient sizes do not match."); - } - - if (m_electronEnergyDist.size() != cs_array.size()) { - throw CanteraError("updateElasticElectronEnergyLossCoefficient", - "EEDF and cross-section sizes do not match."); - } - - if (m_electronEnergyDist.size() != m_electronEnergyLevels.size()) { - throw CanteraError("updateElasticElectronEnergyLossCoefficient", - "EEDF and energy level sizes do not match."); - } // Calculate the rate using Simpson's rule or trapezoidal rule Eigen::ArrayXd f0_plus = m_electronEnergyDist + Boltzmann * temperature() / ElectronCharge * m_electronEnergyDistDiff; - m_elasticElectronEnergyLossCoefficients[i] = 2.0 * mass_ratio * gamma * numericalQuadrature( m_quadratureMethod, 1.0 / 3.0 * f0_plus.cwiseProduct(cs_array), diff --git a/test/data/consistency-cases.yaml b/test/data/consistency-cases.yaml index 94969b53f09..752318669cb 100644 --- a/test/data/consistency-cases.yaml +++ b/test/data/consistency-cases.yaml @@ -147,6 +147,12 @@ plasma: file: oxygen-plasma.yaml phase: discretized-electron-energy-plasma known-failures: + g_eq_h_minus_Ts/.+: Test does not account for distinct electron temperature + u_eq_sum_uk_Xk/.+: Test does not account for distinct electron temperature + s_eq_sum_sk_Xk/.+: Test does not account for distinct electron temperature + cp_eq_dhdT/(0|2): Test does not account for distinct electron temperature + cv_eq_dudT/(0|2): Test does not account for distinct electron temperature + c_eq_sqrt_dP_drho_const_s/1: Test does not account for distinct electron temperature cp_eq_.+/1: Test does not account for distinct electron temperature cv_eq_.+/1: Test does not account for distinct electron temperature c._eq_dsdT_const_._times_T: Test does not account for distinct electron temperature From 22357313cb1395f480478cdbe9f2337213ab7d64 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Mon, 19 May 2025 01:40:51 -0400 Subject: [PATCH 08/29] added EEDF solver test to python test suite --- test/data/air-plasma.yaml | 183 +++++++++++++++++++++++++++++++++++++ test/python/test_thermo.py | 36 ++++++++ 2 files changed, 219 insertions(+) create mode 100644 test/data/air-plasma.yaml diff --git a/test/data/air-plasma.yaml b/test/data/air-plasma.yaml new file mode 100644 index 00000000000..dbeaf8852ce --- /dev/null +++ b/test/data/air-plasma.yaml @@ -0,0 +1,183 @@ +description: |- + Incomplete, simplified subset of the Phelps cross sections for O2/N2. + https://www.lxcat.net/Phelps. For testing purposes only. + +units: {length: cm, quantity: molec, activation-energy: K} + +phases: +- name: air-plasma-Phelps + thermo: plasma + kinetics: gas + species: + - nasa_gas.yaml/species: [Electron, O2, N2, O2+, N2+, O, O2-] + state: {T: 300.0, P: 1 atm, X: {O2: 0.21, N2: 0.79}} + electron-energy-distribution: + type: TwoTermApproximation + energy-levels: [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, + 24.0, 26.0, 28.0, 30.0, 32.0, 34.0, 36.0, 38.0, 40.0] + +reactions: +- equation: O2 + Electron => Electron + Electron + O2+ + type: electron-collision-plasma + energy-levels: [12.06, 13.0, 18.0, 28.0, 38.0, 48.0, 58.0, 68.0, 78.0, 88.0, 100.0, + 150.0, 200.0, 300.0, 500.0, 700.0, 1000.0, 1500.0, 2000.0, 3000.0, 5000.0, 7000.0, + 10000.0] + cross-sections: [0.0, 2.3e-22, 2e-21, 7.4e-21, 1.32e-20, 1.8e-20, 2.1e-20, 2.33e-20, + 2.5e-20, 2.6e-20, 2.7e-20, 2.7e-20, 2.5e-20, 2.17e-20, 1.66e-20, 1.35e-20, 1.04e-20, + 7.6e-21, 6e-21, 4.2e-21, 2.7e-21, 2e-21, 1.4e-21] + threshold: 12.06 +- equation: N2 + Electron => N2+ + 2 Electron + type: electron-collision-plasma + energy-levels: [0.0, 15.6, 16.0, 16.5, 17.0, 17.5, 18.0, 18.5, 19.0, 19.5, 20.0, 21.0, + 22.0, 23.0, 25.0, 30.0, 34.0, 45.0, 60.0, 75.0, 100.0, 150.0, 200.0] + cross-sections: [0.0, 0.0, 1.95e-22, 4.28e-22, 6.6e-22, 9.11e-22, 1.2e-21, 1.516e-21, + 1.841e-21, 2.13e-21, 2.502e-21, 3.181e-21, 3.869e-21, 4.557e-21, 5.924e-21, + 9.579e-21, 1.1718e-20, 1.6461e-20, 2.0181e-20, 2.2134e-20, 2.3436e-20, 2.2692e-20, + 2.1018e-20] + threshold: 15.6 +- equation: O2 + Electron => O2- + type: electron-collision-plasma + energy-levels: [0.0, 0.058, 0.073, 0.083, 0.089, 0.095, 0.103, 0.109, 0.15, 0.17, 0.2, + 0.21, 0.23, 0.32, 0.33, 0.35, 0.44, 0.45, 0.47, 0.56, 0.57, 0.59, 0.68, 0.69, 0.71, + 0.79, 0.8, 0.82, 0.9, 0.91, 0.93, 1.02, 1.03, 1.05, 1.5, 100.0] + cross-sections: [0.0, 0.0, 5.60795e-41, 1.80256e-40, 4.20596e-41, 8.41193e-41, + 1.80256e-40, 0.0, 0.0, 0.0, 0.0, 3.56506e-41, 0.0, 0.0, 2.30327e-41, 0.0, 0.0, + 1.45206e-41, 0.0, 0.0, 1.10156e-41, 0.0, 0.0, 8.01136e-42, 0.0, 0.0, 7.00994e-42, + 0.0, 0.0, 5.50781e-42, 0.0, 0.0, 4.20596e-42, 0.0, 0.0, 0.0] + threshold: 0.0 + +cross-sections: +- target: N2 + energy-levels: [0.0, 0.015, 0.03, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.7, 1.2, 1.5, 1.9, + 2.2, 2.8, 3.3, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 75.0, 150.0] + cross-sections: [1.1e-20, 2.55e-20, 3.4e-20, 4.33e-20, 5.95e-20, 7.1e-20, 7.9e-20, + 9e-20, 9.7e-20, 1e-19, 1.04e-19, 1.2e-19, 1.96e-19, 2.85e-19, 2.8e-19, 1.72e-19, + 1.26e-19, 1.09e-19, 1.01e-19, 1.04e-19, 1.1e-19, 1.02e-19, 9e-20, 6.6e-20, 4.9e-20] + kind: effective +- target: N2 + product: N2(rot) + energy-levels: [0.02, 0.03, 0.4, 0.8, 1.2, 1.6, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, + 2.4, 2.5, 2.6, 2.7, 2.8, 2.9, 3.0, 3.1, 3.2, 3.3, 3.6, 5.0] + cross-sections: [0.0, 2.5e-22, 2.5e-22, 2.5e-22, 4.7e-22, 8.6e-22, 1.5e-21, 2.35e-21, + 1.08e-20, 1.9e-20, 2.03e-20, 2.77e-20, 2.5e-20, 2.19e-20, 2.4e-20, 2.17e-20, + 1.62e-20, 1.38e-20, 1.18e-20, 1.03e-20, 8.4e-21, 6.9e-21, 5e-21, 1.7e-21, 0.0] + threshold: 0.02 + kind: excitation +- target: N2 + product: N2(v1) + energy-levels: [0.29, 0.3, 0.33, 0.4, 0.75, 0.9, 1.0, 1.1, 1.16, 1.2, 1.22, 1.4, 1.5, + 1.6, 1.65, 3.6, 4.0, 5.0, 15.0, 18.0, 20.0, 22.0, 23.0, 25.0, 29.0, 32.0, 50.0, + 80.0] + cross-sections: [0.0, 1e-23, 1.7e-23, 2.5e-23, 3.7e-23, 5.5e-23, 6.5e-23, 9e-23, + 1.1e-22, 1.25e-22, 1.35e-22, 7e-22, 1e-21, 1.5e-21, 0.0, 0.0, 5.5e-22, 3.5e-22, + 3.5e-22, 4e-22, 6.5e-22, 8.5e-22, 8.5e-22, 6e-22, 3e-22, 1.5e-22, 1.2e-22, 0.0] + threshold: 0.29 + kind: excitation +- target: N2 + product: N2(v1res) + energy-levels: [0.0, 0.291, 1.6, 1.65, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, + 2.6, 2.7, 2.75, 2.8, 2.9, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 4.0, 100.0] + cross-sections: [0.0, 0.0, 0.0, 2.7e-21, 3.15e-21, 5.4e-21, 1.485e-20, 4.8e-20, + 2.565e-20, 1.2e-20, 4.5e-20, 2.76e-20, 1.59e-20, 3.15e-20, 1.545e-20, 6e-21, + 1.35e-20, 5.25e-21, 8.7e-21, 1.17e-20, 8.55e-21, 6.6e-21, 6e-21, 5.85e-21, 5.7e-21, + 0.0, 0.0] + threshold: 0.291 + kind: excitation +- target: N2 + product: N2(v2) + energy-levels: [0.0, 0.59, 1.7, 1.8, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, + 2.75, 2.8, 2.9, 3.0, 3.1, 3.2, 3.3, 3.4, 3.5, 3.6, 100.0] + cross-sections: [0.0, 0.0, 0.0, 1.5e-22, 6.3e-21, 1.935e-20, 3.3e-20, 1.47e-20, + 5.4e-21, 2.115e-20, 3e-20, 5.4e-21, 1.05e-20, 1.725e-20, 1.275e-20, 3.3e-21, 9e-21, + 6.45e-21, 3.75e-21, 3.45e-21, 3e-21, 2.13e-21, 0.0, 0.0] + threshold: 0.59 + kind: excitation +- target: N2 + product: N2(v3) + energy-levels: [0.0, 0.88, 1.9, 2.0, 2.1, 2.2, 2.3, 2.4, 2.5, 2.6, 2.7, 2.75, 2.8, + 2.9, 3.0, 3.1, 3.2, 3.3, 3.4, 100.0] + cross-sections: [0.0, 0.0, 0.0, 9.6e-21, 2.055e-20, 2.7e-20, 1.695e-20, 7.5e-22, + 9.6e-21, 1.47e-20, 4.5e-21, 9.6e-21, 5.4e-21, 8.55e-21, 4.05e-21, 2.82e-21, + 2.91e-21, 6.15e-22, 0.0, 0.0] + threshold: 0.88 + kind: excitation +- target: N2 + product: N2(C3) + energy-levels: [11.03, 11.5, 12.0, 12.5, 13.0, 13.5, 13.8, 14.0, 14.2, 14.5, 15.0, + 16.0, 17.0, 18.0, 19.0, 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, 36.0, 40.0, 50.0, 70.0, + 100.0, 150.0] + cross-sections: [0.0, 2.7e-22, 6.2e-22, 1.31e-21, 2.9e-21, 4.9e-21, 6.2e-21, 6.5e-21, + 6.4e-21, 6.3e-21, 5.5e-21, 4.3e-21, 3.5e-21, 3e-21, 2.7e-21, 2.5e-21, 2.1e-21, + 1.77e-21, 1.5e-21, 1.28e-21, 1.11e-21, 7.8e-22, 6.3e-22, 3.9e-22, 1.5e-22, 1.5e-23, + 0.0] + threshold: 11.03 + kind: excitation +- target: N2 + product: N2(B3) + energy-levels: [0.0, 7.35, 8.0, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, + 18.0, 20.0, 22.0, 26.0, 30.0, 34.0, 40.0, 50.0, 70.0, 150.0] + cross-sections: [0.0, 0.0, 3.62e-22, 9.38e-22, 1.508e-21, 1.863e-21, 2.003e-21, + 1.99e-21, 1.816e-21, 1.615e-21, 1.447e-21, 1.307e-21, 1.199e-21, 1.112e-21, + 9.51e-22, 8.04e-22, 6.77e-22, 5.63e-22, 4.29e-22, 2.68e-22, 6.7e-23, 0.0] + threshold: 7.35 + kind: excitation +- target: N2 + product: N2(a1) + energy-levels: [0.0, 8.55, 9.0, 14.0, 15.0, 16.0, 17.0, 18.0, 19.0, 20.0, 24.0, 26.0, + 30.0, 40.0, 50.0, 70.0, 100.0, 150.0, 200.0] + cross-sections: [0.0, 0.0, 1.27e-22, 1.474e-21, 1.715e-21, 1.916e-21, 2.023e-21, + 1.99e-21, 1.923e-21, 1.849e-21, 1.621e-21, 1.528e-21, 1.367e-21, 1.065e-21, + 8.51e-22, 6.03e-22, 4.02e-22, 2.68e-22, 2.01e-22] + threshold: 8.55 + kind: excitation +- target: N2 + product: N2(w1) + energy-levels: [0.0, 8.89, 9.0, 10.0, 11.0, 12.0, 13.0, 14.0, 15.0, 16.0, 17.0, 18.0, + 20.0, 22.0, 30.0, 38.0, 50.0, 150.0] + cross-sections: [0.0, 0.0, 1.3e-23, 2.61e-22, 4.76e-22, 6.63e-22, 7.84e-22, 7.71e-22, + 6.7e-22, 5.43e-22, 4.42e-22, 3.75e-22, 2.88e-22, 2.41e-22, 1.54e-22, 9.4e-23, + 4.7e-23, 0.0] + threshold: 8.89 + kind: excitation +- target: O2 + energy-levels: [4.4, 4.9, 5.38, 5.86, 6.1, 6.48, 6.77, 7.05, 7.3, 7.53, 7.77, 8.0, + 8.25, 8.73, 9.2, 9.68, 10.15, 11.35, 100.0] + cross-sections: [0.0, 0.0, 2.3e-23, 7.2e-23, 1.08e-22, 1.38e-22, 1.52e-22, 1.56e-22, + 1.48e-22, 1.31e-22, 1.1e-22, 8.4e-23, 5.4e-23, 2.8e-23, 1.4e-23, 8e-24, 8e-24, + 8e-24, 0.0] + threshold: 0.0 + product: O^-+O + kind: attachment +- target: O2 + energy-levels: [0.0, 0.015, 0.03, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.7, 1.2, 1.5, 1.9, + 2.2, 2.8, 3.3, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 75.0, 150.0, 200.0] + cross-sections: [3.5e-21, 8.7e-21, 1.24e-20, 1.6e-20, 2.5e-20, 3.1e-20, 3.6e-20, + 4.5e-20, 5.2e-20, 6.1e-20, 7.9e-20, 7.6e-20, 6.9e-20, 6.5e-20, 5.8e-20, 5.5e-20, + 5.5e-20, 5.6e-20, 6.6e-20, 8e-20, 8.8e-20, 8.6e-20, 8e-20, 6.8e-20, 6.7e-20, 6e-20] + kind: effective +- target: O2 + product: O2(rot) + energy-levels: [0.07, 0.08, 0.1, 0.2, 0.21, 0.22, 0.32, 0.33, 0.35, 0.44, 0.45, 0.47, + 0.56, 0.57, 0.59, 0.68, 0.69, 0.71, 0.79, 0.8, 0.81, 0.9, 0.91, 0.93, 1.02, 1.03, + 1.05, 1.13, 1.14, 1.16, 1.22, 1.23, 1.26, 1.34, 1.35, 1.37, 1.44, 1.45, 1.47, 1.54, + 1.55, 1.57, 1.64, 1.65, 1.67] + cross-sections: [0.0, 5.4e-23, 0.0, 0.0, 2.16e-22, 0.0, 0.0, 3.84e-22, 0.0, 0.0, + 5.4e-22, 0.0, 0.0, 6.72e-22, 0.0, 0.0, 8.04e-22, 0.0, 0.0, 9.36e-22, 0.0, 0.0, + 8.4e-22, 0.0, 0.0, 7.2e-22, 0.0, 0.0, 4.68e-22, 0.0, 0.0, 6e-22, 0.0, 0.0, 3.6e-22, + 0.0, 0.0, 2.4e-22, 0.0, 0.0, 1.2e-22, 0.0, 0.0, 4.8e-23, 0.0] + threshold: 0.02 + kind: excitation +- target: O2 + product: O2(a1) + energy-levels: [0.977, 1.5, 3.5, 5.62, 6.53, 7.89, 13.0, 20.5, 41.0, 100.0] + cross-sections: [0.0, 5.8e-23, 4.9e-22, 8.25e-22, 9.08e-22, 8.63e-22, 5.27e-22, + 3.24e-22, 1.37e-22, 0.0] + threshold: 0.977 + kind: excitation +- target: O2 + product: O2(b1) + energy-levels: [1.627, 3.0, 4.0, 7.34, 9.26, 13.0, 17.0, 20.7, 24.0, 35.1, 45.1, 100] + cross-sections: [0.0, 9.7e-23, 1.49e-22, 1.91e-22, 1.74e-22, 1.3e-22, 1.3e-22, + 1.25e-22, 1e-22, 6.3e-23, 5e-24, 0.0] + threshold: 1.627 + kind: excitation diff --git a/test/python/test_thermo.py b/test/python/test_thermo.py index 1d880285612..02379c3f765 100644 --- a/test/python/test_thermo.py +++ b/test/python/test_thermo.py @@ -1297,6 +1297,42 @@ def test_elastic_power_loss_change_shape_factor(self, phase): phase.isotropic_shape_factor = 1.1 assert phase.elastic_power_loss == approx(7408711810) + def test_eedf_solver(self): + + phase = ct.Solution('air-plasma.yaml') + phase.TPX = 300., 101325., 'N2:0.79, O2:0.21, N2+:1E-10, Electron:1E-10' + phase.EN = 200.0 * 1e-21 # Reduced electric field [V.m^2] + phase.update_EEDF() + + grid = phase.electron_energy_levels + eedf = phase.electron_energy_distribution + + reference_grid = np.logspace(-1, np.log10(60)) + + reference_eedf = np.array([ + 9.1027381e-02, 9.1026393e-02, 9.1025267e-02, 9.1023985e-02, 9.1022523e-02, + 9.1020858e-02, 9.1015025e-02, 9.1006713e-02, 9.0997242e-02, 9.0986450e-02, + 9.0974154e-02, 9.0954654e-02, 9.0923885e-02, 9.0888824e-02, 9.0842837e-02, + 9.0775447e-02, 9.0695937e-02, 9.0578309e-02, 9.0398980e-02, 9.0118320e-02, + 8.9293838e-02, 8.7498617e-02, 8.3767419e-02, 7.5765714e-02, 6.4856820e-02, + 5.5592157e-02, 4.9309310e-02, 4.5268611e-02, 4.2261381e-02, 3.9440745e-02, + 3.6437762e-02, 3.3181527e-02, 2.9616717e-02, 2.5795007e-02, 2.1676205e-02, + 1.7347058e-02, 1.3022044e-02, 8.9705614e-03, 5.5251937e-03, 3.1894295e-03, + 1.7301525e-03, 8.4647152e-04, 3.6030983e-04, 1.2894755e-04, 3.7416645e-05, + 8.4693678e-06, 1.4299900e-06, 1.7026957e-07, 1.3992350e-08, 1.5340110e-09 + ]) + + interp = np.interp(reference_grid, grid, eedf, left=0.0, right=0.0) + + mask = reference_eedf > 1e-8 + rel_error = np.abs(interp[mask] - reference_eedf[mask]) / reference_eedf[mask] + + assert max(rel_error) < 0.01 + + l2_norm = np.linalg.norm(interp - reference_eedf) + assert l2_norm < 1e-3 + + class TestImport: """ Tests the various ways of creating a Solution object From 78dd76221803d315b8839bb8fad4ce9349f6476e Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Mon, 19 May 2025 01:48:43 -0400 Subject: [PATCH 09/29] writelog cleanup --- src/thermo/PlasmaPhase.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 9fca2667d57..2895429b03f 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -319,11 +319,9 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) // Check for 'cross-sections' block (Old Format) if (rootNode.hasKey("cross-sections")) { - writelog("Using explicit `cross-sections` block.\n"); for (const auto& item : rootNode["cross-sections"].asVector()) { addElectronCrossSection(newElectronCrossSection(item)); } - writelog("m_ncs = {:3d}\n", m_ncs); foundCrossSections = true; } @@ -557,7 +555,6 @@ void PlasmaPhase::initThermo() // If the reaction already has 'energy-levels' and 'cross-sections', use them if (R.hasKey("energy-levels") && R.hasKey("cross-sections")) { - writelog("Using embedded cross-section data in reaction.\n"); rate->set_energyLevels(R["energy-levels"].asVector()); rate->set_crossSections(R["cross-sections"].asVector()); rate->set_threshold(R.getDouble("threshold", 0.0)); @@ -607,7 +604,6 @@ void PlasmaPhase::initThermo() } // Interpolate cross-sections - //writelog("Final electron species index: " + std::to_string(m_electronSpeciesIndex) + "\n"); updateInterpolatedCrossSections(); } From 08e1e09e974da838f15b3ec2d0560c1df76f6c9e Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Mon, 26 May 2025 02:43:43 -0400 Subject: [PATCH 10/29] Ensure proper initialization in ElectronCollisionPlasma modifyRateConstants() --- src/kinetics/ElectronCollisionPlasmaRate.cpp | 13 +++++++++++++ 1 file changed, 13 insertions(+) diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index 2815e075ede..690c4245546 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -154,6 +154,19 @@ void ElectronCollisionPlasmaRate::modifyRateConstants( return; } + if (m_crossSectionsOffset.size() != shared_data.energyLevels.size()) { + m_crossSectionsOffset.resize(shared_data.energyLevels.size()); + vector superElasticEnergyLevels{0.0}; + for (size_t i = 1; i < m_energyLevels.size(); i++) { + superElasticEnergyLevels.push_back(m_energyLevels[i] - m_energyLevels[0]); + } + for (size_t i = 0; i < shared_data.energyLevels.size(); i++) { + m_crossSectionsOffset[i] = linearInterp(shared_data.energyLevels[i], + superElasticEnergyLevels, + m_crossSections); + } + } + // Interpolate cross-sections data to the energy levels of // the electron energy distribution function if (shared_data.levelChanged) { From d4a7ec0935a3a5c66372aebbda3dc67db075775f Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Thu, 5 Jun 2025 16:53:58 -0400 Subject: [PATCH 11/29] Add nanosecond pulse plasma example --- samples/python/reactors/nanosecondPulse.py | 115 +++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 samples/python/reactors/nanosecondPulse.py diff --git a/samples/python/reactors/nanosecondPulse.py b/samples/python/reactors/nanosecondPulse.py new file mode 100644 index 00000000000..be7298f404f --- /dev/null +++ b/samples/python/reactors/nanosecondPulse.py @@ -0,0 +1,115 @@ +""" +Nanosecond Pulse Plasma Simulation +================================== + +This example simulates a nanosecond-scale pulse discharge in a reactor. +A Gaussian-shaped electric field pulse is applied over a short timescale. + +Requires: cantera >= 3.0, matplotlib >= 2.0 + +.. tags:: Python, plasma +""" + +import cantera as ct +ct.CanteraError.set_stack_trace_depth(10) + +import numpy as np +import matplotlib.pyplot as plt + +# Gaussian pulse parameters +EN_peak = 190 * 1e-21 # Td +pulse_center = 24e-9 +pulse_width = 3e-9 # standard deviation in ns + +def gaussian_EN(t): + return EN_peak * np.exp(-((t - pulse_center)**2) / (2 * pulse_width**2)) + +# setup +gas = ct.Solution('example_data/gri30_plasma_cpavan.yaml') +gas.TPX = 300., 101325., 'CH4:0.095, O2:0.19, N2:0.715, e:1E-11' +gas.EN = gaussian_EN(0) +gas.update_EEDF() + +r = ct.ConstPressureReactor(gas, energy="off") +#r.dis_vol = 5e-3 * np.pi * (1e-3)**2 / 4 + +sim = ct.ReactorNet([r]) +sim.verbose = False + +# simulation parameters +t_total = 90e-9 +dt_max = 1e-10 +dt_chunk = 1e-9 # 1 ns chunk +states = ct.SolutionArray(gas, extra=['t']) + +print('{:>10} {:>10} {:>10} {:>14}'.format('t [s]', 'T [K]', 'P [Pa]', 'h [J/kg]')) + +# simulate in 1 ns chunks +t = 0.0 +while t < t_total: + + # integrate over the next chunk + t_end = min(t + dt_chunk, t_total) + while sim.time < t_end: + sim.advance(sim.time + dt_max) #use sim.step + states.append(r.thermo.state, t=sim.time) + print('{:10.3e} {:10.3f} {:10.3f} {:14.6f}'.format( + sim.time, r.T, r.thermo.P, r.thermo.h)) + + EN_t = gaussian_EN(t) + gas.EN = EN_t + gas.update_EEDF() + + # reinitialize integrator with new source terms + sim.reinitialize() + + t = t_end + +# Plotting +fig, ax = plt.subplots(2) + +ax[0].plot(states.t, states.X[:, gas.species_index('e')], label='e') +ax[0].plot(states.t, states.X[:, gas.species_index('O2+')], label='O2+') +ax[0].plot(states.t, states.X[:, gas.species_index('N2+')], label='N2+') +ax[0].plot(states.t, states.X[:, gas.species_index('H2O+')], label='H2O+') +ax[0].plot(states.t, states.X[:, gas.species_index('CH4+')], label='CH4+') +ax[0].plot(states.t, states.X[:, gas.species_index('O')], label='O') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(A)')], label='N2(A)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(B)')], label='N2(B)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(C)')], label='N2(C)') +ax[0].plot(states.t, states.X[:, gas.species_index("N2(a')")], label="N2(a')") +ax[0].plot(states.t, states.X[:, gas.species_index('CH3')], label='CH3', linestyle='--') +ax[0].plot(states.t, states.X[:, gas.species_index('CO2')], label='CO2', linestyle='--') +ax[0].plot(states.t, states.X[:, gas.species_index('CO')], label='CO', linestyle='--') +ax[0].plot(states.t, states.X[:, gas.species_index('H2O')], label='H2O', linestyle='--') +ax[0].plot(states.t, states.X[:, gas.species_index('H')], label='H', linestyle='--') +ax[0].plot(states.t, states.X[:, gas.species_index('OH')], label='OH', linestyle='--') +# N2 vibrational states +""" ax[0].plot(states.t, states.X[:, gas.species_index('N2(v1)')], label='N2(v1)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v2)')], label='N2(v2)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v3)')], label='N2(v3)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v4)')], label='N2(v4)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v5)')], label='N2(v5)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v6)')], label='N2(v6)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v7)')], label='N2(v7)') +ax[0].plot(states.t, states.X[:, gas.species_index('N2(v8)')], label='N2(v8)') """ + +ax[0].set_yscale('log') +ax[0].set_ylim([1e-14, 1e-3]) + +ax[1].plot(states.t, states.T, label='T') +ax2 = ax[1].twinx() +EN_values = [gaussian_EN(t) for t in states.t] +ax2.plot(states.t, EN_values, label='E/N', color='tab:red', linestyle='--') +ax2.set_ylabel('E/N', color='tab:red') +ax2.tick_params(axis='y', labelcolor='tab:red') + +for axx in ax: + axx.legend(loc='lower right') + axx.set_xlabel('Time [s]') + +ax[0].set_ylabel('Mole fraction [-]') +ax[1].set_ylabel('Temperature [K]') + +plt.tight_layout() +plt.show() \ No newline at end of file From adb7557fdc4f45bb1b85cbe6e2ae2095fd305d77 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Tue, 27 May 2025 10:47:29 -0400 Subject: [PATCH 12/29] Remove unused variables in EEDFTwoTermApproximation --- src/thermo/EEDFTwoTermApproximation.cpp | 4 ---- 1 file changed, 4 deletions(-) diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index b85f758034b..f5f3452c0bc 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -181,7 +181,6 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou SparseMat_fp Q_k = matrix_Q(g, k); SparseMat_fp P_k = matrix_P(g, k); - double mole_fraction = m_X_targets[m_klocTargets[k]]; PQ += (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; } @@ -596,7 +595,6 @@ void EEDFTwoTermApproximation::calculateTotalCrossSection() productListStr += "}"; for (size_t i = 0; i < options.m_points; i++) { - double cs_value = linearInterp(m_gridCenter[i], x, y); m_totalCrossSectionCenter[i] += m_X_targets[m_klocTargets[k]] * linearInterp(m_gridCenter[i], x, y); @@ -749,8 +747,6 @@ void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vect // Compute integral terms A1, A2, A3 for (size_t j = 1; j < nPoints; j++) { double eps_j = m_gridCenter[j]; // Electron energy level - double f0_j = m_f0[j]; // EEDF at energy level j - double integral_A1 = 0.0; double integral_A2 = 0.0; double integral_A3 = 0.0; From 77f39047b344ea806cc9f8330f595f3e4e8757c6 Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Fri, 30 May 2025 11:58:22 -0400 Subject: [PATCH 13/29] added doxygen style comments to ElectronCollisionPlasmaRate, supressed warning in PlasmaPhase --- .../kinetics/ElectronCollisionPlasmaRate.h | 16 +++++++++++++++- src/thermo/PlasmaPhase.cpp | 4 ++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index 19c47f9feae..1ebc18a2248 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -103,7 +103,15 @@ class ElectronCollisionPlasmaRate : public ReactionRate { public: ElectronCollisionPlasmaRate() = default; - + //! Constructor from YAML input for ElectronCollisionPlasmaRate. + /*! + * This constructor is used to initialize an electron collision plasma rate + * from an input YAML file. It extracts the energy levels, cross-sections, + * and reaction metadata used in the rate coefficient calculation. + * + * @param node The AnyMap node containing rate fields from YAML + * @param rate_units Units used for interpreting the rate fields + */ ElectronCollisionPlasmaRate(const AnyMap& node, const UnitStack& rate_units={}) { @@ -131,6 +139,10 @@ class ElectronCollisionPlasmaRate : public ReactionRate */ double evalFromStruct(const ElectronCollisionPlasmaData& shared_data); + //! Calculate the reverse rate coefficient for super-elastic collisions + //! @param shared_data Data structure with energy levels and EEDF + //! @param kf Forward rate coefficient (input, unused) + //! @param kr Reverse rate coefficient (output, modified) void modifyRateConstants(const ElectronCollisionPlasmaData& shared_data, double& kf, double& kr); @@ -173,10 +185,12 @@ class ElectronCollisionPlasmaRate : public ReactionRate m_threshold = threshold; } + //! Mark the cross-section as valid and available for use. void set_cs_ok() { cs_ok = true; } + //! Check if the cross-section data has been set and validated. const bool get_cs_ok() const { return cs_ok; } diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 2895429b03f..b3e1821107e 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -190,13 +190,13 @@ void PlasmaPhase::checkElectronEnergyDistribution() const throw CanteraError("PlasmaPhase::checkElectronEnergyDistribution", "Values of electron energy distribution cannot be negative."); } - if (m_electronEnergyDist[m_nPoints - 1] > 0.01) { + /* if (m_electronEnergyDist[m_nPoints - 1] > 0.01) { warn_user("PlasmaPhase::checkElectronEnergyDistribution", "The value of the last element of electron energy distribution exceed 0.01. " "This indicates that the value of electron energy level is not high enough " "to contain the isotropic distribution at mean electron energy of " "{} eV", meanElectronEnergy()); - } + } */ } void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* levels, From f1595687ca2db7716a74e75bdbf2196926569e7b Mon Sep 17 00:00:00 2001 From: Matthew Quiram Date: Sun, 1 Jun 2025 17:22:02 -0400 Subject: [PATCH 14/29] fixed docstringstring in plasmatest and removed whitespace --- .../cantera/thermo/EEDFTwoTermApproximation.h | 2 +- interfaces/cython/cantera/thermo.pyx | 2 +- samples/python/thermo/plasmatest.py | 2 +- src/kinetics/ElectronCollisionPlasmaRate.cpp | 4 +-- src/kinetics/ElectronCrossSection.cpp | 8 ++--- src/thermo/EEDFTwoTermApproximation.cpp | 32 +++++++++---------- src/thermo/PlasmaPhase.cpp | 14 ++++---- 7 files changed, 32 insertions(+), 32 deletions(-) diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h index 8180f86987f..4b8047614e3 100644 --- a/include/cantera/thermo/EEDFTwoTermApproximation.h +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -251,7 +251,7 @@ class EEDFTwoTermApproximation bool m_eeCol = false; //! Compute electron-electron collision integrals - void eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, + void eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, double& a, size_t nPoints); //! flag of having an EEDF diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index e259dca51a8..df30681e879 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -1826,7 +1826,7 @@ cdef class ThermoPhase(_SolutionBase): raise TypeError('This method is invalid for ' f'thermo model: {self.thermo_model}.') self.plasma.updateElectronEnergyDistribution() - + property n_electron_energy_levels: """ Number of electron energy levels """ def __get__(self): diff --git a/samples/python/thermo/plasmatest.py b/samples/python/thermo/plasmatest.py index 94d54875396..97425bf6bb7 100644 --- a/samples/python/thermo/plasmatest.py +++ b/samples/python/thermo/plasmatest.py @@ -1,6 +1,6 @@ """ EEDF calculation -============== +================ Compute EEDF with two term approximation solver at constant E/N. Compare with results from BOLOS. diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index 690c4245546..968a646338d 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -46,7 +46,7 @@ bool ElectronCollisionPlasmaData::update(const ThermoPhase& phase, const Kinetic return true; } -void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitStack& rate_units) +void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitStack& rate_units) { ReactionRate::setParameters(node, rate_units); @@ -85,7 +85,7 @@ void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitSt } cs_ok = true; // Mark as valid cross-section data - } + } // **If no cross-section data was found, defer to PlasmaPhase (old format)** else { diff --git a/src/kinetics/ElectronCrossSection.cpp b/src/kinetics/ElectronCrossSection.cpp index 023e36cb6ee..7bc800166d8 100644 --- a/src/kinetics/ElectronCrossSection.cpp +++ b/src/kinetics/ElectronCrossSection.cpp @@ -47,9 +47,9 @@ unique_ptr newElectronCrossSection(const AnyMap& node) } if (node.hasKey("threshold")){ - ecs->threshold = node["threshold"].asDouble(); //std::stof(node.attrib("threshold")); + ecs->threshold = node["threshold"].asDouble(); //std::stof(node.attrib("threshold")); } else { - ecs->threshold = 0.0; + ecs->threshold = 0.0; if (ecs->kind == "excitation" || ecs->kind == "ionization" || ecs->kind == "attachment") { for (size_t i = 0; i < ecs->energyLevel.size(); i++) { @@ -74,9 +74,9 @@ unique_ptr newElectronCrossSection(const AnyMap& node) } /*if (node.hasKey("product")) { - ecs->product = node["product"].asString(); + ecs->product = node["product"].asString(); } else { - ecs->product = ecs->target; + ecs->product = ecs->target; }*/ // Some writelog to check the datas loaded concerning the cross section diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index f5f3452c0bc..a459ca43f77 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -50,9 +50,9 @@ int EEDFTwoTermApproximation::calculateDistributionFunction() { for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { - + std::string target = m_phase->target(k); - std::vector products = m_phase->products(k); + std::vector products = m_phase->products(k); // Print all identified products std::string productListStr = "{ "; @@ -111,22 +111,22 @@ void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) double delta = options.m_delta0; if (options.m_maxn == 0) { - throw CanteraError("EEDFTwoTermApproximation::converge", + throw CanteraError("EEDFTwoTermApproximation::converge", "options.m_maxn is zero; no iterations will occur."); } if (options.m_points == 0) { - throw CanteraError("EEDFTwoTermApproximation::converge", + throw CanteraError("EEDFTwoTermApproximation::converge", "options.m_points is zero; the EEDF grid is empty."); } if (std::isnan(delta) || delta == 0.0) { - throw CanteraError("EEDFTwoTermApproximation::converge", + throw CanteraError("EEDFTwoTermApproximation::converge", "options.m_delta0 is NaN or zero; solver cannot update."); } for (size_t n = 0; n < options.m_maxn; n++) { if (0.0 < err1 && err1 < err0) { delta *= log(options.m_factorM) / (log(err0) - log(err1)); - } + } Eigen::VectorXd f0_old = f0; f0 = iterate(f0_old, delta); @@ -138,12 +138,12 @@ void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) } err1 = norm(Df0, m_gridCenter); - if ((f0.array() != f0.array()).any()) { - throw CanteraError("EEDFTwoTermApproximation::converge", + if ((f0.array() != f0.array()).any()) { + throw CanteraError("EEDFTwoTermApproximation::converge", "NaN detected in EEDF solution."); } if ((f0.array().abs() > 1e300).any()) { - throw CanteraError("EEDFTwoTermApproximation::converge", + throw CanteraError("EEDFTwoTermApproximation::converge", "Inf detected in EEDF solution."); } @@ -162,7 +162,7 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou // must be refactored!! if ((f0.array() != f0.array()).any()) { - throw CanteraError("EEDFTwoTermApproximation::iterate", + throw CanteraError("EEDFTwoTermApproximation::iterate", "NaN detected in input f0."); } @@ -206,7 +206,7 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou A += I; if (A.rows() == 0 || A.cols() == 0) { - throw CanteraError("EEDFTwoTermApproximation::iterate", + throw CanteraError("EEDFTwoTermApproximation::iterate", "Matrix A has zero rows/columns."); } @@ -233,12 +233,12 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou } if ((f1.array() != f1.array()).any()) { - throw CanteraError("EEDFTwoTermApproximation::iterate", + throw CanteraError("EEDFTwoTermApproximation::iterate", "NaN detected in computed f1."); } f1 /= norm(f1, m_gridCenter); - + return f1; } @@ -269,7 +269,7 @@ double EEDFTwoTermApproximation::integralPQ(double a, double b, double u0, doubl return c0 * A1 + c1 * A2; } -vector_fp EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) +vector_fp EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) { vector_fp g(options.m_points, 0.0); const double f_min = 1e-300; // Smallest safe floating-point value @@ -710,7 +710,7 @@ double EEDFTwoTermApproximation::norm(const Eigen::VectorXd& f, const Eigen::Vec } -void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, +void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, double& a, size_t nPoints) { // Ensure vectors are initialized @@ -718,7 +718,7 @@ void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vect A2.assign(nPoints, 0.0); A3.assign(nPoints, 0.0); - // Compute net production frequency + // Compute net production frequency double nu = netProductionFreq(m_f0); // simulations with repeated calls to update EEDF will produce numerical instability here double nu_floor = 1e-40; // adjust as needed for stability diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index b3e1821107e..521a2961b32 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -295,8 +295,8 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) eedf["energy-levels"].asVector().size()); } setIsotropicElectronEnergyDistribution(); - } - + } + else if (m_distributionType == "discretized") { if (!eedf.hasKey("energy-levels")) { throw CanteraError("PlasmaPhase::setParameters", @@ -312,8 +312,8 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) setDiscretizedElectronEnergyDist(eedf["energy-levels"].asVector().data(), eedf["distribution"].asVector().data(), eedf["energy-levels"].asVector().size()); - } - + } + else if (m_distributionType == "TwoTermApproximation") { bool foundCrossSections = false; @@ -417,8 +417,8 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) // store the correctly identified data newCrossSection["target"] = targetSpecies; - newCrossSection["products"] = productSpeciesList; - newCrossSection["product"] = productSpeciesList.empty() ? "unknown" : productSpeciesList[0]; + newCrossSection["products"] = productSpeciesList; + newCrossSection["product"] = productSpeciesList.empty() ? "unknown" : productSpeciesList[0]; newCrossSection["kind"] = kind; newCrossSection["energy-levels"] = collisionItem["energy-levels"].asVector(); newCrossSection["cross-sections"] = collisionItem["cross-sections"].asVector(); @@ -576,7 +576,7 @@ void PlasmaPhase::initThermo() } // Check if cross-section data exists - if (!(rate->get_cs_ok())) { + if (!(rate->get_cs_ok())) { throw CanteraError("PlasmaPhase::initThermo", "Energy levels and cross-sections are undefined for an electron-collision-plasma reaction."); } From 26eeddffa144976b8a0af85c7e7f246a35b46226 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 15 Jul 2025 20:48:31 -0400 Subject: [PATCH 15/29] [Plasma] Fix caching of interpolated cross sections --- .../kinetics/ElectronCollisionPlasmaRate.h | 14 +++++-- src/kinetics/ElectronCollisionPlasmaRate.cpp | 37 +++++++------------ 2 files changed, 23 insertions(+), 28 deletions(-) diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index 1ebc18a2248..83b911a64cb 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -36,14 +36,13 @@ struct ElectronCollisionPlasmaData : public ReactionData vector energyLevels; //!< electron energy levels vector distribution; //!< electron energy distribution - bool levelChanged; + + //! integer that is incremented when electron energy levels change + int levelNumber = -1; protected: //! integer that is incremented when electron energy distribution changes int m_dist_number = -1; - - //! integer that is incremented when electron energy level changes - int m_level_number = -1; }; @@ -238,6 +237,13 @@ class ElectronCollisionPlasmaRate : public ReactionRate //! electron energy levels [eV] vector m_energyLevels; + //! Counter used to indicate when #m_energyLevels needs to be synced with the phase + int m_levelNumber = -3; + + //! Counter used to indicate when #m_crossSectionsOffset needs to be synced with the + //! phase + int m_levelNumberSuperelastic = -2; + //! collision cross sections [m2] at #m_energyLevels vector m_crossSections; diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index 968a646338d..111cff4f55e 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -36,13 +36,11 @@ bool ElectronCollisionPlasmaData::update(const ThermoPhase& phase, const Kinetic pp.getElectronEnergyDistribution(distribution.data()); // Update energy levels - // levelChanged = pp.levelNumber() != m_level_number; - // if (levelChanged) { - m_level_number = pp.levelNumber(); + if (pp.levelNumber() != levelNumber || energyLevels.empty()) { + levelNumber = pp.levelNumber(); energyLevels.resize(pp.nElectronEnergyLevels()); pp.getElectronEnergyLevels(energyLevels.data()); - // } - + } return true; } @@ -114,17 +112,20 @@ double ElectronCollisionPlasmaRate::evalFromStruct( const ElectronCollisionPlasmaData& shared_data) { // Interpolate cross-sections data to the energy levels of - // the electron energy distribution function when the interpolated - // cross section is empty - // Note that the PlasmaPhase should handle the interpolated cross sections - // for all ElectronCollisionPlasmaRate reactions - if (m_crossSectionsInterpolated.size() == 0) { + // the electron energy distribution function when the EEDF from the phase changes + if (m_levelNumber != shared_data.levelNumber) { + m_crossSectionsInterpolated.clear(); for (double level : shared_data.energyLevels) { m_crossSectionsInterpolated.push_back(linearInterp(level, m_energyLevels, m_crossSections)); } + m_levelNumber = shared_data.levelNumber; } + AssertThrowMsg(m_crossSectionsInterpolated.size() == shared_data.distribution.size(), + "ECPR:evalFromStruct", "Size mismatch: len(interp) = {}, len(distrib) = {}", + m_crossSectionsInterpolated.size(), shared_data.distribution.size()); + // Map cross sections to Eigen::ArrayXd auto cs_array = Eigen::Map( m_crossSectionsInterpolated.data(), m_crossSectionsInterpolated.size() @@ -154,22 +155,9 @@ void ElectronCollisionPlasmaRate::modifyRateConstants( return; } - if (m_crossSectionsOffset.size() != shared_data.energyLevels.size()) { - m_crossSectionsOffset.resize(shared_data.energyLevels.size()); - vector superElasticEnergyLevels{0.0}; - for (size_t i = 1; i < m_energyLevels.size(); i++) { - superElasticEnergyLevels.push_back(m_energyLevels[i] - m_energyLevels[0]); - } - for (size_t i = 0; i < shared_data.energyLevels.size(); i++) { - m_crossSectionsOffset[i] = linearInterp(shared_data.energyLevels[i], - superElasticEnergyLevels, - m_crossSections); - } - } - // Interpolate cross-sections data to the energy levels of // the electron energy distribution function - if (shared_data.levelChanged) { + if (m_levelNumberSuperelastic != shared_data.levelNumber) { // super elastic collision energy levels and cross-sections vector superElasticEnergyLevels{0.0}; m_crossSectionsOffset.resize(shared_data.energyLevels.size()); @@ -184,6 +172,7 @@ void ElectronCollisionPlasmaRate::modifyRateConstants( superElasticEnergyLevels, m_crossSections); } + m_levelNumberSuperelastic = shared_data.levelNumber; } // Map energyLevels in Eigen::ArrayXd From cf569f6ced97154831b3a40bbc4b91c388694816 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 11 Jul 2025 18:25:18 -0400 Subject: [PATCH 16/29] [Kinetics] Avoid unnecessary reinitialization of collisions --- include/cantera/thermo/PlasmaPhase.h | 6 ++++++ include/cantera/thermo/ThermoPhase.h | 9 --------- src/kinetics/KineticsFactory.cpp | 10 +--------- src/thermo/PlasmaPhase.cpp | 3 ++- 4 files changed, 9 insertions(+), 19 deletions(-) diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index 6b5b2f724ec..47d3f6acc7c 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -521,6 +521,9 @@ class PlasmaPhase: public IdealGasPhase //! array of cross-section object vector> m_ecss; + //! Kinetics object used for handling elastic collision rates + shared_ptr m_kinetics; + //! Cross section data. m_crossSections[i][j], where i is the specific process, //! j is the index of vector. Unit: [m^2] std::vector> m_crossSections; @@ -619,6 +622,9 @@ class PlasmaPhase: public IdealGasPhase //! the list of target species using #addCollision. void setCollisions(); + //! The last Kinetics object seen by setCollisions. Used to avoid spurious updates + Kinetics* m_collisionKinSource = nullptr; + //! Add a collision and record the target species void addCollision(std::shared_ptr collision); diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index af5c049a233..266b74d681e 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -2031,10 +2031,6 @@ class ThermoPhase : public Phase, public std::enable_shared_from_this kinetics() { - return m_kinetics; - } - protected: //! Store the parameters of a ThermoPhase object such that an identical //! one could be reconstructed using the newThermo(AnyMap&) function. This @@ -2074,11 +2070,6 @@ class ThermoPhase : public Phase, public std::enable_shared_from_this m_soln; - - //! The kinetics object associates with ThermoPhase - //! Some phase requires Kinetics to perform calculation - //! such as PlasmaPhase - shared_ptr m_kinetics; }; } diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index f87fe082035..dacd0afe788 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -83,15 +83,7 @@ shared_ptr newKinetics(const vector>& phases, } } - shared_ptr kin; - if (soln && soln->thermo() && soln->thermo()->kinetics()) { - // If kinetics was initiated in thermo already, use it directly - kin = soln->thermo()->kinetics(); - } else { - // Otherwise, create a new kinetics - kin = std::shared_ptr(KineticsFactory::factory()->newKinetics(kinType)); - } - + shared_ptr kin(KineticsFactory::factory()->newKinetics(kinType)); if (soln) { soln->setKinetics(kin); } diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 521a2961b32..c9d6f815a80 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -626,9 +626,10 @@ void PlasmaPhase::setCollisions() if (shared_ptr soln = m_soln.lock()) { shared_ptr kin = soln->kinetics(); - if (!kin) { + if (!kin || kin.get() == m_collisionKinSource) { return; } + m_collisionKinSource = kin.get(); // add collision from the initial list of reactions for (size_t i = 0; i < kin->nReactions(); i++) { From d34b38cbe42c5e4f89c2c49602a9d0d884a047d7 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 11 Jul 2025 22:17:59 -0400 Subject: [PATCH 17/29] [Plasma] Use consistent YAML formatting for tabulated cross-sections --- src/kinetics/ElectronCrossSection.cpp | 8 +++----- src/thermo/PlasmaPhase.cpp | 17 ----------------- 2 files changed, 3 insertions(+), 22 deletions(-) diff --git a/src/kinetics/ElectronCrossSection.cpp b/src/kinetics/ElectronCrossSection.cpp index 7bc800166d8..b128d0379b5 100644 --- a/src/kinetics/ElectronCrossSection.cpp +++ b/src/kinetics/ElectronCrossSection.cpp @@ -40,11 +40,9 @@ unique_ptr newElectronCrossSection(const AnyMap& node) ecs->kind = node["kind"].asString(); ecs->target = node["target"].asString(); - auto& data = node["data"].asVector>(); - for (size_t i = 0; i < data.size(); i++) { - ecs->energyLevel.push_back(data[i][0]); - ecs->crossSection.push_back(data[i][1]); - } + ecs->energyLevel = node["energy-levels"].asVector(); + ecs->crossSection = node["cross-sections"].asVector( + ecs->energyLevel.size()); if (node.hasKey("threshold")){ ecs->threshold = node["threshold"].asDouble(); //std::stof(node.attrib("threshold")); diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index c9d6f815a80..98d38f3c79d 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -423,23 +423,6 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) newCrossSection["energy-levels"] = collisionItem["energy-levels"].asVector(); newCrossSection["cross-sections"] = collisionItem["cross-sections"].asVector(); - // Convert 'energy-levels' and 'cross-sections' into 'data' format - std::vector> dataPairs; - std::vector energyLevels = collisionItem["energy-levels"].asVector(); - std::vector crossSections = collisionItem["cross-sections"].asVector(); - - if (energyLevels.size() != crossSections.size()) { - throw CanteraError("PlasmaPhase::setParameters", - "Mismatch: `energy-levels` and `cross-sections` must have the same length."); - } - - // Properly format 'data' field - for (size_t i = 0; i < energyLevels.size(); i++) { - dataPairs.push_back({energyLevels[i], crossSections[i]}); - } - - newCrossSection["data"] = dataPairs; - addElectronCrossSection(newElectronCrossSection(newCrossSection)); foundCrossSections = true; } From b67e39477ae6550d36421dfdc5695a702176cc02 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 13 Jul 2025 14:26:53 -0400 Subject: [PATCH 18/29] [Plasma] Consolidate initialization of collisions from plasma rate --- .../kinetics/ElectronCollisionPlasmaRate.h | 31 --- .../cantera/thermo/EEDFTwoTermApproximation.h | 4 +- src/kinetics/ElectronCollisionPlasmaRate.cpp | 8 - src/numerics/funcs.cpp | 4 + src/thermo/PlasmaPhase.cpp | 243 ++++-------------- 5 files changed, 60 insertions(+), 230 deletions(-) diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index 83b911a64cb..a7ee900172d 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -167,34 +167,6 @@ class ElectronCollisionPlasmaRate : public ReactionRate return m_product; } - - //! Set the value of #m_energyLevels [eV] - void set_energyLevels(vector epsilon) { - m_energyLevels = epsilon; - } - - //! Set the value of #m_crossSections [eV] - void set_crossSections(vector sigma) { - m_crossSections = sigma; - } - - //! Set the value of m_threshold [eV] - void set_threshold(double threshold) - { - m_threshold = threshold; - } - - //! Mark the cross-section as valid and available for use. - void set_cs_ok() { - cs_ok = true; - } - - //! Check if the cross-section data has been set and validated. - const bool get_cs_ok() const { - return cs_ok; - } - - //! The value of #m_energyLevels [eV] const vector& energyLevels() const { return m_energyLevels; @@ -231,9 +203,6 @@ class ElectronCollisionPlasmaRate : public ReactionRate //! The energy threshold of electron collision double m_threshold; - //! Check if a cross-section is define for this rate - bool cs_ok = false; - //! electron energy levels [eV] vector m_energyLevels; diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h index 4b8047614e3..c36b382c090 100644 --- a/include/cantera/thermo/EEDFTwoTermApproximation.h +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -69,6 +69,8 @@ class EEDFTwoTermApproximation void setLinearGrid(double& kTe_max, size_t& ncell); + void setGridCache(); + /** * Options controlling how the calculation is carried out. * @see TwoTermOpt @@ -184,8 +186,6 @@ class EEDFTwoTermApproximation void calculateTotalCrossSection(); - void setGridCache(); - double norm(const Eigen::VectorXd& f, const Eigen::VectorXd& grid); double m_electronMobility; diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index 111cff4f55e..ad38abc537b 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -81,14 +81,6 @@ void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitSt throw CanteraError("ElectronCollisionPlasmaRate::setParameters", "Mismatch: `energy-levels` and `cross-sections` must have the same length."); } - - cs_ok = true; // Mark as valid cross-section data - } - - // **If no cross-section data was found, defer to PlasmaPhase (old format)** - else { - //writelog("No cross-section data found in reaction, relying on PlasmaPhase initialization.\n"); - cs_ok = false; // This will be handled later in `PlasmaPhase::initThermo()` } } diff --git a/src/numerics/funcs.cpp b/src/numerics/funcs.cpp index 70f9b5f14b0..c8cf46b7c03 100644 --- a/src/numerics/funcs.cpp +++ b/src/numerics/funcs.cpp @@ -12,6 +12,10 @@ namespace Cantera double linearInterp(double x, const vector& xpts, const vector& fpts) { + AssertThrowMsg(!xpts.empty(), "linearInterp", "x data empty"); + AssertThrowMsg(!fpts.empty(), "linearInterp", "f(x) data empty"); + AssertThrowMsg(xpts.size() == fpts.size(), "linearInterp", + "len(xpts) = {}, len(fpts) = {}", xpts.size(), fpts.size()); if (x <= xpts[0]) { return fpts[0]; } diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 98d38f3c79d..1295abc1026 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -8,6 +8,7 @@ #include #include "cantera/thermo/Species.h" #include "cantera/base/global.h" +#include "cantera/numerics/eigen_dense.h" #include "cantera/numerics/funcs.h" #include "cantera/kinetics/Kinetics.h" #include "cantera/kinetics/KineticsFactory.h" @@ -27,11 +28,18 @@ PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) initThermoFile(inputFile, id_); - // initial grid - m_electronEnergyLevels = Eigen::ArrayXd::LinSpaced(m_nPoints, 0.0, 1.0); - // initial electron temperature m_electronTemp = temperature(); + + // Initialize the Boltzmann Solver + ptrEEDFSolver = make_unique(*this); + + // Set Energy Grid (Hardcoded Defaults for Now) + double kTe_max = 60; + size_t nGridCells = 301; + m_nPoints = nGridCells + 1; + ptrEEDFSolver->setLinearGrid(kTe_max, nGridCells); + m_electronEnergyLevels = MappedVector(ptrEEDFSolver->getGridEdge().data(), m_nPoints); } void PlasmaPhase::initialize() @@ -72,10 +80,7 @@ void PlasmaPhase::updateElectronEnergyDistribution() } else if (m_distributionType == "TwoTermApproximation") { auto ierr = ptrEEDFSolver->calculateDistributionFunction(); if (ierr == 0) { - auto x = ptrEEDFSolver->getGridEdge(); auto y = ptrEEDFSolver->getEEDFEdge(); - m_nPoints = x.size(); - m_electronEnergyLevels = Eigen::Map(x.data(), m_nPoints); m_electronEnergyDist = Eigen::Map(y.data(), m_nPoints); } else { throw CanteraError("PlasmaPhase::updateElectronEnergyDistribution", @@ -315,134 +320,12 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) } else if (m_distributionType == "TwoTermApproximation") { - bool foundCrossSections = false; - // Check for 'cross-sections' block (Old Format) if (rootNode.hasKey("cross-sections")) { for (const auto& item : rootNode["cross-sections"].asVector()) { addElectronCrossSection(newElectronCrossSection(item)); } - foundCrossSections = true; - } - - // Check for 'electron-collision-plasma' reactions (New Format) - if (rootNode.hasKey("reactions")) { - for (const auto& reactionItem : rootNode["reactions"].asVector()) { - if (reactionItem.hasKey("type") && - reactionItem["type"].asString() == "electron-collision-plasma" && - reactionItem.hasKey("energy-levels") && - reactionItem.hasKey("cross-sections")) { - - // Convert reaction into cross-section format - AnyMap newCrossSection; - newCrossSection["target"] = reactionItem.getString("target", "unknown"); - newCrossSection["product"] = reactionItem.getString("product", "unknown"); - newCrossSection["kind"] = reactionItem.getString("kind", "unknown"); - newCrossSection["energy-levels"] = reactionItem["energy-levels"].asVector(); - newCrossSection["cross-sections"] = reactionItem["cross-sections"].asVector(); - - addElectronCrossSection(newElectronCrossSection(newCrossSection)); - foundCrossSections = true; - } - } - } - - if (rootNode.hasKey("collisions")) { - for (const auto& collisionItem : rootNode["collisions"].asVector()) { - if (collisionItem.hasKey("type") && - collisionItem["type"].asString() == "electron-collision-plasma" && - collisionItem.hasKey("energy-levels") && - collisionItem.hasKey("cross-sections")) { - - AnyMap newCrossSection; - - // Extract target from equation if not explicitly defined - std::string equation = collisionItem.getString("equation", ""); - std::string targetSpecies = "unknown"; - std::vector productSpeciesList; - - if (!equation.empty()) { - size_t arrowPos = equation.find("=>"); - if (arrowPos != std::string::npos) { - // Extract reactants - std::string reactantSide = equation.substr(0, arrowPos); - std::vector reactants; - std::stringstream ssReactants(reactantSide); - std::string reactant; - while (ssReactants >> reactant) { - if (reactant != "+" && reactant != "e") { // Ignore '+' and 'e' - reactants.push_back(reactant); - } - } - - // First reactant (non-electron) is the target species - if (!reactants.empty()) { - targetSpecies = reactants[0]; - } - - // Extract products - std::string productSide = equation.substr(arrowPos + 2); - std::stringstream ssProducts(productSide); - std::string product; - while (ssProducts >> product) { - if (product != "+" && product != "e") { - productSpeciesList.push_back(product); - } - } - - } - } - - std::string productListStr = "{ "; - for (const auto& p : productSpeciesList) { - productListStr += p + " "; - } - productListStr += "}"; - - std::string kind = "excitation"; // Default type - if (productSpeciesList.size() == 1 && productSpeciesList[0] == targetSpecies) { - kind = "effective"; // Elastic collision (momentum transfer) - } else { - for (const auto& p : productSpeciesList) { - if (p.back() == '+') { - kind = "ionization"; - break; - } - if (p.back() == '-') { - kind = "attachment"; - break; - } - } - } - - // store the correctly identified data - newCrossSection["target"] = targetSpecies; - newCrossSection["products"] = productSpeciesList; - newCrossSection["product"] = productSpeciesList.empty() ? "unknown" : productSpeciesList[0]; - newCrossSection["kind"] = kind; - newCrossSection["energy-levels"] = collisionItem["energy-levels"].asVector(); - newCrossSection["cross-sections"] = collisionItem["cross-sections"].asVector(); - - addElectronCrossSection(newElectronCrossSection(newCrossSection)); - foundCrossSections = true; - } - } - } - - // Throw error if no valid cross-section data is found - if (!foundCrossSections) { - throw CanteraError("PlasmaPhase::setParameters", - "No valid electron collision cross-section data found."); } - - // Initialize the Boltzmann Solver - ptrEEDFSolver = make_unique(*this); - - // Set Energy Grid (Hardcoded Defaults for Now) - double kTe_max = 60; - size_t nGridCells = 301; - m_nPoints = nGridCells + 1; - ptrEEDFSolver->setLinearGrid(kTe_max, nGridCells); } } } @@ -490,6 +373,7 @@ bool PlasmaPhase::addElectronCrossSection(shared_ptr ecs) m_ncs++; m_f0_ok = false; + ptrEEDFSolver->setGridCache(); return true; } @@ -527,67 +411,6 @@ void PlasmaPhase::initThermo() // Initialize kinetics m_kinetics = newKinetics("bulk"); m_kinetics->addThermo(shared_from_this()); - - vector> reactions; - for (AnyMap R : reactionsAnyMapList(*m_kinetics, m_input, m_root)) { - shared_ptr reaction = newReaction(R, *m_kinetics); - - // Check if this is an 'electron-collision-plasma' reaction - if (reaction->type() == "electron-collision-plasma") { - auto rate = std::dynamic_pointer_cast(reaction->rate()); - - // If the reaction already has 'energy-levels' and 'cross-sections', use them - if (R.hasKey("energy-levels") && R.hasKey("cross-sections")) { - rate->set_energyLevels(R["energy-levels"].asVector()); - rate->set_crossSections(R["cross-sections"].asVector()); - rate->set_threshold(R.getDouble("threshold", 0.0)); - rate->set_cs_ok(); // Mark as valid cross-section data - - } else { - // Try to match with preloaded cross-sections in 'm_ecss[]' (old format) - for (size_t k = 0; k < m_ncs; k++) { - if (rate->target() == m_ecss[k]->target && - rate->product() == m_ecss[k]->product && - rate->kind() == m_ecss[k]->kind) { - - rate->set_crossSections(m_ecss[k]->crossSection); - rate->set_energyLevels(m_ecss[k]->energyLevel); - rate->set_threshold(m_ecss[k]->threshold); - rate->set_cs_ok(); - } - } - } - - // Check if cross-section data exists - if (!(rate->get_cs_ok())) { - throw CanteraError("PlasmaPhase::initThermo", - "Energy levels and cross-sections are undefined for an electron-collision-plasma reaction."); - } - } - - reactions.push_back(reaction); - } - - // Add reactions to the kinetics object - addReactions(*m_kinetics, reactions); - - // Initialize 'm_collisions' and identify elastic collisions - m_collisions.clear(); - size_t i = 0; - for (shared_ptr reaction : reactions) { - if (reaction->type() == "electron-collision-plasma") { - m_collisions.push_back(reaction); - - // Check if the reaction is elastic (reactants = products) - if (reaction->reactants == reaction->products) { - m_elasticCollisionIndices.push_back(i); - } - i++; // Count only 'electron-collision-plasma' reactions - } - } - - // Interpolate cross-sections - updateInterpolatedCrossSections(); } void PlasmaPhase::setSolution(std::weak_ptr soln) { @@ -659,9 +482,51 @@ void PlasmaPhase::addCollision(std::shared_ptr collision) m_collisionRates.emplace_back( std::dynamic_pointer_cast(collision->rate())); m_interp_cs_ready.emplace_back(false); + // Check if the reaction is elastic (reactants = products) + if (collision->reactants == collision->products) { + m_elasticCollisionIndices.push_back(i); + } // resize parameters m_elasticElectronEnergyLossCoefficients.resize(nCollisions()); + updateInterpolatedCrossSections(); + + // Set up data used by Boltzmann solver + auto& rate = *m_collisionRates.back(); + AnyMap cs; + cs["equation"] = collision->equation(); + auto target = collision->reactants; + auto products = collision->products; + target.erase(speciesName(electronSpeciesIndex())); + if (target.empty()) { + throw CanteraError("PlasmaPhase::addCollision", "Error identifying target for" + " collision with equation '{}'", collision->equation()); + } + cs["target"] = target.begin()->first; + products.erase(speciesName(electronSpeciesIndex())); + + // Determine collision type from reaction equation + string kind = "excitation"; // default + vector productList; + if (products == target) { + kind = "effective"; + } else { + for (const auto& [p, stoich] : products) { + double q = charge(speciesIndex(p)); + productList.push_back(p); + if (q > 0) { + kind = "ionization"; + } else if (q < 0) { + kind = "attachment"; + } + } + } + + cs["kind"] = kind; + cs["products"] = productList; + cs["energy-levels"] = rate.energyLevels(); + cs["cross-sections"] = rate.crossSections(); + addElectronCrossSection(newElectronCrossSection(cs)); } bool PlasmaPhase::updateInterpolatedCrossSection(size_t i) From 5d48840c38a0ea6c1958515575f8e7aa34187ebf Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 14 Jul 2025 13:41:20 -0400 Subject: [PATCH 19/29] [Plasma] Use ElectronCollisionPlasmaRate for all collisions --- .../kinetics/ElectronCollisionPlasmaRate.h | 5 + .../cantera/kinetics/ElectronCrossSection.h | 61 ------- include/cantera/thermo/PlasmaPhase.h | 38 +--- src/kinetics/BulkKinetics.cpp | 7 +- src/kinetics/ElectronCollisionPlasmaRate.cpp | 41 ++++- src/kinetics/ElectronCrossSection.cpp | 93 ---------- src/kinetics/InterfaceKinetics.cpp | 7 +- src/kinetics/Reaction.cpp | 18 +- src/thermo/EEDFTwoTermApproximation.cpp | 45 +---- src/thermo/PlasmaPhase.cpp | 165 ++++++++---------- 10 files changed, 147 insertions(+), 333 deletions(-) delete mode 100644 include/cantera/kinetics/ElectronCrossSection.h delete mode 100644 src/kinetics/ElectronCrossSection.cpp diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index a7ee900172d..c9df68ce298 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -167,6 +167,11 @@ class ElectronCollisionPlasmaRate : public ReactionRate return m_product; } + //! Get the energy threshold of electron collision [eV] + double threshold() const { + return m_threshold; + } + //! The value of #m_energyLevels [eV] const vector& energyLevels() const { return m_energyLevels; diff --git a/include/cantera/kinetics/ElectronCrossSection.h b/include/cantera/kinetics/ElectronCrossSection.h deleted file mode 100644 index 334c450a257..00000000000 --- a/include/cantera/kinetics/ElectronCrossSection.h +++ /dev/null @@ -1,61 +0,0 @@ -//! @file ElectronCrossSection.h Declaration for class Cantera::ElectronCrossSection. - -// This file is part of Cantera. See License.txt in the top-level directory or -// at https://cantera.org/license.txt for license and copyright information. - -#ifndef CT_ELECTRONCROSSSECTION_H -#define CT_ELECTRONCROSSSECTION_H - -#include "cantera/base/ct_defs.h" -#include "cantera/base/AnyMap.h" - -namespace Cantera -{ - -//! Contains data about the cross sections of electron collision -class ElectronCrossSection -{ -public: - ElectronCrossSection(); - - //! ElectronCrossSection objects are not copyable or assignable - ElectronCrossSection(const ElectronCrossSection&) = delete; - ElectronCrossSection& operator=(const ElectronCrossSection& other) = delete; - ~ElectronCrossSection(); - - //! Validate the cross-section data. - // void validate(); - - //! The name of the kind of electron collision - string kind; - - //! The name of the target of electron collision - string target; - - //! The product of electron collision - string product; - - vector products; - - //! Data of cross section. [m^2] - vector crossSection; - - //! The energy level corresponding to the cross section. [eV] - vector energyLevel; - - //! The threshold of a process in [eV] - double threshold; - - //! Extra data used for specific models - // AnyMap extra; -}; - -//! create an ElectronCrossSection object to store data. -unique_ptr newElectronCrossSection(const AnyMap& node); - -//! Get a vector of ElectronCrossSection objects to access the data. -// std::vector> getElectronCrossSections(const AnyValue& items); - -} - -#endif diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index 47d3f6acc7c..599bb45bc44 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -10,7 +10,6 @@ #define CT_PLASMAPHASE_H #include "cantera/thermo/IdealGasPhase.h" -#include "cantera/kinetics/ElectronCrossSection.h" #include "cantera/thermo/EEDFTwoTermApproximation.h" #include "cantera/numerics/eigen_sparse.h" @@ -109,8 +108,6 @@ class PlasmaPhase: public IdealGasPhase //! Overload to signal updating electron energy density function. virtual void setTemperature(const double temp) override; - bool addElectronCrossSection(shared_ptr ecs); - //! Set electron energy levels. //! @param levels The vector of electron energy levels (eV). //! Length: #m_nPoints. @@ -312,32 +309,19 @@ class PlasmaPhase: public IdealGasPhase // number of cross section dataset size_t nElectronCrossSections() const { - return m_ncs; + return m_collisions.size(); } // target of a specific process - string target(size_t k) { - return m_ecss[k]->target; - } - - // product of a specific process - string product(size_t k) { - return m_ecss[k]->product; - } - - const std::vector& products(size_t k) const { - return m_ecss[k]->products; // Directly retrieve the stored product list + size_t targetIndex(size_t i) const { + return m_targetSpeciesIndices[i]; } // kind of a specific process - string kind(size_t k) { - return m_ecss[k]->kind; - } + string kind(size_t k) const; // threshold of a specific process - double threshold(size_t k) { - return m_ecss[k]->threshold; - } + double threshold(size_t k) const; vector shiftFactor() const { return m_shiftFactor; @@ -515,15 +499,6 @@ class PlasmaPhase: public IdealGasPhase //! list of target species indices in local X EEDF numbering (1 index per cs) //std::vector m_klocTargets; - //! number of cross section sets - size_t m_ncs; - - //! array of cross-section object - vector> m_ecss; - - //! Kinetics object used for handling elastic collision rates - shared_ptr m_kinetics; - //! Cross section data. m_crossSections[i][j], where i is the specific process, //! j is the index of vector. Unit: [m^2] std::vector> m_crossSections; @@ -622,9 +597,6 @@ class PlasmaPhase: public IdealGasPhase //! the list of target species using #addCollision. void setCollisions(); - //! The last Kinetics object seen by setCollisions. Used to avoid spurious updates - Kinetics* m_collisionKinSource = nullptr; - //! Add a collision and record the target species void addCollision(std::shared_ptr collision); diff --git a/src/kinetics/BulkKinetics.cpp b/src/kinetics/BulkKinetics.cpp index 10562353ce1..f26a618a11c 100644 --- a/src/kinetics/BulkKinetics.cpp +++ b/src/kinetics/BulkKinetics.cpp @@ -14,6 +14,11 @@ BulkKinetics::BulkKinetics() { bool BulkKinetics::addReaction(shared_ptr r, bool resize) { + shared_ptr rate = r->rate(); + if (rate) { + rate->setContext(*r, *this); + } + bool added = Kinetics::addReaction(r, resize); if (!added) { // undeclared species, etc. @@ -29,7 +34,6 @@ bool BulkKinetics::addReaction(shared_ptr r, bool resize) m_dn.push_back(dn); - shared_ptr rate = r->rate(); string rtype = rate->subType(); if (rtype == "") { rtype = rate->type(); @@ -44,7 +48,6 @@ bool BulkKinetics::addReaction(shared_ptr r, bool resize) // Set index of rate to number of reaction within kinetics rate->setRateIndex(nReactions() - 1); - rate->setContext(*r, *this); // Add reaction rate to evaluator size_t index = m_rateTypes[rtype]; diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index ad38abc537b..b50d2814551 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -82,12 +82,17 @@ void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitSt "Mismatch: `energy-levels` and `cross-sections` must have the same length."); } } + + m_threshold = node.getDouble("threshold", 0.0); } void ElectronCollisionPlasmaRate::getParameters(AnyMap& node) const { node["type"] = type(); node["energy-levels"] = m_energyLevels; node["cross-sections"] = m_crossSections; + if (!m_kind.empty()) { + node["kind"] = m_kind; + } } void ElectronCollisionPlasmaRate::updateInterpolatedCrossSection( @@ -185,10 +190,11 @@ void ElectronCollisionPlasmaRate::modifyRateConstants( void ElectronCollisionPlasmaRate::setContext(const Reaction& rxn, const Kinetics& kin) { + const ThermoPhase& thermo = kin.thermo(); // get electron species name string electronName; - if (kin.thermo().type() == "plasma") { - electronName = dynamic_cast(kin.thermo()).electronSpeciesName(); + if (thermo.type() == "plasma") { + electronName = dynamic_cast(thermo).electronSpeciesName(); } else { throw CanteraError("ElectronCollisionPlasmaRate::setContext", "ElectronCollisionPlasmaRate requires plasma phase"); @@ -207,6 +213,37 @@ void ElectronCollisionPlasmaRate::setContext(const Reaction& rxn, const Kinetics "ElectronCollisionPlasmaRate requires one and only one electron"); } + // Determine the "kind" of collision if not specified explicitly + if (m_kind.empty()) { + m_kind = "excitation"; // default + if (rxn.reactants == rxn.products) { + m_kind = "effective"; + } else { + for (const auto& [p, stoich] : rxn.products) { + if (p == electronName) { + continue; + } + double q = thermo.charge(thermo.speciesIndex(p)); + if (q > 0) { + m_kind = "ionization"; + } else if (q < 0) { + m_kind = "attachment"; + } + } + } + } + + if (m_threshold == 0.0 && + (m_kind == "excitation" || m_kind == "ionization" || m_kind == "attachment")) + { + for (size_t i = 0; i < m_energyLevels.size(); i++) { + if (m_energyLevels[i] > 0.0) { // Look for first non-zero cross-section + m_threshold = m_energyLevels[i]; + break; + } + } + } + if (!rxn.reversible) { return; // end checking of forward reaction } diff --git a/src/kinetics/ElectronCrossSection.cpp b/src/kinetics/ElectronCrossSection.cpp deleted file mode 100644 index b128d0379b5..00000000000 --- a/src/kinetics/ElectronCrossSection.cpp +++ /dev/null @@ -1,93 +0,0 @@ -/** - * @file ElectronCrossSection.cpp Definition file for class ElectronCrossSection. - */ -// This file is part of Cantera. See License.txt in the top-level directory or -// at https://cantera.org/license.txt for license and copyright information. - -#include "cantera/kinetics/ElectronCrossSection.h" -#include "cantera/base/global.h" - -namespace Cantera { - -ElectronCrossSection::ElectronCrossSection() - : threshold(0.0) -{ -} - -ElectronCrossSection::~ElectronCrossSection() -{ -} - -// void ElectronCrossSection::validate() -// { -// if (kind == "ionization" || kind == "attachment" || kind == "excitation") { -// if (threshold < 0.0) { -// throw CanteraError("ElectronCrossSection::validate", -// "The threshold of the process", -// "(kind = '{}', target = '{}', product = '{}')", -// "cannot be negative", kind, target, product); -// } -// } else if (kind != "effective" && kind != "elastic") { -// throw CanteraError("ElectronCrossSection::validate", -// "'{}' is an unknown type of cross section data.", kind); -// } -// } - -unique_ptr newElectronCrossSection(const AnyMap& node) -{ - unique_ptr ecs(new ElectronCrossSection()); - - ecs->kind = node["kind"].asString(); - ecs->target = node["target"].asString(); - - ecs->energyLevel = node["energy-levels"].asVector(); - ecs->crossSection = node["cross-sections"].asVector( - ecs->energyLevel.size()); - - if (node.hasKey("threshold")){ - ecs->threshold = node["threshold"].asDouble(); //std::stof(node.attrib("threshold")); - } else { - ecs->threshold = 0.0; - - if (ecs->kind == "excitation" || ecs->kind == "ionization" || ecs->kind == "attachment") { - for (size_t i = 0; i < ecs->energyLevel.size(); i++) { - if (ecs->energyLevel[i] > 0.0) { // Look for first non-zero cross-section - ecs->threshold = ecs->energyLevel[i]; - break; - } - } - } - - } - - if (node.hasKey("products")) { - ecs->products = node["products"].asVector(); // Store all products - ecs->product = ecs->products.empty() ? ecs->target : ecs->products[0]; // Keep first product for compatibility - } else if (node.hasKey("product")) { - ecs->product = node["product"].asString(); - ecs->products.push_back(ecs->product); // Ensure products list is always populated - } else { - ecs->product = ecs->target; - ecs->products.push_back(ecs->product); - } - - /*if (node.hasKey("product")) { - ecs->product = node["product"].asString(); - } else { - ecs->product = ecs->target; - }*/ - - // Some writelog to check the datas loaded concerning the cross section - //writelog("Kind : {:s}\n",ecs->kind); - //writelog("Target : {:s}\n",ecs->target); - //writelog("Product : {:s}\n",ecs->product); - //writelog("Threshold : {:14.5g} eV\n",ecs->threshold); - //writelog("Energy : \n"); - //for (size_t i = 0; i < ecs->energyLevel.size(); i++){ - // writelog("{:9.4g} {:9.4g} \n",ecs->energyLevel[i], ecs->crossSection[i]); - //} - - return ecs; -} - -} \ No newline at end of file diff --git a/src/kinetics/InterfaceKinetics.cpp b/src/kinetics/InterfaceKinetics.cpp index 8b0e069d470..3ba5a515d07 100644 --- a/src/kinetics/InterfaceKinetics.cpp +++ b/src/kinetics/InterfaceKinetics.cpp @@ -388,6 +388,11 @@ bool InterfaceKinetics::addReaction(shared_ptr r_base, bool resize) } size_t i = nReactions(); + shared_ptr rate = r_base->rate(); + if (rate) { + rate->setContext(*r_base, *this); + } + bool added = Kinetics::addReaction(r_base, resize); if (!added) { return false; @@ -408,9 +413,7 @@ bool InterfaceKinetics::addReaction(shared_ptr r_base, bool resize) } // Set index of rate to number of reaction within kinetics - shared_ptr rate = r_base->rate(); rate->setRateIndex(nReactions() - 1); - rate->setContext(*r_base, *this); string rtype = rate->subType(); if (rtype == "") { diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index cb98c81dc99..753f0d68f52 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -28,11 +28,19 @@ Reaction::Reaction(const Composition& reactants_, const Composition& products_, shared_ptr rate_, shared_ptr tbody_) - : reactants(reactants_) - , products(products_) - , m_from_composition(true) + : m_from_composition(true) , m_third_body(tbody_) { + for (auto& [species, stoich] : reactants_) { + if (stoich != 0.0) { + reactants[species] = stoich; + } + } + for (auto& [species, stoich] : products_) { + if (stoich != 0.0) { + products[species] = stoich; + } + } if (reactants.count("M") || products.count("M")) { throw CanteraError("Reaction::Reaction", "Third body 'M' must not be included in either reactant or product maps."); @@ -56,7 +64,9 @@ Reaction::Reaction(const Composition& reactants_, if (name != "M") { m_third_body->explicit_3rd = true; } - } else if (!tbody_ && third.size() == 1) { + } else if (!tbody_ && third.size() == 1 + && m_rate->type() != "electron-collision-plasma") + { // implicit third body string name = third.begin()->first; m_third_body = make_shared(name); diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index a459ca43f77..e50efdc4a0c 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -46,26 +46,9 @@ void EEDFTwoTermApproximation::setLinearGrid(double& kTe_max, size_t& ncell) int EEDFTwoTermApproximation::calculateDistributionFunction() { - if (m_first_call) - { - - for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { - - std::string target = m_phase->target(k); - std::vector products = m_phase->products(k); - - // Print all identified products - std::string productListStr = "{ "; - for (const auto& p : products) { - productListStr += p + " "; - } - productListStr += "}"; - + if (m_first_call) { initSpeciesIndexCS(); m_first_call = false; - } - } else { - } update_mole_fractions(); @@ -170,18 +153,8 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou vector_fp g = vector_g(f0); for (size_t k : m_phase->kInelastic()) { - std::vector products = m_phase->products(k); // Retrieve all products - - // Format product list as a string - std::string productListStr = "{ "; - for (const auto& p : products) { - productListStr += p + " "; - } - productListStr += "}"; - SparseMat_fp Q_k = matrix_Q(g, k); SparseMat_fp P_k = matrix_P(g, k); - PQ += (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; } @@ -503,12 +476,7 @@ void EEDFTwoTermApproximation::initSpeciesIndexCS() for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { - m_kTargets[k] = m_phase->speciesIndex(m_phase->target(k)); - if (m_kTargets[k] == string::npos) { - throw CanteraError("EEDFTwoTermApproximation::initSpeciesIndexCS" - " species not found!", - m_phase->target(k)); - } + m_kTargets[k] = m_phase->targetIndex(k); // Check if it is a new target or not : auto it = find(m_k_lg_Targets.begin(), m_k_lg_Targets.end(), m_kTargets[k]); @@ -585,15 +553,6 @@ void EEDFTwoTermApproximation::calculateTotalCrossSection() vector_fp x = m_phase->energyLevels()[k]; vector_fp y = m_phase->crossSections()[k]; - std::vector products = m_phase->products(k); // Retrieve all products - - // Format product list as a string - std::string productListStr = "{ "; - for (const auto& p : products) { - productListStr += p + " "; - } - productListStr += "}"; - for (size_t i = 0; i < options.m_points; i++) { m_totalCrossSectionCenter[i] += m_X_targets[m_klocTargets[k]] * linearInterp(m_gridCenter[i], x, y); diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 1295abc1026..f0cb3afef86 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -13,6 +13,7 @@ #include "cantera/kinetics/Kinetics.h" #include "cantera/kinetics/KineticsFactory.h" #include "cantera/kinetics/Reaction.h" +#include "cantera/kinetics/ReactionRateFactory.h" #include #include "cantera/kinetics/ElectronCollisionPlasmaRate.h" @@ -44,7 +45,6 @@ PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) void PlasmaPhase::initialize() { - m_ncs = 0; m_f0_ok = false; m_EN = 0.0; m_E = 0.0; @@ -319,65 +319,30 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) eedf["energy-levels"].asVector().size()); } - else if (m_distributionType == "TwoTermApproximation") { - // Check for 'cross-sections' block (Old Format) - if (rootNode.hasKey("cross-sections")) { - for (const auto& item : rootNode["cross-sections"].asVector()) { - addElectronCrossSection(newElectronCrossSection(item)); + if (rootNode.hasKey("cross-sections")) { + for (const auto& item : rootNode["cross-sections"].asVector()) { + auto rate = make_shared(item); + Composition reactants, products; + reactants[item["target"].asString()] = 1; + reactants[electronSpeciesName()] = 1; + if (item.hasKey("product")) { + products[item["product"].asString()] = 1; + } else { + products[item["target"].asString()] = 1; } + products[electronSpeciesName()] = 1; + if (rate->kind() == "ionization") { + products[electronSpeciesName()] += 1; + } else if (rate->kind() == "attachment") { + products[electronSpeciesName()] -= 1; + } + auto R = make_shared(reactants, products, rate); + addCollision(R); } } } } - -bool PlasmaPhase::addElectronCrossSection(shared_ptr ecs) -{ - // ecs->validate(); - m_ecss.push_back(ecs); - - m_energyLevels.push_back(ecs->energyLevel); - m_crossSections.push_back(ecs->crossSection); - - // shift factor - if (ecs->kind == "ionization") { - m_shiftFactor.push_back(2); - } else { - m_shiftFactor.push_back(1); - } - - // scattering-in factor - if (ecs->kind == "ionization") { - m_inFactor.push_back(2); - } else if (ecs->kind == "attachment") { - m_inFactor.push_back(0); - } else { - m_inFactor.push_back(1); - } - - if (ecs->kind == "effective" || ecs->kind == "elastic") { - for (size_t k = 0; k < m_ncs; k++) { - if (target(k) == ecs->target) - if (kind(k) == "elastic" || kind(k) == "effective") { - throw CanteraError("PlasmaPhase::addElectronCrossSection" - "Already contains a data of effective/ELASTIC cross section for '{}'.", - ecs->target); - } - } - m_kElastic.push_back(m_ncs); - } else { - m_kInelastic.push_back(m_ncs); - } - - // add one to number of cross sections - m_ncs++; - - m_f0_ok = false; - ptrEEDFSolver->setGridCache(); - - return true; -} - bool PlasmaPhase::addSpecies(shared_ptr spec) { bool added = IdealGasPhase::addSpecies(spec); @@ -407,10 +372,6 @@ void PlasmaPhase::initThermo() throw CanteraError("PlasmaPhase::initThermo", "No electron species found."); } - - // Initialize kinetics - m_kinetics = newKinetics("bulk"); - m_kinetics->addThermo(shared_from_this()); } void PlasmaPhase::setSolution(std::weak_ptr soln) { @@ -426,21 +387,22 @@ void PlasmaPhase::setSolution(std::weak_ptr soln) { void PlasmaPhase::setCollisions() { - m_collisions.clear(); - m_collisionRates.clear(); - m_targetSpeciesIndices.clear(); - if (shared_ptr soln = m_soln.lock()) { shared_ptr kin = soln->kinetics(); - if (!kin || kin.get() == m_collisionKinSource) { + if (!kin) { return; } - m_collisionKinSource = kin.get(); - // add collision from the initial list of reactions + // add collision from the initial list of reactions. Only add reactions we + // haven't seen before + set existing; + for (auto& R : m_collisions) { + existing.insert(R.get()); + } for (size_t i = 0; i < kin->nReactions(); i++) { std::shared_ptr R = kin->reaction(i); - if (R->rate()->type() != "electron-collision-plasma") { + if (R->rate()->type() != "electron-collision-plasma" + || existing.count(R.get())) { continue; } addCollision(R); @@ -470,13 +432,19 @@ void PlasmaPhase::addCollision(std::shared_ptr collision) }); // Identify target species for electron-collision reactions + string target; for (const auto& [name, _] : collision->reactants) { // Reactants are expected to be electrons and the target species if (name != electronSpeciesName()) { m_targetSpeciesIndices.emplace_back(speciesIndex(name)); + target = name; break; } } + if (target.empty()) { + throw CanteraError("PlasmaPhase::addCollision", "Error identifying target for" + " collision with equation '{}'", collision->equation()); + } m_collisions.emplace_back(collision); m_collisionRates.emplace_back( @@ -493,40 +461,42 @@ void PlasmaPhase::addCollision(std::shared_ptr collision) // Set up data used by Boltzmann solver auto& rate = *m_collisionRates.back(); - AnyMap cs; - cs["equation"] = collision->equation(); - auto target = collision->reactants; - auto products = collision->products; - target.erase(speciesName(electronSpeciesIndex())); - if (target.empty()) { - throw CanteraError("PlasmaPhase::addCollision", "Error identifying target for" - " collision with equation '{}'", collision->equation()); + string kind = m_collisionRates.back()->kind(); + + // shift factor + if (kind == "ionization") { + m_shiftFactor.push_back(2); + } else { + m_shiftFactor.push_back(1); } - cs["target"] = target.begin()->first; - products.erase(speciesName(electronSpeciesIndex())); - // Determine collision type from reaction equation - string kind = "excitation"; // default - vector productList; - if (products == target) { - kind = "effective"; + // scattering-in factor + if (kind == "ionization") { + m_inFactor.push_back(2); + } else if (kind == "attachment") { + m_inFactor.push_back(0); } else { - for (const auto& [p, stoich] : products) { - double q = charge(speciesIndex(p)); - productList.push_back(p); - if (q > 0) { - kind = "ionization"; - } else if (q < 0) { - kind = "attachment"; + m_inFactor.push_back(1); + } + + if (kind == "effective" || kind == "elastic") { + for (size_t k = 0; k < m_collisions.size() - 1; k++) { + if (m_collisions[k]->reactants == collision->reactants) + if (this->kind(k) == "elastic" || this->kind(k) == "effective") { + throw CanteraError("PlasmaPhase::addCollision" + "Already contains a data of effective/ELASTIC cross section for '{}'.", + target); } } + m_kElastic.push_back(i); + } else { + m_kInelastic.push_back(i); } - cs["kind"] = kind; - cs["products"] = productList; - cs["energy-levels"] = rate.energyLevels(); - cs["cross-sections"] = rate.crossSections(); - addElectronCrossSection(newElectronCrossSection(cs)); + m_f0_ok = false; + m_energyLevels.push_back(rate.energyLevels()); + m_crossSections.push_back(rate.crossSections()); + ptrEEDFSolver->setGridCache(); } bool PlasmaPhase::updateInterpolatedCrossSection(size_t i) @@ -707,6 +677,15 @@ void PlasmaPhase::updateThermo() const // update the nDensity array } +string PlasmaPhase::kind(size_t k) const { + return m_collisionRates[k]->kind(); +} + +double PlasmaPhase::threshold(size_t k) const { + return m_collisionRates[k]->threshold(); +} + + double PlasmaPhase::enthalpy_mole() const { double value = IdealGasPhase::enthalpy_mole(); value += GasConstant * (electronTemperature() - temperature()) * From 570471a1a27f43795b0abacd4ffdd7ac8bbb1d4a Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 15 Jul 2025 22:35:55 -0400 Subject: [PATCH 20/29] Cleanup of PlasmaPhase and EEDFTwoTerm solver --- .../cantera/thermo/EEDFTwoTermApproximation.h | 6 ----- include/cantera/thermo/PlasmaPhase.h | 25 +---------------- src/thermo/EEDFTwoTermApproximation.cpp | 27 +++++++++---------- src/thermo/PlasmaPhase.cpp | 6 ----- 4 files changed, 14 insertions(+), 50 deletions(-) diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h index c36b382c090..0dca85e8242 100644 --- a/include/cantera/thermo/EEDFTwoTermApproximation.h +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -90,12 +90,6 @@ class EEDFTwoTermApproximation } protected: - /** - * Prepare for EEDF calculations. - * @param s object representing the solution phase. - */ - void initialize(PlasmaPhase& s); - //! Pointer to the PlasmaPhase object used to initialize this object. /*! * This PlasmaPhase object must be compatible with the PlasmaPhase objects diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index 599bb45bc44..cdf179fa16e 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -105,9 +105,6 @@ class PlasmaPhase: public IdealGasPhase void initThermo() override; - //! Overload to signal updating electron energy density function. - virtual void setTemperature(const double temp) override; - //! Set electron energy levels. //! @param levels The vector of electron energy levels (eV). //! Length: #m_nPoints. @@ -331,11 +328,6 @@ class PlasmaPhase: public IdealGasPhase return m_inFactor; } - // Gas number density [m^-3] - double N() const { - return molarDensity() * Avogadro; - } - double F() const { return m_F; } @@ -348,10 +340,6 @@ class PlasmaPhase: public IdealGasPhase return m_ionDegree; } - double kT() const { - return m_kT; - } - double EN() const { return m_EN; } @@ -484,26 +472,17 @@ class PlasmaPhase: public IdealGasPhase //! reduced electric field [V.m2] double m_EN; - //! reduced electric field [Td] - //double m_EN_Td; - //! electric field freq [Hz] double m_F; //! normalized electron energy distribution function Eigen::VectorXd m_f0; - //! Mole fraction of targets - //vector m_X_targets; - - //! list of target species indices in local X EEDF numbering (1 index per cs) - //std::vector m_klocTargets; - //! Cross section data. m_crossSections[i][j], where i is the specific process, //! j is the index of vector. Unit: [m^2] std::vector> m_crossSections; - //! Electron energy levels correpsonding to the cross section data. m_energyLevels[i][j], + //! Electron energy levels corresponding to the cross section data. m_energyLevels[i][j], //! where i is the specific process, j is the index of vector. Unit: [eV] std::vector> m_energyLevels; @@ -523,8 +502,6 @@ class PlasmaPhase: public IdealGasPhase //! ionization degree for the electron-electron collisions (tmp is the previous one) double m_ionDegree; - //! Boltzmann constant times gas temperature [eV] - double m_kT; //! Data for initiate reaction AnyMap m_root; diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index e50efdc4a0c..d5f16cd2bdf 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -16,11 +16,6 @@ namespace Cantera { EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase& s) -{ - initialize(s); -} - -void EEDFTwoTermApproximation::initialize(PlasmaPhase& s) { // store a pointer to s. m_phase = &s; @@ -326,11 +321,12 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) eeColIntegrals(A1, A2, A3, a, options.m_points); } + double nDensity = m_phase->molarDensity() * Avogadro; double alpha; if (options.m_growth == "spatial") { double mu = electronMobility(f0); double D = electronDiffusivity(f0); - alpha = (mu * m_phase->E() - sqrt(pow(mu * m_phase->E(), 2) - 4 * D * nu * m_phase->N())) / 2.0 / D / m_phase->N(); + alpha = (mu * m_phase->E() - sqrt(pow(mu * m_phase->E(), 2) - 4 * D * nu * nDensity)) / 2.0 / D / nDensity; } else { alpha = 0.0; } @@ -344,18 +340,18 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) else { sigma_tilde = m_totalCrossSectionEdge[j]; } - double q = omega / (m_phase->N() * m_gamma * pow(m_gridEdge[j], 0.5)); + double q = omega / (nDensity * m_gamma * pow(m_gridEdge[j], 0.5)); double W = -m_gamma * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; double F = sigma_tilde * sigma_tilde / (sigma_tilde * sigma_tilde + q * q); - double DA = m_gamma / 3.0 * pow(m_phase->E() / m_phase->N(), 2.0) * m_gridEdge[j]; - double DB = m_gamma * m_phase->kT() * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; + double DA = m_gamma / 3.0 * pow(m_phase->E() / nDensity, 2.0) * m_gridEdge[j]; + double DB = m_gamma * m_phase->temperature() * Boltzmann / ElectronCharge * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; double D = DA / sigma_tilde * F + DB; if (m_eeCol) { W -= 3 * a * m_phase->ionDegree() * A1[j]; D += 2 * a * m_phase->ionDegree() * (A2[j] + pow(m_gridEdge[j], 1.5) * A3[j]); } if (options.m_growth == "spatial") { - W -= m_gamma / 3.0 * 2 * alpha * m_phase->E() / m_phase->N() * m_gridEdge[j] / sigma_tilde; + W -= m_gamma / 3.0 * 2 * alpha * m_phase->E() / nDensity * m_gridEdge[j] / sigma_tilde; } double z = W * (m_gridCenter[j] - m_gridCenter[j-1]) / D; @@ -364,7 +360,7 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) } if (std::abs(z) > 500) { writelog("Warning: Large Peclet number z = {:.3e} at j = {}. W = {:.3e}, D = {:.3e}, E/N = {:.3e}\n", - z, j, W, D, m_phase->E() / m_phase->N()); + z, j, W, D, m_phase->E() / nDensity); } a0[j] = W / (1 - std::exp(-z)); a1[j] = W / (1 - std::exp(z)); @@ -403,10 +399,11 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) } } else if (options.m_growth == "spatial") { + double nDensity = m_phase->molarDensity() * Avogadro; for (size_t i = 0; i < options.m_points; i++) { double sigma_c = 0.5 * (m_totalCrossSectionEdge[i] + m_totalCrossSectionEdge[i + 1]); G.insert(i, i) = - alpha * m_gamma / 3 * (alpha * (pow(m_gridEdge[i + 1], 2) - pow(m_gridEdge[i], 2)) / sigma_c / 2 - - m_phase->E() / m_phase->N() * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i])); + - m_phase->E() / nDensity * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i])); } } @@ -446,9 +443,10 @@ double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) (m_totalCrossSectionCenter[i] + nu / m_gamma / pow(m_gridCenter[i], 0.5)); } } + double nDensity = m_phase->molarDensity() * Avogadro; auto f = Eigen::Map(y.data(), y.size()); auto x = Eigen::Map(m_gridCenter.data(), m_gridCenter.size()); - return 1./3. * m_gamma * simpson(f, x) / m_phase->N(); + return 1./3. * m_gamma * simpson(f, x) / nDensity; } double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) @@ -463,9 +461,10 @@ double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) (m_totalCrossSectionEdge[i] + nu / m_gamma / pow(m_gridEdge[i], 0.5)); } } + double nDensity = m_phase->molarDensity() * Avogadro; auto f = Eigen::Map(y.data(), y.size()); auto x = Eigen::Map(m_gridEdge.data(), m_gridEdge.size()); - return -1./3. * m_gamma * simpson(f, x) / m_phase->N(); + return -1./3. * m_gamma * simpson(f, x) / nDensity; } void EEDFTwoTermApproximation::initSpeciesIndexCS() diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index f0cb3afef86..5969bbbc2fb 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -64,12 +64,6 @@ PlasmaPhase::~PlasmaPhase() } } -void PlasmaPhase::setTemperature(const double temp) -{ - Phase::setTemperature(temp); - m_kT = Boltzmann * temp / ElectronCharge; -} - void PlasmaPhase::updateElectronEnergyDistribution() { if (m_distributionType == "discretized") { From 9b193db6da3ec0ee5d1f4cbe100b765b69de9f83 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 15 Jul 2025 23:48:11 -0400 Subject: [PATCH 21/29] [Reactor] Deprecate intEnergy_mass --- include/cantera/zeroD/ReactorBase.h | 8 +++++++- src/zeroD/Reactor.cpp | 12 ++++++++++-- 2 files changed, 17 insertions(+), 3 deletions(-) diff --git a/include/cantera/zeroD/ReactorBase.h b/include/cantera/zeroD/ReactorBase.h index 2f145fd72fc..18ba16dba3a 100644 --- a/include/cantera/zeroD/ReactorBase.h +++ b/include/cantera/zeroD/ReactorBase.h @@ -232,7 +232,10 @@ class ReactorBase } //! Returns the current internal energy (J/kg) of the reactor's contents. + //! @deprecated To be removed after %Cantera 3.2. double intEnergy_mass() const { + warn_deprecated("ReactorBase::intEnergy_mass", + "To be removed after Cantera 3.2."); return m_intEnergy; } @@ -304,7 +307,10 @@ class ReactorBase double m_vol = 0.0; //!< Current volume of the reactor [m^3] double m_mass = 0.0; //!< Current mass of the reactor [kg] double m_enthalpy = 0.0; //!< Current specific enthalpy of the reactor [J/kg] - double m_intEnergy = 0.0; //!< Current internal energy of the reactor [J/kg] + + //! Current internal energy of the reactor [J/kg] + //! @deprecated To be removed after %Cantera 3.2 + double m_intEnergy = 0.0; double m_pressure = 0.0; //!< Current pressure in the reactor [Pa] vector m_state; vector m_inlet, m_outlet; diff --git a/src/zeroD/Reactor.cpp b/src/zeroD/Reactor.cpp index 3ca537f7fc2..51a8bf3c4ba 100644 --- a/src/zeroD/Reactor.cpp +++ b/src/zeroD/Reactor.cpp @@ -133,7 +133,11 @@ void Reactor::syncState() m_thermo->saveState(m_state); if (m_energy) { m_enthalpy = m_thermo->enthalpy_mass(); - m_intEnergy = m_thermo->intEnergy_mass(); + try { + m_intEnergy = m_thermo->intEnergy_mass(); + } catch (NotImplementedError&) { + m_intEnergy = NAN; + } } m_pressure = m_thermo->pressure(); m_mass = m_thermo->density() * m_vol; @@ -205,7 +209,11 @@ void Reactor::updateConnected(bool updatePressure) { if (updatePressure) { m_pressure = m_thermo->pressure(); } - m_intEnergy = m_thermo->intEnergy_mass(); + try { + m_intEnergy = m_thermo->intEnergy_mass(); + } catch (NotImplementedError&) { + m_intEnergy = NAN; + } m_thermo->saveState(m_state); // Update the mass flow rate of connected flow devices From 24629bac39b1048b72e948fb4e50a344052e878c Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 16 Jul 2025 13:34:48 -0400 Subject: [PATCH 22/29] [CI] Automatically check out linked example-data PRs --- .github/workflows/linters.yml | 22 ++++++++++++++++++++++ .github/workflows/main.yml | 23 +++++++++++++++++++++++ 2 files changed, 45 insertions(+) diff --git a/.github/workflows/linters.yml b/.github/workflows/linters.yml index c6fb4497304..230badaffa3 100644 --- a/.github/workflows/linters.yml +++ b/.github/workflows/linters.yml @@ -65,3 +65,25 @@ jobs: git config --global core.whitespace \ -cr-at-eol,tab-in-indent,blank-at-eol,blank-at-eof git diff --check ${{ github.event.pull_request.base.sha }} + example-data: + name: Check for unmerged example-data pull request + runs-on: ubuntu-latest + if: github.event_name == 'pull_request' + steps: + - uses: actions/checkout@v4 + name: Checkout the repository + with: + fetch-depth: 100 + persist-credentials: true + + - name: Find matching PR in example_data + env: + BASE_PR: ${{ github.event.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd data/example_data + DATA_PR=$(gh pr list --repo Cantera/cantera-example-data --jq '.[] | select(.title | test("Cantera/cantera#'${BASE_PR}'")) | .number' --json title,number) + if [ -n "$DATA_PR" ]; then + echo ":exclamation: Merge https://github.com/Cantera/cantera-example-data/pull/${DATA_PR} and update the submodule commit before merging this PR" >> $GITHUB_STEP_SUMMARY + exit 1 + fi diff --git a/.github/workflows/main.yml b/.github/workflows/main.yml index 5bf79292b11..37dc676b443 100644 --- a/.github/workflows/main.yml +++ b/.github/workflows/main.yml @@ -105,6 +105,18 @@ jobs: with: submodules: recursive persist-credentials: false + - name: Check out updated example data + if: github.event_name == 'pull_request' + env: + BASE_PR: ${{ github.event.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd data/example_data + gh repo set-default Cantera/cantera-example-data + DATA_PR=$(gh pr list --repo Cantera/cantera-example-data --jq '.[] | select(.title | test("Cantera/cantera#'${BASE_PR}'")) | .number' --json title,number) + if [ -n "$DATA_PR" ]; then + gh pr checkout --repo Cantera/cantera-example-data $DATA_PR + fi - name: Setup Python uses: actions/setup-python@v5 with: @@ -419,6 +431,17 @@ jobs: with: submodules: recursive persist-credentials: false + - name: Check out updated example data + if: github.event_name == 'pull_request' + env: + BASE_PR: ${{ github.event.number }} + GH_TOKEN: ${{ secrets.GITHUB_TOKEN }} + run: | + cd data/example_data + DATA_PR=$(gh pr list --repo Cantera/cantera-example-data --jq '.[] | select(.title | test("Cantera/cantera#'${BASE_PR}'")) | .number' --json title,number) + if [ -n "$DATA_PR" ]; then + gh pr checkout --repo Cantera/cantera-example-data $DATA_PR + fi - name: Set up micromamba uses: mamba-org/setup-micromamba@b09ef9b599704322748535812ca03efb2625677b # v2.0.5 with: From caa244987cd0980aa77e56a80184fc43bf431d5f Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Thu, 17 Jul 2025 22:04:59 -0400 Subject: [PATCH 23/29] [Plasma] Allow marked duplicate "effective" collisions --- src/thermo/PlasmaPhase.cpp | 12 ++++++------ test/python/test_thermo.py | 5 ++++- 2 files changed, 10 insertions(+), 7 deletions(-) diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 5969bbbc2fb..860b2e53119 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -473,13 +473,13 @@ void PlasmaPhase::addCollision(std::shared_ptr collision) m_inFactor.push_back(1); } - if (kind == "effective" || kind == "elastic") { + if ((kind == "effective" || kind == "elastic") && !collision->duplicate) { for (size_t k = 0; k < m_collisions.size() - 1; k++) { - if (m_collisions[k]->reactants == collision->reactants) - if (this->kind(k) == "elastic" || this->kind(k) == "effective") { - throw CanteraError("PlasmaPhase::addCollision" - "Already contains a data of effective/ELASTIC cross section for '{}'.", - target); + if (m_collisions[k]->reactants == collision->reactants && + (this->kind(k) == "elastic" || this->kind(k) == "effective")) + { + throw CanteraError("PlasmaPhase::addCollision", "Phase already contains" + " an effective/elastic cross section for '{}'.", target); } } m_kElastic.push_back(i); diff --git a/test/python/test_thermo.py b/test/python/test_thermo.py index 02379c3f765..06802b27d2f 100644 --- a/test/python/test_thermo.py +++ b/test/python/test_thermo.py @@ -1197,7 +1197,8 @@ def collision_data(self): "type": "electron-collision-plasma", "energy-levels": [0.0, 0.1, 0.2, 0.3, 0.4, 0.5, 0.6, 0.7, 0.8, 0.9, 1.0], "cross-sections": [0.0, 3.83e-20, 4.47e-20, 4.79e-20, 5.07e-20, 5.31e-20, - 5.49e-20, 5.64e-20, 5.77e-20, 5.87e-20, 5.97e-20] + 5.49e-20, 5.64e-20, 5.77e-20, 5.87e-20, 5.97e-20], + "duplicate": True, } def test_converting_electron_energy_to_temperature(self, phase): @@ -1266,6 +1267,8 @@ def test_elastic_power_loss_replace_rate(self, phase): assert phase.elastic_power_loss == approx(11765800095) def test_elastic_power_loss_add_reaction(self, phase): + phase2 = ct.Solution(thermo="plasma", kinetics="bulk", + species=phase.species(), reactions=[]) phase.TPX = 1000, ct.one_atm, "O2:1, E:1e-5" phase.add_reaction(ct.Reaction.from_dict(self.collision_data, phase)) assert phase.elastic_power_loss == approx(18612132428) From 4acb55b4a1376d46fa9be320fa4a8dc7370eb370 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 18 Jul 2025 10:22:37 -0400 Subject: [PATCH 24/29] [Kinetics] Remove unnecessary new functions --- include/cantera/kinetics/Kinetics.h | 7 --- include/cantera/kinetics/KineticsFactory.h | 20 ------ src/kinetics/BulkKinetics.cpp | 1 - src/kinetics/Kinetics.cpp | 4 -- src/kinetics/KineticsFactory.cpp | 71 +++++++--------------- 5 files changed, 21 insertions(+), 82 deletions(-) diff --git a/include/cantera/kinetics/Kinetics.h b/include/cantera/kinetics/Kinetics.h index 144ef3df049..f7f40d6329b 100644 --- a/include/cantera/kinetics/Kinetics.h +++ b/include/cantera/kinetics/Kinetics.h @@ -1408,10 +1408,6 @@ class Kinetics return m_root.lock(); } - bool ready() const { - return m_ready; - } - //! Register a function to be called if reaction is added. //! @param id A unique ID corresponding to the object affected by the callback. //! Typically, this is a pointer to an object that also holds a reference to the @@ -1478,9 +1474,6 @@ class Kinetics Eigen::SparseMatrix m_stoichMatrix; //! @} - //! Boolean indicating whether Kinetics object is fully configured - bool m_ready = false; - //! The number of species in all of the phases //! that participate in this kinetics mechanism. size_t m_kk = 0; diff --git a/include/cantera/kinetics/KineticsFactory.h b/include/cantera/kinetics/KineticsFactory.h index 7793ce6915b..4f40287d486 100644 --- a/include/cantera/kinetics/KineticsFactory.h +++ b/include/cantera/kinetics/KineticsFactory.h @@ -85,26 +85,6 @@ shared_ptr newKinetics(const vector>& phases, void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); -/** - * Add reactions to a Kinetics object. - * - * @param kin The Kinetics object to be initialized - * @param rxnList The list of Reaction objects - */ -void addReactions(Kinetics& kin, vector> rxnList); - -/** - * Get the list of reactions in AnyMap. - * - * @param kin The Kinetics object to be initialized - * @param phaseNode Phase entry for the phase where the reactions occur. This - * phase definition is used to determine the source of the reactions added - * to the Kinetics object. - * @param rootNode The root node of the file containing the phase definition, - * which will be treated as the default source for reactions - */ -vector reactionsAnyMapList(Kinetics& kin, const AnyMap& phaseNode, - const AnyMap& rootNode=AnyMap()); //! @} } diff --git a/src/kinetics/BulkKinetics.cpp b/src/kinetics/BulkKinetics.cpp index f26a618a11c..ba11ce331b1 100644 --- a/src/kinetics/BulkKinetics.cpp +++ b/src/kinetics/BulkKinetics.cpp @@ -59,7 +59,6 @@ bool BulkKinetics::addReaction(shared_ptr r, bool resize) } m_concm.push_back(NAN); - m_ready = resize; return true; } diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index f7287cd62ce..9a646c9f180 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -46,8 +46,6 @@ void Kinetics::resizeReactions() m_stoichMatrix = m_productStoich.stoichCoeffs(); // reactants are destroyed for positive net rate of progress m_stoichMatrix -= m_reactantStoich.stoichCoeffs(); - - m_ready = true; } void Kinetics::checkReactionArraySize(size_t ii) const @@ -700,8 +698,6 @@ bool Kinetics::addReaction(shared_ptr r, bool resize) if (resize) { resizeReactions(); - } else { - m_ready = false; } for (const auto& [id, callback] : m_reactionAddedCallbacks) { diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index dacd0afe788..585a1314ebd 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -90,11 +90,8 @@ shared_ptr newKinetics(const vector>& phases, for (auto& phase : phases) { kin->addThermo(phase); } - - if (!kin->ready()) { - kin->init(); - addReactions(*kin, phaseNode, rootNode); - } + kin->init(); + addReactions(*kin, phaseNode, rootNode); return kin; } @@ -107,8 +104,7 @@ shared_ptr newKinetics(const vector>& phases, return newKinetics(phases, phaseNode, root); } -vector reactionsAnyMapList(Kinetics& kin, const AnyMap& phaseNode, - const AnyMap& rootNode) +void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode) { kin.skipUndeclaredThirdBodies( phaseNode.getBool("skip-undeclared-third-bodies", false)); @@ -167,7 +163,6 @@ vector reactionsAnyMapList(Kinetics& kin, const AnyMap& phaseNode, // Add reactions from each section fmt::memory_buffer add_rxn_err; - vector reactionsList; for (size_t i = 0; i < sections.size(); i++) { if (rules[i] == "all") { kin.skipUndeclaredSpecies(false); @@ -188,56 +183,32 @@ vector reactionsAnyMapList(Kinetics& kin, const AnyMap& phaseNode, AnyMap reactions = AnyMap::fromYamlFile(fileName, rootNode.getString("__file__", "")); loadExtensions(reactions); - for (const auto& R : reactions[node].asVector()) { - reactionsList.push_back(R); + #ifdef NDEBUG + try { + kin.addReaction(newReaction(R, kin), false); + } catch (CanteraError& err) { + fmt_append(add_rxn_err, "{}", err.what()); + } + #else + kin.addReaction(newReaction(R, kin), false); + #endif } } else { // specified section is in the current file for (const auto& R : rootNode.at(sections[i]).asVector()) { - reactionsList.push_back(R); + #ifdef NDEBUG + try { + kin.addReaction(newReaction(R, kin), false); + } catch (CanteraError& err) { + fmt_append(add_rxn_err, "{}", err.what()); + } + #else + kin.addReaction(newReaction(R, kin), false); + #endif } } } - return reactionsList; -} - -void addReactions(Kinetics& kin, vector> rxnList) -{ - fmt::memory_buffer add_rxn_err; - for (shared_ptr rxn : rxnList) { - #ifdef NDEBUG - try { - kin.addReaction(rxn, false); - } catch (CanteraError& err) { - fmt_append(add_rxn_err, "{}", err.what()); - } - #else - kin.addReaction(rxn, false); - #endif - } - - if (add_rxn_err.size()) { - throw CanteraError("addReactions", to_string(add_rxn_err)); - } - kin.checkDuplicates(); - kin.resizeReactions(); -} - -void addReactions(Kinetics& kin, const AnyMap& phaseNode, const AnyMap& rootNode) -{ - fmt::memory_buffer add_rxn_err; - for (AnyMap R : reactionsAnyMapList(kin, phaseNode, rootNode)) { - #ifdef NDEBUG - try { - kin.addReaction(newReaction(R, kin), false); - } catch (CanteraError& err) { - fmt_append(add_rxn_err, "{}", err.what()); - } - #else - kin.addReaction(newReaction(R, kin), false); - #endif - } if (add_rxn_err.size()) { throw CanteraError("addReactions", to_string(add_rxn_err)); From f9cb36a2192ac8293774913c4da00e2ad816e3e9 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 19 Jul 2025 19:12:05 -0400 Subject: [PATCH 25/29] Make checkFinite input const --- include/cantera/base/utilities.h | 2 +- src/base/checkFinite.cpp | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/include/cantera/base/utilities.h b/include/cantera/base/utilities.h index 26d0c3ea817..5f6d6975769 100644 --- a/include/cantera/base/utilities.h +++ b/include/cantera/base/utilities.h @@ -179,7 +179,7 @@ void checkFinite(const double tmp); * @param values Array of *N* values to be checked * @param N Number of elements in *values* */ -void checkFinite(const string& name, double* values, size_t N); +void checkFinite(const string& name, const double* values, size_t N); //! Const accessor for a value in a map. /* diff --git a/src/base/checkFinite.cpp b/src/base/checkFinite.cpp index cc97e2a0982..f76f7cf6abd 100644 --- a/src/base/checkFinite.cpp +++ b/src/base/checkFinite.cpp @@ -25,7 +25,7 @@ void checkFinite(const double tmp) } } -void checkFinite(const string& name, double* values, size_t N) +void checkFinite(const string& name, const double* values, size_t N) { for (size_t i = 0; i < N; i++) { if (!std::isfinite(values[i])) { From 3295c24bd8cd52e8fa80ba8b658e974642d9b30e Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 18 Jul 2025 11:54:19 -0400 Subject: [PATCH 26/29] [Plasma] Cleanup of EEDF solver and PlasmaPhase classes --- .../kinetics/ElectronCollisionPlasmaRate.h | 11 +- .../cantera/thermo/EEDFTwoTermApproximation.h | 53 ++-- include/cantera/thermo/PlasmaPhase.h | 147 ++++------- include/cantera/thermo/ThermoPhase.h | 2 +- interfaces/cython/cantera/thermo.pxd | 4 +- interfaces/cython/cantera/thermo.pyx | 16 +- ...Pulse.py => nanosecond-pulse-discharge.py} | 26 +- .../thermo/{plasmatest.py => plasma-eedf.py} | 19 +- src/kinetics/ElectronCollisionPlasmaRate.cpp | 35 +-- src/thermo/EEDFTwoTermApproximation.cpp | 242 ++++++++---------- src/thermo/PlasmaPhase.cpp | 197 ++++---------- test/data/air-plasma.yaml | 4 +- test/python/test_thermo.py | 2 +- 13 files changed, 279 insertions(+), 479 deletions(-) rename samples/python/reactors/{nanosecondPulse.py => nanosecond-pulse-discharge.py} (89%) rename samples/python/thermo/{plasmatest.py => plasma-eedf.py} (82%) diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index c9df68ce298..63d4086610a 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -7,10 +7,10 @@ #ifndef CT_ELECTRONCOLLISIONPLASMARATE_H #define CT_ELECTRONCOLLISIONPLASMARATE_H -#include "cantera/thermo/PlasmaPhase.h" #include "cantera/kinetics/ReactionData.h" #include "ReactionRate.h" #include "MultiRate.h" +#include "cantera/numerics/eigen_dense.h" namespace Cantera { @@ -153,21 +153,25 @@ class ElectronCollisionPlasmaRate : public ReactionRate } //! The kind of the process + //! @since New in Cantera 3.2. const string& kind() const { return m_kind; } //! The target of the process + //! @since New in Cantera 3.2. const string& target() const { return m_target; } //! The product of the process + //! @since New in Cantera 3.2. const string& product() const { return m_product; } //! Get the energy threshold of electron collision [eV] + //! @since New in Cantera 3.2. double threshold() const { return m_threshold; } @@ -187,11 +191,6 @@ class ElectronCollisionPlasmaRate : public ReactionRate return m_crossSectionsInterpolated; } - //! Set the value of #m_crossSectionsInterpolated - void setCrossSectionInterpolated(vector& cs) { - m_crossSectionsInterpolated = cs; - } - //! Update the value of #m_crossSectionsInterpolated [m2] void updateInterpolatedCrossSection(const vector&); diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h index 0dca85e8242..5a7ddc62530 100644 --- a/include/cantera/thermo/EEDFTwoTermApproximation.h +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -10,19 +10,12 @@ #include "cantera/base/ct_defs.h" #include "cantera/numerics/eigen_sparse.h" -#include "cantera/numerics/funcs.h" -#include "cantera/base/global.h" namespace Cantera { class PlasmaPhase; -typedef Eigen::SparseMatrix SparseMat_fp; -typedef Eigen::Triplet Triplet_fp; -typedef vector vector_fp; - - /** * EEDF solver options. Used internally by class EEDFTwoTermApproximation. */ @@ -43,12 +36,17 @@ class TwoTermOpt }; // end of class TwoTermOpt +//! Boltzmann equation solver for the electron energy distribution function based on +//! the two-term approximation. +//! +//! @since New in %Cantera 3.2. +//! @warning This class is an experimental part of %Cantera and may be changed without +//! notice. class EEDFTwoTermApproximation { public: EEDFTwoTermApproximation() = default; - //! Constructor combined with the initialization function /*! * This constructor initializes the EEDFTwoTermApproximation object with everything @@ -115,7 +113,7 @@ class EEDFTwoTermApproximation * g_i = \frac{1}{\epsilon_{i+1} - \epsilon_{i-1}} \ln(\frac{F_{0, i+1}}{F_{0, i-1}}) * \f] */ - vector_fp vector_g(const Eigen::VectorXd& f0); + vector vector_g(const Eigen::VectorXd& f0); //! The matrix of scattering-out. /** @@ -124,7 +122,7 @@ class EEDFTwoTermApproximation * \epsilon \sigma_k exp[(\epsilon_i - \epsilon)g_i] d \epsilon * \f] */ - SparseMat_fp matrix_P(const vector_fp& g, size_t k); + Eigen::SparseMatrix matrix_P(const vector& g, size_t k); //! The matrix of scattering-in /** @@ -143,7 +141,7 @@ class EEDFTwoTermApproximation * \epsilon_2 = \min(\max(\epsilon_{i+1/2}+u_k, \epsilon_{j-1/2}),\epsilon_{j+1/2}) * \f] */ - SparseMat_fp matrix_Q(const vector_fp& g, size_t k); + Eigen::SparseMatrix matrix_Q(const vector& g, size_t k); //! Matrix A (Ax = b) of the equation of EEDF, which is discretized by the exponential scheme //! of Scharfetter and Gummel, @@ -155,7 +153,7 @@ class EEDFTwoTermApproximation * \f] * where \f$ z_{i+1/2} = \tilde{w}_{i+1/2} / \tilde{D}_{i+1/2} \f$ (Peclet number). */ - SparseMat_fp matrix_A(const Eigen::VectorXd& f0); + Eigen::SparseMatrix matrix_A(const Eigen::VectorXd& f0); //! Reduced net production frequency. Equation (10) of ref. [1] //! divided by N. @@ -188,7 +186,7 @@ class EEDFTwoTermApproximation Eigen::VectorXd m_gridCenter; //! Grid of electron energy (cell boundary i-1/2) [eV] - vector_fp m_gridEdge; + vector m_gridEdge; //! Location of cell j for grid cache vector> m_j; @@ -197,29 +195,29 @@ class EEDFTwoTermApproximation vector> m_i; //! Cross section at the boundaries of the overlap of cell i and j - vector> m_sigma; + vector>> m_sigma; //! The energy boundaries of the overlap of cell i and j - vector> m_eps; + vector>> m_eps; //! The cross section at the center of a cell - std::vector m_sigma_offset; + vector> m_sigma_offset; //! normalized electron energy distribution function Eigen::VectorXd m_f0; //! EEDF at grid edges (cell boundaries) - vector_fp m_f0_edge; + vector m_f0_edge; //! Total electron cross section on the cell center of energy grid - vector_fp m_totalCrossSectionCenter; + vector m_totalCrossSectionCenter; //! Total electron cross section on the cell boundary (i-1/2) of //! energy grid - vector_fp m_totalCrossSectionEdge; + vector m_totalCrossSectionEdge; //! vector of total elastic cross section weighted with mass ratio - vector_fp m_sigmaElastic; + vector m_sigmaElastic; //! list of target species indices in global Cantera numbering (1 index per cs) vector m_kTargets; @@ -234,10 +232,14 @@ class EEDFTwoTermApproximation vector m_k_lg_Targets; //! Mole fraction of targets - vector_fp m_X_targets; + vector m_X_targets; //! Previous mole fraction of targets used to compute eedf - vector_fp m_X_targets_prev; + vector m_X_targets_prev; + + //! in factor. This is used for calculating the Q matrix of + //! scattering-in processes. + vector m_inFactor; double m_gamma; @@ -245,7 +247,7 @@ class EEDFTwoTermApproximation bool m_eeCol = false; //! Compute electron-electron collision integrals - void eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, + void eeColIntegrals(vector& A1, vector& A2, vector& A3, double& a, size_t nPoints); //! flag of having an EEDF @@ -253,11 +255,6 @@ class EEDFTwoTermApproximation //! First call to calculateDistributionFunction bool m_first_call; - -private: - - - }; // end of class EEDFTwoTermApproximation } // end of namespace Cantera diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index cdf179fa16e..e41469d0e86 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -219,11 +219,24 @@ class PlasmaPhase: public IdealGasPhase return m_nPoints; } - //! Number of collisions + //! Number of electron collision cross sections size_t nCollisions() const { return m_collisions.size(); } + //! Get the Reaction object associated with electron collision *i*. + //! @since New in %Cantera 3.2. + const shared_ptr collision(size_t i) const { + return m_collisions[i]; + } + + //! Get the ElectronCollisionPlasmaRate object associated with electron collision + //! *i*. + //! @since New in %Cantera 3.2. + const shared_ptr collisionRate(size_t i) const { + return m_collisionRates[i]; + } + //! Electron Species Index size_t electronSpeciesIndex() const { return m_electronSpeciesIndex; @@ -282,7 +295,7 @@ class PlasmaPhase: public IdealGasPhase void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()) override; - //! Update electron energy distribution. + //! Update the electron energy distribution. void updateElectronEnergyDistribution(); //! Electron species name @@ -300,66 +313,47 @@ class PlasmaPhase: public IdealGasPhase return m_levelNum; } - vector kInelastic() const { + //! Get the indicies for inelastic electron collisions + //! @since New in %Cantera 3.2. + const vector& kInelastic() const { return m_kInelastic; } - // number of cross section dataset - size_t nElectronCrossSections() const { - return m_collisions.size(); + //! Get the indices for elastic electron collisions + //! @since New in %Cantera 3.2. + const vector& kElastic() const { + return m_kElastic; } - // target of a specific process + //! target of a specific process + //! @since New in %Cantera 3.2. size_t targetIndex(size_t i) const { return m_targetSpeciesIndices[i]; } - // kind of a specific process - string kind(size_t k) const; - - // threshold of a specific process - double threshold(size_t k) const; - - vector shiftFactor() const { - return m_shiftFactor; - } - - vector inFactor() const { - return m_inFactor; + //! Get the frequency of the applied electric field [Hz] + //! @since New in %Cantera 3.2. + double electricFieldFrequency() const { + return m_electricFieldFrequency; } - double F() const { - return m_F; - } - - double E() const { - return m_E; + //! Get the applied electric field strength [V/m] + double electricField() const { + return m_electricField; } double ionDegree() const { return m_ionDegree; } - double EN() const { - return m_EN; - } - - vector> crossSections() const { - return m_crossSections; - } - - vector> energyLevels() const { - return m_energyLevels; + //! Get the reduced electric field strength [V·m²] + double reducedElectricField() const { + return m_electricField / (molarDensity() * Avogadro); } - vector kElastic() const { - return m_kElastic; - } - - //! Set reduced electric field given in [V.m2] + //! Set reduced electric field given in [V·m²] void setReducedElectricField(double EN) { - m_EN = EN; // [V.m2] - m_E = m_EN * molarDensity() * Avogadro; // [V/m] + m_electricField = EN * molarDensity() * Avogadro; // [V/m] } virtual void setSolution(std::weak_ptr soln) override; @@ -376,16 +370,8 @@ class PlasmaPhase: public IdealGasPhase double elasticPowerLoss(); protected: - - void initialize(); - void updateThermo() const override; - //! update interpolated cross sections - //! This function needs to be called when the EEDF is updated or - //! when the cross sections are updated - void updateInterpolatedCrossSections(); - //! When electron energy distribution changed, plasma properties such as //! electron-collision reaction rates need to be re-evaluated. void electronEnergyDistributionChanged(); @@ -451,9 +437,6 @@ class PlasmaPhase: public IdealGasPhase //! Electron temperature [K] double m_electronTemp; - //! Gas number density - //double m_N; - //! Electron energy distribution type string m_distributionType = "isotropic"; @@ -466,50 +449,25 @@ class PlasmaPhase: public IdealGasPhase //! Indices of inelastic collisions in m_crossSections vector m_kInelastic; - //! electric field [V/m] - double m_E; + //! Indices of elastic collisions in m_crossSections + vector m_kElastic; - //! reduced electric field [V.m2] - double m_EN; + //! electric field [V/m] + double m_electricField = 0.0; //! electric field freq [Hz] - double m_F; - - //! normalized electron energy distribution function - Eigen::VectorXd m_f0; + double m_electricFieldFrequency = 0.0; //! Cross section data. m_crossSections[i][j], where i is the specific process, //! j is the index of vector. Unit: [m^2] - std::vector> m_crossSections; + vector> m_crossSections; //! Electron energy levels corresponding to the cross section data. m_energyLevels[i][j], //! where i is the specific process, j is the index of vector. Unit: [eV] - std::vector> m_energyLevels; - - //! shift factor. This is used for calculating the collision term. - std::vector m_shiftFactor; - - //! in factor. This is used for calculating the Q matrix of - //! scattering-in processes. - std::vector m_inFactor; - - //! Indices of elastic collisions in m_crossSections - std::vector m_kElastic; - - //! flag of electron energy distribution function - bool m_f0_ok; + vector> m_energyLevels; //! ionization degree for the electron-electron collisions (tmp is the previous one) - double m_ionDegree; - - //! Data for initiate reaction - AnyMap m_root; - - //! get the target species index - size_t targetSpeciesIndex(shared_ptr R); - - //! get cross section interpolated - vector crossSection(shared_ptr reaction); + double m_ionDegree = 0.0; //! Electron energy distribution Difference dF/dε (V^-5/2) Eigen::ArrayXd m_electronEnergyDistDiff; @@ -543,8 +501,8 @@ class PlasmaPhase: public IdealGasPhase private: - //! pointer to EEDF solver - unique_ptr ptrEEDFSolver = nullptr; + //! Solver used to calculate the EEDF based on electron collision rates + unique_ptr m_eedfSolver = nullptr; //! Electron energy distribution change variable. Whenever //! #m_electronEnergyDist changes, this int is incremented. @@ -563,10 +521,6 @@ class PlasmaPhase: public IdealGasPhase //! The collision-target species indices of #m_collisions vector m_targetSpeciesIndices; - //! Interpolated cross sections. This is used for storing - //! interpolated cross sections temporarily. - vector m_interp_cs; - //! The list of whether the interpolated cross sections is ready vector m_interp_cs_ready; @@ -575,14 +529,7 @@ class PlasmaPhase: public IdealGasPhase void setCollisions(); //! Add a collision and record the target species - void addCollision(std::shared_ptr collision); - - //! Indices of elastic collisions - vector m_elasticCollisionIndices; - - //! Collision cross section - vector m_interpolatedCrossSections; - + void addCollision(shared_ptr collision); }; } diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index 266b74d681e..ec3952983dc 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -389,7 +389,7 @@ enum class ThermoBasis * * @ingroup thermoprops */ -class ThermoPhase : public Phase, public std::enable_shared_from_this +class ThermoPhase : public Phase { public: //! Constructor. Note that ThermoPhase is meant to be used as a base class, diff --git a/interfaces/cython/cantera/thermo.pxd b/interfaces/cython/cantera/thermo.pxd index 877445a58eb..a918980e861 100644 --- a/interfaces/cython/cantera/thermo.pxd +++ b/interfaces/cython/cantera/thermo.pxd @@ -211,8 +211,8 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": size_t nElectronEnergyLevels() double electronPressure() string electronSpeciesName() - double EN() - void updateElectronEnergyDistribution() + double reducedElectricField() + void updateElectronEnergyDistribution() except +translate_exception double elasticPowerLoss() except +translate_exception diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index df30681e879..bf83696db35 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -1783,12 +1783,16 @@ cdef class ThermoPhase(_SolutionBase): raise ThermoModelMethodError(self.thermo_model) return self.plasma.electronPressure() - property EN: - """Get/Set EN [V.m2].""" + property reduced_electric_field: + """ + Get/Set the reduced electric field (E/N) [V·m²]. + + .. versionadded:: 3.2 + """ def __get__(self): if not self._enable_plasma: raise ThermoModelMethodError(self.thermo_model) - return self.plasma.EN() + return self.plasma.reducedElectricField() def __set__(self, value): if not self._enable_plasma: @@ -1822,6 +1826,12 @@ cdef class ThermoPhase(_SolutionBase): len(levels)) def update_EEDF(self): + """ + Update the electron energy distribution function to account for changes in + composition, temperature, pressure, or electric field strength. + + .. versionadded:: 3.2 + """ if not self._enable_plasma: raise TypeError('This method is invalid for ' f'thermo model: {self.thermo_model}.') diff --git a/samples/python/reactors/nanosecondPulse.py b/samples/python/reactors/nanosecond-pulse-discharge.py similarity index 89% rename from samples/python/reactors/nanosecondPulse.py rename to samples/python/reactors/nanosecond-pulse-discharge.py index be7298f404f..21f2eacc37c 100644 --- a/samples/python/reactors/nanosecondPulse.py +++ b/samples/python/reactors/nanosecond-pulse-discharge.py @@ -5,33 +5,30 @@ This example simulates a nanosecond-scale pulse discharge in a reactor. A Gaussian-shaped electric field pulse is applied over a short timescale. -Requires: cantera >= 3.0, matplotlib >= 2.0 +Requires: cantera >= 3.2, matplotlib >= 2.0 -.. tags:: Python, plasma +.. tags:: Python, plasma, reactor network """ import cantera as ct -ct.CanteraError.set_stack_trace_depth(10) - import numpy as np import matplotlib.pyplot as plt # Gaussian pulse parameters -EN_peak = 190 * 1e-21 # Td -pulse_center = 24e-9 -pulse_width = 3e-9 # standard deviation in ns +EN_peak = 190 * 1e-21 # 190 Td +pulse_center = 24e-9 # 24 ns +pulse_width = 3e-9 # standard deviation (3 ns) def gaussian_EN(t): return EN_peak * np.exp(-((t - pulse_center)**2) / (2 * pulse_width**2)) # setup -gas = ct.Solution('example_data/gri30_plasma_cpavan.yaml') +gas = ct.Solution('example_data/methane-plasma-pavan-2023.yaml') gas.TPX = 300., 101325., 'CH4:0.095, O2:0.19, N2:0.715, e:1E-11' -gas.EN = gaussian_EN(0) +gas.reduced_electric_field = gaussian_EN(0) gas.update_EEDF() r = ct.ConstPressureReactor(gas, energy="off") -#r.dis_vol = 5e-3 * np.pi * (1e-3)**2 / 4 sim = ct.ReactorNet([r]) sim.verbose = False @@ -57,7 +54,7 @@ def gaussian_EN(t): sim.time, r.T, r.thermo.P, r.thermo.h)) EN_t = gaussian_EN(t) - gas.EN = EN_t + gas.reduced_electric_field = EN_t gas.update_EEDF() # reinitialize integrator with new source terms @@ -66,7 +63,7 @@ def gaussian_EN(t): t = t_end # Plotting -fig, ax = plt.subplots(2) +fig, ax = plt.subplots(2, layout="constrained") ax[0].plot(states.t, states.X[:, gas.species_index('e')], label='e') ax[0].plot(states.t, states.X[:, gas.species_index('O2+')], label='O2+') @@ -105,11 +102,10 @@ def gaussian_EN(t): ax2.tick_params(axis='y', labelcolor='tab:red') for axx in ax: - axx.legend(loc='lower right') + axx.legend(loc='lower right', ncol=2) axx.set_xlabel('Time [s]') ax[0].set_ylabel('Mole fraction [-]') ax[1].set_ylabel('Temperature [K]') -plt.tight_layout() -plt.show() \ No newline at end of file +plt.show() diff --git a/samples/python/thermo/plasmatest.py b/samples/python/thermo/plasma-eedf.py similarity index 82% rename from samples/python/thermo/plasmatest.py rename to samples/python/thermo/plasma-eedf.py index 97425bf6bb7..b54444d71d0 100644 --- a/samples/python/thermo/plasmatest.py +++ b/samples/python/thermo/plasma-eedf.py @@ -4,7 +4,7 @@ Compute EEDF with two term approximation solver at constant E/N. Compare with results from BOLOS. -Requires: cantera >= XX. +Requires: cantera >= 3.2, matplotlib >= 2.0 .. tags:: Python, plasma """ @@ -13,9 +13,9 @@ import matplotlib.pyplot as plt import cantera as ct -gas = ct.Solution('example_data/air-plasma_Phelps.yaml') +gas = ct.Solution('example_data/air-plasma-Phelps.yaml') gas.TPX = 300., 101325., 'N2:0.79, O2:0.21, N2+:1E-10, Electron:1E-10' -gas.EN = 200.0 * 1e-21 # Reduced electric field [V.m^2] +gas.reduced_electric_field = 200.0 * 1e-21 # Reduced electric field [V.m^2] gas.update_EEDF() grid = gas.electron_energy_levels @@ -41,15 +41,8 @@ fig, ax = plt.subplots() -ax.plot(grid, eedf, c='k', label='CANTERA') -ax.plot(cgrid, cf0, ls='None', mfc='None', mec='k', marker='o', label='BOLOS') - -ax.set_xscale('log') -ax.set_yscale('log') - -ax.set_xlim(1e-2, 1e2) -ax.set_ylim(1e-10, 1e4) - +ax.loglog(grid, eedf, c='k', label='Cantera') +ax.loglog(cgrid, cf0, ls='None', mfc='None', mec='k', marker='o', label='BOLOS') +ax.set(xlim=(1e-2, 1e2), ylim=(1e-10, 1e4)) ax.legend() - plt.show() diff --git a/src/kinetics/ElectronCollisionPlasmaRate.cpp b/src/kinetics/ElectronCollisionPlasmaRate.cpp index b50d2814551..696fa4f5d09 100644 --- a/src/kinetics/ElectronCollisionPlasmaRate.cpp +++ b/src/kinetics/ElectronCollisionPlasmaRate.cpp @@ -6,6 +6,7 @@ #include "cantera/kinetics/ElectronCollisionPlasmaRate.h" #include "cantera/kinetics/Reaction.h" #include "cantera/kinetics/Kinetics.h" +#include "cantera/thermo/PlasmaPhase.h" #include "cantera/numerics/funcs.h" namespace Cantera @@ -47,42 +48,22 @@ bool ElectronCollisionPlasmaData::update(const ThermoPhase& phase, const Kinetic void ElectronCollisionPlasmaRate::setParameters(const AnyMap& node, const UnitStack& rate_units) { ReactionRate::setParameters(node, rate_units); + if (!node.hasKey("energy-levels") && !node.hasKey("cross-sections")) { + return; + } - // **Extract kind, target, and product from reaction node** if (node.hasKey("kind")) { m_kind = node["kind"].asString(); - } /*else { - throw CanteraError("ElectronCollisionPlasmaRate::setParameters", - "Missing `kind` field in electron-collision-plasma reaction."); - }*/ - + } if (node.hasKey("target")) { m_target = node["target"].asString(); - } /*else { - throw CanteraError("ElectronCollisionPlasmaRate::setParameters", - "Missing `target` field in electron-collision-plasma reaction."); - }*/ - + } if (node.hasKey("product")) { m_product = node["product"].asString(); - } /*else { - throw CanteraError("ElectronCollisionPlasmaRate::setParameters", - "Missing `product` field in electron-collision-plasma reaction."); - }*/ - - // **First, check if cross-sections are embedded in the reaction itself** - if (node.hasKey("energy-levels") && node.hasKey("cross-sections")) { - //writelog("Using embedded cross-section data from reaction definition.\n"); - - m_energyLevels = node["energy-levels"].asVector(); - m_crossSections = node["cross-sections"].asVector(); - - if (m_energyLevels.size() != m_crossSections.size()) { - throw CanteraError("ElectronCollisionPlasmaRate::setParameters", - "Mismatch: `energy-levels` and `cross-sections` must have the same length."); - } } + m_energyLevels = node["energy-levels"].asVector(); + m_crossSections = node["cross-sections"].asVector(m_energyLevels.size()); m_threshold = node.getDouble("threshold", 0.0); } diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index d5f16cd2bdf..70bfe9e169c 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -9,12 +9,16 @@ #include "cantera/thermo/EEDFTwoTermApproximation.h" #include "cantera/base/ctexceptions.h" +#include "cantera/numerics/eigen_dense.h" +#include "cantera/numerics/funcs.h" #include "cantera/thermo/PlasmaPhase.h" -#include +#include "cantera/kinetics/ElectronCollisionPlasmaRate.h" namespace Cantera { +typedef Eigen::SparseMatrix SparseMat; + EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase& s) { // store a pointer to s. @@ -77,9 +81,7 @@ int EEDFTwoTermApproximation::calculateDistributionFunction() // update electron mobility m_electronMobility = electronMobility(m_f0); - return 0; - } void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) @@ -96,7 +98,7 @@ void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) throw CanteraError("EEDFTwoTermApproximation::converge", "options.m_points is zero; the EEDF grid is empty."); } - if (std::isnan(delta) || delta == 0.0) { + if (isnan(delta) || delta == 0.0) { throw CanteraError("EEDFTwoTermApproximation::converge", "options.m_delta0 is NaN or zero; solver cannot update."); } @@ -108,23 +110,11 @@ void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) Eigen::VectorXd f0_old = f0; f0 = iterate(f0_old, delta); + checkFinite("EEDFTwoTermApproximation::converge: f0", f0.data(), f0.size()); err0 = err1; - Eigen::VectorXd Df0(options.m_points); - for (size_t i = 0; i < options.m_points; i++) { - Df0(i) = abs(f0_old(i) - f0(i)); - } + Eigen::VectorXd Df0 = (f0_old - f0).cwiseAbs(); err1 = norm(Df0, m_gridCenter); - - if ((f0.array() != f0.array()).any()) { - throw CanteraError("EEDFTwoTermApproximation::converge", - "NaN detected in EEDF solution."); - } - if ((f0.array().abs() > 1e300).any()) { - throw CanteraError("EEDFTwoTermApproximation::converge", - "Inf detected in EEDF solution."); - } - if (err1 < options.m_rtol) { break; } else if (n == options.m_maxn - 1) { @@ -139,24 +129,19 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou // probably extremely ineficient // must be refactored!! - if ((f0.array() != f0.array()).any()) { - throw CanteraError("EEDFTwoTermApproximation::iterate", - "NaN detected in input f0."); - } - - SparseMat_fp PQ(options.m_points, options.m_points); - vector_fp g = vector_g(f0); + SparseMat PQ(options.m_points, options.m_points); + vector g = vector_g(f0); for (size_t k : m_phase->kInelastic()) { - SparseMat_fp Q_k = matrix_Q(g, k); - SparseMat_fp P_k = matrix_P(g, k); + SparseMat Q_k = matrix_Q(g, k); + SparseMat P_k = matrix_P(g, k); PQ += (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; } - std::vector> pq_values; + vector> pq_values; int count = 0; for (int j = 0; j < PQ.outerSize(); ++j) { - for (Eigen::SparseMatrix::InnerIterator it(PQ, j); it; ++it) { + for (SparseMat::InnerIterator it(PQ, j); it; ++it) { if (count < 5) { pq_values.push_back({it.row(), it.col(), it.value()}); } @@ -164,8 +149,8 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou } } - SparseMat_fp A = matrix_A(f0); - SparseMat_fp I(options.m_points, options.m_points); + SparseMat A = matrix_A(f0); + SparseMat I(options.m_points, options.m_points); for (size_t i = 0; i < options.m_points; i++) { I.insert(i,i) = 1.0; } @@ -173,13 +158,8 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou A *= delta; A += I; - if (A.rows() == 0 || A.cols() == 0) { - throw CanteraError("EEDFTwoTermApproximation::iterate", - "Matrix A has zero rows/columns."); - } - // SparseLU : - Eigen::SparseLU solver(A); + Eigen::SparseLU solver(A); if (solver.info() == Eigen::NumericalIssue) { throw CanteraError("EEDFTwoTermApproximation::iterate", "Error SparseLU solver: NumericalIssue"); @@ -200,13 +180,8 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou return f0; } - if ((f1.array() != f1.array()).any()) { - throw CanteraError("EEDFTwoTermApproximation::iterate", - "NaN detected in computed f1."); - } - + checkFinite("EEDFTwoTermApproximation::converge: f0", f1.data(), f1.size()); f1 /= norm(f1, m_gridCenter); - return f1; } @@ -237,9 +212,9 @@ double EEDFTwoTermApproximation::integralPQ(double a, double b, double u0, doubl return c0 * A1 + c1 * A2; } -vector_fp EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) +vector EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) { - vector_fp g(options.m_points, 0.0); + vector g(options.m_points, 0.0); const double f_min = 1e-300; // Smallest safe floating-point value // Handle first point (i = 0) @@ -259,13 +234,12 @@ vector_fp EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) double f_down = std::max(f0(i - 1), f_min); g[i] = log(f_up / f_down) / (m_gridCenter[i + 1] - m_gridCenter[i - 1]); } - return g; } -SparseMat_fp EEDFTwoTermApproximation::matrix_P(const vector_fp& g, size_t k) +SparseMat EEDFTwoTermApproximation::matrix_P(const vector& g, size_t k) { - vector tripletList; + SparseTriplets tripletList; for (size_t n = 0; n < m_eps[k].size(); n++) { double eps_a = m_eps[k][n][0]; double eps_b = m_eps[k][n][1]; @@ -275,16 +249,16 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_P(const vector_fp& g, size_t k) double r = integralPQ(eps_a, eps_b, sigma_a, sigma_b, g[j], m_gridCenter[j]); double p = m_gamma * r; - tripletList.push_back(Triplet_fp(j, j, p)); + tripletList.emplace_back(j, j, p); } - SparseMat_fp P(options.m_points, options.m_points); + SparseMat P(options.m_points, options.m_points); P.setFromTriplets(tripletList.begin(), tripletList.end()); return P; } -SparseMat_fp EEDFTwoTermApproximation::matrix_Q(const vector_fp& g, size_t k) +SparseMat EEDFTwoTermApproximation::matrix_Q(const vector& g, size_t k) { - vector tripletList; + SparseTriplets tripletList; for (size_t n = 0; n < m_eps[k].size(); n++) { double eps_a = m_eps[k][n][0]; double eps_b = m_eps[k][n][1]; @@ -293,19 +267,19 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_Q(const vector_fp& g, size_t k) size_t i = m_i[k][n]; size_t j = m_j[k][n]; double r = integralPQ(eps_a, eps_b, sigma_a, sigma_b, g[j], m_gridCenter[j]); - double q = m_phase->inFactor()[k] * m_gamma * r; + double q = m_inFactor[k] * m_gamma * r; - tripletList.push_back(Triplet_fp(i, j, q)); + tripletList.emplace_back(i, j, q); } - SparseMat_fp Q(options.m_points, options.m_points); + SparseMat Q(options.m_points, options.m_points); Q.setFromTriplets(tripletList.begin(), tripletList.end()); return Q; } -SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) +SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) { - vector_fp a0(options.m_points + 1); - vector_fp a1(options.m_points + 1); + vector a0(options.m_points + 1); + vector a1(options.m_points + 1); size_t N = options.m_points - 1; // Scharfetter-Gummel scheme double nu = netProductionFreq(f0); @@ -316,34 +290,34 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) // Electron-electron collisions declarations double a; - vector_fp A1, A2, A3; + vector A1, A2, A3; if (m_eeCol) { eeColIntegrals(A1, A2, A3, a, options.m_points); } double nDensity = m_phase->molarDensity() * Avogadro; double alpha; + double E = m_phase->electricField(); if (options.m_growth == "spatial") { double mu = electronMobility(f0); double D = electronDiffusivity(f0); - alpha = (mu * m_phase->E() - sqrt(pow(mu * m_phase->E(), 2) - 4 * D * nu * nDensity)) / 2.0 / D / nDensity; + alpha = (mu * E - sqrt(pow(mu * E, 2) - 4 * D * nu * nDensity)) / 2.0 / D / nDensity; } else { alpha = 0.0; } double sigma_tilde; - double omega = 2 * Pi * m_phase->F(); + double omega = 2 * Pi * m_phase->electricFieldFrequency(); for (size_t j = 1; j < options.m_points; j++) { if (options.m_growth == "temporal") { sigma_tilde = m_totalCrossSectionEdge[j] + nu / pow(m_gridEdge[j], 0.5) / m_gamma; - } - else { + } else { sigma_tilde = m_totalCrossSectionEdge[j]; } double q = omega / (nDensity * m_gamma * pow(m_gridEdge[j], 0.5)); double W = -m_gamma * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; double F = sigma_tilde * sigma_tilde / (sigma_tilde * sigma_tilde + q * q); - double DA = m_gamma / 3.0 * pow(m_phase->E() / nDensity, 2.0) * m_gridEdge[j]; + double DA = m_gamma / 3.0 * pow(E / nDensity, 2.0) * m_gridEdge[j]; double DB = m_gamma * m_phase->temperature() * Boltzmann / ElectronCharge * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; double D = DA / sigma_tilde * F + DB; if (m_eeCol) { @@ -351,7 +325,7 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) D += 2 * a * m_phase->ionDegree() * (A2[j] + pow(m_gridEdge[j], 1.5) * A3[j]); } if (options.m_growth == "spatial") { - W -= m_gamma / 3.0 * 2 * alpha * m_phase->E() / nDensity * m_gridEdge[j] / sigma_tilde; + W -= m_gamma / 3.0 * 2 * alpha * E / nDensity * m_gridEdge[j] / sigma_tilde; } double z = W * (m_gridCenter[j] - m_gridCenter[j-1]) / D; @@ -360,74 +334,68 @@ SparseMat_fp EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) } if (std::abs(z) > 500) { writelog("Warning: Large Peclet number z = {:.3e} at j = {}. W = {:.3e}, D = {:.3e}, E/N = {:.3e}\n", - z, j, W, D, m_phase->E() / nDensity); + z, j, W, D, E / nDensity); } a0[j] = W / (1 - std::exp(-z)); a1[j] = W / (1 - std::exp(z)); } - std::vector tripletList; + SparseTriplets tripletList; // center diagonal // zero flux b.c. at energy = 0 - tripletList.push_back(Triplet_fp(0, 0, a0[1])); + tripletList.emplace_back(0, 0, a0[1]); for (size_t j = 1; j < options.m_points - 1; j++) { - tripletList.push_back(Triplet_fp(j, j, a0[j+1] - a1[j])); + tripletList.emplace_back(j, j, a0[j+1] - a1[j]); } // upper diagonal for (size_t j = 0; j < options.m_points - 1; j++) { - tripletList.push_back(Triplet_fp(j, j+1, a1[j+1])); + tripletList.emplace_back(j, j+1, a1[j+1]); } // lower diagonal for (size_t j = 1; j < options.m_points; j++) { - tripletList.push_back(Triplet_fp(j, j-1, -a0[j])); + tripletList.emplace_back(j, j-1, -a0[j]); } // zero flux b.c. - tripletList.push_back(Triplet_fp(N, N, -a1[N])); + tripletList.emplace_back(N, N, -a1[N]); - SparseMat_fp A(options.m_points, options.m_points); + SparseMat A(options.m_points, options.m_points); A.setFromTriplets(tripletList.begin(), tripletList.end()); //plus G - SparseMat_fp G(options.m_points, options.m_points); + SparseMat G(options.m_points, options.m_points); if (options.m_growth == "temporal") { for (size_t i = 0; i < options.m_points; i++) { G.insert(i, i) = 2.0 / 3.0 * (pow(m_gridEdge[i+1], 1.5) - pow(m_gridEdge[i], 1.5)) * nu; } - } - else if (options.m_growth == "spatial") { + } else if (options.m_growth == "spatial") { double nDensity = m_phase->molarDensity() * Avogadro; for (size_t i = 0; i < options.m_points; i++) { double sigma_c = 0.5 * (m_totalCrossSectionEdge[i] + m_totalCrossSectionEdge[i + 1]); G.insert(i, i) = - alpha * m_gamma / 3 * (alpha * (pow(m_gridEdge[i + 1], 2) - pow(m_gridEdge[i], 2)) / sigma_c / 2 - - m_phase->E() / nDensity * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i])); + - E / nDensity * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i])); } } - return A + G; } double EEDFTwoTermApproximation::netProductionFreq(const Eigen::VectorXd& f0) { double nu = 0.0; - vector_fp g = vector_g(f0); + vector g = vector_g(f0); - for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { - if (m_phase->kind(k) == "ionization" || - m_phase->kind(k) == "attachment") { - SparseMat_fp PQ = (matrix_Q(g, k) - matrix_P(g, k)) * + for (size_t k = 0; k < m_phase->nCollisions(); k++) { + if (m_phase->collisionRate(k)->kind() == "ionization" || + m_phase->collisionRate(k)->kind() == "attachment") { + SparseMat PQ = (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; Eigen::VectorXd s = PQ * f0; - for (size_t i = 0; i < options.m_points; i++) { - nu += s[i]; - if (!std::isfinite(s[i])) { - writelog("NaN in netProductionFreq at s[{}] for k = {}\n", i, k); - break; - } - } + checkFinite("EEDFTwoTermApproximation::netProductionFreq: s", + s.data(), s.size()); + nu += s.sum(); } } return nu; @@ -435,7 +403,7 @@ double EEDFTwoTermApproximation::netProductionFreq(const Eigen::VectorXd& f0) double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) { - vector_fp y(options.m_points, 0.0); + vector y(options.m_points, 0.0); double nu = netProductionFreq(f0); for (size_t i = 0; i < options.m_points; i++) { if (m_gridCenter[i] != 0.0) { @@ -452,7 +420,7 @@ double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) { double nu = netProductionFreq(f0); - vector_fp y(options.m_points + 1, 0.0); + vector y(options.m_points + 1, 0.0); for (size_t i = 1; i < options.m_points; i++) { // calculate df0 at i-1/2 double df0 = (f0(i) - f0(i-1)) / (m_gridCenter[i] - m_gridCenter[i-1]); @@ -462,19 +430,18 @@ double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) } } double nDensity = m_phase->molarDensity() * Avogadro; - auto f = Eigen::Map(y.data(), y.size()); - auto x = Eigen::Map(m_gridEdge.data(), m_gridEdge.size()); + auto f = ConstMappedVector(y.data(), y.size()); + auto x = ConstMappedVector(m_gridEdge.data(), m_gridEdge.size()); return -1./3. * m_gamma * simpson(f, x) / nDensity; } void EEDFTwoTermApproximation::initSpeciesIndexCS() { // set up target index - m_kTargets.resize(m_phase->nElectronCrossSections()); - m_klocTargets.resize(m_phase->nElectronCrossSections()); - for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) - { - + m_kTargets.resize(m_phase->nCollisions()); + m_klocTargets.resize(m_phase->nCollisions()); + m_inFactor.resize(m_phase->nCollisions()); + for (size_t k = 0; k < m_phase->nCollisions(); k++) { m_kTargets[k] = m_phase->targetIndex(k); // Check if it is a new target or not : auto it = find(m_k_lg_Targets.begin(), m_k_lg_Targets.end(), m_kTargets[k]); @@ -485,20 +452,28 @@ void EEDFTwoTermApproximation::initSpeciesIndexCS() } else { m_klocTargets[k] = distance(m_k_lg_Targets.begin(), it); } + + const auto& kind = m_phase->collisionRate(k)->kind(); + + if (kind == "ionization") { + m_inFactor[k] = 2; + } else if (kind == "attachment") { + m_inFactor[k] = 0; + } else { + m_inFactor[k] = 1; + } } m_X_targets.resize(m_k_lg_Targets.size()); m_X_targets_prev.resize(m_k_lg_Targets.size()); - for (size_t k = 0; k < m_X_targets.size(); k++) - { + for (size_t k = 0; k < m_X_targets.size(); k++) { size_t k_glob = m_k_lg_Targets[k]; m_X_targets[k] = m_phase->moleFraction(k_glob); m_X_targets_prev[k] = m_phase->moleFraction(k_glob); } // set up indices of species which has no cross-section data - for (size_t k = 0; k < m_phase->nSpecies(); k++) - { + for (size_t k = 0; k < m_phase->nSpecies(); k++) { auto it = std::find(m_kTargets.begin(), m_kTargets.end(), k); if (it == m_kTargets.end()) { m_kOthers.push_back(k); @@ -527,35 +502,29 @@ void EEDFTwoTermApproximation::updateCS() // Update the species mole fractions used for EEDF computation void EEDFTwoTermApproximation::update_mole_fractions() { - double tmp_sum = 0.0; - for (size_t k = 0; k < m_X_targets.size(); k++) - { + for (size_t k = 0; k < m_X_targets.size(); k++) { m_X_targets[k] = m_phase->moleFraction(m_k_lg_Targets[k]); tmp_sum = tmp_sum + m_phase->moleFraction(m_k_lg_Targets[k]); } // Normalize the mole fractions to unity: - for (size_t k = 0; k < m_X_targets.size(); k++) - { + for (size_t k = 0; k < m_X_targets.size(); k++) { m_X_targets[k] = m_X_targets[k] / tmp_sum; } - } void EEDFTwoTermApproximation::calculateTotalCrossSection() { - m_totalCrossSectionCenter.assign(options.m_points, 0.0); m_totalCrossSectionEdge.assign(options.m_points + 1, 0.0); - for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { - vector_fp x = m_phase->energyLevels()[k]; - vector_fp y = m_phase->crossSections()[k]; + for (size_t k = 0; k < m_phase->nCollisions(); k++) { + auto& x = m_phase->collisionRate(k)->energyLevels(); + auto& y = m_phase->collisionRate(k)->crossSections(); for (size_t i = 0; i < options.m_points; i++) { m_totalCrossSectionCenter[i] += m_X_targets[m_klocTargets[k]] * linearInterp(m_gridCenter[i], x, y); - } for (size_t i = 0; i < options.m_points + 1; i++) { m_totalCrossSectionEdge[i] += m_X_targets[m_klocTargets[k]] * @@ -569,8 +538,8 @@ void EEDFTwoTermApproximation::calculateTotalElasticCrossSection() m_sigmaElastic.clear(); m_sigmaElastic.resize(options.m_points, 0.0); for (size_t k : m_phase->kElastic()) { - vector_fp x = m_phase->energyLevels()[k]; - vector_fp y = m_phase->crossSections()[k]; + auto& x = m_phase->collisionRate(k)->energyLevels(); + auto& y = m_phase->collisionRate(k)->crossSections(); // Note: // moleFraction(m_kTargets[k]) <=> m_X_targets[m_klocTargets[k]] double mass_ratio = ElectronMass / (m_phase->molecularWeight(m_kTargets[k]) / Avogadro); @@ -584,24 +553,27 @@ void EEDFTwoTermApproximation::calculateTotalElasticCrossSection() void EEDFTwoTermApproximation::setGridCache() { m_sigma.clear(); - m_sigma.resize(m_phase->nElectronCrossSections()); + m_sigma.resize(m_phase->nCollisions()); m_sigma_offset.clear(); - m_sigma_offset.resize(m_phase->nElectronCrossSections()); + m_sigma_offset.resize(m_phase->nCollisions()); m_eps.clear(); - m_eps.resize(m_phase->nElectronCrossSections()); + m_eps.resize(m_phase->nCollisions()); m_j.clear(); - m_j.resize(m_phase->nElectronCrossSections()); + m_j.resize(m_phase->nCollisions()); m_i.clear(); - m_i.resize(m_phase->nElectronCrossSections()); - for (size_t k = 0; k < m_phase->nElectronCrossSections(); k++) { - auto x = m_phase->energyLevels()[k]; - auto y = m_phase->crossSections()[k]; - vector_fp eps1(options.m_points + 1); + m_i.resize(m_phase->nCollisions()); + for (size_t k = 0; k < m_phase->nCollisions(); k++) { + auto& collision = m_phase->collisionRate(k); + auto& x = collision->energyLevels(); + auto& y = collision->crossSections(); + vector eps1(options.m_points + 1); + int shiftFactor = (collision->kind() == "ionization") ? 2 : 1; + for (size_t i = 0; i < options.m_points + 1; i++) { - eps1[i] = clip(m_phase->shiftFactor()[k] * m_gridEdge[i] + m_phase->threshold(k), + eps1[i] = clip(shiftFactor * m_gridEdge[i] + collision->threshold(), m_gridEdge[0] + 1e-9, m_gridEdge[options.m_points] - 1e-9); } - vector_fp nodes = eps1; + vector nodes = eps1; for (size_t i = 0; i < options.m_points + 1; i++) { if (m_gridEdge[i] >= eps1[0] && m_gridEdge[i] <= eps1[options.m_points]) { nodes.push_back(m_gridEdge[i]); @@ -614,10 +586,9 @@ void EEDFTwoTermApproximation::setGridCache() } std::sort(nodes.begin(), nodes.end()); - auto last = std::unique(nodes.begin(), nodes.end()); nodes.resize(std::distance(nodes.begin(), last)); - vector_fp sigma0(nodes.size()); + vector sigma0(nodes.size()); for (size_t i = 0; i < nodes.size(); i++) { sigma0[i] = linearInterp(nodes[i], x, y); } @@ -636,20 +607,18 @@ void EEDFTwoTermApproximation::setGridCache() // construct sigma for (size_t i = 0; i < nodes.size() - 1; i++) { - vector_fp sigma{sigma0[i], sigma0[i+1]}; - m_sigma[k].push_back(sigma); + m_sigma[k].push_back({sigma0[i], sigma0[i+1]}); } // construct eps for (size_t i = 0; i < nodes.size() - 1; i++) { - vector_fp eps{nodes[i], nodes[i+1]}; - m_eps[k].push_back(eps); + m_eps[k].push_back({nodes[i], nodes[i+1]}); } // construct sigma_offset - auto x_offset = m_phase->energyLevels()[k]; + auto x_offset = collision->energyLevels(); for (auto& element : x_offset) { - element -= m_phase->threshold(k); + element -= collision->threshold(); } for (size_t i = 0; i < options.m_points; i++) { m_sigma_offset[k].push_back(linearInterp(m_gridCenter[i], x_offset, y)); @@ -668,7 +637,7 @@ double EEDFTwoTermApproximation::norm(const Eigen::VectorXd& f, const Eigen::Vec } -void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vector_fp& A3, +void EEDFTwoTermApproximation::eeColIntegrals(vector& A1, vector& A2, vector& A3, double& a, size_t nPoints) { // Ensure vectors are initialized @@ -725,7 +694,6 @@ void EEDFTwoTermApproximation::eeColIntegrals(vector_fp& A1, vector_fp& A2, vect A2[j] = integral_A2; A3[j] = integral_A3; } - } } diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index 860b2e53119..d59cc6a3071 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -11,9 +11,7 @@ #include "cantera/numerics/eigen_dense.h" #include "cantera/numerics/funcs.h" #include "cantera/kinetics/Kinetics.h" -#include "cantera/kinetics/KineticsFactory.h" #include "cantera/kinetics/Reaction.h" -#include "cantera/kinetics/ReactionRateFactory.h" #include #include "cantera/kinetics/ElectronCollisionPlasmaRate.h" @@ -25,31 +23,20 @@ namespace { PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) { - initialize(); - initThermoFile(inputFile, id_); // initial electron temperature m_electronTemp = temperature(); // Initialize the Boltzmann Solver - ptrEEDFSolver = make_unique(*this); + m_eedfSolver = make_unique(*this); // Set Energy Grid (Hardcoded Defaults for Now) double kTe_max = 60; size_t nGridCells = 301; m_nPoints = nGridCells + 1; - ptrEEDFSolver->setLinearGrid(kTe_max, nGridCells); - m_electronEnergyLevels = MappedVector(ptrEEDFSolver->getGridEdge().data(), m_nPoints); -} - -void PlasmaPhase::initialize() -{ - m_f0_ok = false; - m_EN = 0.0; - m_E = 0.0; - m_F = 0.0; - m_ionDegree = 0.0; + m_eedfSolver->setLinearGrid(kTe_max, nGridCells); + m_electronEnergyLevels = MappedVector(m_eedfSolver->getGridEdge().data(), m_nPoints); } PlasmaPhase::~PlasmaPhase() @@ -71,10 +58,10 @@ void PlasmaPhase::updateElectronEnergyDistribution() "Invalid for discretized electron energy distribution."); } else if (m_distributionType == "isotropic") { setIsotropicElectronEnergyDistribution(); - } else if (m_distributionType == "TwoTermApproximation") { - auto ierr = ptrEEDFSolver->calculateDistributionFunction(); + } else if (m_distributionType == "Boltzmann-two-term") { + auto ierr = m_eedfSolver->calculateDistributionFunction(); if (ierr == 0) { - auto y = ptrEEDFSolver->getEEDFEdge(); + auto y = m_eedfSolver->getEEDFEdge(); m_electronEnergyDist = Eigen::Map(y.data(), m_nPoints); } else { throw CanteraError("PlasmaPhase::updateElectronEnergyDistribution", @@ -92,10 +79,12 @@ void PlasmaPhase::updateElectronEnergyDistribution() } else { writelog("Skipping Te update: EEDF is empty, non-finite, or unnormalized.\n"); } + } else { + throw CanteraError("PlasmaPhase::updateElectronEnergyDistribution", + "Unknown method '{}' for determining EEDF", m_distributionType); } updateElectronEnergyDistDifference(); electronEnergyDistributionChanged(); - } void PlasmaPhase::normalizeElectronEnergyDistribution() { @@ -114,7 +103,7 @@ void PlasmaPhase::setElectronEnergyDistributionType(const string& type) { if (type == "discretized" || type == "isotropic" || - type == "TwoTermApproximation") { + type == "Boltzmann-two-term") { m_distributionType = type; } else { throw CanteraError("PlasmaPhase::setElectronEnergyDistributionType", @@ -163,11 +152,17 @@ void PlasmaPhase::electronEnergyDistributionChanged() void PlasmaPhase::electronEnergyLevelChanged() { + m_levelNum++; // Cross sections are interpolated on the energy levels if (m_collisions.size() > 0) { - updateInterpolatedCrossSections(); + vector energyLevels(m_nPoints); + MappedVector(energyLevels.data(), m_nPoints) = m_electronEnergyLevels; + for (shared_ptr collision : m_collisions) { + const auto& rate = boost::polymorphic_pointer_downcast + (collision->rate()); + rate->updateInterpolatedCrossSection(energyLevels); + } } - m_levelNum++; } void PlasmaPhase::checkElectronEnergyLevels() const @@ -189,13 +184,13 @@ void PlasmaPhase::checkElectronEnergyDistribution() const throw CanteraError("PlasmaPhase::checkElectronEnergyDistribution", "Values of electron energy distribution cannot be negative."); } - /* if (m_electronEnergyDist[m_nPoints - 1] > 0.01) { + if (m_electronEnergyDist[m_nPoints - 1] > 0.01) { warn_user("PlasmaPhase::checkElectronEnergyDistribution", "The value of the last element of electron energy distribution exceed 0.01. " "This indicates that the value of electron energy level is not high enough " "to contain the isotropic distribution at mean electron energy of " "{} eV", meanElectronEnergy()); - } */ + } } void PlasmaPhase::setDiscretizedElectronEnergyDist(const double* levels, @@ -242,7 +237,6 @@ void PlasmaPhase::updateElectronTemperatureFromEnergyDist() void PlasmaPhase::setIsotropicShapeFactor(double x) { m_isotropicShapeFactor = x; updateElectronEnergyDistribution(); - //setIsotropicElectronEnergyDistribution(); } void PlasmaPhase::getParameters(AnyMap& phaseNode) const @@ -265,16 +259,12 @@ void PlasmaPhase::getParameters(AnyMap& phaseNode) const phaseNode["electron-energy-distribution"] = std::move(eedf); } - void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) { IdealGasPhase::setParameters(phaseNode, rootNode); - m_root = rootNode; - if (phaseNode.hasKey("electron-energy-distribution")) { const AnyMap eedf = phaseNode["electron-energy-distribution"].as(); m_distributionType = eedf["type"].asString(); - if (m_distributionType == "isotropic") { if (eedf.hasKey("shape-factor")) { setIsotropicShapeFactor(eedf["shape-factor"].asDouble()); @@ -290,13 +280,11 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) "isotropic type requires electron-temperature key."); } if (eedf.hasKey("energy-levels")) { - setElectronEnergyLevels(eedf["energy-levels"].asVector().data(), - eedf["energy-levels"].asVector().size()); + auto levels = eedf["energy-levels"].asVector(); + setElectronEnergyLevels(levels.data(), levels.size()); } setIsotropicElectronEnergyDistribution(); - } - - else if (m_distributionType == "discretized") { + } else if (m_distributionType == "discretized") { if (!eedf.hasKey("energy-levels")) { throw CanteraError("PlasmaPhase::setParameters", "Cannot find key energy-levels."); @@ -308,31 +296,32 @@ void PlasmaPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) if (eedf.hasKey("normalize")) { enableNormalizeElectronEnergyDist(eedf["normalize"].asBool()); } - setDiscretizedElectronEnergyDist(eedf["energy-levels"].asVector().data(), - eedf["distribution"].asVector().data(), - eedf["energy-levels"].asVector().size()); + auto levels = eedf["energy-levels"].asVector(); + auto distribution = eedf["distribution"].asVector(levels.size()); + setDiscretizedElectronEnergyDist(levels.data(), distribution.data(), + levels.size()); } + } - if (rootNode.hasKey("cross-sections")) { - for (const auto& item : rootNode["cross-sections"].asVector()) { - auto rate = make_shared(item); - Composition reactants, products; - reactants[item["target"].asString()] = 1; - reactants[electronSpeciesName()] = 1; - if (item.hasKey("product")) { - products[item["product"].asString()] = 1; - } else { - products[item["target"].asString()] = 1; - } - products[electronSpeciesName()] = 1; - if (rate->kind() == "ionization") { - products[electronSpeciesName()] += 1; - } else if (rate->kind() == "attachment") { - products[electronSpeciesName()] -= 1; - } - auto R = make_shared(reactants, products, rate); - addCollision(R); + if (rootNode.hasKey("electron-collisions")) { + for (const auto& item : rootNode["electron-collisions"].asVector()) { + auto rate = make_shared(item); + Composition reactants, products; + reactants[item["target"].asString()] = 1; + reactants[electronSpeciesName()] = 1; + if (item.hasKey("product")) { + products[item["product"].asString()] = 1; + } else { + products[item["target"].asString()] = 1; + } + products[electronSpeciesName()] = 1; + if (rate->kind() == "ionization") { + products[electronSpeciesName()] += 1; + } else if (rate->kind() == "attachment") { + products[electronSpeciesName()] -= 1; } + auto R = make_shared(reactants, products, rate); + addCollision(R); } } } @@ -394,7 +383,7 @@ void PlasmaPhase::setCollisions() existing.insert(R.get()); } for (size_t i = 0; i < kin->nReactions(); i++) { - std::shared_ptr R = kin->reaction(i); + shared_ptr R = kin->reaction(i); if (R->rate()->type() != "electron-collision-plasma" || existing.count(R.get())) { continue; @@ -413,7 +402,7 @@ void PlasmaPhase::setCollisions() } } -void PlasmaPhase::addCollision(std::shared_ptr collision) +void PlasmaPhase::addCollision(shared_ptr collision) { size_t i = nCollisions(); @@ -444,39 +433,20 @@ void PlasmaPhase::addCollision(std::shared_ptr collision) m_collisionRates.emplace_back( std::dynamic_pointer_cast(collision->rate())); m_interp_cs_ready.emplace_back(false); - // Check if the reaction is elastic (reactants = products) - if (collision->reactants == collision->products) { - m_elasticCollisionIndices.push_back(i); - } // resize parameters m_elasticElectronEnergyLossCoefficients.resize(nCollisions()); - updateInterpolatedCrossSections(); + updateInterpolatedCrossSection(i); // Set up data used by Boltzmann solver auto& rate = *m_collisionRates.back(); string kind = m_collisionRates.back()->kind(); - // shift factor - if (kind == "ionization") { - m_shiftFactor.push_back(2); - } else { - m_shiftFactor.push_back(1); - } - - // scattering-in factor - if (kind == "ionization") { - m_inFactor.push_back(2); - } else if (kind == "attachment") { - m_inFactor.push_back(0); - } else { - m_inFactor.push_back(1); - } - - if ((kind == "effective" || kind == "elastic") && !collision->duplicate) { + if ((kind == "effective" || kind == "elastic")) { for (size_t k = 0; k < m_collisions.size() - 1; k++) { if (m_collisions[k]->reactants == collision->reactants && - (this->kind(k) == "elastic" || this->kind(k) == "effective")) + (m_collisionRates[k]->kind() == "elastic" || + m_collisionRates[k]->kind() == "effective") && !collision->duplicate) { throw CanteraError("PlasmaPhase::addCollision", "Phase already contains" " an effective/elastic cross section for '{}'.", target); @@ -487,10 +457,9 @@ void PlasmaPhase::addCollision(std::shared_ptr collision) m_kInelastic.push_back(i); } - m_f0_ok = false; m_energyLevels.push_back(rate.energyLevels()); m_crossSections.push_back(rate.crossSections()); - ptrEEDFSolver->setGridCache(); + m_eedfSolver->setGridCache(); } bool PlasmaPhase::updateInterpolatedCrossSection(size_t i) @@ -505,56 +474,6 @@ bool PlasmaPhase::updateInterpolatedCrossSection(size_t i) return true; } -void PlasmaPhase::updateInterpolatedCrossSections() -{ - for (shared_ptr collision : m_collisions) { - auto rate = boost::polymorphic_pointer_downcast - (collision->rate()); - - vector cs_interp; - for (double level : m_electronEnergyLevels) { - cs_interp.push_back(linearInterp(level, - rate->energyLevels(), rate->crossSections())); - } - - // Set interpolated cross-section - rate->setCrossSectionInterpolated(cs_interp); - } -} - -size_t PlasmaPhase::targetSpeciesIndex(shared_ptr R) -{ - if (R->type() != "electron-collision-plasma") { - throw CanteraError("PlasmaPhase::targetSpeciesIndex", - "Invalid reaction type. Type electron-collision-plasma is needed."); - } - for (const auto& [name, stoich] : R->reactants) { - if (name != electronSpeciesName()) { - return speciesIndex(name); - } - } - throw CanteraError("PlasmaPhase::targetSpeciesIndex", - "No target found. Target cannot be electron."); -} - -vector PlasmaPhase::crossSection(shared_ptr reaction) -{ - if (reaction->type() != "electron-collision-plasma") { - throw CanteraError("PlasmaPhase::crossSection", - "Invalid reaction type. Type electron-collision-plasma is needed."); - } else { - auto rate = boost::polymorphic_pointer_downcast - (reaction->rate()); - std::vector cs_interp; - for (double level : m_electronEnergyLevels) { - cs_interp.push_back(linearInterp(level, - rate->energyLevels(), - rate->crossSections())); - } - return cs_interp; - } -} - void PlasmaPhase::updateElectronEnergyDistDifference() { m_electronEnergyDistDiff.resize(nElectronEnergyLevels()); @@ -668,18 +587,8 @@ void PlasmaPhase::updateThermo() const } // update the species Gibbs functions m_g0_RT[k] = m_h0_RT[k] - m_s0_R[k]; - // update the nDensity array -} - -string PlasmaPhase::kind(size_t k) const { - return m_collisionRates[k]->kind(); } -double PlasmaPhase::threshold(size_t k) const { - return m_collisionRates[k]->threshold(); -} - - double PlasmaPhase::enthalpy_mole() const { double value = IdealGasPhase::enthalpy_mole(); value += GasConstant * (electronTemperature() - temperature()) * diff --git a/test/data/air-plasma.yaml b/test/data/air-plasma.yaml index dbeaf8852ce..4ff6bb09284 100644 --- a/test/data/air-plasma.yaml +++ b/test/data/air-plasma.yaml @@ -12,7 +12,7 @@ phases: - nasa_gas.yaml/species: [Electron, O2, N2, O2+, N2+, O, O2-] state: {T: 300.0, P: 1 atm, X: {O2: 0.21, N2: 0.79}} electron-energy-distribution: - type: TwoTermApproximation + type: Boltzmann-two-term energy-levels: [0.0, 2.0, 4.0, 6.0, 8.0, 10.0, 12.0, 14.0, 16.0, 18.0, 20.0, 22.0, 24.0, 26.0, 28.0, 30.0, 32.0, 34.0, 36.0, 38.0, 40.0] @@ -46,7 +46,7 @@ reactions: 0.0, 0.0, 5.50781e-42, 0.0, 0.0, 4.20596e-42, 0.0, 0.0, 0.0] threshold: 0.0 -cross-sections: +electron-collisions: - target: N2 energy-levels: [0.0, 0.015, 0.03, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.7, 1.2, 1.5, 1.9, 2.2, 2.8, 3.3, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 75.0, 150.0] diff --git a/test/python/test_thermo.py b/test/python/test_thermo.py index 06802b27d2f..abcc02aafd0 100644 --- a/test/python/test_thermo.py +++ b/test/python/test_thermo.py @@ -1304,7 +1304,7 @@ def test_eedf_solver(self): phase = ct.Solution('air-plasma.yaml') phase.TPX = 300., 101325., 'N2:0.79, O2:0.21, N2+:1E-10, Electron:1E-10' - phase.EN = 200.0 * 1e-21 # Reduced electric field [V.m^2] + phase.reduced_electric_field = 200.0 * 1e-21 # Reduced electric field [V.m^2] phase.update_EEDF() grid = phase.electron_energy_levels From 4d19e6eec9a6d77f549eee47842a59da60e83e3e Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 19 Jul 2025 19:30:42 -0400 Subject: [PATCH 27/29] Update authors list --- AUTHORS.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/AUTHORS.md b/AUTHORS.md index d776a824692..41258fa3347 100644 --- a/AUTHORS.md +++ b/AUTHORS.md @@ -12,6 +12,7 @@ update, please report on Cantera's - **Halla Ali** [@hallaali](https://github.com/hallaali) - **Emil Atz** [@EmilAtz](https://github.com/EmilAtz) - **Jongyoon Bae** [@jongyoonbae](https://github.com/jongyoonbae) - Brown University +- **Nicolas Barléon** [@NicolasBarleon](https://github.com/NicolasBarleon) - CERFACS - **Philip Berndt** - **Guus Bertens** [@guusbertens](https://github.com/guusbertens) - Eindhoven University of Technology - **Wolfgang Bessler** [@wbessler](https://github.com/wbessler) - Offenburg University of Applied Science @@ -49,6 +50,7 @@ update, please report on Cantera's - **Kyle Linevitch, Jr.** [@KyleLinevitchJr](https://github.com/KyleLinevitchJr) - **Christopher Leuth** - **Nicholas Malaya** [@nicholasmalaya](https://github.com/nicholasmalaya) - University of Texas at Austin +- **Quentin Malé** [@QuentinMale](https://github.com/QuentinMale) - Harvard University - **Thanasis Mattas** [@ThanasisMattas](https://github.com/ThanasisMattas) - Aristotle University of Thessaloniki - **Manik Mayur** [@manikmayur](https://github.com/manikmayur) - **Evan McCorkle** [@emccorkle](https://github.com/emccorkle) @@ -61,6 +63,7 @@ update, please report on Cantera's - **Paul Northrop** [@pwcnorthrop](https://github.com/pwcnorthrop) - **Chris Pilko** [@cpilko](https://github.com/cpilko) - **Sebastian Pinnau** [@spinnau](https://github.com/spinnau) +- **Matthew Quiram** [@mquiram](https://github.com/mquiram) - Massachusetts Institute of Technology - **Corey R. Randall** [@c-randall](https://github.com/c-randall) - Colorado School of Mines - **Andreas Rücker** [@cannondale1492](https://github.com/cannondale1492) - **Jeff Santner** [@jsantner](https://github.com/jsantner) From 1271967a233b4e9a6addaecc161bd4d511e4975f Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 4 Aug 2025 09:51:16 -0400 Subject: [PATCH 28/29] addressed comments on PR --- doc/sphinx/yaml/index.md | 2 + doc/sphinx/yaml/phases.md | 4 + doc/sphinx/yaml/reactions.md | 43 +++++++ .../kinetics/ElectronCollisionPlasmaRate.h | 23 +++- .../cantera/thermo/EEDFTwoTermApproximation.h | 70 ++++++---- include/cantera/thermo/PlasmaPhase.h | 14 +- interfaces/cython/cantera/thermo.pxd | 2 + interfaces/cython/cantera/thermo.pyx | 21 ++- .../reactors/nanosecond-pulse-discharge.py | 20 ++- samples/python/reactors/pfr.py | 22 ++-- samples/python/thermo/plasma-eedf.py | 2 +- src/thermo/EEDFTwoTermApproximation.cpp | 121 ++---------------- src/thermo/PlasmaPhase.cpp | 2 +- test/python/test_thermo.py | 2 +- 14 files changed, 196 insertions(+), 152 deletions(-) diff --git a/doc/sphinx/yaml/index.md b/doc/sphinx/yaml/index.md index b70c7b97bd1..a397533d056 100644 --- a/doc/sphinx/yaml/index.md +++ b/doc/sphinx/yaml/index.md @@ -31,6 +31,8 @@ state](sec-yaml-species-eos), [transport property](sec-yaml-species-transport), YAML reaction definitions include specification of common elements such as the reaction equation and [efficiencies](sec-yaml-efficiencies), as well as parameters specific to the type of [rate parameterization](sec-yaml-rate-types). +See also the [electron collision data format](reactions.html#sec-yaml-electron-collisions) +used in plasma-phase simulations. ## Mechanism Conversion diff --git a/doc/sphinx/yaml/phases.md b/doc/sphinx/yaml/phases.md index d61abb129bb..c73f969ece7 100644 --- a/doc/sphinx/yaml/phases.md +++ b/doc/sphinx/yaml/phases.md @@ -862,6 +862,7 @@ Additional fields: - `isotropic` - `discretized` + - `Boltzmann-two-term` `shape-factor` : A constant in the isotropic distribution, which is shown as x in the detailed @@ -909,6 +910,9 @@ Examples: normalize: False ``` +See also the [electron collision data format](reactions.html#sec-yaml-electron-collisions), +which is used to specify electron-impact cross sections relevant to plasma simulations. + :::{versionadded} 2.6 ::: diff --git a/doc/sphinx/yaml/reactions.md b/doc/sphinx/yaml/reactions.md index e1984a40451..963a5426974 100644 --- a/doc/sphinx/yaml/reactions.md +++ b/doc/sphinx/yaml/reactions.md @@ -18,6 +18,7 @@ The fields common to all `reaction` entries are: - [`Blowers-Masel`](sec-yaml-Blowers-Masel) - [`two-temperature-plasma`](sec-yaml-two-temperature-plasma) - [`electron-collision-plasma`](sec-yaml-electron-collision-plasma) + - [`electron-collisions`](sec-yaml-electron-collisions) - [`falloff`](sec-yaml-falloff) - [`chemically-activated`](sec-yaml-chemically-activated) - [`pressure-dependent-Arrhenius`](sec-yaml-pressure-dependent-Arrhenius) @@ -308,6 +309,48 @@ Example: :::{versionadded} 3.1 ::: +(sec-yaml-electron-collisions)= +### `electron-collisions` + +The `electron-collisions` field defines a list of cross-section datasets for +electron-impact processes that are used in plasma-phase simulations. These entries +are not formal reactions (they are not added to `Kinetics` objects), but serve +as data inputs for computing the electron energy distribution function. + +Each entry includes: + +`target` +: The name of the species that is the target of the collision + +`energy-levels` +: A list of electron energy values [eV] at which the cross-section is provided + +`cross-sections` +: Corresponding cross-section values [m²] for each energy level + +`kind` +: A string indicating the process type. Options include: + - `"effective"` – lumped or total effect of several channels + - `"excitation"` – electronic excitation + - `"ionization"` – electron-impact ionization + - `"attachment"` – electron attachment processes + +Example: + +```yaml +electron-collisions: +- target: N2 + energy-levels: [0.0, 0.015, 0.03, 0.05, 0.1, 0.15, 0.2, 0.3, 0.4, 0.7, 1.2, 1.5, 1.9, + 2.2, 2.8, 3.3, 4.0, 5.0, 7.0, 10.0, 15.0, 20.0, 30.0, 75.0, 150.0] + cross-sections: [1.1e-20, 2.55e-20, 3.4e-20, 4.33e-20, 5.95e-20, 7.1e-20, 7.9e-20, + 9e-20, 9.7e-20, 1e-19, 1.04e-19, 1.2e-19, 1.96e-19, 2.85e-19, 2.8e-19, 1.72e-19, + 1.26e-19, 1.09e-19, 1.01e-19, 1.04e-19, 1.1e-19, 1.02e-19, 9e-20, 6.6e-20, 4.9e-20] + kind: effective +``` + +:::{versionadded} 3.2 +::: + (sec-yaml-falloff)= ### `falloff` diff --git a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h index 63d4086610a..fb0a2156eca 100644 --- a/include/cantera/kinetics/ElectronCollisionPlasmaRate.h +++ b/include/cantera/kinetics/ElectronCollisionPlasmaRate.h @@ -152,25 +152,42 @@ class ElectronCollisionPlasmaRate : public ReactionRate throw NotImplementedError("ElectronCollisionPlasmaRate::ddTScaledFromStruct"); } - //! The kind of the process + //! The kind of the process which will be one of the following: + //! - `"effective"`: A generic effective collision + //! - `"excitation"`: Electronic or vibrational excitation + //! - `"ionization"`: Electron-impact ionization + //! - `"attachment"`: Electron attachment //! @since New in Cantera 3.2. const string& kind() const { return m_kind; } - //! The target of the process + //! Get the target species of the electron collision process. + //! This is the name of the neutral or ionic species that the electron interacts with //! @since New in Cantera 3.2. const string& target() const { return m_target; } - //! The product of the process + //! Get the product of the electron collision process. + //! This is the name of the species or excited state of + //! some species resulting from the process. + //! @note this may not necessarily be represented by + //! a distinct species in the mixture. //! @since New in Cantera 3.2. const string& product() const { return m_product; } //! Get the energy threshold of electron collision [eV] + //! + //! By default, the threshold is set to the first non-zero energy value + //! listed in the tabulated cross section data. + //! + //! @note This behavior may be subject to change. A more robust approach + //! may use the energy corresponding to the first non-zero cross section + //! value, rather than the first non-zero energy point in the data table. + //! //! @since New in Cantera 3.2. double threshold() const { return m_threshold; diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h index 5a7ddc62530..ba73b08dd75 100644 --- a/include/cantera/thermo/EEDFTwoTermApproximation.h +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -38,10 +38,33 @@ class TwoTermOpt //! Boltzmann equation solver for the electron energy distribution function based on //! the two-term approximation. -//! -//! @since New in %Cantera 3.2. -//! @warning This class is an experimental part of %Cantera and may be changed without -//! notice. +/*! + * This class implements a solver for the electron energy distribution function + * based on a steady-state solution to the Boltzmann equation using the classical + * two-term expansion, applicable to weakly ionized plasmas. The numerical approach + * and theory are primarily derived from the work of Hagelaar and Pitchford + * @cite hagelaar2005. + * + * The two-term approximation assumes that the EEDF can be represented as: + * @f[ + * f(\epsilon, \mu) = f_0(\epsilon) + \mu f_1(\epsilon), + * @f] + * where @f$ \epsilon @f$ is the electron energy and @f$ \mu @f$ is the cosine + * of the angle between the electron velocity vector and the electric field. + * The Boltzmann equation is projected onto the zeroth moment over mu to obtain + * an equation for @f$ f_0(\epsilon) @f$ , the isotropic part of the distribution. + * The first-order anisotropic term @f$ f_1(\epsilon) @f$ is not solved directly, + * but is approximated and substituted into the drift and collision terms. This + * results in a second-order differential equation for @f$ f_0(\epsilon) @f$ alone. + * + * The governing equation for @f$ f_0(\epsilon) @f$ is discretized on an energy + * grid using a finite difference method and solved using a tridiagonal matrix + * algorithm. + * + * @since New in %Cantera 3.2. + * @warning This class is an experimental part of %Cantera and may be changed without + * notice. + */ class EEDFTwoTermApproximation { public: @@ -54,7 +77,7 @@ class EEDFTwoTermApproximation * * @param s PlasmaPhase object that will be used in the solver calls. */ - EEDFTwoTermApproximation(PlasmaPhase& s); + EEDFTwoTermApproximation(PlasmaPhase* s); virtual ~EEDFTwoTermApproximation() = default; @@ -90,9 +113,10 @@ class EEDFTwoTermApproximation protected: //! Pointer to the PlasmaPhase object used to initialize this object. /*! - * This PlasmaPhase object must be compatible with the PlasmaPhase objects - * input from the compute function. Currently, this means that the 2 - * PlasmaPhases have to have consist of the same species and elements. + * This PlasmaPhase object provides species, element, and cross-section + * data used by the EEDF solver. It is set during construction and is not + * modified afterwards. All subsequent calls to compute functions must + * use the same PlasmaPhase context. */ PlasmaPhase* m_phase; @@ -158,7 +182,7 @@ class EEDFTwoTermApproximation //! Reduced net production frequency. Equation (10) of ref. [1] //! divided by N. //! @param f0 EEDF - double netProductionFreq(const Eigen::VectorXd& f0); + double netProductionFrequency(const Eigen::VectorXd& f0); //! Diffusivity double electronDiffusivity(const Eigen::VectorXd& f0); @@ -166,20 +190,28 @@ class EEDFTwoTermApproximation //! Mobility double electronMobility(const Eigen::VectorXd& f0); - void initSpeciesIndexCS(); - - void checkSpeciesNoCrossSection(); + //! Initialize species indices associated with cross-section data + void initSpeciesIndexCrossSections(); - void updateCS(); + //! Update the total cross sections based on the current state + void updateCrossSections(); - void update_mole_fractions(); + //! Update the vector of species mole fractions + void updateMoleFractions(); + //! Compute the total elastic collision cross section void calculateTotalElasticCrossSection(); + //! Compute the total (elastic + inelastic) cross section void calculateTotalCrossSection(); + //! Compute the L1 norm of a function f defined over a given energy grid. + //! + //! @param f Vector representing the function values (EEDF) + //! @param grid Vector representing the energy grid corresponding to f double norm(const Eigen::VectorXd& f, const Eigen::VectorXd& grid); + //! Electron mobility [m²/V·s] double m_electronMobility; //! Grid of electron energy (cell center) [eV] @@ -200,9 +232,6 @@ class EEDFTwoTermApproximation //! The energy boundaries of the overlap of cell i and j vector>> m_eps; - //! The cross section at the center of a cell - vector> m_sigma_offset; - //! normalized electron energy distribution function Eigen::VectorXd m_f0; @@ -243,13 +272,6 @@ class EEDFTwoTermApproximation double m_gamma; - //! boolean for the electron-electron collisions - bool m_eeCol = false; - - //! Compute electron-electron collision integrals - void eeColIntegrals(vector& A1, vector& A2, vector& A3, - double& a, size_t nPoints); - //! flag of having an EEDF bool m_has_EEDF; diff --git a/include/cantera/thermo/PlasmaPhase.h b/include/cantera/thermo/PlasmaPhase.h index e41469d0e86..7670bf0b3c0 100644 --- a/include/cantera/thermo/PlasmaPhase.h +++ b/include/cantera/thermo/PlasmaPhase.h @@ -342,10 +342,18 @@ class PlasmaPhase: public IdealGasPhase return m_electricField; } - double ionDegree() const { - return m_ionDegree; + //! Set the absolute electric field strength [V/m] + void setElectricField(double E) { + m_electricField = E; } + //! Calculate the degree of ionization + //double ionDegree() const { + // double ne = concentration(m_electronSpeciesIndex); // [kmol/m³] + // double n_total = molarDensity(); // [kmol/m³] + // return ne / n_total; + //} + //! Get the reduced electric field strength [V·m²] double reducedElectricField() const { return m_electricField / (molarDensity() * Avogadro); @@ -467,7 +475,7 @@ class PlasmaPhase: public IdealGasPhase vector> m_energyLevels; //! ionization degree for the electron-electron collisions (tmp is the previous one) - double m_ionDegree = 0.0; + //double m_ionDegree = 0.0; //! Electron energy distribution Difference dF/dε (V^-5/2) Eigen::ArrayXd m_electronEnergyDistDiff; diff --git a/interfaces/cython/cantera/thermo.pxd b/interfaces/cython/cantera/thermo.pxd index a918980e861..80fdaa4e322 100644 --- a/interfaces/cython/cantera/thermo.pxd +++ b/interfaces/cython/cantera/thermo.pxd @@ -194,6 +194,7 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": CxxPlasmaPhase() void setElectronTemperature(double) except +translate_exception void setReducedElectricField(double) except +translate_exception + void setElectricField(double) except +translate_exception void setElectronEnergyLevels(double*, size_t) except +translate_exception void getElectronEnergyLevels(double*) void setDiscretizedElectronEnergyDist(double*, double*, size_t) except +translate_exception @@ -212,6 +213,7 @@ cdef extern from "cantera/thermo/PlasmaPhase.h": double electronPressure() string electronSpeciesName() double reducedElectricField() + double electricField() void updateElectronEnergyDistribution() except +translate_exception double elasticPowerLoss() except +translate_exception diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index bf83696db35..cdc215b884f 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -1799,6 +1799,25 @@ cdef class ThermoPhase(_SolutionBase): raise ThermoModelMethodError(self.thermo_model) self.plasma.setReducedElectricField(value) + property electric_field: + """ + Get/Set the electric field (E) [V/m]. + + This is the absolute electric field strength. It is related to the reduced + electric field (E/N) through the number density of neutrals. + + .. versionadded:: 3.2 + """ + def __get__(self): + if not self._enable_plasma: + raise ThermoModelMethodError(self.thermo_model) + return self.plasma.electricField() + + def __set__(self, value): + if not self._enable_plasma: + raise ThermoModelMethodError(self.thermo_model) + self.plasma.setElectricField(value) + def set_discretized_electron_energy_distribution(self, levels, distribution): """ Set electron energy distribution. When this method is used, electron @@ -1825,7 +1844,7 @@ cdef class ThermoPhase(_SolutionBase): &data_dist[0], len(levels)) - def update_EEDF(self): + def update_electron_energy_distribution(self): """ Update the electron energy distribution function to account for changes in composition, temperature, pressure, or electric field strength. diff --git a/samples/python/reactors/nanosecond-pulse-discharge.py b/samples/python/reactors/nanosecond-pulse-discharge.py index 21f2eacc37c..0296e95591d 100644 --- a/samples/python/reactors/nanosecond-pulse-discharge.py +++ b/samples/python/reactors/nanosecond-pulse-discharge.py @@ -4,6 +4,22 @@ This example simulates a nanosecond-scale pulse discharge in a reactor. A Gaussian-shaped electric field pulse is applied over a short timescale. +The plasma reaction mechanism used is based on Colin Pavan's mechanism +for methane-air plasmas which is described in his Ph.D. dissertation and the +corresponding AIAA SciTech conference papers: + +C. A. Pavan, "Nanosecond Pulsed Plasmas in Dynamic Combustion Environments," +Ph.D. Thesis, Massachusetts Institute of Technology, 2023. Chapter 5. + +C. A. Pavan and C. Guerra-Garcia, "Modelling the Impact of a Repetitively +Pulsed Nanosecond DBD Plasma on a Mesoscale Flame," in AIAA SCITECH 2022 +Forum, Reston, Virginia: American Institute of Aeronautics and +Astronautics, Jan. 2022, pp. 1-15. DOI: 10.2514/6.2022-0975. + +C. A. Pavan and C. Guerra-Garcia, "Modeling Flame Speed Modification by +Nanosecond Pulsed Discharges to Inform Experimental Design," in AIAA +SCITECH 2023 Forum, Reston, Virginia: American Institute of Aeronautics +and Astronautics, Jan. 2023, pp. 1-15. DOI: 10.2514/6.2023-2056. Requires: cantera >= 3.2, matplotlib >= 2.0 @@ -26,7 +42,7 @@ def gaussian_EN(t): gas = ct.Solution('example_data/methane-plasma-pavan-2023.yaml') gas.TPX = 300., 101325., 'CH4:0.095, O2:0.19, N2:0.715, e:1E-11' gas.reduced_electric_field = gaussian_EN(0) -gas.update_EEDF() +gas.update_electron_energy_distribution() r = ct.ConstPressureReactor(gas, energy="off") @@ -55,7 +71,7 @@ def gaussian_EN(t): EN_t = gaussian_EN(t) gas.reduced_electric_field = EN_t - gas.update_EEDF() + gas.update_electron_energy_distribution() # reinitialize integrator with new source terms sim.reinitialize() diff --git a/samples/python/reactors/pfr.py b/samples/python/reactors/pfr.py index d59ca382241..8e414970f46 100644 --- a/samples/python/reactors/pfr.py +++ b/samples/python/reactors/pfr.py @@ -124,19 +124,25 @@ states2 = ct.SolutionArray(r2.thermo) # iterate through the PFR cells for n in range(n_steps): - # Set the state of the reservoir to match that of the previous reactor - gas2.TDY = r2.thermo.TDY - upstream.syncState() - # integrate the reactor forward in time until steady state is reached - sim2.reinitialize() + # create new reactor from updated gas + r2 = ct.IdealGasReactor(gas2) + r2.volume = r_vol + upstream = ct.Reservoir(gas2) + downstream = ct.Reservoir(gas2) + m = ct.MassFlowController(upstream, r2, mdot=mass_flow_rate2) + v = ct.PressureController(r2, downstream, primary=m, K=1e-5) + sim2 = ct.ReactorNet([r2]) + sim2.advance_to_steady_state() - # compute velocity and transform into time + u2[n] = mass_flow_rate2 / area / r2.thermo.density - t_r2[n] = r2.mass / mass_flow_rate2 # residence time in this reactor + t_r2[n] = r2.mass / mass_flow_rate2 t2[n] = np.sum(t_r2) - # write output data states2.append(r2.thermo.state) + # update gas2 for next segment + gas2.TDY = r2.thermo.TDY + ##################################################################### diff --git a/samples/python/thermo/plasma-eedf.py b/samples/python/thermo/plasma-eedf.py index b54444d71d0..b7df9890349 100644 --- a/samples/python/thermo/plasma-eedf.py +++ b/samples/python/thermo/plasma-eedf.py @@ -16,7 +16,7 @@ gas = ct.Solution('example_data/air-plasma-Phelps.yaml') gas.TPX = 300., 101325., 'N2:0.79, O2:0.21, N2+:1E-10, Electron:1E-10' gas.reduced_electric_field = 200.0 * 1e-21 # Reduced electric field [V.m^2] -gas.update_EEDF() +gas.update_electron_energy_distribution() grid = gas.electron_energy_levels eedf = gas.electron_energy_distribution diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index 70bfe9e169c..be8083461ee 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -19,10 +19,10 @@ namespace Cantera typedef Eigen::SparseMatrix SparseMat; -EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase& s) +EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase* s) { // store a pointer to s. - m_phase = &s; + m_phase = s; m_first_call = true; m_has_EEDF = false; m_gamma = pow(2.0 * ElectronCharge / ElectronMass, 0.5); @@ -46,13 +46,12 @@ void EEDFTwoTermApproximation::setLinearGrid(double& kTe_max, size_t& ncell) int EEDFTwoTermApproximation::calculateDistributionFunction() { if (m_first_call) { - initSpeciesIndexCS(); + initSpeciesIndexCrossSections(); m_first_call = false; } - update_mole_fractions(); - checkSpeciesNoCrossSection(); - updateCS(); + updateMoleFractions(); + updateCrossSections(); if (!m_has_EEDF) { writelog("No existing EEDF. Using first guess method: {}\n", options.m_firstguess); @@ -138,17 +137,6 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou PQ += (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; } - vector> pq_values; - int count = 0; - for (int j = 0; j < PQ.outerSize(); ++j) { - for (SparseMat::InnerIterator it(PQ, j); it; ++it) { - if (count < 5) { - pq_values.push_back({it.row(), it.col(), it.value()}); - } - count++; - } - } - SparseMat A = matrix_A(f0); SparseMat I(options.m_points, options.m_points); for (size_t i = 0; i < options.m_points; i++) { @@ -282,7 +270,7 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) vector a1(options.m_points + 1); size_t N = options.m_points - 1; // Scharfetter-Gummel scheme - double nu = netProductionFreq(f0); + double nu = netProductionFrequency(f0); a0[0] = NAN; a1[0] = NAN; a0[N+1] = NAN; @@ -291,9 +279,6 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) // Electron-electron collisions declarations double a; vector A1, A2, A3; - if (m_eeCol) { - eeColIntegrals(A1, A2, A3, a, options.m_points); - } double nDensity = m_phase->molarDensity() * Avogadro; double alpha; @@ -320,10 +305,6 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) double DA = m_gamma / 3.0 * pow(E / nDensity, 2.0) * m_gridEdge[j]; double DB = m_gamma * m_phase->temperature() * Boltzmann / ElectronCharge * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; double D = DA / sigma_tilde * F + DB; - if (m_eeCol) { - W -= 3 * a * m_phase->ionDegree() * A1[j]; - D += 2 * a * m_phase->ionDegree() * (A2[j] + pow(m_gridEdge[j], 1.5) * A3[j]); - } if (options.m_growth == "spatial") { W -= m_gamma / 3.0 * 2 * alpha * E / nDensity * m_gridEdge[j] / sigma_tilde; } @@ -382,7 +363,7 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) return A + G; } -double EEDFTwoTermApproximation::netProductionFreq(const Eigen::VectorXd& f0) +double EEDFTwoTermApproximation::netProductionFrequency(const Eigen::VectorXd& f0) { double nu = 0.0; vector g = vector_g(f0); @@ -393,7 +374,7 @@ double EEDFTwoTermApproximation::netProductionFreq(const Eigen::VectorXd& f0) SparseMat PQ = (matrix_Q(g, k) - matrix_P(g, k)) * m_X_targets[m_klocTargets[k]]; Eigen::VectorXd s = PQ * f0; - checkFinite("EEDFTwoTermApproximation::netProductionFreq: s", + checkFinite("EEDFTwoTermApproximation::netProductionFrequency: s", s.data(), s.size()); nu += s.sum(); } @@ -404,7 +385,7 @@ double EEDFTwoTermApproximation::netProductionFreq(const Eigen::VectorXd& f0) double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) { vector y(options.m_points, 0.0); - double nu = netProductionFreq(f0); + double nu = netProductionFrequency(f0); for (size_t i = 0; i < options.m_points; i++) { if (m_gridCenter[i] != 0.0) { y[i] = m_gridCenter[i] * f0(i) / @@ -419,7 +400,7 @@ double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) { - double nu = netProductionFreq(f0); + double nu = netProductionFrequency(f0); vector y(options.m_points + 1, 0.0); for (size_t i = 1; i < options.m_points; i++) { // calculate df0 at i-1/2 @@ -435,7 +416,7 @@ double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) return -1./3. * m_gamma * simpson(f, x) / nDensity; } -void EEDFTwoTermApproximation::initSpeciesIndexCS() +void EEDFTwoTermApproximation::initSpeciesIndexCrossSections() { // set up target index m_kTargets.resize(m_phase->nCollisions()); @@ -481,18 +462,7 @@ void EEDFTwoTermApproximation::initSpeciesIndexCS() } } -void EEDFTwoTermApproximation::checkSpeciesNoCrossSection() -{ - // warn that a specific species needs cross-section data. - for (size_t k : m_kOthers) { - if (m_phase->moleFraction(k) > options.m_moleFractionThreshold) { - writelog("EEDFTwoTermApproximation:checkSpeciesNoCrossSection\n"); - writelog("Warning:The mole fraction of species {} is more than 0.01 (X = {:.3g}) but it has no cross-section data\n", m_phase->speciesName(k), m_phase->moleFraction(k)); - } - } -} - -void EEDFTwoTermApproximation::updateCS() +void EEDFTwoTermApproximation::updateCrossSections() { // Compute sigma_m and sigma_\epsilon calculateTotalCrossSection(); @@ -500,7 +470,7 @@ void EEDFTwoTermApproximation::updateCS() } // Update the species mole fractions used for EEDF computation -void EEDFTwoTermApproximation::update_mole_fractions() +void EEDFTwoTermApproximation::updateMoleFractions() { double tmp_sum = 0.0; for (size_t k = 0; k < m_X_targets.size(); k++) { @@ -554,8 +524,6 @@ void EEDFTwoTermApproximation::setGridCache() { m_sigma.clear(); m_sigma.resize(m_phase->nCollisions()); - m_sigma_offset.clear(); - m_sigma_offset.resize(m_phase->nCollisions()); m_eps.clear(); m_eps.resize(m_phase->nCollisions()); m_j.clear(); @@ -620,9 +588,6 @@ void EEDFTwoTermApproximation::setGridCache() for (auto& element : x_offset) { element -= collision->threshold(); } - for (size_t i = 0; i < options.m_points; i++) { - m_sigma_offset[k].push_back(linearInterp(m_gridCenter[i], x_offset, y)); - } } } @@ -636,64 +601,4 @@ double EEDFTwoTermApproximation::norm(const Eigen::VectorXd& f, const Eigen::Vec return numericalQuadrature(m_quadratureMethod, p, grid); } - -void EEDFTwoTermApproximation::eeColIntegrals(vector& A1, vector& A2, vector& A3, - double& a, size_t nPoints) -{ - // Ensure vectors are initialized - A1.assign(nPoints, 0.0); - A2.assign(nPoints, 0.0); - A3.assign(nPoints, 0.0); - - // Compute net production frequency - double nu = netProductionFreq(m_f0); - // simulations with repeated calls to update EEDF will produce numerical instability here - double nu_floor = 1e-40; // adjust as needed for stability - if (nu < nu_floor) { - writelog("eeColIntegrals: nu = {:.3e} too small, applying floor\n", nu); - nu = nu_floor; - } - - // Compute effective cross-section term - double sigma_tilde; - for (size_t j = 1; j < nPoints; j++) { - sigma_tilde = m_totalCrossSectionCenter[j] + nu / pow(m_gridEdge[j], 0.5) / m_gamma; - } - - // Compute Coulomb logarithm - double lnLambda; - if (nu > 0.0) { - lnLambda = log(sigma_tilde / nu); - } else { - lnLambda = log(4.0 * Pi * pow(m_gridEdge.back(), 3) / 3.0); - } - - // Compute e-e collision prefactor - a = 4.0 * Pi * ElectronCharge * ElectronCharge * lnLambda / (m_gamma * pow(nu, 2)); - - // Compute integral terms A1, A2, A3 - for (size_t j = 1; j < nPoints; j++) { - double eps_j = m_gridCenter[j]; // Electron energy level - double integral_A1 = 0.0; - double integral_A2 = 0.0; - double integral_A3 = 0.0; - - for (size_t i = 1; i < nPoints; i++) { - double eps_i = m_gridCenter[i]; - double f0_i = m_f0[i]; - - double weight = f0_i * pow(eps_i, 0.5) * exp(-abs(eps_i - eps_j) / eps_i); - - integral_A1 += weight * pow(eps_i, 1.5); - integral_A2 += weight * pow(eps_i, 0.5); - integral_A3 += weight; - } - - // Store computed values - A1[j] = integral_A1; - A2[j] = integral_A2; - A3[j] = integral_A3; - } -} - } diff --git a/src/thermo/PlasmaPhase.cpp b/src/thermo/PlasmaPhase.cpp index d59cc6a3071..9b67d1df8c2 100644 --- a/src/thermo/PlasmaPhase.cpp +++ b/src/thermo/PlasmaPhase.cpp @@ -29,7 +29,7 @@ PlasmaPhase::PlasmaPhase(const string& inputFile, const string& id_) m_electronTemp = temperature(); // Initialize the Boltzmann Solver - m_eedfSolver = make_unique(*this); + m_eedfSolver = make_unique(this); // Set Energy Grid (Hardcoded Defaults for Now) double kTe_max = 60; diff --git a/test/python/test_thermo.py b/test/python/test_thermo.py index abcc02aafd0..da199ff2fe6 100644 --- a/test/python/test_thermo.py +++ b/test/python/test_thermo.py @@ -1305,7 +1305,7 @@ def test_eedf_solver(self): phase = ct.Solution('air-plasma.yaml') phase.TPX = 300., 101325., 'N2:0.79, O2:0.21, N2+:1E-10, Electron:1E-10' phase.reduced_electric_field = 200.0 * 1e-21 # Reduced electric field [V.m^2] - phase.update_EEDF() + phase.update_electron_energy_distribution() grid = phase.electron_energy_levels eedf = phase.electron_energy_distribution From 9d4dc0028ee5fc0948c1a404ea87da555325d31b Mon Sep 17 00:00:00 2001 From: unknown Date: Mon, 18 Aug 2025 10:34:09 -0400 Subject: [PATCH 29/29] removed TwoTermOpt class and cited Phelps database on plasma-eedf example, reversed changes to pfr example, and addressed CI build docs and LLVM errors --- doc/sphinx/yaml/index.md | 2 +- doc/sphinx/yaml/phases.md | 2 +- .../cantera/thermo/EEDFTwoTermApproximation.h | 56 ++++---- samples/python/reactors/pfr.py | 23 ++- samples/python/thermo/plasma-eedf.py | 7 + src/thermo/EEDFTwoTermApproximation.cpp | 134 +++++++++--------- 6 files changed, 112 insertions(+), 112 deletions(-) diff --git a/doc/sphinx/yaml/index.md b/doc/sphinx/yaml/index.md index a397533d056..2209c1c2074 100644 --- a/doc/sphinx/yaml/index.md +++ b/doc/sphinx/yaml/index.md @@ -31,7 +31,7 @@ state](sec-yaml-species-eos), [transport property](sec-yaml-species-transport), YAML reaction definitions include specification of common elements such as the reaction equation and [efficiencies](sec-yaml-efficiencies), as well as parameters specific to the type of [rate parameterization](sec-yaml-rate-types). -See also the [electron collision data format](reactions.html#sec-yaml-electron-collisions) +See also the {ref}`electron collision data format ` used in plasma-phase simulations. ## Mechanism Conversion diff --git a/doc/sphinx/yaml/phases.md b/doc/sphinx/yaml/phases.md index c73f969ece7..811a6d04ccd 100644 --- a/doc/sphinx/yaml/phases.md +++ b/doc/sphinx/yaml/phases.md @@ -910,7 +910,7 @@ Examples: normalize: False ``` -See also the [electron collision data format](reactions.html#sec-yaml-electron-collisions), +See also {ref}`sec-yaml-electron-collisions`, which is used to specify electron-impact cross sections relevant to plasma simulations. :::{versionadded} 2.6 diff --git a/include/cantera/thermo/EEDFTwoTermApproximation.h b/include/cantera/thermo/EEDFTwoTermApproximation.h index ba73b08dd75..bd39137006e 100644 --- a/include/cantera/thermo/EEDFTwoTermApproximation.h +++ b/include/cantera/thermo/EEDFTwoTermApproximation.h @@ -16,26 +16,6 @@ namespace Cantera class PlasmaPhase; -/** - * EEDF solver options. Used internally by class EEDFTwoTermApproximation. - */ -class TwoTermOpt -{ -public: - TwoTermOpt() = default; - - double m_delta0 = 1e14; //!< Initial value of the iteration parameter - size_t m_maxn = 200; //!< Maximum number of iterations - double m_factorM = 4.0; //!< Reduction factor of error - size_t m_points = 150; //!< Number of points for energy grid - double m_rtol = 1e-5; //!< Relative tolerance of EEDF for solving Boltzmann equation - string m_growth = "temporal"; //!< String for the growth model (none, temporal or spatial) - double m_moleFractionThreshold = 0.01; //!< Threshold for species not considered in the Boltzmann solver but present in the mixture - string m_firstguess = "maxwell"; //!< String for EEDF first guess - double m_init_kTe = 2.0; //!< Initial electron mean energy - -}; // end of class TwoTermOpt - //! Boltzmann equation solver for the electron energy distribution function based on //! the two-term approximation. /*! @@ -92,12 +72,6 @@ class EEDFTwoTermApproximation void setGridCache(); - /** - * Options controlling how the calculation is carried out. - * @see TwoTermOpt - */ - TwoTermOpt options; - vector getGridEdge() const { return m_gridEdge; } @@ -111,6 +85,36 @@ class EEDFTwoTermApproximation } protected: + + //! Formerly options for the EEDF solver + + //! The first step size + double m_delta0 = 1e14; + + //! Maximum number of iterations + size_t m_maxn = 200; + + //! The factor for step size change + double m_factorM = 4.0; + + //! The number of points in the EEDF grid + size_t m_points = 150; + + //! Error tolerance for convergence + double m_rtol = 1e-5; + + //! The growth model of EEDF + std::string m_growth = "temporal"; + + //! The threshold for species mole fractions + double m_moleFractionThreshold = 0.01; + + //! The first guess for the EEDF + std::string m_firstguess = "maxwell"; + + //! The initial electron temperature [eV] + double m_init_kTe = 2.0; + //! Pointer to the PlasmaPhase object used to initialize this object. /*! * This PlasmaPhase object provides species, element, and cross-section diff --git a/samples/python/reactors/pfr.py b/samples/python/reactors/pfr.py index 8e414970f46..2bc07c14123 100644 --- a/samples/python/reactors/pfr.py +++ b/samples/python/reactors/pfr.py @@ -124,25 +124,18 @@ states2 = ct.SolutionArray(r2.thermo) # iterate through the PFR cells for n in range(n_steps): - # create new reactor from updated gas - r2 = ct.IdealGasReactor(gas2) - r2.volume = r_vol - upstream = ct.Reservoir(gas2) - downstream = ct.Reservoir(gas2) - m = ct.MassFlowController(upstream, r2, mdot=mass_flow_rate2) - v = ct.PressureController(r2, downstream, primary=m, K=1e-5) - sim2 = ct.ReactorNet([r2]) - + # Set the state of the reservoir to match that of the previous reactor + gas2.TDY = r2.thermo.TDY + upstream.syncState() + # integrate the reactor forward in time until steady state is reached + sim2.reinitialize() sim2.advance_to_steady_state() - + # compute velocity and transform into time u2[n] = mass_flow_rate2 / area / r2.thermo.density - t_r2[n] = r2.mass / mass_flow_rate2 + t_r2[n] = r2.mass / mass_flow_rate2 # residence time in this reactor t2[n] = np.sum(t_r2) + # write output data states2.append(r2.thermo.state) - - # update gas2 for next segment - gas2.TDY = r2.thermo.TDY - ##################################################################### diff --git a/samples/python/thermo/plasma-eedf.py b/samples/python/thermo/plasma-eedf.py index b7df9890349..370d83e4fd5 100644 --- a/samples/python/thermo/plasma-eedf.py +++ b/samples/python/thermo/plasma-eedf.py @@ -4,6 +4,13 @@ Compute EEDF with two term approximation solver at constant E/N. Compare with results from BOLOS. +Air-plasma-Phelps.yaml mechanism file is derived from Phelps cross section data +A compilation of atomic and molecular cross-section data assembled by A. V. Phelps +in the 1970s–1980s for gases such as O₂, N₂, He, Ar, etc. The compilation itself +is unpublished; data used from it are cited as: A. V. Phelps, private communication +(compilation of electron cross-sections), retrieved [8/1/25], from Phelps collection. +(see http://www.lxcat.net/contributors/#d19). + Requires: cantera >= 3.2, matplotlib >= 2.0 .. tags:: Python, plasma diff --git a/src/thermo/EEDFTwoTermApproximation.cpp b/src/thermo/EEDFTwoTermApproximation.cpp index be8083461ee..6ec29158328 100644 --- a/src/thermo/EEDFTwoTermApproximation.cpp +++ b/src/thermo/EEDFTwoTermApproximation.cpp @@ -30,16 +30,16 @@ EEDFTwoTermApproximation::EEDFTwoTermApproximation(PlasmaPhase* s) void EEDFTwoTermApproximation::setLinearGrid(double& kTe_max, size_t& ncell) { - options.m_points = ncell; - m_gridCenter.resize(options.m_points); - m_gridEdge.resize(options.m_points + 1); - m_f0.resize(options.m_points); - m_f0_edge.resize(options.m_points + 1); - for (size_t j = 0; j < options.m_points; j++) { - m_gridCenter[j] = kTe_max * ( j + 0.5 ) / options.m_points; - m_gridEdge[j] = kTe_max * j / options.m_points; - } - m_gridEdge[options.m_points] = kTe_max; + m_points = ncell; + m_gridCenter.resize(m_points); + m_gridEdge.resize(m_points + 1); + m_f0.resize(m_points); + m_f0_edge.resize(m_points + 1); + for (size_t j = 0; j < m_points; j++) { + m_gridCenter[j] = kTe_max * ( j + 0.5 ) / m_points; + m_gridEdge[j] = kTe_max * j / m_points; + } + m_gridEdge[m_points] = kTe_max; setGridCache(); } @@ -54,12 +54,12 @@ int EEDFTwoTermApproximation::calculateDistributionFunction() updateCrossSections(); if (!m_has_EEDF) { - writelog("No existing EEDF. Using first guess method: {}\n", options.m_firstguess); - if (options.m_firstguess == "maxwell") { + writelog("No existing EEDF. Using first guess method: {}\n", m_firstguess); + if (m_firstguess == "maxwell") { writelog("First guess EEDF maxwell\n"); - for (size_t j = 0; j < options.m_points; j++) { - m_f0(j) = 2.0 * pow(1.0 / Pi, 0.5) * pow(options.m_init_kTe, -3. / 2.) * - exp(-m_gridCenter[j] / options.m_init_kTe); + for (size_t j = 0; j < m_points; j++) { + m_f0(j) = 2.0 * pow(1.0 / Pi, 0.5) * pow(m_init_kTe, -3. / 2.) * + exp(-m_gridCenter[j] / m_init_kTe); } } else { throw CanteraError("EEDFTwoTermApproximation::calculateDistributionFunction", @@ -72,7 +72,7 @@ int EEDFTwoTermApproximation::calculateDistributionFunction() // write the EEDF at grid edges vector f(m_f0.data(), m_f0.data() + m_f0.rows() * m_f0.cols()); vector x(m_gridCenter.data(), m_gridCenter.data() + m_gridCenter.rows() * m_gridCenter.cols()); - for (size_t i = 0; i < options.m_points + 1; i++) { + for (size_t i = 0; i < m_points + 1; i++) { m_f0_edge[i] = linearInterp(m_gridEdge[i], x, f); } @@ -87,24 +87,24 @@ void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) { double err0 = 0.0; double err1 = 0.0; - double delta = options.m_delta0; + double delta = m_delta0; - if (options.m_maxn == 0) { + if (m_maxn == 0) { throw CanteraError("EEDFTwoTermApproximation::converge", - "options.m_maxn is zero; no iterations will occur."); + "m_maxn is zero; no iterations will occur."); } - if (options.m_points == 0) { + if (m_points == 0) { throw CanteraError("EEDFTwoTermApproximation::converge", - "options.m_points is zero; the EEDF grid is empty."); + "m_points is zero; the EEDF grid is empty."); } if (isnan(delta) || delta == 0.0) { throw CanteraError("EEDFTwoTermApproximation::converge", - "options.m_delta0 is NaN or zero; solver cannot update."); + "m_delta0 is NaN or zero; solver cannot update."); } - for (size_t n = 0; n < options.m_maxn; n++) { + for (size_t n = 0; n < m_maxn; n++) { if (0.0 < err1 && err1 < err0) { - delta *= log(options.m_factorM) / (log(err0) - log(err1)); + delta *= log(m_factorM) / (log(err0) - log(err1)); } Eigen::VectorXd f0_old = f0; @@ -114,9 +114,9 @@ void EEDFTwoTermApproximation::converge(Eigen::VectorXd& f0) err0 = err1; Eigen::VectorXd Df0 = (f0_old - f0).cwiseAbs(); err1 = norm(Df0, m_gridCenter); - if (err1 < options.m_rtol) { + if (err1 < m_rtol) { break; - } else if (n == options.m_maxn - 1) { + } else if (n == m_maxn - 1) { throw CanteraError("WeaklyIonizedGas::converge", "Convergence failed"); } } @@ -128,7 +128,7 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou // probably extremely ineficient // must be refactored!! - SparseMat PQ(options.m_points, options.m_points); + SparseMat PQ(m_points, m_points); vector g = vector_g(f0); for (size_t k : m_phase->kInelastic()) { @@ -138,8 +138,8 @@ Eigen::VectorXd EEDFTwoTermApproximation::iterate(const Eigen::VectorXd& f0, dou } SparseMat A = matrix_A(f0); - SparseMat I(options.m_points, options.m_points); - for (size_t i = 0; i < options.m_points; i++) { + SparseMat I(m_points, m_points); + for (size_t i = 0; i < m_points; i++) { I.insert(i,i) = 1.0; } A -= PQ; @@ -202,7 +202,7 @@ double EEDFTwoTermApproximation::integralPQ(double a, double b, double u0, doubl vector EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) { - vector g(options.m_points, 0.0); + vector g(m_points, 0.0); const double f_min = 1e-300; // Smallest safe floating-point value // Handle first point (i = 0) @@ -211,7 +211,7 @@ vector EEDFTwoTermApproximation::vector_g(const Eigen::VectorXd& f0) g[0] = log(f1 / f0_) / (m_gridCenter[1] - m_gridCenter[0]); // Handle last point (i = N) - size_t N = options.m_points - 1; + size_t N = m_points - 1; double fN = std::max(f0(N), f_min); double fNm1 = std::max(f0(N - 1), f_min); g[N] = log(fN / fNm1) / (m_gridCenter[N] - m_gridCenter[N - 1]); @@ -239,7 +239,7 @@ SparseMat EEDFTwoTermApproximation::matrix_P(const vector& g, size_t k) tripletList.emplace_back(j, j, p); } - SparseMat P(options.m_points, options.m_points); + SparseMat P(m_points, m_points); P.setFromTriplets(tripletList.begin(), tripletList.end()); return P; } @@ -259,16 +259,16 @@ SparseMat EEDFTwoTermApproximation::matrix_Q(const vector& g, size_t k) tripletList.emplace_back(i, j, q); } - SparseMat Q(options.m_points, options.m_points); + SparseMat Q(m_points, m_points); Q.setFromTriplets(tripletList.begin(), tripletList.end()); return Q; } SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) { - vector a0(options.m_points + 1); - vector a1(options.m_points + 1); - size_t N = options.m_points - 1; + vector a0(m_points + 1); + vector a1(m_points + 1); + size_t N = m_points - 1; // Scharfetter-Gummel scheme double nu = netProductionFrequency(f0); a0[0] = NAN; @@ -276,14 +276,10 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) a0[N+1] = NAN; a1[N+1] = NAN; - // Electron-electron collisions declarations - double a; - vector A1, A2, A3; - double nDensity = m_phase->molarDensity() * Avogadro; double alpha; double E = m_phase->electricField(); - if (options.m_growth == "spatial") { + if (m_growth == "spatial") { double mu = electronMobility(f0); double D = electronDiffusivity(f0); alpha = (mu * E - sqrt(pow(mu * E, 2) - 4 * D * nu * nDensity)) / 2.0 / D / nDensity; @@ -293,8 +289,8 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) double sigma_tilde; double omega = 2 * Pi * m_phase->electricFieldFrequency(); - for (size_t j = 1; j < options.m_points; j++) { - if (options.m_growth == "temporal") { + for (size_t j = 1; j < m_points; j++) { + if (m_growth == "temporal") { sigma_tilde = m_totalCrossSectionEdge[j] + nu / pow(m_gridEdge[j], 0.5) / m_gamma; } else { sigma_tilde = m_totalCrossSectionEdge[j]; @@ -305,7 +301,7 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) double DA = m_gamma / 3.0 * pow(E / nDensity, 2.0) * m_gridEdge[j]; double DB = m_gamma * m_phase->temperature() * Boltzmann / ElectronCharge * m_gridEdge[j] * m_gridEdge[j] * m_sigmaElastic[j]; double D = DA / sigma_tilde * F + DB; - if (options.m_growth == "spatial") { + if (m_growth == "spatial") { W -= m_gamma / 3.0 * 2 * alpha * E / nDensity * m_gridEdge[j] / sigma_tilde; } @@ -326,35 +322,35 @@ SparseMat EEDFTwoTermApproximation::matrix_A(const Eigen::VectorXd& f0) // zero flux b.c. at energy = 0 tripletList.emplace_back(0, 0, a0[1]); - for (size_t j = 1; j < options.m_points - 1; j++) { + for (size_t j = 1; j < m_points - 1; j++) { tripletList.emplace_back(j, j, a0[j+1] - a1[j]); } // upper diagonal - for (size_t j = 0; j < options.m_points - 1; j++) { + for (size_t j = 0; j < m_points - 1; j++) { tripletList.emplace_back(j, j+1, a1[j+1]); } // lower diagonal - for (size_t j = 1; j < options.m_points; j++) { + for (size_t j = 1; j < m_points; j++) { tripletList.emplace_back(j, j-1, -a0[j]); } // zero flux b.c. tripletList.emplace_back(N, N, -a1[N]); - SparseMat A(options.m_points, options.m_points); + SparseMat A(m_points, m_points); A.setFromTriplets(tripletList.begin(), tripletList.end()); //plus G - SparseMat G(options.m_points, options.m_points); - if (options.m_growth == "temporal") { - for (size_t i = 0; i < options.m_points; i++) { + SparseMat G(m_points, m_points); + if (m_growth == "temporal") { + for (size_t i = 0; i < m_points; i++) { G.insert(i, i) = 2.0 / 3.0 * (pow(m_gridEdge[i+1], 1.5) - pow(m_gridEdge[i], 1.5)) * nu; } - } else if (options.m_growth == "spatial") { + } else if (m_growth == "spatial") { double nDensity = m_phase->molarDensity() * Avogadro; - for (size_t i = 0; i < options.m_points; i++) { + for (size_t i = 0; i < m_points; i++) { double sigma_c = 0.5 * (m_totalCrossSectionEdge[i] + m_totalCrossSectionEdge[i + 1]); G.insert(i, i) = - alpha * m_gamma / 3 * (alpha * (pow(m_gridEdge[i + 1], 2) - pow(m_gridEdge[i], 2)) / sigma_c / 2 - E / nDensity * (m_gridEdge[i + 1] / m_totalCrossSectionEdge[i + 1] - m_gridEdge[i] / m_totalCrossSectionEdge[i])); @@ -384,9 +380,9 @@ double EEDFTwoTermApproximation::netProductionFrequency(const Eigen::VectorXd& f double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) { - vector y(options.m_points, 0.0); + vector y(m_points, 0.0); double nu = netProductionFrequency(f0); - for (size_t i = 0; i < options.m_points; i++) { + for (size_t i = 0; i < m_points; i++) { if (m_gridCenter[i] != 0.0) { y[i] = m_gridCenter[i] * f0(i) / (m_totalCrossSectionCenter[i] + nu / m_gamma / pow(m_gridCenter[i], 0.5)); @@ -401,8 +397,8 @@ double EEDFTwoTermApproximation::electronDiffusivity(const Eigen::VectorXd& f0) double EEDFTwoTermApproximation::electronMobility(const Eigen::VectorXd& f0) { double nu = netProductionFrequency(f0); - vector y(options.m_points + 1, 0.0); - for (size_t i = 1; i < options.m_points; i++) { + vector y(m_points + 1, 0.0); + for (size_t i = 1; i < m_points; i++) { // calculate df0 at i-1/2 double df0 = (f0(i) - f0(i-1)) / (m_gridCenter[i] - m_gridCenter[i-1]); if (m_gridEdge[i] != 0.0) { @@ -486,17 +482,17 @@ void EEDFTwoTermApproximation::updateMoleFractions() void EEDFTwoTermApproximation::calculateTotalCrossSection() { - m_totalCrossSectionCenter.assign(options.m_points, 0.0); - m_totalCrossSectionEdge.assign(options.m_points + 1, 0.0); + m_totalCrossSectionCenter.assign(m_points, 0.0); + m_totalCrossSectionEdge.assign(m_points + 1, 0.0); for (size_t k = 0; k < m_phase->nCollisions(); k++) { auto& x = m_phase->collisionRate(k)->energyLevels(); auto& y = m_phase->collisionRate(k)->crossSections(); - for (size_t i = 0; i < options.m_points; i++) { + for (size_t i = 0; i < m_points; i++) { m_totalCrossSectionCenter[i] += m_X_targets[m_klocTargets[k]] * linearInterp(m_gridCenter[i], x, y); } - for (size_t i = 0; i < options.m_points + 1; i++) { + for (size_t i = 0; i < m_points + 1; i++) { m_totalCrossSectionEdge[i] += m_X_targets[m_klocTargets[k]] * linearInterp(m_gridEdge[i], x, y); } @@ -506,14 +502,14 @@ void EEDFTwoTermApproximation::calculateTotalCrossSection() void EEDFTwoTermApproximation::calculateTotalElasticCrossSection() { m_sigmaElastic.clear(); - m_sigmaElastic.resize(options.m_points, 0.0); + m_sigmaElastic.resize(m_points, 0.0); for (size_t k : m_phase->kElastic()) { auto& x = m_phase->collisionRate(k)->energyLevels(); auto& y = m_phase->collisionRate(k)->crossSections(); // Note: // moleFraction(m_kTargets[k]) <=> m_X_targets[m_klocTargets[k]] double mass_ratio = ElectronMass / (m_phase->molecularWeight(m_kTargets[k]) / Avogadro); - for (size_t i = 0; i < options.m_points; i++) { + for (size_t i = 0; i < m_points; i++) { m_sigmaElastic[i] += 2.0 * mass_ratio * m_X_targets[m_klocTargets[k]] * linearInterp(m_gridEdge[i], x, y); } @@ -534,21 +530,21 @@ void EEDFTwoTermApproximation::setGridCache() auto& collision = m_phase->collisionRate(k); auto& x = collision->energyLevels(); auto& y = collision->crossSections(); - vector eps1(options.m_points + 1); + vector eps1(m_points + 1); int shiftFactor = (collision->kind() == "ionization") ? 2 : 1; - for (size_t i = 0; i < options.m_points + 1; i++) { + for (size_t i = 0; i < m_points + 1; i++) { eps1[i] = clip(shiftFactor * m_gridEdge[i] + collision->threshold(), - m_gridEdge[0] + 1e-9, m_gridEdge[options.m_points] - 1e-9); + m_gridEdge[0] + 1e-9, m_gridEdge[m_points] - 1e-9); } vector nodes = eps1; - for (size_t i = 0; i < options.m_points + 1; i++) { - if (m_gridEdge[i] >= eps1[0] && m_gridEdge[i] <= eps1[options.m_points]) { + for (size_t i = 0; i < m_points + 1; i++) { + if (m_gridEdge[i] >= eps1[0] && m_gridEdge[i] <= eps1[m_points]) { nodes.push_back(m_gridEdge[i]); } } for (size_t i = 0; i < x.size(); i++) { - if (x[i] >= eps1[0] && x[i] <= eps1[options.m_points]) { + if (x[i] >= eps1[0] && x[i] <= eps1[m_points]) { nodes.push_back(x[i]); } }