/** * * @file ChemEquil.cpp * * Chemical equilibrium. Implementation file for class * ChemEquil. * * Copyright 2001 California Institute of Technology * */ #ifdef WIN32 #pragma warning(disable:4786) #pragma warning(disable:4503) #endif #include using namespace std; #include "ChemEquil.h" #include "DenseMatrix.h" #include "sort.h" #include "PropertyCalculator.h" #include "ctexceptions.h" #include "vec_functions.h" #include "stringUtils.h" #include "MultiPhase.h" namespace Cantera { /// map property strings to integers int _equilflag(const char* xy) { string flag = string(xy); if (flag == "TP") return TP; else if (flag == "TV") return TV; else if (flag == "HP") return HP; else if (flag == "UV") return UV; else if (flag == "SP") return SP; else if (flag == "SV") return SV; else if (flag == "UP") return UP; else throw CanteraError("_equilflag","unknown property pair "+flag); } //----------------------------------------------------------- // construction / destruction //----------------------------------------------------------- /// Default Constructor. ChemEquil::ChemEquil() : m_skip(-1), m_p1(0), m_p2(0), m_p0(OneAtm), m_eloc(-1), m_abscharge(Tiny) {} /// Destructor ChemEquil::~ChemEquil(){ delete m_p1; delete m_p2; } /** * Prepare for equilibrium calculations. * @param s object representing the solution phase. */ void ChemEquil::initialize(thermo_t& s) { // store a pointer to s and some of its properties locally. // Note: the use of two pointers is a historical artifact. m_thermo = &s; m_phase = &s; m_p0 = s.refPressure(); m_kk = m_phase->nSpecies(); m_mm = m_phase->nElements(); if (m_kk < m_mm) { throw CanteraError("ChemEquil::initialize", "number of species cannot be less than the number of elements."); } // allocate space in internal work arrays m_molefractions.resize(m_kk); m_lambda.resize(m_mm, -100.0); m_elementmolefracs.resize(m_mm); m_comp.resize(m_mm * m_kk); m_jwork1.resize(m_mm+2); m_jwork2.resize(m_mm+2); m_startSoln.resize(m_mm+1); m_grt.resize(m_kk); m_mu_RT.resize(m_kk); m_component.resize(m_mm,-2); // set up elemental composition matrix int m, k, mneg = -1; doublereal na, ewt; for (m = 0; m < m_mm; m++) { for (k = 0; k < m_kk; k++) { na = m_phase->nAtoms(k,m); // handle the case of negative atom numbers (used to // represent positive ions, where the 'element' is an // electron if (na < 0.0) { // if negative atom numbers have already been specified // for some element other than this one, throw // an exception if (mneg >= 0 && mneg != m) throw CanteraError("ChemEquil::initialize", "negative atom numbers allowed for only one element"); mneg = m; ewt = m_phase->atomicWeight(m); // the element should be an electron... if it isn't // print a warning. if (ewt > 1.0e-3) writelog(string("WARNING: species " +m_phase->speciesName(k) +" has "+fp2str(m_phase->nAtoms(k,m)) +" atoms of element " +m_phase->elementName(m)+ ", but this element is not an electron.\n")); } } } m_eloc = mneg; // set up the elemental composition matrix for (k = 0; k < m_kk; k++) { for (m = 0; m < m_mm; m++) { m_comp[k*m_mm + m] = m_phase->nAtoms(k,m); } } } /** * Set mixture to an equilibrium state consistent with specified * element potentials and temperature. * * @param lambda_RT vector of non-dimensional element potentials * \f[ \lambda_m/RT \f]. * @param t temperature in K. * */ void ChemEquil::setToEquilState(thermo_t& s, const vector_fp& lambda_RT, doublereal t) { // construct the chemical potentials by summing element potentials fill(m_mu_RT.begin(), m_mu_RT.end(), 0.0); for (int k = 0; k < m_kk; k++) for (int m = 0; m < m_mm; m++) m_mu_RT[k] += lambda_RT[m]*nAtoms(k,m); // set the temperature s.setTemperature(t); // call the phase-specific method to set the phase to the // equilibrium state with the specified species chemical // potentials. s.setToEquilState(DATA_PTR(m_mu_RT)); update(s); } /** * update internally stored state information. * @todo argument not used. */ void ChemEquil::update(const thermo_t& s) { // get the mole fractions, temperature, and density m_phase->getMoleFractions(DATA_PTR(m_molefractions)); m_temp = m_phase->temperature(); m_dens = m_phase->density(); // compute the elemental mole fractions doublereal sum = 0.0; int m, k; for (m = 0; m < m_mm; m++) { m_elementmolefracs[m] = 0.0; for (k = 0; k < m_kk; k++) { m_elementmolefracs[m] += nAtoms(k,m) * m_molefractions[k]; if (m_molefractions[k] < 0.0) { throw CanteraError("update", "negative mole fraction for "+m_phase->speciesName(k)+ ": "+fp2str(m_molefractions[k])); } } sum += m_elementmolefracs[m]; } // normalize the element mole fractions for (m = 0; m < m_mm; m++) m_elementmolefracs[m] /= sum; } /// Estimate the initial mole numbers. This version borrows from the /// MultiPhaseEquil solver. int ChemEquil::setInitialMoles(thermo_t& s) { MultiPhase* mp = 0; MultiPhaseEquil* e = 0; int iok = 0; beginLogGroup("ChemEquil::setInitialMoles"); try { mp = new MultiPhase; mp->addPhase(&s, 1.0); mp->init(); e = new MultiPhaseEquil(mp, true); e->setInitialMixMoles(); // store component indices for (int m = 0; m < m_mm; m++) { m_component[m] = e->componentIndex(m); } for (int k = 0; k < m_kk; k++) { if (m_phase->moleFraction(k) > 0.0) { addLogEntry(m_phase->speciesName(k), m_phase->moleFraction(k)); } } update(s); delete e; delete mp; iok = 0; } catch (CanteraError) { delete e; delete mp; iok = -1; } endLogGroup(); return iok; } /** * Generate a starting estimate for the element potentials. */ int ChemEquil::estimateElementPotentials(thermo_t& s, vector_fp& lambda) { int m, n; beginLogGroup("estimateElementPotentials"); //for (k = 0; k < m_kk; k++) { // if (m_molefractions[k] > 0.0) { // m_molefractions[k] = fmaxx(m_molefractions[k], 0.05); // } //} //s.setState_PX(s.pressure(), m_molefractions.begin()); DenseMatrix aa(m_mm, m_mm, 0.0); vector_fp b(m_mm, -999.0); vector_fp mu_RT(m_kk, 0.0); s.getChemPotentials(DATA_PTR(mu_RT)); doublereal rrt = 1.0/(GasConstant*m_phase->temperature()); scale(mu_RT.begin(), mu_RT.end(), mu_RT.begin(), rrt); for (m = 0; m < m_mm; m++) { for (n = 0; n < m_mm; n++) { aa(m,n) = nAtoms(m_component[m], n); } b[m] = mu_RT[m_component[m]]; } int info; try { info = solve(aa, DATA_PTR(b)); } catch (CanteraError) { addLogEntry("failed to estimate initial element potentials."); info = -2; } if (info == 0) { for (m = 0; m < m_mm; m++) { lambda[m] = b[m]; addLogEntry(m_phase->elementName(m),b[m]); } } endLogGroup(); return info; } /** * Equilibrate a phase, holding the elemental composition fixed * at the initial vaollue. */ int ChemEquil::equilibrate(thermo_t& s, const char* XY) { vector_fp emol(s.nElements()); initialize(s); update(s); copy(m_elementmolefracs.begin(), m_elementmolefracs.end(), emol.begin()); return equilibrate(s, XY, emol); } /** * compute the equilibrium composition for 2 specified * properties and specified element moles. */ int ChemEquil::equilibrate(thermo_t& s, const char* XYstr, vector_fp& elMoles) { doublereal xval, yval; int fail = 0; delete m_p1; delete m_p2; bool tempFixed = true; int XY = _equilflag(XYstr); vector_fp state; s.saveState(state); beginLogGroup("ChemEquil::equilibrate"); initialize(s); update(s); switch (XY) { case TP: case PT: m_p1 = new TemperatureCalculator; m_p2 = new PressureCalculator; break; case HP: case PH: tempFixed = false; m_p1 = new EnthalpyCalculator; m_p2 = new PressureCalculator; break; case SP: case PS: tempFixed = false; m_p1 = new EntropyCalculator; m_p2 = new PressureCalculator; break; case SV: case VS: tempFixed = false; m_p1 = new EntropyCalculator; m_p2 = new DensityCalculator; break; case TV: case VT: m_p1 = new TemperatureCalculator; m_p2 = new DensityCalculator; break; case UV: case VU: tempFixed = false; m_p1 = new IntEnergyCalculator; m_p2 = new DensityCalculator; break; default: endLogGroup(); throw CanteraError("equilibrate","illegal property pair."); } addLogEntry("Problem type","fixed "+m_p1->symbol()+", "+m_p2->symbol()); addLogEntry(m_p1->symbol(), m_p1->value(s)); addLogEntry(m_p2->symbol(), m_p2->value(s)); // If the temperature is one of the specified variables, and // it is outside the valid range, throw an exception. if (tempFixed) { double tfixed = s.temperature(); if (tfixed > s.maxTemp() + 1.0 || tfixed < s.minTemp() - 1.0) { endLogGroup(); throw CanteraError("ChemEquil","Specified temperature (" +fp2str(m_thermo->temperature())+" K) outside " "valid range of "+fp2str(m_thermo->minTemp())+" K to " +fp2str(m_thermo->maxTemp())+" K\n"); } } xval = m_p1->value(s); yval = m_p2->value(s); int mm = m_mm; int m; int nvar = mm + 1; DenseMatrix jac(nvar, nvar); // jacobian vector_fp x(nvar, -102.0); // solution vector vector_fp res_trial(nvar); for (m = 0; m < mm; m++) { if (m_skip < 0 && elMoles[m] > 0.0 ) m_skip = m; } // start with a composition with everything non-zero. Note // that since we have already save the target element moles, // changing the composition at this point only affects the // starting point, not the final solution. vector_fp xmm(m_kk,0.0); for (int k = 0; k < m_kk; k++) { xmm[k] = m_phase->moleFraction(k) + Cutoff; } m_phase->setMoleFractions(DATA_PTR(xmm)); update(s); // loop to estimate T if (!tempFixed) { beginLogGroup("Initial T Estimate"); doublereal tmax = m_thermo->maxTemp(); doublereal tmin = m_thermo->minTemp(); doublereal slope, phigh, plow, pval, dt; // first get the property values at the upper and lower // temperature limits. Since p1 (h, s, or u) is monotonic // in T, these values determine the upper and lower // bounnds (phigh, plow) for p1. m_phase->setTemperature(tmax); setInitialMoles(s); phigh = m_p1->value(s); m_phase->setTemperature(tmin); setInitialMoles(s); plow = m_p1->value(s); // start with T at the midpoint of the range doublereal t0 = 0.5*(tmin + tmax); m_phase->setTemperature(t0); // loop up to 5 times for (int it = 0; it < 5; it++) { // set the composition and get p1 setInitialMoles(s); pval = m_p1->value(s); // If this value of p1 is greater than the specified // property value, then the current temperature is too // high. Use it as the new upper bound. Otherwise, it // is too low, so use it as the new lower bound. if (pval > xval) { tmax = t0; phigh = pval; } else { tmin = t0; plow = pval; } // Determine the new T estimate by linearly intepolation // between the upper and lower bounds slope = (phigh - plow)/(tmax - tmin); dt = (xval - plow)/slope; // If within 100 K, terminate the search if (fabs(dt) < 100.0) break; // update the T estimate t0 = tmin + dt; addLogEntry("new T estimate", t0); m_phase->setTemperature(t0); } endLogGroup(); // initial T estimate } //if (m_lambda[0] == -100.0) { setInitialMoles(s); for (int ii = 0; ii < m_mm; ii++) x[ii] = -101.0; estimateElementPotentials(s, x); //} //else { // doublereal rt = GasConstant * m_phase->temperature(); // for (int ii = 0; ii < m_mm; ii++) x[ii] = m_lambda[ii]/rt; //} x[m_mm] = log(m_phase->temperature()); vector_fp above(nvar); vector_fp below(nvar); for (m = 0; m < mm; m++) { above[m] = 200.0; below[m] = -2000.0; if (elMoles[m] < Cutoff && m != m_eloc) x[m] = -1000.0; } above[mm] = log(m_thermo->maxTemp() + 1.0); below[mm] = log(m_thermo->minTemp() - 1.0); vector_fp grad(nvar, 0.0); // gradient of f = F*F/2 vector_fp oldx(nvar, 0.0); // old solution //vector_fp prevx(nvar, 0.0); // old solution vector_fp oldresid(nvar, 0.0); doublereal f, oldf; int iter = 0; int info=0; doublereal fctr = 1.0, newval; goto converge; next: // if the problem involves charged species, then the // "electron" element equation is a charge balance. Compute // the sum of the absolute values of the charge to use as the // normalizing factor. if (m_eloc >= 0) { m_abscharge = 0.0; int k; for (k = 0; k < m_kk; k++) m_abscharge += fabs(m_phase->charge(k)*m_molefractions[k]); } iter++; if (iter > 1) endLogGroup(); // iteration beginLogGroup("Iteration "+int2str(iter)); // compute the residual and the jacobian using the current // solution vector equilResidual(s, x, elMoles, res_trial, xval, yval); f = 0.5*dot(res_trial.begin(), res_trial.end(), res_trial.begin()); addLogEntry("Residual norm", f); equilJacobian(s, x, elMoles, jac, xval, yval); // compute grad f = F*J jac.leftMult(DATA_PTR(res_trial), DATA_PTR(grad)); copy(x.begin(), x.end(), oldx.begin()); oldf = f; scale(res_trial.begin(), res_trial.end(), res_trial.begin(), -1.0); try { info = solve(jac, DATA_PTR(res_trial)); } catch (CanteraError) { addLogEntry("Jacobian is singular."); endLogGroup(); // iteration endLogGroup(); // equilibrate s.restoreState(state); throw CanteraError("equilibrate", "Jacobian is singular. \nTry adding more species, " "changing the elemental composition slightly, \nor removing " "unused elements."); return -3; } // find the factor by which the Newton step can be multiplied // to keep the solution within bounds. fctr = 1.0; for (m = 0; m < nvar; m++) { newval = x[m] + res_trial[m]; if (newval > above[m]) { fctr = fmaxx( 0.0, fminn( fctr, 0.8*(above[m] - x[m])/(newval - x[m]))); } else if (newval < below[m]) { fctr = fminn(fctr, 0.8*(x[m] - below[m])/(x[m] - newval)); } } if (fctr != 1.0) addLogEntry("factor to keep solution in bounds", fctr); // multiply the step by the scaling factor scale(res_trial.begin(), res_trial.end(), res_trial.begin(), fctr); if (!dampStep(s, oldx, oldf, grad, res_trial, x, f, elMoles , xval, yval)) { fail++; if (fail > 3) { addLogEntry("dampStep","Failed 3 times. Giving up."); endLogGroup(); // iteration endLogGroup(); // equilibrate s.restoreState(state); throw CanteraError("equilibrate", "Cannot find an acceptable Newton damping coefficient."); return -4; } } else fail = 0; converge: // check for convergence. equilResidual(s, x, elMoles, res_trial, xval, yval); f = 0.5*dot(res_trial.begin(), res_trial.end(), res_trial.begin()); doublereal xx, yy, deltax, deltay; xx = m_p1->value(s); yy = m_p2->value(s); deltax = (xx - xval)/xval; deltay = (yy - yval)/yval; doublereal rmax = absmax(res_trial.begin(), res_trial.end()); if (iter > 0 && rmax < options.relTolerance && fabs(deltax) < options.relTolerance && fabs(deltay) < options.relTolerance) { options.iterations = iter; endLogGroup(); // iteration m_lambda.resize(m_mm); beginLogGroup("Converged solution"); addLogEntry("Iterations",iter); addLogEntry("Relative error in "+m_p1->symbol(),deltax); addLogEntry("Relative error in "+m_p2->symbol(),deltay); addLogEntry("Max residual",rmax); beginLogGroup("Element potentials"); doublereal rt = GasConstant*m_thermo->temperature(); for (m = 0; m < m_mm; m++) { m_lambda[m] = x[m]*rt; addLogEntry("element "+m_phase->elementName(m), fp2str(x[m])); } endLogGroup(); // element potentials if (m_thermo->temperature() > m_thermo->maxTemp() + 1.0 || m_thermo->temperature() < m_thermo->minTemp() - 1.0 ) { writelog("Warning: Temperature (" +fp2str(m_thermo->temperature())+" K) outside " "valid range of "+fp2str(m_thermo->minTemp())+" K to " +fp2str(m_thermo->maxTemp())+" K\n"); } endLogGroup(); // converged solution endLogGroup(); // equilibrate return 0; } // no convergence if (iter > options.maxIterations) { addLogEntry("equilibrate","no convergence"); endLogGroup(); // iteration endLogGroup(); // equilibrate s.restoreState(state); throw CanteraError("equilibrate", "no convergence in "+int2str(options.maxIterations) +" iterations."); return -1; } goto next; } int ChemEquil::dampStep(thermo_t& mix, vector_fp& oldx, double oldf, vector_fp& grad, vector_fp& step, vector_fp& x, double& f, vector_fp& elmols, double xval, double yval ) { int nvar = x.size(); double slope; double f2 = 0.0; double oldf2 = 0.0; double alpha = 1.e-4; double tmpdamp = 0.0; double rhs1; double rhs2; double damp = 1.0; double damp2=0.0; double a; double bb; double disc; double minDamp = 0.0; double xTol = 1.e-7; vector_fp res_new(nvar); // fix //slope = grad * step; slope = dot(grad.begin(), grad.end(), step.begin()); double temp, test = 0.0; for (int i=0; i test) test = temp; } minDamp = xTol/test; retry: x = step; scale(x, damp); add_each(x, oldx); equilResidual(mix, x, elmols, res_new, xval, yval); //f = 0.5*(res_new*res_new); f = 0.5*dot(res_new.begin(), res_new.end(), res_new.begin()); if (damp < minDamp && damp < 1.0) { return 0; // check that this is not a spurious min of f } else if (f <= oldf + alpha * damp * slope) { return 1; // good damping coefficient } else { if (damp == 1.0) // first time { tmpdamp = -slope/(2.0*(f - oldf - slope)); } else { rhs1 = f - oldf - damp*slope; rhs2 = f2 - oldf2 - damp2*slope; a = (rhs1/(damp*damp) - rhs2/(damp2*damp2))/(damp - damp2); bb = (-damp2*rhs1/(damp*damp) + damp*rhs2/(damp2*damp2)) /(damp - damp2); if (a == 0.0) tmpdamp = -slope/(2.0*bb); else { disc = bb*bb - 3.0*a*slope; if (disc < 0.0) tmpdamp = -slope/(2.0*bb); else tmpdamp = (-bb +sqrt(disc))/(3.0*a); } if (tmpdamp > 0.5*damp) tmpdamp = 0.5*damp; } damp2 = damp; f2 = f; oldf2 = oldf; damp = fmaxx(tmpdamp, 0.1*damp); goto retry; } } /** * evaluates the residual vector F, of length mm */ void ChemEquil::equilResidual(thermo_t& mix, const vector_fp& x, const vector_fp& elmtotal, vector_fp& resid, doublereal xval, doublereal yval) { beginLogGroup("ChemEquil::equilResidual"); int n; doublereal xx, yy; doublereal temp = exp(x[m_mm]); setToEquilState(mix, x, temp); // residuals are the total element moles vector_fp& elm = m_elementmolefracs; for (n=0; n < m_mm; n++) { // drive element potential for absent elements to -1000 if (elmtotal[n] < Cutoff && n != m_eloc) resid[n] = x[n] + 1000.0; else resid[n] = log( (1.0 + elmtotal[n]) / (1.0 + elm[n]) ); addLogEntry(m_phase->elementName(n),fp2str(elm[n])+" (" +fp2str(elmtotal[n])+")"); } if (m_eloc >= 0) { doublereal chrg, sumnet = 0.0, sumabs = 0.0; for (int k = 0; k < m_kk; k++) { chrg = m_molefractions[k]*m_phase->charge(k); sumnet += chrg; sumabs += fabs(chrg); } addLogEntry("net charge",sumnet); resid[m_eloc] = sumnet/m_abscharge; // log((1.0 + sumnet/sumabs)); } xx = m_p1->value(mix); yy = m_p2->value(mix); resid[m_mm] = xx/xval - 1.0; resid[m_skip] = yy/yval - 1.0; string xstr = fp2str(xx)+" ("+fp2str(xval)+")"; addLogEntry(m_p1->symbol(), xstr); string ystr = fp2str(yy)+" ("+fp2str(yval)+")"; addLogEntry(m_p2->symbol(), ystr); endLogGroup(); } //-------------------- Jacobian evaluation --------------------------- void ChemEquil::equilJacobian(thermo_t& mix, vector_fp& x, const vector_fp& elmols, DenseMatrix& jac, doublereal xval, doublereal yval) { beginLogGroup("equilJacobian",0); int len = x.size(); vector_fp& r0 = m_jwork1; vector_fp& r1 = m_jwork2; r0.resize(len); r1.resize(len); int n, m; doublereal rdx, dx, xsave; doublereal atol = 1.e-10; equilResidual(mix, x, elmols, r0, xval, yval); for (n = 0; n < len; n++) { // perturb x(n) xsave = x[n]; dx = atol; x[n] = xsave + dx; dx = x[n] - xsave; rdx = 1.0/dx; // calculate perturbed residual equilResidual(mix, x, elmols, r1, xval, yval); // compute nth column of Jacobian for (m = 0; m < len; m++) { jac(m, n) = (r1[m] - r0[m])*rdx; } x[n] = xsave; } endLogGroup(); } } // namespace // $Log: ChemEquil.cpp,v