diff --git a/data/inputs/critProperties.xml b/data/inputs/critProperties.xml index 654d41c733..bf9f49d150 100644 --- a/data/inputs/critProperties.xml +++ b/data/inputs/critProperties.xml @@ -76,7 +76,7 @@ - + diff --git a/include/cantera/thermo/MixtureFugacityTP.h b/include/cantera/thermo/MixtureFugacityTP.h index 1658cac337..518c133e40 100644 --- a/include/cantera/thermo/MixtureFugacityTP.h +++ b/include/cantera/thermo/MixtureFugacityTP.h @@ -119,6 +119,13 @@ class MixtureFugacityTP : public ThermoPhase throw NotImplementedError("MixtureFugacityTP::getdlnActCoeffdlnN_diag"); } + + //! @name Molar Thermodynamic properties + //! @{ + + virtual double enthalpy_mole() const; + virtual double entropy_mole() const; + //@} /// @name Partial Molar Properties of the Solution //@{ @@ -272,37 +279,9 @@ class MixtureFugacityTP : public ThermoPhase */ virtual void setPressure(doublereal p); -protected: - /** - * Calculate the density of the mixture using the partial molar volumes and - * mole fractions as input - * - * The formula for this is - * - * \f[ - * \rho = \frac{\sum_k{X_k W_k}}{\sum_k{X_k V_k}} - * \f] - * - * where \f$X_k\f$ are the mole fractions, \f$W_k\f$ are the molecular - * weights, and \f$V_k\f$ are the pure species molar volumes. - * - * Note, the basis behind this formula is that in an ideal solution the - * partial molar volumes are equal to the pure species molar volumes. We - * have additionally specified in this class that the pure species molar - * volumes are independent of temperature and pressure. - */ - virtual void calcDensity(); - -public: - virtual void setState_TP(doublereal T, doublereal pres); - virtual void setState_TR(doublereal T, doublereal rho); - virtual void setState_TPX(doublereal t, doublereal p, const doublereal* x); - protected: virtual void compositionChanged(); - void setMoleFractions_NoState(const doublereal* const x); -protected: //! Updates the reference state thermodynamic functions at the current T of //! the solution. /*! @@ -318,6 +297,9 @@ class MixtureFugacityTP : public ThermoPhase * - m_s0_R; */ virtual void _updateReferenceStateThermo() const; + + //! Temporary storage - length = m_kk. + mutable vector_fp m_tmpV; public: /// @name Thermodynamic Values for the Species Reference States @@ -430,7 +412,6 @@ class MixtureFugacityTP : public ThermoPhase * setState_TP() routines. Infinite loops would result if it were not * protected. * - * -> why is this not const? * * @param TKelvin Temperature in Kelvin * @param pressure Pressure in Pascals (Newton/m**2) @@ -509,6 +490,7 @@ class MixtureFugacityTP : public ThermoPhase * @return The saturation pressure at the given temperature */ virtual doublereal satPressure(doublereal TKelvin); + virtual void getActivityConcentrations(double* c) const; protected: //! Calculate the pressure given the temperature and the molar volume @@ -534,9 +516,46 @@ class MixtureFugacityTP : public ThermoPhase virtual void updateMixingExpressions(); //@} + /// @name Critical State Properties. + //@{ -protected: - virtual void invalidateCache(); + virtual double critTemperature() const; + virtual double critPressure() const; + virtual double critVolume() const; + virtual double critCompressibility() const; + virtual double critDensity() const; + virtual void calcCriticalConditions(double& pc, double& tc, double& vc) const; + + //! Solve the cubic equation of state + /*! + * + * Returns the number of solutions found. For the gas phase solution, it returns + * a positive number (1 or 2). If it only finds the liquid branch solution, + * it will return -1 or -2 instead of 1 or 2. + * If it returns 0, then there is an error. + * The cubic equation is solved using Nickall's method + * (Ref: The Mathematical Gazette(1993), 77(November), 354--359, + * https://www.jstor.org/stable/3619777) + * + * @param T temperature (kelvin) + * @param pres pressure (Pa) + * @param a "a" parameter in the non-ideal EoS [Pa-m^6/kmol^2] + * @param b "b" parameter in the non-ideal EoS [m^3/kmol] + * @param aAlpha a*alpha (temperature dependent function for P-R EoS, 1 for R-K EoS) + * @param Vroot Roots of the cubic equation for molar volume (m3/kmol) + * @param an constant used in cubic equation + * @param bn constant used in cubic equation + * @param cn constant used in cubic equation + * @param dn constant used in cubic equation + * @param tc Critical temperature (kelvin) + * @param vc Critical volume + * @returns the number of solutions found + */ + int solveCubic(double T, double pres, double a, double b, + double aAlpha, double Vroot[3], double an, + double bn, double cn, double dn, double tc, double vc) const; + + //@} //! Storage for the current values of the mole fractions of the species /*! @@ -556,10 +575,6 @@ class MixtureFugacityTP : public ThermoPhase //! Force the system to be on a particular side of the spinodal curve int forcedState_; - //! The last temperature at which the reference state thermodynamic - //! properties were calculated at. - mutable doublereal m_Tlast_ref; - //! Temporary storage for dimensionless reference state enthalpies mutable vector_fp m_h0_RT; @@ -571,6 +586,7 @@ class MixtureFugacityTP : public ThermoPhase //! Temporary storage for dimensionless reference state entropies mutable vector_fp m_s0_R; + }; } diff --git a/include/cantera/thermo/PengRobinson.h b/include/cantera/thermo/PengRobinson.h new file mode 100644 index 0000000000..ba7577d125 --- /dev/null +++ b/include/cantera/thermo/PengRobinson.h @@ -0,0 +1,317 @@ +//! @file PengRobinson.h + +// 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_PENGROBINSON_H +#define CT_PENGROBINSON_H + +#include "MixtureFugacityTP.h" +#include "cantera/base/Array.h" + +namespace Cantera +{ +/** + * Implementation of a multi-species Peng-Robinson equation of state + * + * @ingroup thermoprops + */ +class PengRobinson : public MixtureFugacityTP +{ +public: + + //! Construct and initialize a PengRobinson object directly from an + //! input file + /*! + * @param infile Name of the input file containing the phase YAML data. + * If blank, an empty phase will be created. + * @param id ID of the phase in the input file. If empty, the + * first phase definition in the input file will be used. + */ + explicit PengRobinson(const std::string& infile="", + const std::string& id=""); + + virtual std::string type() const { + return "PengRobinson"; + } + + //! @name Molar Thermodynamic properties + //! @{ + + virtual double cp_mole() const; + virtual double cv_mole() const; + + //! @} + //! @name Mechanical Properties + //! @{ + + //! Return the thermodynamic pressure (Pa). + /*! + * Since the mass density, temperature, and mass fractions are stored, + * this method uses these values to implement the + * mechanical equation of state \f$ P(T, \rho, Y_1, \dots, Y_K) \f$. + * + * \f[ + * P = \frac{RT}{v-b_{mix}} - \frac{\left(\alpha a\right)_{mix}}{v^2 + 2b_{mix}v - b_{mix}^2} + * \f] + * + * where: + * + * \f[ + * \alpha = \left[ 1 + \kappa \left(1-T_r^{0.5}\right)\right]^2 + * \f] + * + * and + * + * \f[ + * \kappa = \left(0.37464 + 1.54226\omega - 0.26992\omega^2\right), \qquad \qquad \text{For } \omega <= 0.491 \\ + * \kappa = \left(0.379642 + 1.487503\omega - 0.164423\omega^2 + 0.016667\omega^3 \right), \qquad \text{For } \omega > 0.491 + * \f] + * + * Coefficients \f$ a_mix, b_mix \f$ and \f$(a \alpha)_{mix}\f$ are calculated as + * + * \f[ + * a_{mix} = \sum_i \sum_j X_i X_j a_{i, j} = \sum_i \sum_j X_i X_j \sqrt{a_i a_j} + * \f] + * + * \f[ + * b_{mix} = \sum_i X_i b_i + * \f] + * + * \f[ + * {a \alpha}_{mix} = \sum_i \sum_j X_i X_j {a \alpha}_{i, j} = \sum_i \sum_j X_i X_j \sqrt{a_i a_j} \sqrt{\alpha_i \alpha_j} + * \f] + */ + virtual double pressure() const; + + // @} + + //! Returns the standard concentration \f$ C^0_k \f$, which is used to + //! normalize the generalized concentration. + /*! + * This is defined as the concentration by which the generalized + * concentration is normalized to produce the activity. + * The ideal gas mixture is considered as the standard or reference state here. + * Since the activity for an ideal gas mixture is simply the mole fraction, + * for an ideal gas, \f$ C^0_k = P/\hat R T \f$. + * + * @param k Optional parameter indicating the species. The default is to + * assume this refers to species 0. + * @return + * Returns the standard Concentration in units of m^3 / kmol. + */ + virtual double standardConcentration(size_t k=0) const; + + //! Get the array of non-dimensional activity coefficients at the current + //! solution temperature, pressure, and solution concentration. + /*! + * For all objects with the Mixture Fugacity approximation, we define the + * standard state as an ideal gas at the current temperature and pressure of + * the solution. The activities are based on this standard state. + * + * @param ac Output vector of activity coefficients. Length: m_kk. + */ + virtual void getActivityCoefficients(double* ac) const; + + /// @name Partial Molar Properties of the Solution + //@{ + + virtual void getChemPotentials(double* mu) const; + virtual void getPartialMolarEnthalpies(double* hbar) const; + virtual void getPartialMolarEntropies(double* sbar) const; + virtual void getPartialMolarIntEnergies(double* ubar) const; + virtual void getPartialMolarCp(double* cpbar) const; + virtual void getPartialMolarVolumes(double* vbar) const; + + //! Calculate species-specific critical temperature + /*! + * The temperature dependent parameter in P-R EoS is calculated as + * \f$ T_{crit} = (0.0778 a)/(0.4572 b R) \f$ + * Units: Kelvin + * + * @param a species-specific coefficients used in P-R EoS + * @param b species-specific coefficients used in P-R EoS + */ + virtual double speciesCritTemperature(double a, double b) const; + + //@} + //! @name Initialization Methods - For Internal use + /*! + * The following methods are used in the process of constructing + * the phase and setting its parameters from a specification in an + * input file. They are not normally used in application programs. + * To see how they are used, see importPhase(). + */ + //@{ + + virtual bool addSpecies(shared_ptr spec); + virtual void initThermo(); + + //! Retrieve a and b coefficients by looking up tabulated critical parameters + /*! + * If pureFluidParameters are not provided for any species in the phase, + * consult the critical properties tabulated in data/inputs/critProperties.xml. + * If the species is found there, calculate pure fluid parameters a_k and b_k as: + * \f[ a_k = 0.4278 R^2 T_c^2 / P_c \f] + * + * and: + * \f[ b_k = 0.08664 R T_c/ P_c \f] + * + * @param iName Name of the species + */ + virtual vector_fp getCoeff(const std::string& iName); + + //! Set the pure fluid interaction parameters for a species + /*! + * @param species Name of the species + * @param a "a" parameter in the Peng-Robinson model [Pa-m^6/kmol^2] + * @param b "b" parameter in the Peng-Robinson model [m^3/kmol] + * @param w acentric factor + */ + void setSpeciesCoeffs(const std::string& species, double a, double b, + double w); + + //! Set values for the interaction parameter between two species + /*! + * The "a" parameter for interactions between species *i* and *j* is + * assumed by default to be computed as: + * \f[ a_{ij} = \sqrt{a_{i, 0} a_{j, 0}} + \sqrt{a_{i, 1} a_{j, 1}} T \f] + * + * @param species_i Name of one species + * @param species_j Name of the other species + * @param a constant term in the "a" expression [Pa-m^6/kmol^2] + * @param alpha dimensionless function of T_r and \omega + */ + void setBinaryCoeffs(const std::string& species_i, + const std::string& species_j, double a); + +protected: + // Special functions inherited from MixtureFugacityTP + virtual double sresid() const; + virtual double hresid() const; + + virtual double liquidVolEst(double TKelvin, double& pres) const; + virtual double densityCalc(double TKelvin, double pressure, int phase, double rhoguess); + + virtual double densSpinodalLiquid() const; + virtual double densSpinodalGas() const; + virtual double dpdVCalc(double TKelvin, double molarVol, double& presCalc) const; + + // Special functions not inherited from MixtureFugacityTP + + //! Calculate temperature derivative d(a*alpha)/dT + /*! + * These are stored internally. + */ + double daAlpha_dT() const; + + //! Calculate second derivative d2(a*alpha)/dT2 + /*! + * These are stored internally. + */ + double d2aAlpha_dT2() const; + +public: + + //! Calculate dpdV and dpdT at the current conditions + /*! + * These are stored internally. + */ + void calculatePressureDerivatives() const; + + //! Update the a, b and alpha parameters + /*! + * The a and the b parameters depend on the mole fraction and the + * parameter alpha depends on the temperature. This function updates + * the internal numbers based on the state of the object. + */ + virtual void updateMixingExpressions(); + + //! Calculate the a, b and the alpha parameters given the temperature + /*! + * This function doesn't change the internal state of the object, so it is a + * const function. It does use the stored mole fractions in the object. + * + * @param aCalc (output) Returns the a value + * @param bCalc (output) Returns the b value. + * @param aAlpha (output) Returns the (a*alpha) value. + */ + void calculateAB(double& aCalc, double& bCalc, double& aAlpha) const; + + void calcCriticalConditions(double& pc, double& tc, double& vc) const; + + //! Prepare variables and call the function to solve the cubic equation of state + int solveCubic(double T, double pres, double a, double b, double aAlpha, + double Vroot[3]) const; +protected: + //! Value of b in the equation of state + /*! + * m_b is a function the mole fractions and species-specific b values. + */ + double m_b; + + //! Value of a and alpha in the equation of state + /*! + * m_aAlpha_mix is a function of the temperature and the mole fractions. m_a depends only on the mole fractions. + */ + double m_a; + double m_aAlpha_mix; + + // Vectors required to store a_coeff, b_coeff, alpha, kappa and other values for every species. Length = m_kk + vector_fp m_b_coeffs; + vector_fp m_kappa; + mutable vector_fp m_dalphadT; + mutable vector_fp m_d2alphadT2; + vector_fp m_alpha; + + //Matrices for Binary coefficients a_{i,j} and {a*alpha}_{i.j} are saved in an Array form. Length = (m_kk, m_kk) + Array2D m_a_coeffs; + Array2D m_aAlpha_binary; + + int m_NSolns; + + double m_Vroot[3]; + + //! Temporary storage - length = m_kk. + mutable vector_fp m_pp; + + // Partial molar volumes of the species + mutable vector_fp m_partialMolarVolumes; + + //! The derivative of the pressure with respect to the volume + /*! + * Calculated at the current conditions. temperature and mole number kept + * constant + */ + mutable double m_dpdV; + + //! The derivative of the pressure with respect to the temperature + /*! + * Calculated at the current conditions. Total volume and mole number kept + * constant + */ + mutable double m_dpdT; + + //! Vector of derivatives of pressure with respect to mole number + /*! + * Calculated at the current conditions. Total volume, temperature and + * other mole number kept constant + */ + mutable vector_fp m_dpdni; + +private: + //! Omega constants: a0 (= omega_a) and b0 (= omega_b) values used in Peng-Robinson equation of state + /*! + * These values are calculated by solving P-R cubic equation at the critical point. + */ + static const double omega_a; + + //! Omega constant for b + static const double omega_b; + + //! Omega constant for the critical molar volume + static const double omega_vc; +}; +} + +#endif diff --git a/include/cantera/thermo/RedlichKwongMFTP.h b/include/cantera/thermo/RedlichKwongMFTP.h index d6317e33d9..f28b58bb13 100644 --- a/include/cantera/thermo/RedlichKwongMFTP.h +++ b/include/cantera/thermo/RedlichKwongMFTP.h @@ -46,13 +46,9 @@ class RedlichKwongMFTP : public MixtureFugacityTP } //! @name Molar Thermodynamic properties - //! @{ - - virtual doublereal enthalpy_mole() const; - virtual doublereal entropy_mole() const; + //! @{ virtual doublereal cp_mole() const; virtual doublereal cv_mole() const; - //! @} //! @name Mechanical Properties //! @{ @@ -71,32 +67,7 @@ class RedlichKwongMFTP : public MixtureFugacityTP // @} -protected: - /** - * Calculate the density of the mixture using the partial molar volumes and - * mole fractions as input - * - * The formula for this is - * - * \f[ - * \rho = \frac{\sum_k{X_k W_k}}{\sum_k{X_k V_k}} - * \f] - * - * where \f$X_k\f$ are the mole fractions, \f$W_k\f$ are the molecular - * weights, and \f$V_k\f$ are the pure species molar volumes. - * - * Note, the basis behind this formula is that in an ideal solution the - * partial molar volumes are equal to the species standard state molar - * volumes. The species molar volumes may be functions of temperature and - * pressure. - */ - virtual void calcDensity(); - - virtual void setTemperature(const doublereal temp); - virtual void compositionChanged(); - public: - virtual void getActivityConcentrations(doublereal* c) const; //! Returns the standard concentration \f$ C^0_k \f$, which is used to //! normalize the generalized concentration. @@ -149,16 +120,6 @@ class RedlichKwongMFTP : public MixtureFugacityTP virtual void getPartialMolarCp(doublereal* cpbar) const; virtual void getPartialMolarVolumes(doublereal* vbar) const; - //@} - /// @name Critical State Properties. - //@{ - - virtual doublereal critTemperature() const; - virtual doublereal critPressure() const; - virtual doublereal critVolume() const; - virtual doublereal critCompressibility() const; - virtual doublereal critDensity() const; - public: //@} //! @name Initialization Methods - For Internal use @@ -245,11 +206,10 @@ class RedlichKwongMFTP : public MixtureFugacityTP public: virtual doublereal liquidVolEst(doublereal TKelvin, doublereal& pres) const; - virtual doublereal densityCalc(doublereal TKelvin, doublereal pressure, int phase, doublereal rhoguess); + virtual doublereal densityCalc(doublereal T, doublereal pressure, int phase, doublereal rhoguess); virtual doublereal densSpinodalLiquid() const; virtual doublereal densSpinodalGas() const; - virtual doublereal pressureCalc(doublereal TKelvin, doublereal molarVol) const; virtual doublereal dpdVCalc(doublereal TKelvin, doublereal molarVol, doublereal& presCalc) const; //! Calculate dpdV and dpdT at the current conditions @@ -258,15 +218,13 @@ class RedlichKwongMFTP : public MixtureFugacityTP */ void pressureDerivatives() const; - virtual void updateMixingExpressions(); - //! Update the a and b parameters /*! * The a and the b parameters depend on the mole fraction and the * temperature. This function updates the internal numbers based on the * state of the object. */ - void updateAB(); + virtual void updateMixingExpressions(); //! Calculate the a and the b parameters given the temperature /*! @@ -283,21 +241,10 @@ class RedlichKwongMFTP : public MixtureFugacityTP doublereal da_dt() const; - void calcCriticalConditions(doublereal a, doublereal b, doublereal a0_coeff, doublereal aT_coeff, - doublereal& pc, doublereal& tc, doublereal& vc) const; + void calcCriticalConditions(doublereal& pc, doublereal& tc, doublereal& vc) const; - //! Solve the cubic equation of state - /*! - * The R-K equation of state may be solved via the following formula: - * - * V**3 - V**2(RT/P) - V(RTb/P - a/(P T**.5) + b*b) - (a b / (P T**.5)) = 0 - * - * Returns the number of solutions found. If it only finds the liquid - * branch solution, it will return a -1 or a -2 instead of 1 or 2. If it - * returns 0, then there is an error. - */ - int NicholsSolve(double TKelvin, double pres, doublereal a, doublereal b, - doublereal Vroot[3]) const; + //! Prepare variables and call the function to solve the cubic equation of state + int solveCubic(double T, double pres, double a, double b, double Vroot[3]) const; protected: //! Form of the temperature parameterization @@ -331,9 +278,6 @@ class RedlichKwongMFTP : public MixtureFugacityTP //! Temporary storage - length = m_kk. mutable vector_fp m_pp; - //! Temporary storage - length = m_kk. - mutable vector_fp m_tmpV; - // Partial molar volumes of the species mutable vector_fp m_partialMolarVolumes; @@ -358,7 +302,7 @@ class RedlichKwongMFTP : public MixtureFugacityTP */ mutable vector_fp dpdni_; -public: +private: //! Omega constant for a -> value of a in terms of critical properties /*! * this was calculated from a small nonlinear solve diff --git a/interfaces/cython/cantera/ctml_writer.py b/interfaces/cython/cantera/ctml_writer.py index 5c65bd1c85..5e750088db 100644 --- a/interfaces/cython/cantera/ctml_writer.py +++ b/interfaces/cython/cantera/ctml_writer.py @@ -853,11 +853,11 @@ def build(self,a): f= a.addChild("pureFluidParameters") f['species'] = self._species s = '%.10g, %.10g\n' % (self._acoeff[0], self._acoeff[1]) - ac = f.addChild("a_coeff",s) + ac = f.addChild("a_coeff", s) ac["units"] = _upres+'-'+_ulen+'6/'+_umol+'2' ac["model"] = "linear_a" s = '%.10g\n' % self._bcoeff - bc = f.addChild("b_coeff",s) + bc = f.addChild("b_coeff", s) bc["units"] = _ulen+'3/'+_umol @@ -872,12 +872,12 @@ def build(self,a): f["species2"] = self._species2 f["species1"] = self._species1 s = '%.10g, %.10g\n' % (self._acoeff[0], self._acoeff[1]) - ac = f.addChild("a_coeff",s) + ac = f.addChild("a_coeff", s) ac["units"] = _upres+'-'+_ulen+'6/'+_umol+'2' ac["model"] = "linear_a" if self._bcoeff: s = '%.10g\n' % self._bcoeff - bc = f.addChild("b_coeff",s) + bc = f.addChild("b_coeff", s) bc["units"] = _ulen+'3/'+_umol @@ -2397,7 +2397,6 @@ def build(self, p): k = ph.addChild("kinetics") k['model'] = self._kin - class ideal_interface(phase): """A chemically-reacting ideal surface solution of multiple species.""" def __init__(self, diff --git a/interfaces/cython/cantera/test/test_thermo.py b/interfaces/cython/cantera/test/test_thermo.py index 05d74c1852..51a63eaccc 100644 --- a/interfaces/cython/cantera/test/test_thermo.py +++ b/interfaces/cython/cantera/test/test_thermo.py @@ -913,7 +913,6 @@ def test_nondimensional(self): self.assertNear(np.dot(g.standard_gibbs_RT, g.X) - Smix_R, g.gibbs_mole / (R*g.T)) - class TestInterfacePhase(utilities.CanteraTest): def setUp(self): self.gas = ct.Solution('diamond.xml', 'gas') diff --git a/src/thermo/MixtureFugacityTP.cpp b/src/thermo/MixtureFugacityTP.cpp index b16d3e8357..46d1fa435d 100644 --- a/src/thermo/MixtureFugacityTP.cpp +++ b/src/thermo/MixtureFugacityTP.cpp @@ -19,8 +19,7 @@ namespace Cantera MixtureFugacityTP::MixtureFugacityTP() : iState_(FLUID_GAS), - forcedState_(FLUID_UNDEFINED), - m_Tlast_ref(-1.0) + forcedState_(FLUID_UNDEFINED) { } @@ -44,6 +43,23 @@ int MixtureFugacityTP::reportSolnBranchActual() const return iState_; } +// ---- Molar Thermodynamic Properties --------------------------- +double MixtureFugacityTP::enthalpy_mole() const +{ + double h_ideal = RT() * mean_X(m_h0_RT); + double h_nonideal = hresid(); + return h_ideal + h_nonideal; +} + + +double MixtureFugacityTP::entropy_mole() const +{ + double s_ideal = GasConstant * (mean_X(m_s0_R) - sum_xlogx() + - std::log(pressure()/refPressure())); + double s_nonideal = sresid(); + return s_ideal + s_nonideal; +} + // ---- Partial Molar Properties of the Solution ----------------- void MixtureFugacityTP::getChemPotentials_RT(doublereal* muRT) const @@ -58,7 +74,6 @@ void MixtureFugacityTP::getChemPotentials_RT(doublereal* muRT) const void MixtureFugacityTP::getStandardChemPotentials(doublereal* g) const { - _updateReferenceStateThermo(); copy(m_g0_RT.begin(), m_g0_RT.end(), g); double tmp = log(pressure() / refPressure()); for (size_t k = 0; k < m_kk; k++) { @@ -73,7 +88,6 @@ void MixtureFugacityTP::getEnthalpy_RT(doublereal* hrt) const void MixtureFugacityTP::getEntropy_R(doublereal* sr) const { - _updateReferenceStateThermo(); copy(m_s0_R.begin(), m_s0_R.end(), sr); double tmp = log(pressure() / refPressure()); for (size_t k = 0; k < m_kk; k++) { @@ -83,7 +97,6 @@ void MixtureFugacityTP::getEntropy_R(doublereal* sr) const void MixtureFugacityTP::getGibbs_RT(doublereal* grt) const { - _updateReferenceStateThermo(); copy(m_g0_RT.begin(), m_g0_RT.end(), grt); double tmp = log(pressure() / refPressure()); for (size_t k = 0; k < m_kk; k++) { @@ -93,7 +106,6 @@ void MixtureFugacityTP::getGibbs_RT(doublereal* grt) const void MixtureFugacityTP::getPureGibbs(doublereal* g) const { - _updateReferenceStateThermo(); scale(m_g0_RT.begin(), m_g0_RT.end(), g, RT()); double tmp = log(pressure() / refPressure()) * RT(); for (size_t k = 0; k < m_kk; k++) { @@ -103,7 +115,6 @@ void MixtureFugacityTP::getPureGibbs(doublereal* g) const void MixtureFugacityTP::getIntEnergy_RT(doublereal* urt) const { - _updateReferenceStateThermo(); copy(m_h0_RT.begin(), m_h0_RT.end(), urt); for (size_t i = 0; i < m_kk; i++) { urt[i] -= 1.0; @@ -112,13 +123,11 @@ void MixtureFugacityTP::getIntEnergy_RT(doublereal* urt) const void MixtureFugacityTP::getCp_R(doublereal* cpr) const { - _updateReferenceStateThermo(); copy(m_cp0_R.begin(), m_cp0_R.end(), cpr); } void MixtureFugacityTP::getStandardVolumes(doublereal* vol) const { - _updateReferenceStateThermo(); for (size_t i = 0; i < m_kk; i++) { vol[i] = RT() / pressure(); } @@ -128,13 +137,11 @@ void MixtureFugacityTP::getStandardVolumes(doublereal* vol) const void MixtureFugacityTP::getEnthalpy_RT_ref(doublereal* hrt) const { - _updateReferenceStateThermo(); copy(m_h0_RT.begin(), m_h0_RT.end(), hrt); } void MixtureFugacityTP::getGibbs_RT_ref(doublereal* grt) const { - _updateReferenceStateThermo(); copy(m_g0_RT.begin(), m_g0_RT.end(), grt); } @@ -146,25 +153,21 @@ void MixtureFugacityTP::getGibbs_ref(doublereal* g) const const vector_fp& MixtureFugacityTP::gibbs_RT_ref() const { - _updateReferenceStateThermo(); return m_g0_RT; } void MixtureFugacityTP::getEntropy_R_ref(doublereal* er) const { - _updateReferenceStateThermo(); copy(m_s0_R.begin(), m_s0_R.end(), er); } void MixtureFugacityTP::getCp_R_ref(doublereal* cpr) const { - _updateReferenceStateThermo(); copy(m_cp0_R.begin(), m_cp0_R.end(), cpr); } void MixtureFugacityTP::getStandardVolumes_ref(doublereal* vol) const { - _updateReferenceStateThermo(); for (size_t i = 0; i < m_kk; i++) { vol[i]= RT() / refPressure(); } @@ -198,7 +201,7 @@ void MixtureFugacityTP::setStateFromXML(const XML_Node& state) double rho = getFloat(state, "density", "density"); setState_TR(t, rho); } else if (doTP) { - double rho = Phase::density(); + double rho = density(); setState_TR(t, rho); } } @@ -222,113 +225,91 @@ bool MixtureFugacityTP::addSpecies(shared_ptr spec) void MixtureFugacityTP::setTemperature(const doublereal temp) { + Phase::setTemperature(temp); _updateReferenceStateThermo(); - setState_TR(temperature(), density()); -} - -void MixtureFugacityTP::setPressure(doublereal p) -{ - setState_TP(temperature(), p); - } - -void MixtureFugacityTP::compositionChanged() -{ - Phase::compositionChanged(); - getMoleFractions(moleFractions_.data()); -} - -void MixtureFugacityTP::setMoleFractions_NoState(const doublereal* const x) -{ - Phase::setMoleFractions(x); - getMoleFractions(moleFractions_.data()); + // depends on mole fraction and temperature updateMixingExpressions(); + iState_ = phaseState(true); } -void MixtureFugacityTP::calcDensity() -{ - throw NotImplementedError("MixtureFugacityTP::calcDensity"); -} - -void MixtureFugacityTP::setState_TP(doublereal t, doublereal pres) +void MixtureFugacityTP::setPressure(doublereal p) { // A pretty tricky algorithm is needed here, due to problems involving // standard states of real fluids. For those cases you need to combine the T // and P specification for the standard state, or else you may venture into // the forbidden zone, especially when nearing the triple point. Therefore, // we need to do the standard state thermo calc with the (t, pres) combo. - getMoleFractions(moleFractions_.data()); - - Phase::setTemperature(t); - _updateReferenceStateThermo(); - // Depends on the mole fractions and the temperature - updateMixingExpressions(); + double t = temperature(); + double rhoNow = density(); if (forcedState_ == FLUID_UNDEFINED) { - double rhoNow = Phase::density(); - double rho = densityCalc(t, pres, iState_, rhoNow); + double rho = densityCalc(t, p, iState_, rhoNow); if (rho > 0.0) { - Phase::setDensity(rho); + setDensity(rho); iState_ = phaseState(true); } else { if (rho < -1.5) { - rho = densityCalc(t, pres, FLUID_UNDEFINED , rhoNow); + rho = densityCalc(t, p, FLUID_UNDEFINED , rhoNow); if (rho > 0.0) { - Phase::setDensity(rho); + setDensity(rho); iState_ = phaseState(true); } else { - throw CanteraError("MixtureFugacityTP::setState_TP", "neg rho"); + throw CanteraError("MixtureFugacityTP::setPressure", + "neg rho"); } } else { - throw CanteraError("MixtureFugacityTP::setState_TP", "neg rho"); + throw CanteraError("MixtureFugacityTP::setPressure", + "neg rho"); } } } else if (forcedState_ == FLUID_GAS) { // Normal density calculation if (iState_ < FLUID_LIQUID_0) { - double rhoNow = Phase::density(); - double rho = densityCalc(t, pres, iState_, rhoNow); + double rho = densityCalc(t, p, iState_, rhoNow); if (rho > 0.0) { - Phase::setDensity(rho); + setDensity(rho); iState_ = phaseState(true); if (iState_ >= FLUID_LIQUID_0) { - throw CanteraError("MixtureFugacityTP::setState_TP", "wrong state"); + throw CanteraError("MixtureFugacityTP::setPressure", + "wrong state"); } } else { - throw CanteraError("MixtureFugacityTP::setState_TP", "neg rho"); + throw CanteraError("MixtureFugacityTP::setPressure", + "neg rho"); } } } else if (forcedState_ > FLUID_LIQUID_0) { if (iState_ >= FLUID_LIQUID_0) { - double rhoNow = Phase::density(); - double rho = densityCalc(t, pres, iState_, rhoNow); + double rho = densityCalc(t, p, iState_, rhoNow); if (rho > 0.0) { - Phase::setDensity(rho); + setDensity(rho); iState_ = phaseState(true); if (iState_ == FLUID_GAS) { - throw CanteraError("MixtureFugacityTP::setState_TP", "wrong state"); + throw CanteraError("MixtureFugacityTP::setPressure", + "wrong state"); } } else { - throw CanteraError("MixtureFugacityTP::setState_TP", "neg rho"); + throw CanteraError("MixtureFugacityTP::setPressure", + "neg rho"); } } } } -void MixtureFugacityTP::setState_TR(doublereal T, doublereal rho) +void MixtureFugacityTP::compositionChanged() { + Phase::compositionChanged(); getMoleFractions(moleFractions_.data()); - Phase::setTemperature(T); - _updateReferenceStateThermo(); - Phase::setDensity(rho); - // depends on mole fraction and temperature updateMixingExpressions(); - iState_ = phaseState(true); } -void MixtureFugacityTP::setState_TPX(doublereal t, doublereal p, const doublereal* x) +void MixtureFugacityTP::getActivityConcentrations(doublereal* c) const { - setMoleFractions_NoState(x); - setState_TP(t,p); + getActivityCoefficients(c); + double p_RT = pressure() / RT(); + for (size_t k = 0; k < m_kk; k++) { + c[k] *= moleFraction(k)*p_RT; + } } doublereal MixtureFugacityTP::z() const @@ -491,7 +472,8 @@ doublereal MixtureFugacityTP::densityCalc(doublereal TKelvin, doublereal presPa, double densBase = 0.0; if (! conv) { molarVolBase = 0.0; - throw CanteraError("MixtureFugacityTP::densityCalc", "Process did not converge"); + throw CanteraError("MixtureFugacityTP::densityCalc", + "Process did not converge"); } else { densBase = mmw / molarVolBase; } @@ -795,9 +777,9 @@ void MixtureFugacityTP::_updateReferenceStateThermo() const // If the temperature has changed since the last time these // properties were computed, recompute them. - if (m_Tlast_ref != Tnow) { + if (m_tlast != Tnow) { m_spthermo.update(Tnow, &m_cp0_R[0], &m_h0_RT[0], &m_s0_R[0]); - m_Tlast_ref = Tnow; + m_tlast = Tnow; // update the species Gibbs functions for (size_t k = 0; k < m_kk; k++) { @@ -805,15 +787,254 @@ void MixtureFugacityTP::_updateReferenceStateThermo() const } doublereal pref = refPressure(); if (pref <= 0.0) { - throw CanteraError("MixtureFugacityTP::_updateReferenceStateThermo", "neg ref pressure"); + throw CanteraError("MixtureFugacityTP::_updateReferenceStateThermo", + "negative reference pressure"); } } } -void MixtureFugacityTP::invalidateCache() +double MixtureFugacityTP::critTemperature() const { - ThermoPhase::invalidateCache(); - m_Tlast_ref += 0.001234; + double pc, tc, vc; + calcCriticalConditions(pc, tc, vc); + return tc; +} + +double MixtureFugacityTP::critPressure() const +{ + double pc, tc, vc; + calcCriticalConditions(pc, tc, vc); + return pc; +} + +double MixtureFugacityTP::critVolume() const +{ + double pc, tc, vc; + calcCriticalConditions(pc, tc, vc); + return vc; +} + +double MixtureFugacityTP::critCompressibility() const +{ + double pc, tc, vc; + calcCriticalConditions(pc, tc, vc); + return pc*vc/tc/GasConstant; +} + +double MixtureFugacityTP::critDensity() const +{ + double pc, tc, vc; + calcCriticalConditions(pc, tc, vc); + double mmw = meanMolecularWeight(); + return mmw / vc; +} + +void MixtureFugacityTP::calcCriticalConditions(double& pc, double& tc, double& vc) const +{ + throw NotImplementedError("MixtureFugacityTP::calcCriticalConditions"); +} + +int MixtureFugacityTP::solveCubic(double T, double pres, double a, double b, + double aAlpha, double Vroot[3], double an, + double bn, double cn, double dn, double tc, double vc) const +{ + fill_n(Vroot, 3, 0.0); + if (T <= 0.0) { + throw CanteraError("MixtureFugacityTP::solveCubic", + "negative temperature T = {}", T); + } + + // Derive the center of the cubic, x_N + double xN = - bn /(3 * an); + + // Derive the value of delta**2. This is a key quantity that determines the number of turning points + double delta2 = (bn * bn - 3 * an * cn) / (9 * an * an); + double delta = 0.0; + + // Calculate a couple of ratios + // Cubic equation in z : z^3 - (1-B) z^2 + (A -2B -3B^2)z - (AB- B^2- B^3) = 0 + double ratio1 = 3.0 * an * cn / (bn * bn); + double ratio2 = pres * b / (GasConstant * T); // B + if (fabs(ratio1) < 1.0E-7) { + double ratio3 = aAlpha / (GasConstant * T) * pres / (GasConstant * T); // A + if (fabs(ratio2) < 1.0E-5 && fabs(ratio3) < 1.0E-5) { + // A and B terms in cubic equation for z are almost zero, then z is near to 1 + double zz = 1.0; + for (int i = 0; i < 10; i++) { + double znew = zz / (zz - ratio2) - ratio3 / (zz + ratio1); + double deltaz = znew - zz; + zz = znew; + if (fabs(deltaz) < 1.0E-14) { + break; + } + } + double v = zz * GasConstant * T / pres; + Vroot[0] = v; + return 1; + } + } + + int nSolnValues; // Represents number of solutions to the cubic equation + double h2 = 4. * an * an * delta2 * delta2 * delta2; // h^2 + if (delta2 > 0.0) { + delta = sqrt(delta2); + } + + double h = 2.0 * an * delta * delta2; + double yN = 2.0 * bn * bn * bn / (27.0 * an * an) - bn * cn / (3.0 * an) + dn; // y_N term + double disc = yN * yN - h2; // discriminant + + //check if y = h + if (fabs(fabs(h) - fabs(yN)) < 1.0E-10) { + if (disc > 1e-10) { + throw CanteraError("MixtureFugacityTP::solveCubic", + "value of yN and h are too high, unrealistic roots may be obtained"); + } + disc = 0.0; + } + + if (disc < -1e-14) { + // disc<0 then we have three distinct roots. + nSolnValues = 3; + } else if (fabs(disc) < 1e-14) { + // disc=0 then we have two distinct roots (third one is repeated root) + nSolnValues = 2; + // We are here as p goes to zero. + } else if (disc > 1e-14) { + // disc> 0 then we have one real root. + nSolnValues = 1; + } + + double tmp; + // One real root -> have to determine whether gas or liquid is the root + if (disc > 0.0) { + double tmpD = sqrt(disc); + double tmp1 = (- yN + tmpD) / (2.0 * an); + double sgn1 = 1.0; + if (tmp1 < 0.0) { + sgn1 = -1.0; + tmp1 = -tmp1; + } + double tmp2 = (- yN - tmpD) / (2.0 * an); + double sgn2 = 1.0; + if (tmp2 < 0.0) { + sgn2 = -1.0; + tmp2 = -tmp2; + } + double p1 = pow(tmp1, 1./3.); + double p2 = pow(tmp2, 1./3.); + double alpha = xN + sgn1 * p1 + sgn2 * p2; + Vroot[0] = alpha; + Vroot[1] = 0.0; + Vroot[2] = 0.0; + } else if (disc < 0.0) { + // Three real roots alpha, beta, gamma are obtained. + double val = acos(-yN / h); + double theta = val / 3.0; + double twoThirdPi = 2. * Pi / 3.; + double alpha = xN + 2. * delta * cos(theta); + double beta = xN + 2. * delta * cos(theta + twoThirdPi); + double gamma = xN + 2. * delta * cos(theta + 2.0 * twoThirdPi); + Vroot[0] = beta; + Vroot[1] = gamma; + Vroot[2] = alpha; + + for (int i = 0; i < 3; i++) { + tmp = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; + if (fabs(tmp) > 1.0E-4) { + for (int j = 0; j < 3; j++) { + if (j != i && fabs(Vroot[i] - Vroot[j]) < 1.0E-4 * (fabs(Vroot[i]) + fabs(Vroot[j]))) { + writelog("MixtureFugacityTP::solveCubic(T ={}, p ={}):" + " WARNING roots have merged: {}, {}\n", + T, pres, Vroot[i], Vroot[j]); + } + } + } + } + } else if (disc == 0.0) { + //Three equal roots are obtained, i.e. alpha = beta = gamma + if (yN < 1e-18 && h < 1e-18) { + // yN = 0.0 and h = 0 i.e. disc = 0 + Vroot[0] = xN; + Vroot[1] = xN; + Vroot[2] = xN; + } else { + // h and yN need to figure out whether delta^3 is positive or negative + if (yN > 0.0) { + tmp = pow(yN/(2*an), 1./3.); + // In this case, tmp and delta must be equal. + if (fabs(tmp - delta) > 1.0E-9) { + throw CanteraError("MixtureFugacityTP::solveCubic", + "Inconsistency in solver: solver is ill-conditioned."); + } + Vroot[1] = xN + delta; + Vroot[0] = xN - 2.0*delta; // liquid phase root + } else { + tmp = pow(yN/(2*an), 1./3.); + // In this case, tmp and delta must be equal. + if (fabs(tmp - delta) > 1.0E-9) { + throw CanteraError("MixtureFugacityTP::solveCubic", + "Inconsistency in solver: solver is ill-conditioned."); + } + delta = -delta; + Vroot[0] = xN + delta; + Vroot[1] = xN - 2.0*delta; // gas phase root + } + } + } + + // Find an accurate root, since there might be a heavy amount of roundoff error due to bad conditioning in this solver. + double res, dresdV = 0.0; + for (int i = 0; i < nSolnValues; i++) { + for (int n = 0; n < 20; n++) { + res = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; + if (fabs(res) < 1.0E-14) { // accurate root is obtained + break; + } + dresdV = 3.0 * an * Vroot[i] * Vroot[i] + 2.0 * bn * Vroot[i] + cn; // derivative of the residual + double del = - res / dresdV; + Vroot[i] += del; + if (fabs(del) / (fabs(Vroot[i]) + fabs(del)) < 1.0E-14) { + break; + } + double res2 = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; + if (fabs(res2) < fabs(res)) { + continue; + } else { + Vroot[i] -= del; // Go back to previous value of Vroot. + Vroot[i] += 0.1 * del; // under-relax by 0.1 + } + } + if ((fabs(res) > 1.0E-14) && (fabs(res) > 1.0E-14 * fabs(dresdV) * fabs(Vroot[i]))) { + writelog("MixtureFugacityTP::solveCubic(T = {}, p = {}): " + "WARNING root didn't converge V = {}", T, pres, Vroot[i]); + writelogendl(); + } + } + + if (nSolnValues == 1) { + // Determine the phase of the single root. + // nSolnValues = 1 represents the gas phase by default. + if (T > tc) { + if (Vroot[0] < vc) { + // Supercritical phase + nSolnValues = -1; + } + } else { + if (Vroot[0] < xN) { + //Liquid phase + nSolnValues = -1; + } + } + } else { + // Determine if we have two distinct roots or three equal roots + // nSolnValues = 2 represents 2 equal roots by default. + if (nSolnValues == 2 && delta > 1e-14) { + //If delta > 0, we have two distinct roots (and one repeated root) + nSolnValues = -2; + } + } + return nSolnValues; } } diff --git a/src/thermo/PengRobinson.cpp b/src/thermo/PengRobinson.cpp new file mode 100644 index 0000000000..f4032cdb20 --- /dev/null +++ b/src/thermo/PengRobinson.cpp @@ -0,0 +1,845 @@ +//! @file PengRobinson.cpp + +// 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/PengRobinson.h" +#include "cantera/thermo/ThermoFactory.h" +#include "cantera/thermo/Species.h" +#include "cantera/base/stringUtils.h" +#include "cantera/base/ctml.h" + +#include + +#define _USE_MATH_DEFINES +#include + +using namespace std; +namespace bmt = boost::math::tools; + +namespace Cantera +{ + +const double PengRobinson::omega_a = 4.5723552892138218E-01; +const double PengRobinson::omega_b = 7.77960739038885E-02; +const double PengRobinson::omega_vc = 3.07401308698703833E-01; + +PengRobinson::PengRobinson(const std::string& infile, const std::string& id_) : + m_b(0.0), + m_a(0.0), + m_aAlpha_mix(0.0), + m_NSolns(0), + m_dpdV(0.0), + m_dpdT(0.0) +{ + fill_n(m_Vroot, 3, 0.0); + initThermoFile(infile, id_); +} + +void PengRobinson::setSpeciesCoeffs(const std::string& species, double a, double b, double w) +{ + size_t k = speciesIndex(species); + if (k == npos) { + throw CanteraError("PengRobinson::setSpeciesCoeffs", + "Unknown species '{}'.", species); + } + + // Calculate value of kappa (independent of temperature) + // w is an acentric factor of species + if (w <= 0.491) { + m_kappa[k] = 0.37464 + 1.54226*w - 0.26992*w*w; + } else { + m_kappa[k] = 0.374642 + 1.487503*w - 0.164423*w*w + 0.016666*w*w*w; + } + + //Calculate alpha (temperature dependent interaction parameter) + double critTemp = speciesCritTemperature(a, b); // critical temperature of individual species + double sqt_T_r = sqrt(temperature() / critTemp); + double sqt_alpha = 1 + m_kappa[k] * (1 - sqt_T_r); + m_alpha[k] = sqt_alpha*sqt_alpha; + + m_a_coeffs(k,k) = a; + double aAlpha_k = a*m_alpha[k]; + m_aAlpha_binary(k,k) = aAlpha_k; + + // standard mixing rule for cross-species interaction term + for (size_t j = 0; j < m_kk; j++) { + if (k == j) { + continue; + } + double a0kj = sqrt(m_a_coeffs(j,j) * a); + double aAlpha_j = a*m_alpha[j]; + double a_Alpha = sqrt(aAlpha_j*aAlpha_k); + if (m_a_coeffs(j, k) == 0) { + m_a_coeffs(j, k) = a0kj; + m_aAlpha_binary(j, k) = a_Alpha; + m_a_coeffs(k, j) = a0kj; + m_aAlpha_binary(k, j) = a_Alpha; + } + } + m_b_coeffs[k] = b; +} + +void PengRobinson::setBinaryCoeffs(const std::string& species_i, + const std::string& species_j, double a0) +{ + size_t ki = speciesIndex(species_i); + if (ki == npos) { + throw CanteraError("PengRobinson::setBinaryCoeffs", + "Unknown species '{}'.", species_i); + } + size_t kj = speciesIndex(species_j); + if (kj == npos) { + throw CanteraError("PengRobinson::setBinaryCoeffs", + "Unknown species '{}'.", species_j); + } + + m_a_coeffs(ki, kj) = m_a_coeffs(kj, ki) = a0; + // Calculate alpha_ij + double alpha_ij = m_alpha[ki] * m_alpha[kj]; + m_aAlpha_binary(ki, kj) = m_aAlpha_binary(kj, ki) = a0*alpha_ij; +} + +// ------------Molar Thermodynamic Properties ------------------------- + +double PengRobinson::cp_mole() const +{ + _updateReferenceStateThermo(); + double T = temperature(); + double mv = molarVolume(); + double vpb = mv + (1 + M_SQRT2)*m_b; + double vmb = mv + (1 - M_SQRT2)*m_b; + calculatePressureDerivatives(); + double cpref = GasConstant * mean_X(m_cp0_R); + double dHdT_V = cpref + mv * m_dpdT - GasConstant + + 1.0 / (2.0 * M_SQRT2 * m_b) * log(vpb / vmb) * T * d2aAlpha_dT2(); + return dHdT_V - (mv + T * m_dpdT / m_dpdV) * m_dpdT; +} + +double PengRobinson::cv_mole() const +{ + _updateReferenceStateThermo(); + double T = temperature(); + calculatePressureDerivatives(); + return (cp_mole() + T * m_dpdT * m_dpdT / m_dpdV); +} + +double PengRobinson::pressure() const +{ + _updateReferenceStateThermo(); + // Get a copy of the private variables stored in the State object + double T = temperature(); + double mv = molarVolume(); + double denom = mv * mv + 2 * mv * m_b - m_b * m_b; + double pp = GasConstant * T / (mv - m_b) - m_aAlpha_mix / denom; + return pp; +} + +double PengRobinson::standardConcentration(size_t k) const +{ + getStandardVolumes(m_tmpV.data()); + return 1.0 / m_tmpV[k]; +} + +void PengRobinson::getActivityCoefficients(double* ac) const +{ + double mv = molarVolume(); + double vpb2 = mv + (1 + M_SQRT2)*m_b; + double vmb2 = mv + (1 - M_SQRT2)*m_b; + double vmb = mv - m_b; + double pres = pressure(); + + for (size_t k = 0; k < m_kk; k++) { + m_pp[k] = 0.0; + for (size_t i = 0; i < m_kk; i++) { + m_pp[k] += moleFractions_[i] * m_aAlpha_binary(k, i); + } + } + double num = 0; + double den = 2 * M_SQRT2 * m_b * m_b; + double den2 = m_b * (mv * mv + 2 * mv * m_b - m_b * m_b); + double RTkelvin = RT(); + for (size_t k = 0; k < m_kk; k++) { + num = 2 * m_b * m_pp[k] - m_aAlpha_mix * m_b_coeffs[k]; + ac[k] = (-RTkelvin * log(pres * mv/ RTkelvin) + RTkelvin * log(mv / vmb) + + RTkelvin * m_b_coeffs[k] / vmb + - (num /den) * log(vpb2/vmb2) + - m_aAlpha_mix * m_b_coeffs[k] * mv/den2 + ); + } + for (size_t k = 0; k < m_kk; k++) { + ac[k] = exp(ac[k]/ RTkelvin); + } +} + +// ---- Partial Molar Properties of the Solution ----------------- + +void PengRobinson::getChemPotentials(double* mu) const +{ + getGibbs_ref(mu); + double RTkelvin = RT(); + for (size_t k = 0; k < m_kk; k++) { + double xx = std::max(SmallNumber, moleFraction(k)); + mu[k] += RTkelvin * (log(xx)); + } + + double mv = molarVolume(); + double vmb = mv - m_b; + double vpb2 = mv + (1 + M_SQRT2)*m_b; + double vmb2 = mv + (1 - M_SQRT2)*m_b; + + for (size_t k = 0; k < m_kk; k++) { + m_pp[k] = 0.0; + for (size_t i = 0; i < m_kk; i++) { + m_pp[k] += moleFractions_[i] * m_aAlpha_binary(k, i); + } + } + double pres = pressure(); + double refP = refPressure(); + double den = 2 * M_SQRT2 * m_b * m_b; + double den2 = m_b * (mv * mv + 2 * mv * m_b - m_b * m_b); + + for (size_t k = 0; k < m_kk; k++) { + double num = 2 * m_b * m_pp[k] - m_aAlpha_mix * m_b_coeffs[k]; + + mu[k] += (RTkelvin * log(pres/refP) - RTkelvin * log(pres * mv / RTkelvin) + + RTkelvin * log(mv / vmb) + + RTkelvin * m_b_coeffs[k] / vmb + - (num /den) * log(vpb2/vmb2) + - m_aAlpha_mix * m_b_coeffs[k] * mv/den2 + ); + } +} + +void PengRobinson::getPartialMolarEnthalpies(double* hbar) const +{ + // First we get the reference state contributions + getEnthalpy_RT_ref(hbar); + scale(hbar, hbar+m_kk, hbar, RT()); + + // We calculate m_dpdni + double T = temperature(); + double mv = molarVolume(); + double vmb = mv - m_b; + double vpb2 = mv + (1 + M_SQRT2)*m_b; + double vmb2 = mv + (1 - M_SQRT2)*m_b; + + for (size_t k = 0; k < m_kk; k++) { + m_pp[k] = 0.0; + for (size_t i = 0; i < m_kk; i++) { + m_pp[k] += moleFractions_[i] * m_aAlpha_binary(k, i); + } + } + + double den = mv * mv + 2 * mv * m_b - m_b * m_b; + double den2 = den * den; + double RTkelvin = RT(); + for (size_t k = 0; k < m_kk; k++) { + m_dpdni[k] = RTkelvin / vmb + RTkelvin * m_b_coeffs[k] / (vmb * vmb) - 2.0 * m_pp[k] / den + + 2 * vmb * m_aAlpha_mix * m_b_coeffs[k] / den2; + } + + double daAlphadT = daAlpha_dT(); + double fac = T * daAlphadT - m_aAlpha_mix; + + calculatePressureDerivatives(); + double fac2 = mv + T * m_dpdT / m_dpdV; + double fac3 = 2 * M_SQRT2 * m_b * m_b; + for (size_t k = 0; k < m_kk; k++) { + double hE_v = mv * m_dpdni[k] - RTkelvin + (2 * m_b - m_b_coeffs[k]) / fac3 * log(vpb2 / vmb2) * fac + + (mv * m_b_coeffs[k]) /(m_b * den) * fac; + hbar[k] = hbar[k] + hE_v; + hbar[k] -= fac2 * m_dpdni[k]; + } +} + +void PengRobinson::getPartialMolarEntropies(double* sbar) const +{ + getEntropy_R_ref(sbar); + scale(sbar, sbar+m_kk, sbar, GasConstant); + double T = temperature(); + double mv = molarVolume(); + double vmb = mv - m_b; + double vpb2 = mv + (1 + M_SQRT2)*m_b; + double vmb2 = mv + (1 - M_SQRT2)*m_b; + double refP = refPressure(); + double daAlphadT = daAlpha_dT(); + double coeff1 = 0; + double den1 = 2 * M_SQRT2 * m_b * m_b; + double den2 = mv * mv + 2 * mv * m_b - m_b * m_b; + + // Calculate sum(n_j (a alpha)_i, k * (1/alpha_k d/dT(alpha_k))) -> m_pp + // Calculate sum(n_j (a alpha)_i, k * (1/alpha_i d/dT(alpha_i))) -> m_tmpV + for (size_t k = 0; k < m_kk; k++) { + m_pp[k] = 0.0; + m_tmpV[k] = 0; + for (size_t i = 0; i < m_kk; i++) { + m_pp[k] += moleFractions_[i] * m_aAlpha_binary(k, i); + m_tmpV[k] += moleFractions_[i] * m_aAlpha_binary(k, i) * (m_dalphadT[i] / m_alpha[i]); + } + m_pp[k] = m_pp[k] * m_dalphadT[k] / m_alpha[k]; + } + + + for (size_t k = 0; k < m_kk; k++) { + coeff1 = m_b * (m_pp[k] + m_tmpV[k]) - daAlphadT * m_b_coeffs[k]; + sbar[k] += GasConstant * log(GasConstant * T / (refP * mv)) + + GasConstant + + GasConstant * log(mv / vmb) + + GasConstant * m_b_coeffs[k] / vmb + - coeff1 * log(vpb2 / vmb2) / den1 + - m_b_coeffs[k] * mv * daAlphadT / den2 / m_b; + } + calculatePressureDerivatives(); + getPartialMolarVolumes(m_partialMolarVolumes.data()); + for (size_t k = 0; k < m_kk; k++) { + sbar[k] -= m_partialMolarVolumes[k] * m_dpdT; + } +} + +void PengRobinson::getPartialMolarIntEnergies(double* ubar) const +{ + // u_i = h_i - p*v_i + double p = pressure(); + getPartialMolarEnthalpies(ubar); + getPartialMolarVolumes(m_tmpV.data()); + for (size_t k = 0; k < m_kk; k++) { + ubar[k] = ubar[k] - p*m_tmpV[k]; + } +} + +void PengRobinson::getPartialMolarCp(double* cpbar) const +{ + // First we get the reference state contributions + getCp_R(cpbar); + scale(cpbar, cpbar+m_kk, cpbar, GasConstant); + + // We calculate d/dT(m_dpdni) + vector_fp grad_dpdni(m_kk, 0.0); + vector_fp grad_hi(m_kk, 0.0); + double T = temperature(); + double mv = molarVolume(); + double vmb = mv - m_b; + double vpb2 = mv + (1 + M_SQRT2)*m_b; + double vmb2 = mv + (1 - M_SQRT2)*m_b; + + double grad_aAlpha = 0; + for (size_t k = 0; k < m_kk; k++) { + m_pp[k] = 0.0; + for (size_t i = 0; i < m_kk; i++) { + // Calculate temperature derivative of m_aAlpha_binary(i,j) + grad_aAlpha = m_dalphadT[i]/m_alpha[i] + m_dalphadT[k]/m_alpha[k]; + grad_aAlpha = 0.5*m_aAlpha_binary(k, i)*grad_aAlpha; + m_pp[k] += moleFractions_[i] * grad_aAlpha; + } + } + + double den = mv * mv + 2 * mv * m_b - m_b * m_b; + double den2 = den * den; + //double RTkelvin = RT(); + for (size_t k = 0; k < m_kk; k++) { + grad_dpdni[k] = GasConstant / vmb + GasConstant * m_b_coeffs[k] / (vmb * vmb) - 2.0 * m_pp[k] / den + + 2 * vmb * daAlpha_dT() * m_b_coeffs[k] / den2; + } + + double fac = T * d2aAlpha_dT2(); + double fac2 = 2 * M_SQRT2 * m_b * m_b; + + // Calculate d(hi)/dT + for (size_t k = 0; k < m_kk; k++) { + grad_hi[k] = cpbar[k] - GasConstant + mv*grad_dpdni[k] + + (2*m_b - m_b_coeffs[k])/fac2 * log(vpb2/vmb2) * fac + + (mv * m_b_coeffs[k]) /(m_b * den) * fac; + } + + calculatePressureDerivatives(); + double fac3 = mv + T * m_dpdT / m_dpdV; + + for (size_t k = 0; k < m_kk; k++) { + cpbar[k] = moleFractions_[k]*grad_hi[k] - fac3*m_dpdT; + } +} + +void PengRobinson::getPartialMolarVolumes(double* vbar) const +{ + for (size_t k = 0; k < m_kk; k++) { + m_pp[k] = 0.0; + for (size_t i = 0; i < m_kk; i++) { + m_pp[k] += moleFractions_[i] * m_aAlpha_binary(k, i); + } + } + + double mv = molarVolume(); + double vmb = mv - m_b; + double vpb = mv + m_b; + double fac = mv * mv + 2 * mv * m_b - m_b * m_b; + double fac2 = fac * fac; + double RTkelvin = RT(); + + for (size_t k = 0; k < m_kk; k++) { + double num = (RTkelvin + RTkelvin * m_b/ vmb + RTkelvin * m_b_coeffs[k] / vmb + + RTkelvin * m_b * m_b_coeffs[k] /(vmb * vmb) + - 2 * mv * m_pp[k] / fac + + 2 * mv * vmb * m_aAlpha_mix * m_b_coeffs[k] / fac2 + ); + double denom = (pressure() + RTkelvin * m_b / (vmb * vmb) + + m_aAlpha_mix/fac + - 2 * mv* vpb * m_aAlpha_mix / fac2 + ); + vbar[k] = num / denom; + } +} + +double PengRobinson::speciesCritTemperature(double a, double b) const +{ + if (b <= 0.0) { + return 1000000.; + } else if (a <= 0.0) { + return 0.0; + } else { + return a * omega_b / (b * omega_a * GasConstant); + } +} + +bool PengRobinson::addSpecies(shared_ptr spec) +{ + bool added = MixtureFugacityTP::addSpecies(spec); + if (added) { + m_a_coeffs.resize(m_kk, m_kk, 0.0); + m_b_coeffs.push_back(0.0); + m_aAlpha_binary.resize(m_kk, m_kk, 0.0); + m_kappa.push_back(0.0); + + m_alpha.push_back(0.0); + m_dalphadT.push_back(0.0); + m_d2alphadT2.push_back(0.0); + + m_pp.push_back(0.0); + m_tmpV.push_back(0.0); + m_partialMolarVolumes.push_back(0.0); + m_dpdni.push_back(0.0); + } + return added; +} + +vector PengRobinson::getCoeff(const std::string& iName) +{ + vector_fp spCoeff{ NAN, NAN, NAN }; + + // Get number of species in the database + // open xml file critProperties.xml + XML_Node* doc = get_XML_File("critProperties.xml"); + size_t nDatabase = doc->nChildren(); + + // Loop through all species in the database and attempt to match supplied + // species to each. If present, calculate pureFluidParameters a_k and b_k + // based on crit properties T_c and P_c: + for (size_t isp = 0; isp < nDatabase; isp++) { + XML_Node& acNodeDoc = doc->child(isp); + std::string iNameLower = toLowerCopy(iName); + std::string dbName = toLowerCopy(acNodeDoc.attrib("name")); + + // Attempt to match provided species iName to current database species + // dbName: + if (iNameLower == dbName) { + // Read from database and calculate a and b coefficients + double vParams; + double T_crit = 0.0, P_crit = 0.0, w_ac = 0.0; + + if (acNodeDoc.hasChild("Tc")) { + vParams = 0.0; + XML_Node& xmlChildCoeff = acNodeDoc.child("Tc"); + if (xmlChildCoeff.hasAttrib("value")) { + std::string critTemp = xmlChildCoeff.attrib("value"); + vParams = strSItoDbl(critTemp); + } + if (vParams <= 0.0) { //Assuming that Pc and Tc are non zero. + throw CanteraError("PengRobinson::getCoeff", + "Critical Temperature must be positive"); + } + T_crit = vParams; + } + if (acNodeDoc.hasChild("Pc")) { + vParams = 0.0; + XML_Node& xmlChildCoeff = acNodeDoc.child("Pc"); + if (xmlChildCoeff.hasAttrib("value")) { + std::string critPressure = xmlChildCoeff.attrib("value"); + vParams = strSItoDbl(critPressure); + } + if (vParams <= 0.0) { //Assuming that Pc and Tc are non zero. + throw CanteraError("PengRobinson::getCoeff", + "Critical Pressure must be positive"); + } + P_crit = vParams; + } + if (acNodeDoc.hasChild("omega")) { + vParams = 0.0; + XML_Node& xmlChildCoeff = acNodeDoc.child("omega"); + if (xmlChildCoeff.hasChild("value")) { + std::string acentric_factor = xmlChildCoeff.attrib("value"); + vParams = strSItoDbl(acentric_factor); + } + w_ac = vParams; + } + + spCoeff[0] = omega_a * (GasConstant * GasConstant) * (T_crit * T_crit) / P_crit; //coeff a + spCoeff[1] = omega_b * GasConstant * T_crit / P_crit; // coeff b + spCoeff[2] = w_ac; // acentric factor + break; + } + } + return spCoeff; +} + +void PengRobinson::initThermo() +{ + for (auto& item : m_species) { + // Read a and b coefficients and acentric factor w_ac from species 'input' + // information (i.e. as specified in a YAML input file) + if (item.second->input.hasKey("equation-of-state")) { + auto eos = item.second->input["equation-of-state"].getMapWhere( + "model", "Peng-Robinson"); + double a0 = 0; + if (eos["a"].isScalar()) { + a0 = eos.convert("a", "Pa*m^6/kmol^2"); + } + double b = eos.convert("b", "m^3/kmol"); + // unitless acentric factor: + double w = eos["acentric_factor"].asDouble(); + + setSpeciesCoeffs(item.first, a0, b, w); + if (eos.hasKey("binary-a")) { + AnyMap& binary_a = eos["binary-a"].as(); + const UnitSystem& units = binary_a.units(); + for (auto& item2 : binary_a) { + double a0 = 0; + if (item2.second.isScalar()) { + a0 = units.convert(item2.second, "Pa*m^6/kmol^2"); + } + setBinaryCoeffs(item.first, item2.first, a0); + } + } + } else { + // Check if a and b are already populated for this species (only the + // diagonal elements of a). If not, then search 'critProperties.xml' + // to find critical temperature and pressure to calculate a and b. + size_t k = speciesIndex(item.first); + if (m_a_coeffs(k, k) == 0.0) { + vector coeffs = getCoeff(item.first); + + // Check if species was found in the database of critical + // properties, and assign the results + if (!isnan(coeffs[0])) { + setSpeciesCoeffs(item.first, coeffs[0], coeffs[1], coeffs[2]); + } + } + } + } +} + +double PengRobinson::sresid() const +{ + double molarV = molarVolume(); + double hh = m_b / molarV; + double zz = z(); + double alpha_1 = daAlpha_dT(); + double vpb = molarV + (1.0 + M_SQRT2) * m_b; + double vmb = molarV + (1.0 - M_SQRT2) * m_b; + double fac = alpha_1 / (2.0 * M_SQRT2 * m_b); + double sresid_mol_R = log(zz*(1.0 - hh)) + fac * log(vpb / vmb) / GasConstant; + return GasConstant * sresid_mol_R; +} + +double PengRobinson::hresid() const +{ + double molarV = molarVolume(); + double zz = z(); + double aAlpha_1 = daAlpha_dT(); + double T = temperature(); + double vpb = molarV + (1 + M_SQRT2) * m_b; + double vmb = molarV + (1 - M_SQRT2) * m_b; + double fac = 1 / (2.0 * M_SQRT2 * m_b); + return GasConstant * T * (zz - 1.0) + fac * log(vpb / vmb) * (T * aAlpha_1 - m_aAlpha_mix); +} + +double PengRobinson::liquidVolEst(double T, double& presGuess) const +{ + double v = m_b * 1.1; + double atmp; + double btmp; + double aAlphatmp; + calculateAB(atmp, btmp, aAlphatmp); + double pres = std::max(psatEst(T), presGuess); + double Vroot[3]; + bool foundLiq = false; + int m = 0; + while (m < 100 && !foundLiq) { + int nsol = solveCubic(T, pres, atmp, btmp, aAlphatmp, Vroot); + if (nsol == 1 || nsol == 2) { + double pc = critPressure(); + if (pres > pc) { + foundLiq = true; + } + pres *= 1.04; + } else { + foundLiq = true; + } + } + + if (foundLiq) { + v = Vroot[0]; + presGuess = pres; + } else { + v = -1.0; + } + return v; +} + +double PengRobinson::densityCalc(double T, double presPa, int phaseRequested, double rhoGuess) +{ + // It's necessary to set the temperature so that m_aAlpha_mix is set correctly. + setTemperature(T); + double tcrit = critTemperature(); + double mmw = meanMolecularWeight(); + if (rhoGuess == -1.0) { + if (phaseRequested >= FLUID_LIQUID_0) { + double lqvol = liquidVolEst(T, presPa); + rhoGuess = mmw / lqvol; + } + } else { + // Assume the Gas phase initial guess, if nothing is specified to the routine + rhoGuess = presPa * mmw / (GasConstant * T); + } + + double volGuess = mmw / rhoGuess; + m_NSolns = solveCubic(T, presPa, m_a, m_b, m_aAlpha_mix, m_Vroot); + + double molarVolLast = m_Vroot[0]; + if (m_NSolns >= 2) { + if (phaseRequested >= FLUID_LIQUID_0) { + molarVolLast = m_Vroot[0]; + } else if (phaseRequested == FLUID_GAS || phaseRequested == FLUID_SUPERCRIT) { + molarVolLast = m_Vroot[2]; + } else { + if (volGuess > m_Vroot[1]) { + molarVolLast = m_Vroot[2]; + } else { + molarVolLast = m_Vroot[0]; + } + } + } else if (m_NSolns == 1) { + if (phaseRequested == FLUID_GAS || phaseRequested == FLUID_SUPERCRIT || phaseRequested == FLUID_UNDEFINED) { + molarVolLast = m_Vroot[0]; + } else { + return -2.0; + } + } else if (m_NSolns == -1) { + if (phaseRequested >= FLUID_LIQUID_0 || phaseRequested == FLUID_UNDEFINED || phaseRequested == FLUID_SUPERCRIT) { + molarVolLast = m_Vroot[0]; + } else if (T > tcrit) { + molarVolLast = m_Vroot[0]; + } else { + return -2.0; + } + } else { + molarVolLast = m_Vroot[0]; + return -1.0; + } + return mmw / molarVolLast; +} + +double PengRobinson::densSpinodalLiquid() const +{ + double Vroot[3]; + double T = temperature(); + int nsol = solveCubic(T, pressure(), m_a, m_b, m_aAlpha_mix, Vroot); + if (nsol != 3) { + return critDensity(); + } + + auto resid = [this, T](double v) { + double pp; + return dpdVCalc(T, v, pp); + }; + + boost::uintmax_t maxiter = 100; + std::pair vv = bmt::toms748_solve( + resid, Vroot[0], Vroot[1], bmt::eps_tolerance(48), maxiter); + + double mmw = meanMolecularWeight(); + return mmw / (0.5 * (vv.first + vv.second)); +} + +double PengRobinson::densSpinodalGas() const +{ + double Vroot[3]; + double T = temperature(); + int nsol = solveCubic(T, pressure(), m_a, m_b, m_aAlpha_mix, Vroot); + if (nsol != 3) { + return critDensity(); + } + + auto resid = [this, T](double v) { + double pp; + return dpdVCalc(T, v, pp); + }; + + boost::uintmax_t maxiter = 100; + std::pair vv = bmt::toms748_solve( + resid, Vroot[1], Vroot[2], bmt::eps_tolerance(48), maxiter); + + double mmw = meanMolecularWeight(); + return mmw / (0.5 * (vv.first + vv.second)); +} + +double PengRobinson::dpdVCalc(double T, double molarVol, double& presCalc) const +{ + double den = molarVol * molarVol + 2 * molarVol * m_b - m_b * m_b; + presCalc = GasConstant * T / (molarVol - m_b) - m_aAlpha_mix / den; + + double vpb = molarVol + m_b; + double vmb = molarVol - m_b; + double dpdv = -GasConstant * T / (vmb * vmb) + 2 * m_aAlpha_mix * vpb / (den*den); + return dpdv; +} + +void PengRobinson::calculatePressureDerivatives() const +{ + double T = temperature(); + double mv = molarVolume(); + double pres; + + m_dpdV = dpdVCalc(T, mv, pres); + double vmb = mv - m_b; + double den = mv * mv + 2 * mv * m_b - m_b * m_b; + m_dpdT = (GasConstant / vmb - daAlpha_dT() / den); +} + +void PengRobinson::updateMixingExpressions() +{ + double temp = temperature(); + + // Update individual alpha + for (size_t j = 0; j < m_kk; j++) { + double critTemp_j = speciesCritTemperature(m_a_coeffs(j,j), m_b_coeffs[j]); + double sqt_alpha = 1 + m_kappa[j] * (1 - sqrt(temp / critTemp_j)); + m_alpha[j] = sqt_alpha*sqt_alpha; + } + + //Update aAlpha_i, j + for (size_t i = 0; i < m_kk; i++) { + for (size_t j = 0; j < m_kk; j++) { + m_aAlpha_binary(i, j) = sqrt(m_alpha[i] * m_alpha[j]) * m_a_coeffs(i,j); + } + } + calculateAB(m_a,m_b,m_aAlpha_mix); +} + +void PengRobinson::calculateAB(double& aCalc, double& bCalc, double& aAlphaCalc) const +{ + bCalc = 0.0; + aCalc = 0.0; + aAlphaCalc = 0.0; + for (size_t i = 0; i < m_kk; i++) { + bCalc += moleFractions_[i] * m_b_coeffs[i]; + for (size_t j = 0; j < m_kk; j++) { + double a_vec_Curr = m_a_coeffs(i, j); + aCalc += a_vec_Curr * moleFractions_[i] * moleFractions_[j]; + aAlphaCalc += m_aAlpha_binary(i, j) * moleFractions_[i] * moleFractions_[j]; + } + } +} + +double PengRobinson::daAlpha_dT() const +{ + double daAlphadT = 0.0, temp, k, Tc, sqtTr, coeff1, coeff2; + for (size_t i = 0; i < m_kk; i++) { + // Calculate first derivative of alpha for individual species + Tc = speciesCritTemperature(m_a_coeffs(i,i), m_b_coeffs[i]); + sqtTr = sqrt(temperature() / Tc); //we need species critical temperature + coeff1 = 1 / (Tc*sqtTr); + coeff2 = sqtTr - 1; + k = m_kappa[i]; + m_dalphadT[i] = coeff1 * (k*k*coeff2 - k); + } + //Calculate mixture derivative + for (size_t i = 0; i < m_kk; i++) { + for (size_t j = 0; j < m_kk; j++) { + temp = 0.5 * sqrt((m_a_coeffs(i, i) * m_a_coeffs(j, j)) / (m_alpha[i] * m_alpha[j])); + daAlphadT += moleFractions_[i] * moleFractions_[j] * temp + * (m_dalphadT[j] * m_alpha[i] + m_dalphadT[i] * m_alpha[j]); + } + } + return daAlphadT; +} + +double PengRobinson::d2aAlpha_dT2() const +{ + for (size_t i = 0; i < m_kk; i++) { + double Tcrit_i = speciesCritTemperature(m_a_coeffs(i, i), m_b_coeffs[i]); + double sqt_Tr = sqrt(temperature() / Tcrit_i); //we need species critical temperature + double coeff1 = 1 / (Tcrit_i*Tcrit_i*sqt_Tr); + double coeff2 = sqt_Tr - 1; + // Calculate first and second derivatives of alpha for individual species + double k = m_kappa[i]; + m_dalphadT[i] = coeff1 * (k*k*coeff2 - k); + m_d2alphadT2[i] = (k*k + k) * coeff1 / (2*sqt_Tr*sqt_Tr); + } + + //Calculate mixture derivative + double d2aAlphadT2 = 0.0; + for (size_t i = 0; i < m_kk; i++) { + double alphai = m_alpha[i]; + for (size_t j = 0; j < m_kk; j++) { + double alphaj = m_alpha[j]; + double alphaij = alphai * alphaj; + double temp = 0.5 * sqrt((m_a_coeffs(i, i) * m_a_coeffs(j, j)) / (alphaij)); + double num = (m_dalphadT[j] * alphai + m_dalphadT[i] * alphaj); + double fac1 = -(0.5 / alphaij) * num * num; + double fac2 = alphaj * m_d2alphadT2[i] + alphai * m_d2alphadT2[j] + 2. * m_dalphadT[i] * m_dalphadT[j]; + d2aAlphadT2 += moleFractions_[i] * moleFractions_[j] * temp * (fac1 + fac2); + } + } + return d2aAlphadT2; +} + +void PengRobinson::calcCriticalConditions(double& pc, double& tc, double& vc) const +{ + if (m_b <= 0.0) { + tc = 1000000.; + pc = 1.0E13; + vc = omega_vc * GasConstant * tc / pc; + return; + } + if (m_a <= 0.0) { + tc = 0.0; + pc = 0.0; + vc = 2.0 * m_b; + return; + } + tc = m_a * omega_b / (m_b * omega_a * GasConstant); + pc = omega_b * GasConstant * tc / m_b; + vc = omega_vc * GasConstant * tc / pc; +} + +int PengRobinson::solveCubic(double T, double pres, double a, double b, double aAlpha, double Vroot[3]) const +{ + // Derive the coefficients of the cubic polynomial (in terms of molar volume v) to solve. + double bsqr = b * b; + double RT_p = GasConstant * T / pres; + double aAlpha_p = aAlpha / pres; + double an = 1.0; + double bn = (b - RT_p); + double cn = -(2 * RT_p * b - aAlpha_p + 3 * bsqr); + double dn = (bsqr * RT_p + bsqr * b - aAlpha_p * b); + + double tc = a * omega_b / (b * omega_a * GasConstant); + double pc = omega_b * GasConstant * tc / b; + double vc = omega_vc * GasConstant * tc / pc; + + int nSolnValues = MixtureFugacityTP::solveCubic(T, pres, a, b, aAlpha, Vroot, an, bn, cn, dn, tc, vc); + + return nSolnValues; +} + +} diff --git a/src/thermo/RedlichKwongMFTP.cpp b/src/thermo/RedlichKwongMFTP.cpp index 50ca199855..b31b44586d 100644 --- a/src/thermo/RedlichKwongMFTP.cpp +++ b/src/thermo/RedlichKwongMFTP.cpp @@ -115,23 +115,6 @@ void RedlichKwongMFTP::setBinaryCoeffs(const std::string& species_i, // ------------Molar Thermodynamic Properties ------------------------- -doublereal RedlichKwongMFTP::enthalpy_mole() const -{ - _updateReferenceStateThermo(); - doublereal h_ideal = RT() * mean_X(m_h0_RT); - doublereal h_nonideal = hresid(); - return h_ideal + h_nonideal; -} - -doublereal RedlichKwongMFTP::entropy_mole() const -{ - _updateReferenceStateThermo(); - doublereal sr_ideal = GasConstant * (mean_X(m_s0_R) - - sum_xlogx() - std::log(pressure()/refPressure())); - doublereal sr_nonideal = sresid(); - return sr_ideal + sr_nonideal; -} - doublereal RedlichKwongMFTP::cp_mole() const { _updateReferenceStateThermo(); @@ -173,39 +156,6 @@ doublereal RedlichKwongMFTP::pressure() const return pp; } -void RedlichKwongMFTP::calcDensity() -{ - // Calculate the molarVolume of the solution (m**3 kmol-1) - const doublereal* const dtmp = moleFractdivMMW(); - getPartialMolarVolumes(m_tmpV.data()); - double invDens = dot(m_tmpV.begin(), m_tmpV.end(), dtmp); - - // Set the density in the parent State object directly, by calling the - // Phase::setDensity() function. - Phase::setDensity(1.0/invDens); -} - -void RedlichKwongMFTP::setTemperature(const doublereal temp) -{ - Phase::setTemperature(temp); - _updateReferenceStateThermo(); - updateAB(); -} - -void RedlichKwongMFTP::compositionChanged() -{ - MixtureFugacityTP::compositionChanged(); - updateAB(); -} - -void RedlichKwongMFTP::getActivityConcentrations(doublereal* c) const -{ - getActivityCoefficients(c); - for (size_t k = 0; k < m_kk; k++) { - c[k] *= moleFraction(k)*pressure()/RT(); - } -} - doublereal RedlichKwongMFTP::standardConcentration(size_t k) const { getStandardVolumes(m_tmpV.data()); @@ -427,87 +377,6 @@ void RedlichKwongMFTP::getPartialMolarVolumes(doublereal* vbar) const } } -doublereal RedlichKwongMFTP::critTemperature() const -{ - double pc, tc, vc; - double a0 = 0.0; - double aT = 0.0; - for (size_t i = 0; i < m_kk; i++) { - for (size_t j = 0; j spec) { bool added = MixtureFugacityTP::addSpecies(spec); @@ -842,7 +711,7 @@ doublereal RedlichKwongMFTP::liquidVolEst(doublereal TKelvin, doublereal& presGu bool foundLiq = false; int m = 0; while (m < 100 && !foundLiq) { - int nsol = NicholsSolve(TKelvin, pres, atmp, btmp, Vroot); + int nsol = solveCubic(TKelvin, pres, atmp, btmp, Vroot); if (nsol == 1 || nsol == 2) { double pc = critPressure(); if (pres > pc) { @@ -868,28 +737,20 @@ doublereal RedlichKwongMFTP::densityCalc(doublereal TKelvin, doublereal presPa, // It's necessary to set the temperature so that m_a_current is set correctly. setTemperature(TKelvin); double tcrit = critTemperature(); - doublereal mmw = meanMolecularWeight(); + double mmw = meanMolecularWeight(); if (rhoguess == -1.0) { - if (phaseRequested != FLUID_GAS) { - if (TKelvin > tcrit) { - rhoguess = presPa * mmw / (GasConstant * TKelvin); - } else { - if (phaseRequested == FLUID_GAS || phaseRequested == FLUID_SUPERCRIT) { - rhoguess = presPa * mmw / (GasConstant * TKelvin); - } else if (phaseRequested >= FLUID_LIQUID_0) { + if (phaseRequested >= FLUID_LIQUID_0) { double lqvol = liquidVolEst(TKelvin, presPa); rhoguess = mmw / lqvol; } - } } else { - // Assume the Gas phase initial guess, if nothing is specified to - // the routine + // Assume the Gas phase initial guess, if nothing is specified to the routine rhoguess = presPa * mmw / (GasConstant * TKelvin); - } } + doublereal volguess = mmw / rhoguess; - NSolns_ = NicholsSolve(TKelvin, presPa, m_a_current, m_b_current, Vroot_); + NSolns_ = solveCubic(TKelvin, presPa, m_a_current, m_b_current, Vroot_); doublereal molarVolLast = Vroot_[0]; if (NSolns_ >= 2) { @@ -929,7 +790,7 @@ doublereal RedlichKwongMFTP::densSpinodalLiquid() const { double Vroot[3]; double T = temperature(); - int nsol = NicholsSolve(T, pressure(), m_a_current, m_b_current, Vroot); + int nsol = solveCubic(T, pressure(), m_a_current, m_b_current, Vroot); if (nsol != 3) { return critDensity(); } @@ -951,7 +812,7 @@ doublereal RedlichKwongMFTP::densSpinodalGas() const { double Vroot[3]; double T = temperature(); - int nsol = NicholsSolve(T, pressure(), m_a_current, m_b_current, Vroot); + int nsol = solveCubic(T, pressure(), m_a_current, m_b_current, Vroot); if (nsol != 3) { return critDensity(); } @@ -969,14 +830,6 @@ doublereal RedlichKwongMFTP::densSpinodalGas() const return mmw / (0.5 * (vv.first + vv.second)); } -doublereal RedlichKwongMFTP::pressureCalc(doublereal TKelvin, doublereal molarVol) const -{ - doublereal sqt = sqrt(TKelvin); - double pres = GasConstant * TKelvin / (molarVol - m_b_current) - - m_a_current / (sqt * molarVol * (molarVol + m_b_current)); - return pres; -} - doublereal RedlichKwongMFTP::dpdVCalc(doublereal TKelvin, doublereal molarVol, doublereal& presCalc) const { doublereal sqt = sqrt(TKelvin); @@ -1006,11 +859,6 @@ void RedlichKwongMFTP::pressureDerivatives() const } void RedlichKwongMFTP::updateMixingExpressions() -{ - updateAB(); -} - -void RedlichKwongMFTP::updateAB() { double temp = temperature(); if (m_formTempParam == 1) { @@ -1042,7 +890,7 @@ void RedlichKwongMFTP::updateAB() } } } - throw CanteraError("RedlichKwongMFTP::updateAB", + throw CanteraError("RedlichKwongMFTP::updateMixingExpressions", "Missing Redlich-Kwong coefficients for species: {}", to_string(b)); } } @@ -1086,11 +934,21 @@ doublereal RedlichKwongMFTP::da_dt() const return dadT; } -void RedlichKwongMFTP::calcCriticalConditions(doublereal a, doublereal b, doublereal a0_coeff, doublereal aT_coeff, - doublereal& pc, doublereal& tc, doublereal& vc) const +void RedlichKwongMFTP::calcCriticalConditions(doublereal& pc, doublereal& tc, doublereal& vc) const { + double a0 = 0.0; + double aT = 0.0; + for (size_t i = 0; i < m_kk; i++) { + for (size_t j = 0; j 0.1) { - throw CanteraError("RedlichKwongMFTP::calcCriticalConditions", "didn't converge"); + throw CanteraError("RedlichKwongMFTP::calcCriticalConditions", + "didn't converge"); } } @@ -1128,21 +987,14 @@ void RedlichKwongMFTP::calcCriticalConditions(doublereal a, doublereal b, double vc = omega_vc * GasConstant * tc / pc; } -int RedlichKwongMFTP::NicholsSolve(double TKelvin, double pres, doublereal a, doublereal b, - doublereal Vroot[3]) const +int RedlichKwongMFTP::solveCubic(double T, double pres, double a, double b, double Vroot[3]) const { - Vroot[0] = 0.0; - Vroot[1] = 0.0; - Vroot[2] = 0.0; - if (TKelvin <= 0.0) { - throw CanteraError("RedlichKwongMFTP::NicholsSolve", "neg temperature"); - } - + // Derive the coefficients of the cubic polynomial to solve. doublereal an = 1.0; - doublereal bn = - GasConstant * TKelvin / pres; - doublereal sqt = sqrt(TKelvin); - doublereal cn = - (GasConstant * TKelvin * b / pres - a/(pres * sqt) + b * b); + doublereal bn = - GasConstant * T / pres; + doublereal sqt = sqrt(T); + doublereal cn = - (GasConstant * T * b / pres - a/(pres * sqt) + b * b); doublereal dn = - (a * b / (pres * sqt)); double tmp = a * omega_b / (b * omega_a * GasConstant); @@ -1150,182 +1002,9 @@ int RedlichKwongMFTP::NicholsSolve(double TKelvin, double pres, doublereal a, do double tc = pow(tmp, pp); double pc = omega_b * GasConstant * tc / b; double vc = omega_vc * GasConstant * tc / pc; - // Derive the center of the cubic, x_N - doublereal xN = - bn /(3 * an); - - // Derive the value of delta**2. This is a key quantity that determines the - // number of turning points - doublereal delta2 = (bn * bn - 3 * an * cn) / (9 * an * an); - doublereal delta = 0.0; - - // Calculate a couple of ratios - doublereal ratio1 = 3.0 * an * cn / (bn * bn); - doublereal ratio2 = pres * b / (GasConstant * TKelvin); - if (fabs(ratio1) < 1.0E-7) { - doublereal ratio3 = a / (GasConstant * sqt) * pres / (GasConstant * TKelvin); - if (fabs(ratio2) < 1.0E-5 && fabs(ratio3) < 1.0E-5) { - doublereal zz = 1.0; - for (int i = 0; i < 10; i++) { - doublereal znew = zz / (zz - ratio2) - ratio3 / (zz + ratio1); - doublereal deltaz = znew - zz; - zz = znew; - if (fabs(deltaz) < 1.0E-14) { - break; - } - } - doublereal v = zz * GasConstant * TKelvin / pres; - Vroot[0] = v; - return 1; - } - } - - int nSolnValues; - double h2 = 4. * an * an * delta2 * delta2 * delta2; - if (delta2 > 0.0) { - delta = sqrt(delta2); - } - - doublereal h = 2.0 * an * delta * delta2; - doublereal yN = 2.0 * bn * bn * bn / (27.0 * an * an) - bn * cn / (3.0 * an) + dn; - doublereal desc = yN * yN - h2; - - if (fabs(fabs(h) - fabs(yN)) < 1.0E-10) { - if (desc != 0.0) { - // this is for getting to other cases - throw CanteraError("RedlichKwongMFTP::NicholsSolve", "numerical issues"); - } - desc = 0.0; - } - if (desc < 0.0) { - nSolnValues = 3; - } else if (desc == 0.0) { - nSolnValues = 2; - // We are here as p goes to zero. - } else if (desc > 0.0) { - nSolnValues = 1; - } - - // One real root -> have to determine whether gas or liquid is the root - if (desc > 0.0) { - doublereal tmpD = sqrt(desc); - doublereal tmp1 = (- yN + tmpD) / (2.0 * an); - doublereal sgn1 = 1.0; - if (tmp1 < 0.0) { - sgn1 = -1.0; - tmp1 = -tmp1; - } - doublereal tmp2 = (- yN - tmpD) / (2.0 * an); - doublereal sgn2 = 1.0; - if (tmp2 < 0.0) { - sgn2 = -1.0; - tmp2 = -tmp2; - } - doublereal p1 = pow(tmp1, 1./3.); - doublereal p2 = pow(tmp2, 1./3.); - doublereal alpha = xN + sgn1 * p1 + sgn2 * p2; - Vroot[0] = alpha; - Vroot[1] = 0.0; - Vroot[2] = 0.0; - tmp = an * Vroot[0] * Vroot[0] * Vroot[0] + bn * Vroot[0] * Vroot[0] + cn * Vroot[0] + dn; - } else if (desc < 0.0) { - doublereal tmp = - yN/h; - doublereal val = acos(tmp); - doublereal theta = val / 3.0; - doublereal oo = 2. * Pi / 3.; - doublereal alpha = xN + 2. * delta * cos(theta); - doublereal beta = xN + 2. * delta * cos(theta + oo); - doublereal gamma = xN + 2. * delta * cos(theta + 2.0 * oo); - Vroot[0] = beta; - Vroot[1] = gamma; - Vroot[2] = alpha; - - for (int i = 0; i < 3; i++) { - tmp = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; - if (fabs(tmp) > 1.0E-4) { - for (int j = 0; j < 3; j++) { - if (j != i && fabs(Vroot[i] - Vroot[j]) < 1.0E-4 * (fabs(Vroot[i]) + fabs(Vroot[j]))) { - warn_user("RedlichKwongMFTP::NicholsSolve", - "roots have merged: {}, {} (T = {}, p = {})", - Vroot[i], Vroot[j], TKelvin, pres); - } - } - } - } - } else if (desc == 0.0) { - if (yN == 0.0 && h == 0.0) { - Vroot[0] = xN; - Vroot[1] = xN; - Vroot[2] = xN; - } else { - // need to figure out whether delta is pos or neg - if (yN > 0.0) { - tmp = pow(yN/(2*an), 1./3.); - if (fabs(tmp - delta) > 1.0E-9) { - throw CanteraError("RedlichKwongMFTP::NicholsSolve", "unexpected"); - } - Vroot[1] = xN + delta; - Vroot[0] = xN - 2.0*delta; // liquid phase root - } else { - tmp = pow(yN/(2*an), 1./3.); - if (fabs(tmp - delta) > 1.0E-9) { - throw CanteraError("RedlichKwongMFTP::NicholsSolve", "unexpected"); - } - delta = -delta; - Vroot[0] = xN + delta; - Vroot[1] = xN - 2.0*delta; // gas phase root - } - } - for (int i = 0; i < 2; i++) { - tmp = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; - } - } - - // Unfortunately, there is a heavy amount of roundoff error due to bad - // conditioning in this - double res, dresdV = 0.0; - for (int i = 0; i < nSolnValues; i++) { - for (int n = 0; n < 20; n++) { - res = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; - if (fabs(res) < 1.0E-14) { - break; - } - dresdV = 3.0 * an * Vroot[i] * Vroot[i] + 2.0 * bn * Vroot[i] + cn; - double del = - res / dresdV; - Vroot[i] += del; - if (fabs(del) / (fabs(Vroot[i]) + fabs(del)) < 1.0E-14) { - break; - } - double res2 = an * Vroot[i] * Vroot[i] * Vroot[i] + bn * Vroot[i] * Vroot[i] + cn * Vroot[i] + dn; - if (fabs(res2) < fabs(res)) { - continue; - } else { - Vroot[i] -= del; - Vroot[i] += 0.1 * del; - } - } - if ((fabs(res) > 1.0E-14) && (fabs(res) > 1.0E-14 * fabs(dresdV) * fabs(Vroot[i]))) { - warn_user("RedlichKwongMFTP::NicholsSolve", - "root did not converge: V = {} (T = {}, p = {})", - Vroot[i], TKelvin, pres); - } - } - - if (nSolnValues == 1) { - if (TKelvin > tc) { - if (Vroot[0] < vc) { - nSolnValues = -1; - } - } else { - if (Vroot[0] < xN) { - nSolnValues = -1; - } - } - } else { - if (nSolnValues == 2 && delta > 0.0) { - nSolnValues = -2; - } - } + int nSolnValues = MixtureFugacityTP::solveCubic(T, pres, a, b, a, Vroot, an, bn, cn, dn, tc, pc); + return nSolnValues; } diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index f1a4c52a98..9faeff0f4a 100644 --- a/src/thermo/ThermoFactory.cpp +++ b/src/thermo/ThermoFactory.cpp @@ -23,6 +23,7 @@ #include "cantera/thermo/IonsFromNeutralVPSSTP.h" #include "cantera/thermo/PureFluidPhase.h" #include "cantera/thermo/RedlichKwongMFTP.h" +#include "cantera/thermo/PengRobinson.h" #include "cantera/thermo/SurfPhase.h" #include "cantera/thermo/EdgePhase.h" #include "cantera/thermo/MetalPhase.h" @@ -88,6 +89,7 @@ ThermoFactory::ThermoFactory() addAlias("liquid-water-IAPWS95", "PureLiquidWater"); reg("binary-solution-tabulated", []() { return new BinarySolutionTabulatedThermo(); }); addAlias("binary-solution-tabulated", "BinarySolutionTabulatedThermo"); + reg("Peng-Robinson", []() { return new PengRobinson(); }); } ThermoPhase* ThermoFactory::newThermoPhase(const std::string& model) diff --git a/test/data/co2_PR_example.yaml b/test/data/co2_PR_example.yaml new file mode 100644 index 0000000000..4f67f4e03a --- /dev/null +++ b/test/data/co2_PR_example.yaml @@ -0,0 +1,129 @@ +units: {length: cm, quantity: mol, activation-energy: cal/mol} + +phases: +- name: CO2-PR + species: [CO2, H2O, H2, CO, CH4, O2, N2] + thermo: Peng-Robinson + kinetics: gas + reactions: all + state: {T: 300, P: 1 atm, mole-fractions: {CO2: 0.99, H2: 0.01}} + + +species: +- name: CO2 + composition: {C: 1, O: 2} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [2.35677352, 8.98459677e-03, -7.12356269e-06, 2.45919022e-09, -1.43699548e-13, + -4.83719697e+04, 9.90105222] + - [3.85746029, 4.41437026e-03, -2.21481404e-06, 5.23490188e-10, -4.72084164e-14, + -4.8759166e+04, 2.27163806] + note: L7/88 + equation-of-state: + model: Peng-Robinson + a: 3.958134E+11 + b: 26.6275 + acentric_factor: 0.228 +- name: H2O + composition: {H: 2, O: 1} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [4.19864056, -2.0364341e-03, 6.52040211e-06, -5.48797062e-09, 1.77197817e-12, + -3.02937267e+04, -0.849032208] + - [3.03399249, 2.17691804e-03, -1.64072518e-07, -9.7041987e-11, 1.68200992e-14, + -3.00042971e+04, 4.9667701] + note: L8/89 + equation-of-state: + model: Peng-Robinson + a: 5.998873E+11 + b: 18.9714 + acentric_factor: 0.344 +- name: H2 + composition: {H: 2} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [2.34433112, 7.98052075e-03, -1.9478151e-05, 2.01572094e-08, -7.37611761e-12, + -917.935173, 0.683010238] + - [3.3372792, -4.94024731e-05, 4.99456778e-07, -1.79566394e-10, 2.00255376e-14, + -950.158922, -3.20502331] + note: TPIS78 + equation-of-state: + model: Peng-Robinson + a: 2.668423E+10 + b: 16.5478 + acentric_factor: -0.22 +- name: CO + composition: {C: 1, O: 1} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [3.57953347, -6.1035368e-04, 1.01681433e-06, 9.07005884e-10, -9.04424499e-13, + -1.4344086e+04, 3.50840928] + - [2.71518561, 2.06252743e-03, -9.98825771e-07, 2.30053008e-10, -2.03647716e-14, + -1.41518724e+04, 7.81868772] + note: TPIS79 + equation-of-state: + model: Peng-Robinson + a: 1.607164E+11 + b: 24.6549 + acentric_factor: 0.049 +- name: CH4 + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] + note: L8/88 + equation-of-state: + model: Peng-Robinson + a: 2.496344E+11 + b: 26.8028 + acentric_factor: 0.01 +- name: O2 + composition: {O: 2} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [3.78245636, -2.99673416e-03, 9.84730201e-06, -9.68129509e-09, 3.24372837e-12, + -1063.94356, 3.65767573] + - [3.28253784, 1.48308754e-03, -7.57966669e-07, 2.09470555e-10, -2.16717794e-14, + -1088.45772, 5.45323129] + note: TPIS89 + equation-of-state: + model: Peng-Robinson + a: 1.497732E+11 + b: 19.8281 + acentric_factor: 0.022 +- name: N2 + composition: {N: 2} + thermo: + model: NASA7 + temperature-ranges: [300.0, 1000.0, 5000.0] + data: + - [3.298677, 1.4082404e-03, -3.963222e-06, 5.641515e-09, -2.444854e-12, + -1020.8999, 3.950372] + - [2.92664, 1.4879768e-03, -5.68476e-07, 1.0097038e-10, -6.753351e-15, + -922.7977, 5.980528] + note: '121286' + equation-of-state: + model: Peng-Robinson + a: 1.485031E+11 + b: 28.0810 + acentric_factor: 0.04 + + +reactions: +- equation: CO2 + H2 <=> CO + H2O # Reaction 1 + rate-constant: {A: 1.2E+3, b: 0, Ea: 0} diff --git a/test/data/thermo-models.yaml b/test/data/thermo-models.yaml index 7c1f998ceb..87ebe7cae6 100644 --- a/test/data/thermo-models.yaml +++ b/test/data/thermo-models.yaml @@ -164,6 +164,11 @@ phases: thermo: Redlich-Kwong state: {T: 300, P: 200 atm, mole-fractions: {CO2: 0.9998, H2O: 0.0002}} +- name: CO2-PR + species: [{pr-species: [CO2, H2O, H2]}] + thermo: Peng-Robinson + state: {T: 300, P: 200 atm, mole-fractions: {CO2: 0.9998, H2O: 0.0002}} + - name: nitrogen species: [N2] thermo: pure-fluid @@ -559,6 +564,60 @@ rk-species: b: 27.80 +pr-species: +- name: H2 + composition: {H: 2} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [2.34433112, 0.00798052075, -1.9478151e-05, 2.01572094e-08, -7.37611761e-12, + -917.935173, 0.683010238] + - [3.3372792, -4.94024731e-05, 4.99456778e-07, -1.79566394e-10, 2.00255376e-14, + -950.158922, -3.20502331] + equation-of-state: + model: Peng-Robinson + units: {length: cm, quantity: mol} + a: 2.668423E+10 + b: 16.5478 + acentric_factor: -0.22 +- name: H2O + composition: {H: 2, O: 1} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [4.19864056, -0.0020364341, 6.52040211e-06, -5.48797062e-09, 1.77197817e-12, + -30293.7267, -0.849032208] + - [3.03399249, 0.00217691804, -1.64072518e-07, -9.7041987e-11, 1.68200992e-14, + -30004.2971, 4.9667701] + equation-of-state: + model: Peng-Robinson + units: {length: cm, quantity: mol} + a: 5.998873E+11 + b: 18.9714 + acentric_factor: 0.344 + binary-a: + H2: 4 bar*cm^6/mol^2 + CO2: 7.897e7 bar*cm^6/mol^2 +- name: CO2 + composition: {C: 1, O: 2} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [2.35677352, 0.00898459677, -7.12356269e-06, 2.45919022e-09, -1.43699548e-13, + -48371.9697, 9.90105222] + - [3.85746029, 0.00441437026, -2.21481404e-06, 5.23490188e-10, -4.72084164e-14, + -48759.166, 2.27163806] + equation-of-state: + model: Peng-Robinson + units: {length: cm, quantity: mol} + a: 3.958134E+11 + b: 26.6275 + acentric_factor: 0.228 + + HMW-species: - name: H2O(L) composition: {H: 2, O: 1} diff --git a/test/thermo/PengRobinson_Test.cpp b/test/thermo/PengRobinson_Test.cpp new file mode 100644 index 0000000000..c0c1a3031d --- /dev/null +++ b/test/thermo/PengRobinson_Test.cpp @@ -0,0 +1,344 @@ +#include "gtest/gtest.h" +#include "cantera/thermo/PengRobinson.h" +#include "cantera/thermo/ThermoFactory.h" + + +namespace Cantera +{ + +class PengRobinson_Test : public testing::Test +{ +public: + PengRobinson_Test() { + test_phase.reset(newPhase("../data/thermo-models.yaml", "CO2-PR")); + } + + //vary the composition of a co2-h2 mixture: + void set_r(const double r) { + vector_fp moleFracs(7); + moleFracs[0] = r; + moleFracs[2] = 1-r; + test_phase->setMoleFractions(&moleFracs[0]); + } + + std::unique_ptr test_phase; +}; + +TEST_F(PengRobinson_Test, construct_from_yaml) +{ + PengRobinson* peng_robinson_phase = dynamic_cast(test_phase.get()); + EXPECT_TRUE(peng_robinson_phase != NULL); +} + +TEST_F(PengRobinson_Test, chem_potentials) +{ + test_phase->setState_TP(298.15, 101325.); + /* Chemical potential should increase with increasing co2 mole fraction: + * mu = mu_0 + RT ln(gamma_k*X_k). + * where gamma_k is the activity coefficient. Run regression test against values + * calculated using the model. + */ + const double expected_result[9] = { + -457338129.70445037, + -457327078.87912911, + -457317214.31077951, + -457308354.65227401, + -457300353.74028891, + -457293092.45485628, + -457286472.73969948, + -457280413.14238912, + -457274845.44186872 + }; + + double xmin = 0.6; + double xmax = 0.9; + int numSteps = 9; + double dx = (xmax-xmin)/(numSteps-1); + vector_fp chemPotentials(7); + for(int i=0; i < numSteps; ++i) + { + set_r(xmin + i*dx); + test_phase->getChemPotentials(&chemPotentials[0]); + EXPECT_NEAR(expected_result[i], chemPotentials[0], 1.e-6); + } +} + +TEST_F(PengRobinson_Test, chemPotentials_RT) +{ + test_phase->setState_TP(298., 1.); + + // Test that chemPotentials_RT*RT = chemPotentials + const double RT = GasConstant * 298.; + vector_fp mu(7); + vector_fp mu_RT(7); + double xmin = 0.6; + double xmax = 0.9; + int numSteps = 9; + double dx = (xmax-xmin)/(numSteps-1); + + for(int i=0; i < numSteps; ++i) + { + const double r = xmin + i*dx; + set_r(r); + test_phase->getChemPotentials(&mu[0]); + test_phase->getChemPotentials_RT(&mu_RT[0]); + EXPECT_NEAR(mu[0], mu_RT[0]*RT, 1.e-6); + EXPECT_NEAR(mu[2], mu_RT[2]*RT, 1.e-6); + } +} + +TEST_F(PengRobinson_Test, activityCoeffs) +{ + test_phase->setState_TP(298., 1.); + + // Test that mu0 + RT log(activityCoeff * MoleFrac) == mu + const double RT = GasConstant * 298.; + vector_fp mu0(7); + vector_fp activityCoeffs(7); + vector_fp chemPotentials(7); + double xmin = 0.6; + double xmax = 0.9; + int numSteps = 9; + double dx = (xmax-xmin)/(numSteps-1); + + for(int i=0; i < numSteps; ++i) + { + const double r = xmin + i*dx; + set_r(r); + test_phase->getChemPotentials(&chemPotentials[0]); + test_phase->getActivityCoefficients(&activityCoeffs[0]); + test_phase->getStandardChemPotentials(&mu0[0]); + EXPECT_NEAR(chemPotentials[0], mu0[0] + RT*std::log(activityCoeffs[0] * r), 1.e-6); + EXPECT_NEAR(chemPotentials[2], mu0[2] + RT*std::log(activityCoeffs[2] * (1-r)), 1.e-6); + } +} + +TEST_F(PengRobinson_Test, standardConcentrations) +{ + EXPECT_DOUBLE_EQ(test_phase->pressure()/(test_phase->temperature()*GasConstant), + test_phase->standardConcentration(0)); + EXPECT_DOUBLE_EQ(test_phase->pressure()/(test_phase->temperature()*GasConstant), + test_phase->standardConcentration(1)); +} + +TEST_F(PengRobinson_Test, activityConcentrations) +{ + // Check to make sure activityConcentration_i == standardConcentration_i * gamma_i * X_i + vector_fp standardConcs(7); + vector_fp activityCoeffs(7); + vector_fp activityConcentrations(7); + double xmin = 0.6; + double xmax = 0.9; + int numSteps = 9; + double dx = (xmax-xmin)/(numSteps-1); + + for(int i=0; i < numSteps; ++i) + { + const double r = xmin + i*dx; + set_r(r); + test_phase->getActivityCoefficients(&activityCoeffs[0]); + standardConcs[0] = test_phase->standardConcentration(0); + standardConcs[2] = test_phase->standardConcentration(2); + test_phase->getActivityConcentrations(&activityConcentrations[0]); + + EXPECT_NEAR(standardConcs[0] * r * activityCoeffs[0], activityConcentrations[0], 1.e-6); + EXPECT_NEAR(standardConcs[2] * (1-r) * activityCoeffs[2], activityConcentrations[2], 1.e-6); + } +} + +TEST_F(PengRobinson_Test, setTP) +{ + // Check to make sure that the phase diagram is accurately reproduced for a few select isobars + + // All sub-cooled liquid: + const double rho1[6] = { + 6.6603507723749249e+002, + 1.6824762614489907e+002, + 1.6248354709581241e+002, + 1.5746729362032696e+002, + 1.5302217175386241e+002, + 1.4902908974486667e+002 + }; + // Phase change between temperatures 4 & 5: + const double rho2[6] = { + 7.5732259810273172e+002, + 7.2766981078381912e+002, + 6.935475475396446e+002, + 6.5227027102964917e+002, + 5.9657442842753153e+002, + 3.9966973266966875e+002 + }; + // Supercritical; no discontinuity in rho values: + const double rho3[6] = { + 8.0601205067780199e+002, + 7.8427655940884574e+002, + 7.6105347579146576e+002, + 7.3605202492828505e+002, + 7.0887891410210011e+002, + 6.7898591969734434e+002 + }; + + for(int i=0; i<6; ++i) + { + const double temp = 294 + i*2; + set_r(0.999); + test_phase->setState_TP(temp, 5542027.5); + EXPECT_NEAR(test_phase->density(),rho1[i],1.e-8); + + test_phase->setState_TP(temp, 7388370.); + EXPECT_NEAR(test_phase->density(),rho2[i],1.e-8); + + test_phase->setState_TP(temp, 9236712.5); + EXPECT_NEAR(test_phase->density(),rho3[i],1.e-8); + } +} + +TEST_F(PengRobinson_Test, getPressure) +{ + // Check to make sure that the P-R equation is accurately reproduced for a few selected values + + /* This test uses CO2 as the only species (mole fraction 100%). + * Values of a_coeff, b_coeff are calculated based on the the critical temperature + * and pressure values of CO2 as follows: + * a_coeff = 0.457235(RT_crit)^2/p_crit + * b_coeff = 0.077796(RT_crit)/p_crit + * The temperature dependent parameter in P-R EoS is calculated as + * \alpha = [1 + \kappa(1 - sqrt{T/T_crit}]^2 + * kappa is a function calulated based on the accentric factor. + */ + + double a_coeff = 3.958095109E+5; + double b_coeff = 26.62616317/1000; + double acc_factor = 0.228; + double pres_theoretical, kappa, alpha, mv; + const double rho = 1.0737; + + //Calculate kappa value + kappa = 0.37464 + 1.54226*acc_factor - 0.26992*acc_factor*acc_factor; + + for (int i = 0; i < 15; i++) + { + const double temp = 296 + i * 50; + set_r(1.0); + test_phase->setState_TR(temp, rho); + const double Tcrit = test_phase->critTemperature(); + mv = 1 / rho * test_phase->meanMolecularWeight(); + //Calculate pressure using Peng-Robinson EoS + alpha = pow(1 + kappa*(1 - sqrt(temp / Tcrit)), 2.0); + pres_theoretical = GasConstant*temp / (mv - b_coeff) + - a_coeff*alpha / (mv*mv + 2*b_coeff*mv - b_coeff*b_coeff); + EXPECT_NEAR(test_phase->pressure(), pres_theoretical, 3); + } +} + +TEST_F(PengRobinson_Test, gibbsEnergy) +{ + // Test that g == h - T*s + const double T = 298.; + double xmin = 0.6; + double xmax = 0.9; + int numSteps = 9; + double dx = (xmax - xmin) / (numSteps - 1); + double gibbs_theoretical; + + for (int i = 0; i < numSteps; ++i) + { + const double r = xmin + i * dx; + test_phase->setState_TP(T, 1e5); + set_r(r); + gibbs_theoretical = test_phase->enthalpy_mole() - T * (test_phase->entropy_mole()); + EXPECT_NEAR(test_phase->gibbs_mole(), gibbs_theoretical, 1.e-6); + } +} + +TEST_F(PengRobinson_Test, totalEnthalpy) +{ + // Test that hbar = \sum (h_k*x_k) + double hbar, sum = 0.0; + vector_fp partialEnthalpies(7); + vector_fp moleFractions(7); + double xmin = 0.6; + double xmax = 0.9; + int numSteps = 9; + double dx = (xmax - xmin) / (numSteps - 1); + + for (int i = 0; i < numSteps; ++i) + { + sum = 0.0; + const double r = xmin + i * dx; + test_phase->setState_TP(298., 1e5); + set_r(r); + hbar = test_phase->enthalpy_mole(); + test_phase->getMoleFractions(&moleFractions[0]); + test_phase->getPartialMolarEnthalpies(&partialEnthalpies[0]); + for (int k = 0; k < 7; k++) + { + sum += moleFractions[k] * partialEnthalpies[k]; + } + EXPECT_NEAR(hbar, sum, 1.e-6); + } +} + +TEST_F(PengRobinson_Test, cpValidate) +{ + // Test that cp = dH/dT at constant pressure using finite difference method + + double p = 1e5; + double Tmin = 298; + int numSteps = 1001; + double dT = 1e-5; + double r = 1.0; + vector_fp hbar(numSteps); + vector_fp cp(numSteps); + double dh_dT; + + test_phase->setState_TP(Tmin, p); + set_r(r); + hbar[0] = test_phase->enthalpy_mole(); // J/kmol + cp[0] = test_phase->cp_mole(); // unit is J/kmol/K + + for (int i = 0; i < numSteps; ++i) + { + const double T = Tmin + i * dT; + test_phase->setState_TP(T, p); + set_r(r); + hbar[i] = test_phase->enthalpy_mole(); // J/kmol + cp[i] = test_phase->cp_mole(); // unit is J/kmol/K + + if (i > 0) + { + dh_dT = (hbar[i] - hbar[i - 1]) / dT; + EXPECT_NEAR((cp[i] / dh_dT), 1.0, 1e-5); + } + } +} + +TEST_F(PengRobinson_Test, CoolPropValidate) +{ + // Validate the P-R EoS in Cantera with P-R EoS from CoolProp + + const double rhoCoolProp[10] = { + 9.067928191884574, + 8.318322900591179, + 7.6883521740498155, + 7.150504298001246, + 6.685330199667018, + 6.278630757480957, + 5.919763108091383, + 5.600572727499541, + 5.314694056926007, + 5.057077678380463 + }; + + double p = 5e5; + + // Calculate density using Peng-Robinson EoS from Cantera + for(int i=0; i<10; i++) + { + const double temp = 300 + i*25; + set_r(1.0); + test_phase->setState_TP(temp, p); + EXPECT_NEAR(test_phase->density(),rhoCoolProp[i],1.e-5); + } +} +}; diff --git a/test/thermo/cubicSolver_Test.cpp b/test/thermo/cubicSolver_Test.cpp new file mode 100644 index 0000000000..404b34f53e --- /dev/null +++ b/test/thermo/cubicSolver_Test.cpp @@ -0,0 +1,111 @@ +#include "gtest/gtest.h" +#include "cantera/thermo/PengRobinson.h" +#include "cantera/thermo/ThermoFactory.h" + +namespace Cantera +{ + +class cubicSolver_Test : public testing::Test +{ +public: + cubicSolver_Test() { + test_phase.reset(newPhase("../data/co2_PR_example.yaml")); + } + + //vary the composition of a co2-h2 mixture + void set_r(const double r) { + vector_fp moleFracs(7); + moleFracs[0] = r; + moleFracs[2] = 1-r; + test_phase->setMoleFractions(&moleFracs[0]); + } + + std::unique_ptr test_phase; +}; + +TEST_F(cubicSolver_Test, solve_cubic) +{ + /* This tests validates the cubic solver by considering CO2 as an example. + * Values of a_coeff, b_coeff and accentric factor are hard-coded. + * The temperature dependent parameter in P-R EoS is calculated as + * \alpha = [1 + \kappa(1 - sqrt{T/T_crit}]^2 + * kappa is a function calulated based on the accentric factor. + * + * Three different states are considered as follows: + * 1. T = 300 T, P = 1 bar => Vapor (1 real root of the cubic equation) + * 2. T = 300 K, P = 80 bar => Supercritical (1 real root of the cubic equation) + * 3. T = Tc, P = Pc => Near critical region + */ + + // Define a Peng-Robinson phase + PengRobinson* peng_robinson_phase = dynamic_cast(test_phase.get()); + EXPECT_TRUE(peng_robinson_phase != NULL); + + set_r(1.0); + double a_coeff = 3.958134E+5; + double b_coeff = 26.6275/1000; + double acc_factor = 0.228; + const double Tcrit = test_phase->critTemperature(); + const double pCrit = test_phase->critPressure(); + double kappa = 0.37464 + 1.54226 * acc_factor - 0.26992 * acc_factor * acc_factor; + double temp, pres, alpha, rho, p; + int nSolnValues; + double Vroot[3]; + + const double expected_result[3] = { + 24.809417072270659, + 0.063638901847459045, + 0.10521518550521104 + }; + + //Vapor phase -> nSolnValues = 1 + temp = 300; + pres = 1e5; + //calculate alpha + alpha = pow(1 + kappa * (1 - sqrt(temp / Tcrit)), 2); + //Find cubic roots + nSolnValues = peng_robinson_phase->solveCubic(temp, pres, a_coeff, b_coeff, alpha * a_coeff, Vroot); + EXPECT_NEAR(expected_result[0], Vroot[0], 1.e-6); + EXPECT_NEAR(nSolnValues, 1 , 1.e-6); + + // Obtain pressure using EoS and compare against the given pressure value + set_r(1.0); + rho = test_phase->meanMolecularWeight()/Vroot[0]; + peng_robinson_phase->setState_TR(temp, rho); + p = peng_robinson_phase->pressure(); + EXPECT_NEAR(p, pres, 1); + + //Liquid phase, supercritical -> nSolnValues = -1 + temp = 300; + pres = 80e5; + //calculate alpha + alpha = pow(1 + kappa * (1 - sqrt(temp / Tcrit)), 2); + //Find cubic roots + nSolnValues = peng_robinson_phase->solveCubic(temp, pres, a_coeff, b_coeff, alpha * a_coeff, Vroot); + EXPECT_NEAR(expected_result[1], Vroot[0], 1.e-6); + EXPECT_NEAR(nSolnValues, -1, 1.e-6); + + // Obtain pressure using EoS and compare against the given pressure value + set_r(1.0); + rho = test_phase->meanMolecularWeight()/Vroot[0]; + peng_robinson_phase->setState_TR(temp, rho); + p = peng_robinson_phase->pressure(); + EXPECT_NEAR(p, pres, 1); + + //Near critical point -> nSolnValues = -2 + temp = Tcrit; + //calculate alpha + alpha = pow(1 + kappa * (1 - sqrt(temp / Tcrit)), 2); + //Find cubic roots + nSolnValues = peng_robinson_phase->solveCubic(Tcrit, pCrit, a_coeff, b_coeff, alpha * a_coeff, Vroot); + EXPECT_NEAR(expected_result[2], Vroot[0], 1.e-6); + EXPECT_NEAR(nSolnValues, -2, 1.e-6); + + // Obtain pressure using EoS and compare against the given pressure value + set_r(1.0); + rho = test_phase->meanMolecularWeight()/Vroot[0]; + peng_robinson_phase->setState_TR(Tcrit, rho); + p = peng_robinson_phase->pressure(); + EXPECT_NEAR(p, pCrit, 1); +} +}; diff --git a/test/thermo/thermoFromYaml.cpp b/test/thermo/thermoFromYaml.cpp index abdda8a3b4..6ba3e8701e 100644 --- a/test/thermo/thermoFromYaml.cpp +++ b/test/thermo/thermoFromYaml.cpp @@ -332,6 +332,21 @@ TEST(ThermoFromYaml, RedlichKwong_CO2) EXPECT_NEAR(thermo->cp_mass(), 3358.492543261, 1e-8); } + +TEST(ThermoFromYaml, PengRobinson_CO2) +{ + auto thermo = newThermo("thermo-models.yaml", "CO2-PR"); + EXPECT_NEAR(thermo->density(), 924.3096421928459, 1e-8); + EXPECT_NEAR(thermo->enthalpy_mass(), -9206196.3008209914, 1e-6); + EXPECT_NEAR(thermo->cp_mass(), 2203.2614945393584, 1e-8); + + thermo->setState_TPX(350, 180*OneAtm, "CO2:0.6, H2O:0.02, H2:0.38"); + EXPECT_NEAR(thermo->density(), 606.92307568968181, 1e-8); + EXPECT_NEAR(thermo->enthalpy_mass(), -9067591.6182085164, 1e-6); + EXPECT_NEAR(thermo->cp_mass(), 3072.2397990253207, 1e-8); + EXPECT_NEAR(thermo->cv_mole(), 32420.064214752296, 1e-8); +} + TEST(ThermoFromYaml, PureFluid_nitrogen) { auto thermo = newThermo("thermo-models.yaml", "nitrogen");