Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions include/cantera/thermo/MixtureFugacityTP.h
Original file line number Diff line number Diff line change
Expand Up @@ -298,6 +298,42 @@ class MixtureFugacityTP : public ThermoPhase
*/
double z() const;

//! @name Ideal-Limit Utilities for Cubic EoS
//!
//! These helpers provide shared ideal-gas-limit expressions used by
//! cubic-EoS implementations when mixture non-ideal terms vanish.
//! @{

//! Molar heat capacity at constant pressure in the ideal-gas limit.
//! Units: J/kmol/K.
double cp_mole_ideal() const;

//! Molar heat capacity at constant volume in the ideal-gas limit.
//! Units: J/kmol/K.
double cv_mole_ideal() const;

//! Standard concentration in the ideal-gas limit for species @p k.
//! Units: kmol/m^3.
double standardConcentration_ideal(size_t k) const;

//! Ideal-gas-limit activity coefficients for all species.
//! Sets all entries of @p ac to 1.
void getActivityCoefficients_ideal(span<double> ac) const;

//! Ideal-gas-limit chemical potentials for all species.
//! @param mu Output vector of chemical potentials [J/kmol].
void getChemPotentials_ideal(span<double> mu) const;

//! Ideal-gas-limit partial molar enthalpies for all species.
//! @param hbar Output vector of partial molar enthalpies [J/kmol].
void getPartialMolarEnthalpies_ideal(span<double> hbar) const;

//! Ideal-gas-limit partial molar entropies for all species.
//! @param sbar Output vector of partial molar entropies [J/kmol/K].
void getPartialMolarEntropies_ideal(span<double> sbar) const;

//! @}

//! Calculate the deviation terms for the total entropy of the mixture from
//! the ideal gas mixture
/**
Expand Down
72 changes: 72 additions & 0 deletions src/thermo/MixtureFugacityTP.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,8 @@
#include "cantera/base/utilities.h"
#include "cantera/base/global.h"

#include <algorithm>

using namespace std;

namespace Cantera
Expand Down Expand Up @@ -266,6 +268,60 @@ double MixtureFugacityTP::z() const
return pressure() * meanMolecularWeight() / (density() * RT());
}

double MixtureFugacityTP::cp_mole_ideal() const
{
return GasConstant * mean_X(m_cp0_R);
}

double MixtureFugacityTP::cv_mole_ideal() const
{
return GasConstant * (mean_X(m_cp0_R) - 1.0);
}

double MixtureFugacityTP::standardConcentration_ideal(size_t k) const
{
getStandardVolumes(m_workS);
return 1.0 / m_workS[k];
}

void MixtureFugacityTP::getActivityCoefficients_ideal(span<double> ac) const
{
checkArraySize("MixtureFugacityTP::getActivityCoefficients_ideal", ac.size(), m_kk);
for (size_t k = 0; k < m_kk; k++) {
ac[k] = 1.0;
}
}

void MixtureFugacityTP::getChemPotentials_ideal(span<double> mu) const
{
checkArraySize("MixtureFugacityTP::getChemPotentials_ideal", mu.size(), m_kk);
double RT_ = RT();
getStandardChemPotentials(mu);
for (size_t k = 0; k < m_kk; k++) {
double xx = std::max(SmallNumber, moleFraction(k));
mu[k] += RT_ * log(xx);
}
}

void MixtureFugacityTP::getPartialMolarEnthalpies_ideal(span<double> hbar) const
{
checkArraySize("MixtureFugacityTP::getPartialMolarEnthalpies_ideal", hbar.size(), m_kk);
getEnthalpy_RT_ref(hbar);
scale(hbar.begin(), hbar.end(), hbar.begin(), RT());
}

void MixtureFugacityTP::getPartialMolarEntropies_ideal(span<double> sbar) const
{
checkArraySize("MixtureFugacityTP::getPartialMolarEntropies_ideal", sbar.size(), m_kk);
getEntropy_R_ref(sbar);
scale(sbar.begin(), sbar.end(), sbar.begin(), GasConstant);
double logPres = log(pressure() / refPressure());
for (size_t k = 0; k < m_kk; k++) {
double xx = std::max(SmallNumber, moleFraction(k));
sbar[k] -= GasConstant * (log(xx) + logPres);
}
}

double MixtureFugacityTP::sresid() const
{
throw NotImplementedError("MixtureFugacityTP::sresid");
Expand Down Expand Up @@ -841,6 +897,12 @@ int MixtureFugacityTP::solveCubic(double T, double pres, double a, double b,
}

double tmp;
auto physicalRoot = [b](double v) {
// For cubic EoS, physically admissible roots satisfy V > b and V > 0.
double vmin = std::max(0.0, b * (1.0 + 1e-12));
return std::isfinite(v) && v > vmin;
};

// One real root -> have to determine whether gas or liquid is the root
if (disc > 0.0) {
double tmpD = sqrt(disc);
Expand Down Expand Up @@ -921,6 +983,11 @@ int MixtureFugacityTP::solveCubic(double T, double pres, double a, double b,
// 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++) {
if (!physicalRoot(Vroot[i])) {
// Non-physical roots (for example, negative molar volume) are not
// used for state selection and should not trigger convergence errors.
continue;
}
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
Expand All @@ -946,6 +1013,11 @@ int MixtureFugacityTP::solveCubic(double T, double pres, double a, double b,
"V = {}", T, pres, Vroot[i]);
}
}
if (nSolnValues == 1 && !physicalRoot(Vroot[0])) {
throw CanteraError("MixtureFugacityTP::solveCubic",
"single real root is non-physical for T = {}, p = {} "
"(V = {}, b = {})", T, pres, Vroot[0], b);
}

if (nSolnValues == 1) {
// Determine the phase of the single root.
Expand Down
77 changes: 74 additions & 3 deletions src/thermo/PengRobinson.cpp
Original file line number Diff line number Diff line change
Expand Up @@ -10,10 +10,19 @@
#include "cantera/base/utilities.h"

#include <boost/algorithm/string.hpp>
#include <algorithm>

namespace Cantera
{

namespace
{
bool isIdealCubicLimit(double aMix, double bMix)
{
return aMix == 0.0 && bMix == 0.0;
}
}

const double PengRobinson::omega_a = 4.5723552892138218E-01;
const double PengRobinson::omega_b = 7.77960739038885E-02;
const double PengRobinson::omega_vc = 3.07401308698703833E-01;
Expand Down Expand Up @@ -83,6 +92,9 @@ void PengRobinson::setBinaryCoeffs(const string& species_i,
double PengRobinson::cp_mole() const
{
_updateReferenceStateThermo();
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
return cp_mole_ideal();
}
double T = temperature();
double mv = molarVolume();
double vpb = mv + (1 + Sqrt2) * m_b;
Expand All @@ -97,6 +109,9 @@ double PengRobinson::cp_mole() const
double PengRobinson::cv_mole() const
{
_updateReferenceStateThermo();
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
return cv_mole_ideal();
}
double T = temperature();
calculatePressureDerivatives();
return (cp_mole() + T * m_dpdT * m_dpdT / m_dpdV);
Expand All @@ -114,13 +129,16 @@ double PengRobinson::pressure() const

double PengRobinson::standardConcentration(size_t k) const
{
getStandardVolumes(m_workS);
return 1.0 / m_workS[k];
return standardConcentration_ideal(k);
}

void PengRobinson::getActivityCoefficients(span<double> ac) const
{
checkArraySize("PengRobinson::getActivityCoefficients", ac.size(), m_kk);
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
getActivityCoefficients_ideal(ac);
return;
}
double mv = molarVolume();
double vpb2 = mv + (1 + Sqrt2) * m_b;
double vmb2 = mv + (1 - Sqrt2) * m_b;
Expand Down Expand Up @@ -154,8 +172,13 @@ void PengRobinson::getActivityCoefficients(span<double> ac) const

void PengRobinson::getChemPotentials(span<double> mu) const
{
getGibbs_ref(mu);
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
getChemPotentials_ideal(mu);
return;
}
double RT_ = RT();

getGibbs_ref(mu);
for (size_t k = 0; k < m_kk; k++) {
double xx = std::max(SmallNumber, moleFraction(k));
mu[k] += RT_ * (log(xx));
Expand Down Expand Up @@ -189,6 +212,10 @@ void PengRobinson::getChemPotentials(span<double> mu) const

void PengRobinson::getPartialMolarEnthalpies(span<double> hbar) const
{
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
getPartialMolarEnthalpies_ideal(hbar);
return;
}
// First we get the reference state contributions
getEnthalpy_RT_ref(hbar);
scale(hbar.begin(), hbar.end(), hbar.begin(), RT());
Expand Down Expand Up @@ -239,6 +266,11 @@ void PengRobinson::getPartialMolarEnthalpies(span<double> hbar) const

void PengRobinson::getPartialMolarEntropies(span<double> sbar) const
{
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
getPartialMolarEntropies_ideal(sbar);
return;
}

// Using the identity : (hk - T*sk) = gk
double T = temperature();
getPartialMolarEnthalpies(sbar);
Expand Down Expand Up @@ -444,6 +476,9 @@ void PengRobinson::getSpeciesParameters(const string& name, AnyMap& speciesNode)

double PengRobinson::sresid() const
{
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
return 0.0;
}
double molarV = molarVolume();
double hh = m_b / molarV;
double zz = z();
Expand All @@ -457,6 +492,9 @@ double PengRobinson::sresid() const

double PengRobinson::hresid() const
{
if (isIdealCubicLimit(m_aAlpha_mix, m_b)) {
return 0.0;
}
double molarV = molarVolume();
double zz = z();
double aAlpha_1 = daAlpha_dT();
Expand Down Expand Up @@ -519,6 +557,39 @@ double PengRobinson::densityCalc(double T, double presPa, int phaseRequested,
double volGuess = mmw / rhoGuess;
m_NSolns = solveCubic(T, presPa, m_a, m_b, m_aAlpha_mix, m_Vroot);

// Ensure root ordering used for branch selection only contains physical roots.
// This avoids selecting negative-volume roots in mixed-parameter mechanisms.
double vmin = std::max(0.0, m_b * (1.0 + 1e-12));
vector<double> physicalRoots;
for (double root : m_Vroot) {
if (std::isfinite(root) && root > vmin) {
physicalRoots.push_back(root);
}
}
std::sort(physicalRoots.begin(), physicalRoots.end());
if (physicalRoots.empty()) {
return -1.0;
} else if (physicalRoots.size() == 1) {
// Preserve branch-request semantics for single-root states:
// return -2 when the requested phase is inconsistent with the
// single branch identified by solveCubic() sign convention.
if ((phaseRequested == FLUID_GAS && m_NSolns < 0)
|| (phaseRequested >= FLUID_LIQUID_0 && m_NSolns > 0))
{
return -2.0;
}
// Otherwise, accept the physically admissible root directly.
return mmw / physicalRoots[0];
} else if (physicalRoots.size() == 2) {
m_Vroot[0] = physicalRoots[0];
m_Vroot[1] = 0.5 * (physicalRoots[0] + physicalRoots[1]);
m_Vroot[2] = physicalRoots[1];
} else {
m_Vroot[0] = physicalRoots[0];
m_Vroot[1] = physicalRoots[1];
m_Vroot[2] = physicalRoots[2];
}

double molarVolLast = m_Vroot[0];
if (m_NSolns >= 2) {
if (phaseRequested >= FLUID_LIQUID_0) {
Expand Down
Loading
Loading