Cantera
Loading...
Searching...
No Matches
ChemEquil.cpp
Go to the documentation of this file.
1/**
2 * @file ChemEquil.cpp
3 * Chemical equilibrium. Implementation file for class
4 * ChemEquil.
5 */
6
7// This file is part of Cantera. See License.txt in the top-level directory or
8// at https://cantera.org/license.txt for license and copyright information.
9
15#include "cantera/base/global.h"
16#include "cantera/numerics/eigen_dense.h"
17
18namespace Cantera
19{
20
21int _equilflag(const char* xy)
22{
23 string flag = string(xy);
24 if (flag == "TP") {
25 return TP;
26 } else if (flag == "TV") {
27 return TV;
28 } else if (flag == "HP") {
29 return HP;
30 } else if (flag == "UV") {
31 return UV;
32 } else if (flag == "SP") {
33 return SP;
34 } else if (flag == "SV") {
35 return SV;
36 } else if (flag == "UP") {
37 return UP;
38 } else {
39 throw CanteraError("_equilflag","unknown property pair "+flag);
40 }
41}
42
43namespace
44{
45
46const char* targetPropertyName(int XY)
47{
48 switch (XY) {
49 case HP:
50 case PH:
51 return "enthalpy";
52 case SP:
53 case PS:
54 case SV:
55 case VS:
56 return "entropy";
57 case UV:
58 case VU:
59 return "internal energy";
60 default:
61 return "specified property";
62 }
63}
64
65[[noreturn]] void throwTemperatureBoundError(const string& XYstr, int XY,
66 double target, double current,
67 double currentT, double Tmin,
68 double Tmax, int boundDirection)
69{
70 string bound = boundDirection > 0 ? "upper" : "lower";
71 double Tbound = boundDirection > 0 ? Tmax : Tmin;
72 throw CanteraError("ChemEquil::equilibrate",
73 "Equilibration with the '{}' property pair failed because the solver "
74 "reached the {} temperature bound of {} K. The target {} is {}, but "
75 "the current value is {} at T = {} K. The enforced temperature bounds "
76 "are {} K to {} K. Disable temperature-limit enforcement to allow "
77 "extrapolation beyond this range.",
78 XYstr, bound, Tbound, targetPropertyName(XY), target, current,
79 currentT, Tmin, Tmax);
80}
81
82}
83
84ChemEquil::ChemEquil(ThermoPhase& s)
85{
86 initialize(s);
87}
88
90{
91 // store a pointer to s and some of its properties locally.
92 m_phase = &s;
93 m_kk = s.nSpecies();
94 m_mm = s.nElements();
96
97 // allocate space in internal work arrays within the ChemEquil object
98 m_molefractions.resize(m_kk);
100 m_comp.resize(m_mm * m_kk);
101 m_jwork1.resize(m_mm+2);
102 m_jwork2.resize(m_mm+2);
103 m_mu_RT.resize(m_kk);
104 m_muSS_RT.resize(m_kk);
105 m_component.resize(m_mm,npos);
106 m_orderVectorElements.resize(m_mm);
107
108 for (size_t m = 0; m < m_mm; m++) {
109 m_orderVectorElements[m] = m;
110 }
111 m_orderVectorSpecies.resize(m_kk);
112 for (size_t k = 0; k < m_kk; k++) {
113 m_orderVectorSpecies[k] = k;
114 }
115
116 // set up elemental composition matrix
117 size_t mneg = npos;
118 for (size_t m = 0; m < m_mm; m++) {
119 for (size_t k = 0; k < m_kk; k++) {
120 // handle the case of negative atom numbers (used to
121 // represent positive ions, where the 'element' is an
122 // electron
123 if (s.nAtoms(k,m) < 0.0) {
124 // if negative atom numbers have already been specified
125 // for some element other than this one, throw
126 // an exception
127 if (mneg != npos && mneg != m) {
128 throw CanteraError("ChemEquil::initialize",
129 "negative atom numbers allowed for only one element");
130 }
131 mneg = m;
132
133 // the element should be an electron... if it isn't
134 // print a warning.
135 if (s.atomicWeight(m) > 1.0e-3) {
136 warn_user("ChemEquil::initialize",
137 "species {} has {} atoms of element {}, "
138 "but this element is not an electron.",
139 s.speciesName(k), s.nAtoms(k,m), s.elementName(m));
140 }
141 }
142 }
143 }
144 m_eloc = mneg;
145
146 // set up the elemental composition matrix
147 for (size_t k = 0; k < m_kk; k++) {
148 for (size_t m = 0; m < m_mm; m++) {
149 m_comp[k*m_mm + m] = s.nAtoms(k,m);
150 }
151 }
152}
153
154void ChemEquil::setToEquilState(ThermoPhase& s, span<const double> lambda_RT, double t)
155{
156 // Construct the chemical potentials by summing element potentials
157 fill(m_mu_RT.begin(), m_mu_RT.end(), 0.0);
158 for (size_t k = 0; k < m_kk; k++) {
159 for (size_t m = 0; m < m_mm; m++) {
160 m_mu_RT[k] += lambda_RT[m]*nAtoms(k,m);
161 }
162 }
163
164 // Set the temperature
165 s.setTemperature(t);
166
167 // Call the phase-specific method to set the phase to the
168 // equilibrium state with the specified species chemical
169 // potentials.
170 s.setToEquilState(m_mu_RT);
171 update(s);
172}
173
175{
176 // get the mole fractions
178
179 // compute the elemental mole fractions
180 double sum = 0.0;
181 for (size_t m = 0; m < m_mm; m++) {
182 m_elementmolefracs[m] = 0.0;
183 for (size_t k = 0; k < m_kk; k++) {
185 if (m_molefractions[k] < 0.0) {
186 throw CanteraError("ChemEquil::update",
187 "negative mole fraction for {}: {}",
189 }
190 }
191 sum += m_elementmolefracs[m];
192 }
193 // Store the sum for later use
194 m_elementTotalSum = sum;
195 // normalize the element mole fractions
196 for (size_t m = 0; m < m_mm; m++) {
197 m_elementmolefracs[m] /= sum;
198 }
199}
200
201int ChemEquil::setInitialMoles(ThermoPhase& s, span<double> elMoleGoal, int loglevel)
202{
203 MultiPhase mp;
204 // Create a non-owning shared_ptr, since ThermoPhase `s` is guaranteed to outlive
205 // the MultiPhase object.
206 mp.addPhase(shared_ptr<ThermoPhase>(&s, [](ThermoPhase*) {}), 1.0);
207 mp.init();
208 MultiPhaseEquil e(&mp, true, loglevel-1);
209 e.setInitialMixMoles(loglevel-1);
210
211 // store component indices
212 m_nComponents = std::min(m_nComponents, m_kk);
213 for (size_t m = 0; m < m_nComponents; m++) {
214 m_component[m] = e.componentIndex(m);
215 }
216
217 // Update the current values of the temp, density, and mole fraction,
218 // and element abundance vectors kept within the ChemEquil object.
219 update(s);
220
221 if (m_loglevel > 0) {
222 writelog("setInitialMoles: Estimated Mole Fractions\n");
223 writelogf(" Temperature = %g\n", s.temperature());
224 writelogf(" Pressure = %g\n", s.pressure());
225 for (size_t k = 0; k < m_kk; k++) {
226 writelogf(" %-12s % -10.5g\n",
227 s.speciesName(k), s.moleFraction(k));
228 }
229 writelog(" Element_Name ElementGoal ElementMF\n");
230 for (size_t m = 0; m < m_mm; m++) {
231 writelogf(" %-12s % -10.5g% -10.5g\n",
232 s.elementName(m), elMoleGoal[m], m_elementmolefracs[m]);
233 }
234 }
235 return 0;
236}
237
239 span<double> elMolesGoal, int loglevel)
240{
241 vector<double> b(m_mm, -999.0);
242 vector<double> mu_RT(m_kk, 0.0);
243 vector<double> xMF_est(m_kk, 0.0);
244
245 s.getMoleFractions(xMF_est);
246 for (size_t n = 0; n < s.nSpecies(); n++) {
247 xMF_est[n] = std::max(xMF_est[n], 1e-20);
248 }
249 s.setMoleFractions(xMF_est);
250 s.getMoleFractions(xMF_est);
251
252 MultiPhase mp;
253 mp.addPhase(shared_ptr<ThermoPhase>(&s, [](ThermoPhase*) {}), 1.0);
254 mp.init();
255 bool usedZeroedSpecies = false;
256 vector<double> formRxnMatrix(mp.nSpecies() * mp.nElements());
257 m_nComponents = BasisOptimize(usedZeroedSpecies, false,
258 &mp, m_orderVectorSpecies,
259 m_orderVectorElements, formRxnMatrix);
260
261 for (size_t m = 0; m < m_nComponents; m++) {
262 size_t k = m_orderVectorSpecies[m];
263 m_component[m] = k;
264 xMF_est[k] = std::max(xMF_est[k], 1e-8);
265 }
266 s.setMoleFractions(xMF_est);
267 s.getMoleFractions(xMF_est);
268
269 ElemRearrange(m_nComponents, elMolesGoal, &mp,
270 m_orderVectorSpecies, m_orderVectorElements);
271
272 s.getChemPotentials(mu_RT);
273 scale(mu_RT.begin(), mu_RT.end(), mu_RT.begin(),
274 1.0/(GasConstant* s.temperature()));
275
276 if (loglevel > 0) {
277 for (size_t m = 0; m < m_nComponents; m++) {
278 size_t isp = m_component[m];
279 writelogf("isp = %d, %s\n", isp, s.speciesName(isp));
280 }
281 writelogf("Pressure = %g\n", s.pressure());
282 writelogf("Temperature = %g\n", s.temperature());
283 writelog(" id Name MF mu/RT \n");
284 for (size_t n = 0; n < s.nSpecies(); n++) {
285 writelogf("%10d %15s %10.5g %10.5g\n",
286 n, s.speciesName(n), xMF_est[n], mu_RT[n]);
287 }
288 }
290 for (size_t m = 0; m < m_nComponents; m++) {
291 for (size_t n = 0; n < m_nComponents; n++) {
292 aa(m,n) = nAtoms(m_component[m], m_orderVectorElements[n]);
293 }
294 b[m] = mu_RT[m_component[m]];
295 }
296
297 int info = 0;
298 try {
299 solve(aa, b);
300 } catch (CanteraError&) {
301 info = -2;
302 }
303 for (size_t m = 0; m < m_nComponents; m++) {
304 lambda_RT[m_orderVectorElements[m]] = b[m];
305 }
306 for (size_t m = m_nComponents; m < m_mm; m++) {
307 lambda_RT[m_orderVectorElements[m]] = 0.0;
308 }
309
310 if (loglevel > 0) {
311 writelog(" id CompSpecies ChemPot EstChemPot Diff\n");
312 for (size_t m = 0; m < m_nComponents; m++) {
313 size_t isp = m_component[m];
314 double tmp = 0.0;
315 for (size_t n = 0; n < m_mm; n++) {
316 tmp += nAtoms(isp, n) * lambda_RT[n];
317 }
318 writelogf("%3d %16s %10.5g %10.5g %10.5g\n",
319 m, s.speciesName(isp), mu_RT[isp], tmp, tmp - mu_RT[isp]);
320 }
321
322 writelog(" id ElName Lambda_RT\n");
323 for (size_t m = 0; m < m_mm; m++) {
324 writelogf(" %3d %6s %10.5g\n", m, s.elementName(m), lambda_RT[m]);
325 }
326 }
327 return info;
328}
329
330int ChemEquil::equilibrate(ThermoPhase& s, const char* XY, int loglevel)
331{
332 initialize(s);
333 update(s);
334 vector<double> elMolesGoal = m_elementmolefracs;
335 return equilibrate(s, XY, elMolesGoal, loglevel-1);
336}
337
338int ChemEquil::equilibrate(ThermoPhase& s, const char* XYstr,
339 span<double> elMolesGoal, int loglevel)
340{
341 bool tempFixed = true;
342 int XY = _equilflag(XYstr);
343 vector<double> state(s.stateSize());
344 s.saveState(state);
345 m_loglevel = loglevel;
346
347 // Check Compatibility
348 if (m_mm != s.nElements() || m_kk != s.nSpecies()) {
349 throw CanteraError("ChemEquil::equilibrate",
350 "Input ThermoPhase is incompatible with initialization");
351 }
352
353 initialize(s);
354 update(s);
355 switch (XY) {
356 case TP:
357 case PT:
358 m_p1 = [](ThermoPhase& s) { return s.temperature(); };
359 m_p2 = [](ThermoPhase& s) { return s.pressure(); };
360 break;
361 case HP:
362 case PH:
363 tempFixed = false;
364 m_p1 = [](ThermoPhase& s) { return s.enthalpy_mass(); };
365 m_p2 = [](ThermoPhase& s) { return s.pressure(); };
366 break;
367 case SP:
368 case PS:
369 tempFixed = false;
370 m_p1 = [](ThermoPhase& s) { return s.entropy_mass(); };
371 m_p2 = [](ThermoPhase& s) { return s.pressure(); };
372 break;
373 case SV:
374 case VS:
375 tempFixed = false;
376 m_p1 = [](ThermoPhase& s) { return s.entropy_mass(); };
377 m_p2 = [](ThermoPhase& s) { return s.density(); };
378 break;
379 case TV:
380 case VT:
381 m_p1 = [](ThermoPhase& s) { return s.temperature(); };
382 m_p2 = [](ThermoPhase& s) { return s.density(); };
383 break;
384 case UV:
385 case VU:
386 tempFixed = false;
387 m_p1 = [](ThermoPhase& s) { return s.intEnergy_mass(); };
388 m_p2 = [](ThermoPhase& s) { return s.density(); };
389 break;
390 default:
391 throw CanteraError("ChemEquil::equilibrate",
392 "illegal property pair '{}'", XYstr);
393 }
394 // If the temperature is one of the specified variables, and it is outside
395 // the valid range, throw an exception if strict limits are requested.
396 if (tempFixed && options.enforceTemperatureLimits) {
397 double tfixed = s.temperature();
398 if (tfixed > s.maxTemp() + 1.0 || tfixed < s.minTemp() - 1.0) {
399 throw CanteraError("ChemEquil::equilibrate", "Specified temperature"
400 " ({} K) outside valid range of {} K to {} K\n",
401 s.temperature(), s.minTemp(), s.maxTemp());
402 }
403 }
404
405 // Before we do anything to change the ThermoPhase object, we calculate and
406 // store the two specified thermodynamic properties that we are after.
407 double xval = m_p1(s);
408 double yval = m_p2(s);
409
410 size_t mm = m_mm;
411 size_t nvar = mm + 1;
412 DenseMatrix jac(nvar, nvar); // Jacobian
413 vector<double> x(nvar, -102.0); // solution vector
414 vector<double> res_trial(nvar, 0.0); // residual
415
416 // Replace one of the element abundance fraction equations with the
417 // specified property calculation.
418 //
419 // We choose the equation of the element with the highest element abundance.
420 double tmp = -1.0;
421 for (size_t im = 0; im < m_nComponents; im++) {
422 size_t m = m_orderVectorElements[im];
423 if (elMolesGoal[m] > tmp) {
424 m_skip = m;
425 tmp = elMolesGoal[m];
426 }
427 }
428 if (tmp <= 0.0) {
429 throw CanteraError("ChemEquil::equilibrate",
430 "Element Abundance Vector is zeroed");
431 }
432
433 // start with a composition with everything non-zero. Note that since we
434 // have already save the target element moles, changing the composition at
435 // this point only affects the starting point, not the final solution.
436 vector<double> xmm(m_kk, 0.0);
437 for (size_t k = 0; k < m_kk; k++) {
438 xmm[k] = s.moleFraction(k) + 1.0E-32;
439 }
440 s.setMoleFractions(xmm);
441
442 // Update the internally stored element mole fractions.
443 update(s);
444
445 double tmaxPhase = s.maxTemp();
446 double tminPhase = s.minTemp();
447 double tminSolver = options.enforceTemperatureLimits ? tminPhase :
448 clip(SmallNumber, 0.5 * tminPhase, 100.0);
449 double tmaxSolver = options.enforceTemperatureLimits ? tmaxPhase :
450 std::max(tmaxPhase + 1000.0, 10.0 * tmaxPhase);
451 if (tmaxSolver <= tminSolver) {
452 tmaxSolver = tminSolver + 20.0;
453 }
454 int limitingTemperatureBound = 0;
455
456 // loop to estimate T
457 if (!tempFixed) {
458 double tmin = std::max(s.temperature(), tminSolver);
459 if (tmin > tmaxSolver) {
460 tmin = tmaxSolver - 20;
461 }
462 double tmax = std::min(tmin + 10., tmaxSolver);
463 if (tmax < tminSolver) {
464 tmax = tminSolver + 20;
465 }
466
467 double slope, phigh, plow, pval, dt;
468
469 // first get the property values at the upper and lower temperature
470 // limits. Since p1 (h, s, or u) is monotonic in T, these values
471 // determine the upper and lower bounds (phigh, plow) for p1.
472
473 s.setTemperature(tmax);
474 setInitialMoles(s, elMolesGoal, loglevel - 1);
475 phigh = m_p1(s);
476
477 s.setTemperature(tmin);
478 setInitialMoles(s, elMolesGoal, loglevel - 1);
479 plow = m_p1(s);
480
481 // start with T at the midpoint of the range
482 double t0 = 0.5*(tmin + tmax);
483 s.setTemperature(t0);
484
485 // loop up to 5 times
486 for (int it = 0; it < 10; it++) {
487 // set the composition and get p1
488 setInitialMoles(s, elMolesGoal, loglevel - 1);
489 pval = m_p1(s);
490
491 // If this value of p1 is greater than the specified property value,
492 // then the current temperature is too high. Use it as the new upper
493 // bound. Otherwise, it is too low, so use it as the new lower
494 // bound.
495 if (pval > xval) {
496 tmax = t0;
497 phigh = pval;
498 } else {
499 tmin = t0;
500 plow = pval;
501 }
502
503 // Determine the new T estimate by linearly interpolating
504 // between the upper and lower bounds
505 slope = (phigh - plow)/(tmax - tmin);
506 dt = (xval - pval)/slope;
507
508 // If within 50 K, terminate the search
509 if (fabs(dt) < 50.0) {
510 break;
511 }
512 dt = clip(dt, -200.0, 200.0);
513 if ((t0 + dt) < tminSolver) {
514 dt = 0.5*((t0) + tminSolver) - t0;
515 }
516 if ((t0 + dt) > tmaxSolver) {
517 dt = 0.5*((t0) + tmaxSolver) - t0;
518 }
519 // update the T estimate
520 t0 += dt;
521 if (t0 <= tminSolver || t0 >= tmaxSolver) {
522 double current = m_p1(s);
523 double currentT = s.temperature();
524 s.restoreState(state);
525 throwTemperatureBoundError(XYstr, XY, xval, current, currentT,
526 tminPhase, tmaxPhase,
527 t0 >= tmaxSolver ? 1 : -1);
528 }
529 s.setTemperature(t0);
530 }
531 }
532
533 setInitialMoles(s, elMolesGoal,loglevel);
534
535 // Calculate initial estimates of the element potentials. This algorithm
536 // uses the MultiPhaseEquil object's initialization capabilities to
537 // calculate an initial estimate of the mole fractions for a set of linearly
538 // independent component species. Then, the element potentials are solved
539 // for based on the chemical potentials of the component species.
540 estimateElementPotentials(s, x, elMolesGoal);
541
542 // Do a better estimate of the element potentials. We have found that the
543 // current estimate may not be good enough to avoid drastic numerical issues
544 // associated with the use of a numerically generated Jacobian.
545 //
546 // The Brinkley algorithm assumes a constant T, P system and uses a
547 // linearized analytical Jacobian that turns out to be very stable.
548 int info = estimateEP_Brinkley(s, x, elMolesGoal);
549 if (info == 0) {
550 setToEquilState(s, x, s.temperature());
551 }
552
553 // Install the log(temp) into the last solution unknown slot.
554 x[m_mm] = log(s.temperature());
555
556 // Setting the max and min values for x[]. Also, if element abundance vector
557 // is zero, setting x[] to -1000. This effectively zeroes out all species
558 // containing that element.
559 vector<double> above(nvar);
560 vector<double> below(nvar);
561 for (size_t m = 0; m < mm; m++) {
562 above[m] = 200.0;
563 below[m] = -2000.0;
564 if (elMolesGoal[m] < m_elemFracCutoff && m != m_eloc) {
565 x[m] = -1000.0;
566 }
567 }
568
569 // Set temperature bounds. By default, these are broad numerical guardrails
570 // rather than the nominal validity limits of the thermodynamic fits. The
571 // log(T) step is separately damped below to avoid large extrapolation steps.
572 if (options.enforceTemperatureLimits) {
573 above[mm] = log(tmaxPhase);
574 below[mm] = log(std::max(SmallNumber, tminPhase));
575 } else {
576 above[mm] = log(tmaxSolver);
577 below[mm] = log(tminSolver);
578 }
579
580 vector<double> oldx(nvar, 0.0); // old solution
581 // Stall detection: a mixture whose equilibrium composition is dominated by a few
582 // species can leave some element potentials determined only by trace species,
583 // making the numerically-evaluated Jacobian rank deficient. Plain LU then returns a
584 // meaningless step along the null direction. Detect the resulting lack of progress
585 // and switch to a rank-truncated step, which holds the unresolvable directions
586 // fixed.
587 bool stalled = false;
588 bool rankDeficient = false;
589 int noProgress = 0;
590 double bestResid = BigNumber;
591 double maxResid = 0.0;
592 double dxmax = 0.0;
593 // Largest relative error in the element abundances that will be accepted as a
594 // reduced-accuracy solution. Beyond this, the result is too inaccurate to be
595 // useful, and lack of convergence is reported instead.
596 const double maxInexactResid = 1e-5;
597
598 // Any error raised while evaluating the residual, the Jacobian, or the Newton step
599 // means the iteration has reached a point where the element potential formulation
600 // can no longer be evaluated. How this manifests depends on the linear algebra
601 // backend: LAPACK reports a singular Jacobian from the LU factorization, while
602 // Eigen silently returns a step containing infinities, so that the failure instead
603 // surfaces as a composition that cannot be normalized. The cause and the remedy are
604 // the same in either case, so handle them identically. This helper restores the
605 // saved state and returns the error for the caller to throw.
606 auto iterationError = [&](int iter, const CanteraError& err) {
607 s.restoreState(state);
608 return CanteraError("ChemEquil::equilibrate",
609 fmt::format("The element potential iteration failed at iteration {}.\n",
610 iter)
611 + "The equilibrium state of this mixture cannot be resolved using the "
612 "element potential formulation.\nConsider trying the 'gibbs' or 'vcs' "
613 "solver, which use a different formulation.\n\n"
614 "The underlying error was:\n" + err.getMessage());
615 };
616
617 for (int iter = 0; iter < options.maxIterations; iter++) {
618 // check for convergence.
619 try {
620 equilResidual(s, x, elMolesGoal, res_trial, xval, yval);
621 } catch (CanteraError& err) {
622 throw iterationError(iter, err);
623 }
624 double xx = m_p1(s);
625 double yy = m_p2(s);
626 double deltax = (xx - xval)/xval;
627 double deltay = (yy - yval)/yval;
628 bool passThis = true;
629 maxResid = 0.0;
630 for (size_t m = 0; m < nvar; m++) {
631 double tval = options.relTolerance;
632 if (m < mm) {
633 // Special case convergence requirements for electron element.
634 // This is a special case because the element coefficients may
635 // be both positive and negative. And, typically they sum to
636 // 0.0. Therefore, there is no natural absolute value for this
637 // quantity. We supply the absolute value tolerance here. Note,
638 // this is made easier since the element abundances are
639 // normalized to one within this routine.
640 //
641 // Note, the 1.0E-13 value was recently relaxed from 1.0E-15,
642 // because convergence failures were found to occur for the
643 // lower value at small pressure (0.01 pascal).
644 if (m == m_eloc) {
645 tval = elMolesGoal[m] * options.relTolerance + options.absElemTol
646 + 1.0E-13;
647 } else {
648 tval = elMolesGoal[m] * options.relTolerance + options.absElemTol;
649 }
650 }
651 if (fabs(res_trial[m]) > tval) {
652 passThis = false;
653 }
654 maxResid = std::max(maxResid, fabs(res_trial[m]) / tval);
655 }
656 if (maxResid < 0.9 * bestResid) {
657 bestResid = maxResid;
658 noProgress = 0;
659 } else if (++noProgress > 20) {
660 stalled = true;
661 }
662 // A solution that does not meet the requested tolerance is accepted only if
663 // the Jacobian was actually found to be rank deficient, the iteration is no
664 // longer moving, and the residual that remains is small in absolute terms.
665 // Without these conditions, an ordinary convergence failure would be
666 // indistinguishable from the ill-conditioned case handled here.
667 bool inexact = stalled && rankDeficient && dxmax < 1e-12
668 && maxResid * options.relTolerance < maxInexactResid;
669 if ((passThis || inexact)
670 && fabs(deltax) < options.relTolerance
671 && fabs(deltay) < options.relTolerance) {
672 options.iterations = iter;
673 if (!passThis && options.warnOnInexactConvergence) {
674 // Report the accuracy actually achieved in terms of the largest
675 // relative error in the mole fraction of any element.
676 double relErr = 0.0;
677 size_t worst = 0;
678 for (size_t m = 0; m < mm; m++) {
679 if (elMolesGoal[m] > m_elemFracCutoff) {
680 double err = fabs(m_elementmolefracs[m] - elMolesGoal[m])
681 / elMolesGoal[m];
682 if (err > relErr) {
683 relErr = err;
684 worst = m;
685 }
686 }
687 }
688 warn_user("ChemEquil::equilibrate",
689 "The equilibrium composition of this mixture is dominated by a "
690 "few species, leaving some element potentials determined only "
691 "by species present in trace amounts.\nThese potentials cannot "
692 "be resolved to the requested relative tolerance of {:g}.\n"
693 "Returning the most accurate solution available, in which the "
694 "mole fraction of element {} deviates from the specified value "
695 "by a relative amount of {:g}.\nErrors in the computed species "
696 "mole fractions are expected to be of a similar relative "
697 "magnitude, and may be much larger for species present in trace "
698 "amounts.\nConsider using the 'gibbs' or 'vcs' solvers, which do "
699 "not use the element potential formulation and can usually meet "
700 "the specified tolerance for such mixtures.",
701 options.relTolerance, s.elementName(worst), relErr);
702 }
703
704 if (m_eloc != npos) {
705 adjustEloc(s, elMolesGoal);
706 }
707
708 if (s.temperature() > s.maxTemp() + 1.0 ||
709 s.temperature() < s.minTemp() - 1.0) {
710 warn_user("ChemEquil::equilibrate",
711 "Temperature ({} K) outside valid range of {} K "
712 "to {} K", s.temperature(), s.minTemp(), s.maxTemp());
713 }
714 return passThis ? 0 : 1;
715 }
716 // compute the residual and the Jacobian using the current
717 // solution vector
718 try {
719 equilResidual(s, x, elMolesGoal, res_trial, xval, yval);
720
721 // Compute the Jacobian matrix
722 equilJacobian(s, x, elMolesGoal, jac, xval, yval);
723 } catch (CanteraError& err) {
724 throw iterationError(iter, err);
725 }
726
727 if (m_loglevel > 0) {
728 writelogf("Jacobian matrix %d:\n", iter);
729 for (size_t m = 0; m <= m_mm; m++) {
730 writelog(" [ ");
731 for (size_t n = 0; n <= m_mm; n++) {
732 writelog("{:10.5g} ", jac(m,n));
733 }
734 writelog(" ]");
735 if (m < m_mm) {
736 writelog("x_{:10s}", s.elementName(m));
737 } else if (m_eloc == m) {
738 writelog("x_ELOC");
739 } else if (m == m_skip) {
740 writelog("x_YY");
741 } else {
742 writelog("x_XX");
743 }
744 writelog(" = - ({:10.5g})\n", res_trial[m]);
745 }
746 }
747
748 oldx = x;
749 scale(res_trial.begin(), res_trial.end(), res_trial.begin(), -1.0);
750
751 // Solve the system
752 try {
753 if (stalled) {
754 // Rank-truncated least-squares step: components of the step
755 // along directions the Jacobian cannot resolve are dropped.
756 MappedMatrix J(const_cast<double*>(jac.data().data()),
757 jac.nRows(), jac.nColumns());
758 Eigen::JacobiSVD<Eigen::MatrixXd> svd(
759 J, Eigen::ComputeThinU | Eigen::ComputeThinV);
760 // The Jacobian is evaluated by forward differences with a relative
761 // perturbation of 1e-7 (see equilJacobian()), so its elements are
762 // themselves only accurate to a relative error of that order.
763 // Singular values below this threshold are indistinguishable from
764 // the noise in the finite difference approximation.
765 svd.setThreshold(1e-7);
766 rankDeficient = svd.rank() < nvar;
767 asVectorXd(res_trial) = svd.solve(asVectorXd(res_trial).eval());
768 } else {
769 solve(jac, res_trial);
770 }
771 } catch (CanteraError& err) {
772 throw iterationError(iter, err);
773 }
774
775 // find the factor by which the Newton step can be multiplied
776 // to keep the solution within bounds.
777 double fctr = 1.0;
778 // Track strict temperature bounds reached by the undamped Newton step.
779 // The damped iterate can remain just inside the bound, so remember the
780 // limiting direction across iterations for max-iteration diagnostics.
781 if (options.enforceTemperatureLimits && !tempFixed) {
782 double newTempVal = x[mm] + res_trial[mm];
783 if (newTempVal > above[mm]) {
784 limitingTemperatureBound = 1;
785 } else if (newTempVal < below[mm]) {
786 limitingTemperatureBound = -1;
787 }
788 }
789 for (size_t m = 0; m < nvar; m++) {
790 double newval = x[m] + res_trial[m];
791 if (newval > above[m]) {
792 fctr = std::max(0.0,
793 std::min(fctr,0.8*(above[m] - x[m])/(newval - x[m])));
794 } else if (newval < below[m]) {
795 if (m < m_mm && (m != m_skip)) {
796 res_trial[m] = -50;
797 if (x[m] < below[m] + 50.) {
798 res_trial[m] = below[m] - x[m];
799 }
800 } else {
801 fctr = std::min(fctr, 0.8*(x[m] - below[m])/(x[m] - newval));
802 }
803 }
804 // Delta Damping
805 if (m == mm && fabs(res_trial[mm]) > 0.2) {
806 fctr = std::min(fctr, 0.2/fabs(res_trial[mm]));
807 }
808 }
809 if (fctr != 1.0 && loglevel > 0) {
810 warn_user("ChemEquil::equilibrate",
811 "Soln Damping because of bounds: %g", fctr);
812 }
813
814 // multiply the step by the scaling factor
815 scale(res_trial.begin(), res_trial.end(), res_trial.begin(), fctr);
816
817 dampStep(oldx, res_trial, x);
818 dxmax = 0.0;
819 for (size_t m = 0; m < nvar; m++) {
820 dxmax = std::max(dxmax, fabs(x[m] - oldx[m]));
821 }
822 }
823
824 // no convergence
825 // If no proposed step crossed a bound, the final damped state may still
826 // identify the limiting bound.
827 if (options.enforceTemperatureLimits && !tempFixed && limitingTemperatureBound == 0) {
828 if (x[mm] >= above[mm] - 1e-10) {
829 limitingTemperatureBound = 1;
830 } else if (x[mm] <= below[mm] + 1e-10) {
831 limitingTemperatureBound = -1;
832 }
833 }
834 double current = m_p1(s);
835 double currentT = s.temperature();
836 s.restoreState(state);
837 if (limitingTemperatureBound != 0) {
838 throwTemperatureBoundError(XYstr, XY, xval, current, currentT,
839 tminPhase, tmaxPhase, limitingTemperatureBound);
840 }
841 throw CanteraError("ChemEquil::equilibrate",
842 "no convergence in {} iterations.", options.maxIterations);
843}
844
845
846void ChemEquil::dampStep(span<double> oldx, span<double> step, span<double> x)
847{
848 // Carry out a delta damping approach on the dimensionless element
849 // potentials.
850 double damp = 1.0;
851 for (size_t m = 0; m < m_mm; m++) {
852 if (m == m_eloc) {
853 if (step[m] > 1.25) {
854 damp = std::min(damp, 1.25 /step[m]);
855 }
856 if (step[m] < -1.25) {
857 damp = std::min(damp, -1.25 / step[m]);
858 }
859 } else {
860 if (step[m] > 0.75) {
861 damp = std::min(damp, 0.75 /step[m]);
862 }
863 if (step[m] < -0.75) {
864 damp = std::min(damp, -0.75 / step[m]);
865 }
866 }
867 }
868
869 // Update the solution unknown
870 for (size_t m = 0; m < x.size(); m++) {
871 x[m] = oldx[m] + damp * step[m];
872 }
873 if (m_loglevel > 0) {
874 writelogf("Solution Unknowns: damp = %g\n", damp);
875 writelog(" X_new X_old Step\n");
876 for (size_t m = 0; m < m_mm; m++) {
877 writelogf(" % -10.5g % -10.5g % -10.5g\n", x[m], oldx[m], step[m]);
878 }
879 }
880}
881
882void ChemEquil::equilResidual(ThermoPhase& s, span<const double> x,
883 span<const double> elmFracGoal, span<double> resid,
884 double xval, double yval, int loglevel)
885{
886 setToEquilState(s, x, exp(x[m_mm]));
887
888 // residuals are the total element moles
889 vector<double>& elmFrac = m_elementmolefracs;
890 for (size_t n = 0; n < m_mm; n++) {
891 size_t m = m_orderVectorElements[n];
892 // drive element potential for absent elements to -1000
893 if (elmFracGoal[m] < m_elemFracCutoff && m != m_eloc) {
894 resid[m] = x[m] + 1000.0;
895 } else if (n >= m_nComponents) {
896 resid[m] = x[m];
897 } else {
898 // Change the calculation for small element number, using
899 // L'Hopital's rule. The log formulation is unstable.
900 if (elmFracGoal[m] < 1.0E-10 || elmFrac[m] < 1.0E-10 || m == m_eloc) {
901 resid[m] = elmFracGoal[m] - elmFrac[m];
902 } else {
903 resid[m] = log((1.0 + elmFracGoal[m]) / (1.0 + elmFrac[m]));
904 }
905 }
906 }
907
908 if (loglevel > 0) {
909 writelog("Residual: ElFracGoal ElFracCurrent Resid\n");
910 for (size_t n = 0; n < m_mm; n++) {
911 writelogf(" % -14.7E % -14.7E % -10.5E\n",
912 elmFracGoal[n], elmFrac[n], resid[n]);
913 }
914 }
915
916 double xx = m_p1(s);
917 double yy = m_p2(s);
918 resid[m_mm] = xx/xval - 1.0;
919 resid[m_skip] = yy/yval - 1.0;
920
921 if (loglevel > 0) {
922 writelog(" Goal Xvalue Resid\n");
923 writelogf(" XX : % -14.7E % -14.7E % -10.5E\n", xval, xx, resid[m_mm]);
924 writelogf(" YY(%1d): % -14.7E % -14.7E % -10.5E\n", m_skip, yval, yy, resid[m_skip]);
925 }
926}
927
928void ChemEquil::equilJacobian(ThermoPhase& s, span<double> x, span<const double> elmols,
929 DenseMatrix& jac, double xval, double yval, int loglevel)
930{
931 vector<double>& r0 = m_jwork1;
932 vector<double>& r1 = m_jwork2;
933 size_t len = x.size();
934 r0.resize(len);
935 r1.resize(len);
936 double atol = 1.e-10;
937
938 equilResidual(s, x, elmols, r0, xval, yval, loglevel-1);
939
940 for (size_t n = 0; n < len; n++) {
941 double xsave = x[n];
942 double dx = std::max(atol, fabs(xsave) * 1.0E-7);
943 x[n] = xsave + dx;
944 dx = x[n] - xsave;
945 double rdx = 1.0/dx;
946
947 // calculate perturbed residual
948 equilResidual(s, x, elmols, r1, xval, yval, loglevel-1);
949
950 // compute nth column of Jacobian
951 for (size_t m = 0; m < x.size(); m++) {
952 jac(m, n) = (r1[m] - r0[m])*rdx;
953 }
954 x[n] = xsave;
955 }
956}
957
958double ChemEquil::calcEmoles(ThermoPhase& s, span<double> x, const double& n_t,
959 span<const double> Xmol_i_calc, span<double> eMolesCalc,
960 span<double> n_i_calc, double pressureConst)
961{
962 double n_t_calc = 0.0;
963
964 // Calculate the activity coefficients of the solution, at the previous
965 // solution state.
966 vector<double> actCoeff(m_kk, 1.0);
967 s.setMoleFractions(Xmol_i_calc);
968 s.setPressure(pressureConst);
969 s.getActivityCoefficients(actCoeff);
970
971 for (size_t k = 0; k < m_kk; k++) {
972 double tmp = - (m_muSS_RT[k] + log(actCoeff[k]));
973 for (size_t m = 0; m < m_mm; m++) {
974 tmp += nAtoms(k,m) * x[m];
975 }
976 tmp = std::min(tmp, 100.0);
977 if (tmp < -300.) {
978 n_i_calc[k] = 0.0;
979 } else {
980 n_i_calc[k] = n_t * exp(tmp);
981 }
982 n_t_calc += n_i_calc[k];
983 }
984 for (size_t m = 0; m < m_mm; m++) {
985 eMolesCalc[m] = 0.0;
986 for (size_t k = 0; k < m_kk; k++) {
987 eMolesCalc[m] += nAtoms(k,m) * n_i_calc[k];
988 }
989 }
990 return n_t_calc;
991}
992
993int ChemEquil::estimateEP_Brinkley(ThermoPhase& s, span<double> x, span<double> elMoles)
994{
995 // Before we do anything, we will save the state of the solution. Then, if
996 // things go drastically wrong, we will restore the saved state.
997 vector<double> state(s.stateSize());
998 s.saveState(state);
999 bool modifiedMatrix = false;
1000 size_t neq = m_mm+1;
1001 int retn = 1;
1002 DenseMatrix a1(neq, neq, 0.0);
1003 vector<double> b(neq, 0.0);
1004 vector<double> n_i(m_kk,0.0);
1005 vector<double> n_i_calc(m_kk,0.0);
1006 vector<double> actCoeff(m_kk, 1.0);
1007 double beta = 1.0;
1008
1009 s.getMoleFractions(n_i);
1010 double pressureConst = s.pressure();
1011 vector<double> Xmol_i_calc = n_i;
1012
1013 vector<double> x_old(m_mm+1, 0.0);
1014 vector<double> resid(m_mm+1, 0.0);
1015 vector<int> lumpSum(m_mm+1, 0);
1016
1017 // Get the nondimensional Gibbs functions for the species at their standard
1018 // states of solution at the current T and P of the solution.
1020
1021 vector<double> eMolesCalc(m_mm, 0.0);
1022 vector<double> eMolesFix(m_mm, 0.0);
1023 double elMolesTotal = 0.0;
1024 for (size_t m = 0; m < m_mm; m++) {
1025 elMolesTotal += elMoles[m];
1026 for (size_t k = 0; k < m_kk; k++) {
1027 eMolesFix[m] += nAtoms(k,m) * n_i[k];
1028 }
1029 }
1030
1031 for (size_t m = 0; m < m_mm; m++) {
1032 if (elMoles[m] > 1.0E-70) {
1033 x[m] = clip(x[m], -100.0, 50.0);
1034 } else {
1035 x[m] = clip(x[m], -1000.0, 50.0);
1036 }
1037 }
1038
1039 double n_t = 0.0;
1040 double nAtomsMax = 1.0;
1041 s.setMoleFractions(Xmol_i_calc);
1042 s.setPressure(pressureConst);
1043 s.getActivityCoefficients(actCoeff);
1044 for (size_t k = 0; k < m_kk; k++) {
1045 double tmp = - (m_muSS_RT[k] + log(actCoeff[k]));
1046 double sum2 = 0.0;
1047 for (size_t m = 0; m < m_mm; m++) {
1048 double sum = nAtoms(k,m);
1049 tmp += sum * x[m];
1050 sum2 += sum;
1051 nAtomsMax = std::max(nAtomsMax, sum2);
1052 }
1053 if (tmp > 100.) {
1054 n_t += 2.8E43;
1055 } else {
1056 n_t += exp(tmp);
1057 }
1058 }
1059
1060 if (m_loglevel > 0) {
1061 writelog("estimateEP_Brinkley::\n\n");
1062 writelogf("temp = %g\n", s.temperature());
1063 writelogf("pres = %g\n", s.pressure());
1064 writelog("Initial mole numbers and mu_SS:\n");
1065 writelog(" Name MoleNum mu_SS actCoeff\n");
1066 for (size_t k = 0; k < m_kk; k++) {
1067 writelogf("%15s %13.5g %13.5g %13.5g\n",
1068 s.speciesName(k), n_i[k], m_muSS_RT[k], actCoeff[k]);
1069 }
1070 writelogf("Initial n_t = %10.5g\n", n_t);
1071 writelog("Comparison of Goal Element Abundance with Initial Guess:\n");
1072 writelog(" eName eCurrent eGoal\n");
1073 for (size_t m = 0; m < m_mm; m++) {
1074 writelogf("%5s %13.5g %13.5g\n",
1075 s.elementName(m), eMolesFix[m], elMoles[m]);
1076 }
1077 }
1078 for (size_t m = 0; m < m_mm; m++) {
1079 if (m != m_eloc && elMoles[m] <= options.absElemTol) {
1080 x[m] = -200.;
1081 }
1082 }
1083
1084 // Main Loop.
1085 for (int iter = 0; iter < 20* options.maxIterations; iter++) {
1086 // Save the old solution
1087 for (size_t m = 0; m < m_mm; m++) {
1088 x_old[m] = x[m];
1089 }
1090 x_old[m_mm] = n_t;
1091 // Calculate the mole numbers of species
1092 if (m_loglevel > 0) {
1093 writelogf("START ITERATION %d:\n", iter);
1094 }
1095 // Calculate the mole numbers of species and elements.
1096 double n_t_calc = calcEmoles(s, x, n_t, Xmol_i_calc, eMolesCalc, n_i_calc,
1097 pressureConst);
1098
1099 for (size_t k = 0; k < m_kk; k++) {
1100 Xmol_i_calc[k] = n_i_calc[k]/n_t_calc;
1101 }
1102
1103 if (m_loglevel > 0) {
1104 writelog(" Species: Calculated_Moles Calculated_Mole_Fraction\n");
1105 for (size_t k = 0; k < m_kk; k++) {
1106 writelogf("%15s: %10.5g %10.5g\n",
1107 s.speciesName(k), n_i_calc[k], Xmol_i_calc[k]);
1108 }
1109 writelogf("%15s: %10.5g\n", "Total Molar Sum", n_t_calc);
1110 writelogf("(iter %d) element moles bal: Goal Calculated\n", iter);
1111 for (size_t m = 0; m < m_mm; m++) {
1112 writelogf(" %8s: %10.5g %10.5g \n",
1113 s.elementName(m), elMoles[m], eMolesCalc[m]);
1114 }
1115 }
1116
1117 bool normalStep = true;
1118 // Decide if we are to do a normal step or a modified step
1119 size_t iM = npos;
1120 for (size_t m = 0; m < m_mm; m++) {
1121 if (elMoles[m] > 0.001 * elMolesTotal) {
1122 if (eMolesCalc[m] > 1000. * elMoles[m]) {
1123 normalStep = false;
1124 iM = m;
1125 }
1126 if (1000 * eMolesCalc[m] < elMoles[m]) {
1127 normalStep = false;
1128 iM = m;
1129 }
1130 }
1131 }
1132 if (m_loglevel > 0 && !normalStep) {
1133 writelogf(" NOTE: iter(%d) Doing an abnormal step due to row %d\n", iter, iM);
1134 }
1135 if (!normalStep) {
1136 beta = 1.0;
1137 resid[m_mm] = 0.0;
1138 for (size_t im = 0; im < m_mm; im++) {
1139 size_t m = m_orderVectorElements[im];
1140 resid[m] = 0.0;
1141 if (im < m_nComponents && elMoles[m] > 0.001 * elMolesTotal) {
1142 if (eMolesCalc[m] > 1000. * elMoles[m]) {
1143 resid[m] = -0.5;
1144 resid[m_mm] -= 0.5;
1145 }
1146 if (1000 * eMolesCalc[m] < elMoles[m]) {
1147 resid[m] = 0.5;
1148 resid[m_mm] += 0.5;
1149 }
1150 }
1151 }
1152 if (n_t < (elMolesTotal / nAtomsMax)) {
1153 if (resid[m_mm] < 0.0) {
1154 resid[m_mm] = 0.1;
1155 }
1156 } else if (n_t > elMolesTotal) {
1157 resid[m_mm] = std::min(resid[m_mm], 0.0);
1158 }
1159 } else {
1160 // Determine whether the matrix should be dumbed down because the
1161 // coefficient matrix of species (with significant concentrations)
1162 // is rank deficient.
1163 //
1164 // The basic idea is that at any time during the calculation only a
1165 // small subset of species with sufficient concentration matters. If
1166 // the rank of the element coefficient matrix for that subset of
1167 // species is less than the number of elements, then the matrix
1168 // created by the Brinkley method below may become singular.
1169 //
1170 // The logic below looks for obvious cases where the current element
1171 // coefficient matrix is rank deficient.
1172 //
1173 // The way around rank-deficiency is to lump-sum the corresponding
1174 // row of the matrix. Note, lump-summing seems to work very well in
1175 // terms of its stability properties, that is, it heads in the right
1176 // direction, albeit with lousy convergence rates.
1177 //
1178 // NOTE: This probably should be extended to a full blown Gauss-
1179 // Jordan factorization scheme in the future. For Example the scheme
1180 // below would fail for the set: HCl NH4Cl, NH3. Hopefully, it's
1181 // caught by the equal rows logic below.
1182 for (size_t m = 0; m < m_mm; m++) {
1183 lumpSum[m] = 1;
1184 }
1185
1186 double nCutoff = 1.0E-9 * n_t_calc;
1187 if (m_loglevel > 0) {
1188 writelog(" Lump Sum Elements Calculation: \n");
1189 }
1190 for (size_t m = 0; m < m_mm; m++) {
1191 size_t kMSp = npos;
1192 size_t kMSp2 = npos;
1193 for (size_t k = 0; k < m_kk; k++) {
1194 if (n_i_calc[k] > nCutoff && fabs(nAtoms(k,m)) > 0.001) {
1195 if (kMSp != npos) {
1196 kMSp2 = k;
1197 double factor = fabs(nAtoms(kMSp,m) / nAtoms(kMSp2,m));
1198 for (size_t n = 0; n < m_mm; n++) {
1199 if (fabs(factor * nAtoms(kMSp2,n) - nAtoms(kMSp,n)) > 1.0E-8) {
1200 lumpSum[m] = 0;
1201 break;
1202 }
1203 }
1204 } else {
1205 kMSp = k;
1206 }
1207 }
1208 }
1209 if (m_loglevel > 0) {
1210 writelogf(" %5s %3d : %5d %5d\n",
1211 s.elementName(m), lumpSum[m], kMSp, kMSp2);
1212 }
1213 }
1214
1215 // Formulate the matrix.
1216 for (size_t im = 0; im < m_mm; im++) {
1217 size_t m = m_orderVectorElements[im];
1218 if (im < m_nComponents) {
1219 for (size_t n = 0; n < m_mm; n++) {
1220 a1(m,n) = 0.0;
1221 for (size_t k = 0; k < m_kk; k++) {
1222 a1(m,n) += nAtoms(k,m) * nAtoms(k,n) * n_i_calc[k];
1223 }
1224 }
1225 a1(m,m_mm) = eMolesCalc[m];
1226 a1(m_mm, m) = eMolesCalc[m];
1227 } else {
1228 for (size_t n = 0; n <= m_mm; n++) {
1229 a1(m,n) = 0.0;
1230 }
1231 a1(m,m) = 1.0;
1232 }
1233 }
1234 a1(m_mm, m_mm) = 0.0;
1235
1236 // Formulate the residual, resid, and the estimate for the
1237 // convergence criteria, sum
1238 double sum = 0.0;
1239 for (size_t im = 0; im < m_mm; im++) {
1240 size_t m = m_orderVectorElements[im];
1241 if (im < m_nComponents) {
1242 resid[m] = elMoles[m] - eMolesCalc[m];
1243 } else {
1244 resid[m] = 0.0;
1245 }
1246
1247 // For equations with positive and negative coefficients,
1248 // (electronic charge), we must mitigate the convergence
1249 // criteria by a condition limited by finite precision of
1250 // inverting a matrix. Other equations with just positive
1251 // coefficients aren't limited by this.
1252 double tmp;
1253 if (m == m_eloc) {
1254 tmp = resid[m] / (elMoles[m] + elMolesTotal*1.0E-6 + options.absElemTol);
1255 } else {
1256 tmp = resid[m] / (elMoles[m] + options.absElemTol);
1257 }
1258 sum += tmp * tmp;
1259 }
1260
1261 for (size_t m = 0; m < m_mm; m++) {
1262 if (a1(m,m) < 1.0E-50) {
1263 if (m_loglevel > 0) {
1264 writelogf(" NOTE: Diagonalizing the analytical Jac row %d\n", m);
1265 }
1266 for (size_t n = 0; n < m_mm; n++) {
1267 a1(m,n) = 0.0;
1268 }
1269 a1(m,m) = 1.0;
1270 if (resid[m] > 0.0) {
1271 resid[m] = 1.0;
1272 } else if (resid[m] < 0.0) {
1273 resid[m] = -1.0;
1274 } else {
1275 resid[m] = 0.0;
1276 }
1277 }
1278 }
1279
1280 resid[m_mm] = n_t - n_t_calc;
1281
1282 if (m_loglevel > 0) {
1283 writelog("Matrix:\n");
1284 for (size_t m = 0; m <= m_mm; m++) {
1285 writelog(" [");
1286 for (size_t n = 0; n <= m_mm; n++) {
1287 writelogf(" %10.5g", a1(m,n));
1288 }
1289 writelogf("] = %10.5g\n", resid[m]);
1290 }
1291 }
1292
1293 sum += pow(resid[m_mm] /(n_t + 1.0E-15), 2);
1294 if (m_loglevel > 0) {
1295 writelogf("(it %d) Convergence = %g\n", iter, sum);
1296 }
1297
1298 // Insist on 20x accuracy compared to the top routine. There are
1299 // instances, for ill-conditioned or singular matrices where this is
1300 // needed to move the system to a point where the matrices aren't
1301 // singular.
1302 if (sum < 0.05 * options.relTolerance) {
1303 retn = 0;
1304 break;
1305 }
1306
1307 // Row Sum scaling
1308 for (size_t m = 0; m <= m_mm; m++) {
1309 double tmp = 0.0;
1310 for (size_t n = 0; n <= m_mm; n++) {
1311 tmp += fabs(a1(m,n));
1312 }
1313 if (m < m_mm && tmp < 1.0E-30) {
1314 if (m_loglevel > 0) {
1315 writelogf(" NOTE: Diagonalizing row %d\n", m);
1316 }
1317 for (size_t n = 0; n <= m_mm; n++) {
1318 if (n != m) {
1319 a1(m,n) = 0.0;
1320 a1(n,m) = 0.0;
1321 }
1322 }
1323 }
1324 tmp = 1.0/tmp;
1325 for (size_t n = 0; n <= m_mm; n++) {
1326 a1(m,n) *= tmp;
1327 }
1328 resid[m] *= tmp;
1329 }
1330
1331 if (m_loglevel > 0) {
1332 writelog("Row Summed Matrix:\n");
1333 for (size_t m = 0; m <= m_mm; m++) {
1334 writelog(" [");
1335 for (size_t n = 0; n <= m_mm; n++) {
1336 writelogf(" %10.5g", a1(m,n));
1337 }
1338 writelogf("] = %10.5g\n", resid[m]);
1339 }
1340 }
1341
1342 // Next Step: We have row-summed the equations. However, there are
1343 // some degenerate cases where two rows will be multiplies of each
1344 // other in terms of 0 < m, 0 < m part of the matrix. This occurs on
1345 // a case by case basis, and depends upon the current state of the
1346 // element potential values, which affect the concentrations of
1347 // species.
1348 //
1349 // So, the way we have found to eliminate this problem is to lump-
1350 // sum one of the rows of the matrix, except for the last column,
1351 // and stick it all on the diagonal. Then, we at least have a non-
1352 // singular matrix, and the modified equation moves the
1353 // corresponding unknown in the correct direction.
1354 //
1355 // The previous row-sum operation has made the identification of
1356 // identical rows much simpler.
1357 //
1358 // Note at least 6E-4 is necessary for the comparison. I'm guessing
1359 // 1.0E-3. If two rows are anywhere close to being equivalent, the
1360 // algorithm can get stuck in an oscillatory mode.
1361 modifiedMatrix = false;
1362 for (size_t m = 0; m < m_mm; m++) {
1363 size_t sameAsRow = npos;
1364 for (size_t im = 0; im < m; im++) {
1365 bool theSame = true;
1366 for (size_t n = 0; n < m_mm; n++) {
1367 if (fabs(a1(m,n) - a1(im,n)) > 1.0E-7) {
1368 theSame = false;
1369 break;
1370 }
1371 }
1372 if (theSame) {
1373 sameAsRow = im;
1374 }
1375 }
1376 if (sameAsRow != npos || lumpSum[m]) {
1377 if (m_loglevel > 0) {
1378 if (lumpSum[m]) {
1379 writelogf("Lump summing row %d, due to rank deficiency analysis\n", m);
1380 } else if (sameAsRow != npos) {
1381 writelogf("Identified that rows %d and %d are the same\n", m, sameAsRow);
1382 }
1383 }
1384 modifiedMatrix = true;
1385 for (size_t n = 0; n < m_mm; n++) {
1386 if (n != m) {
1387 a1(m,m) += fabs(a1(m,n));
1388 a1(m,n) = 0.0;
1389 }
1390 }
1391 }
1392 }
1393
1394 if (m_loglevel > 0 && modifiedMatrix) {
1395 writelog("Row Summed, MODIFIED Matrix:\n");
1396 for (size_t m = 0; m <= m_mm; m++) {
1397 writelog(" [");
1398 for (size_t n = 0; n <= m_mm; n++) {
1399 writelogf(" %10.5g", a1(m,n));
1400 }
1401 writelogf("] = %10.5g\n", resid[m]);
1402 }
1403 }
1404
1405 try {
1406 solve(a1, resid);
1407 } catch (CanteraError& err) {
1408 s.restoreState(state);
1409 throw CanteraError("ChemEquil::estimateEP_Brinkley",
1410 "The Jacobian used to estimate the initial element potentials is "
1411 "singular.\nThe equilibrium state of this mixture cannot be "
1412 "resolved using the element potential formulation.\nThe 'gibbs' "
1413 "and 'vcs' solvers do not use this formulation and can usually "
1414 "solve such problems.\n\nThe underlying error was:\n"
1415 + err.getMessage());
1416 }
1417
1418 // Figure out the damping coefficient: Use a delta damping
1419 // coefficient formulation: magnitude of change is capped to exp(1).
1420 beta = 1.0;
1421 for (size_t m = 0; m < m_mm; m++) {
1422 if (resid[m] > 1.0) {
1423 beta = std::min(beta, 1.0 / resid[m]);
1424 }
1425 if (resid[m] < -1.0) {
1426 beta = std::min(beta, -1.0 / resid[m]);
1427 }
1428 }
1429 if (m_loglevel > 0 && beta != 1.0) {
1430 writelogf("(it %d) Beta = %g\n", iter, beta);
1431 }
1432 }
1433 // Update the solution vector
1434 for (size_t m = 0; m < m_mm; m++) {
1435 x[m] += beta * resid[m];
1436 }
1437 n_t *= exp(beta * resid[m_mm]);
1438
1439 if (m_loglevel > 0) {
1440 writelogf("(it %d) OLD_SOLUTION NEW SOLUTION (undamped updated)\n", iter);
1441 for (size_t m = 0; m < m_mm; m++) {
1442 writelogf(" %5s %10.5g %10.5g %10.5g\n",
1443 s.elementName(m), x_old[m], x[m], resid[m]);
1444 }
1445 writelogf(" n_t %10.5g %10.5g %10.5g \n", x_old[m_mm], n_t, exp(resid[m_mm]));
1446 }
1447 }
1448 if (m_loglevel > 0) {
1449 double temp = s.temperature();
1450 double pres = s.pressure();
1451
1452 if (retn == 0) {
1453 writelogf(" ChemEquil::estimateEP_Brinkley() SUCCESS: equilibrium found at T = %g, Pres = %g\n",
1454 temp, pres);
1455 } else {
1456 writelogf(" ChemEquil::estimateEP_Brinkley() FAILURE: equilibrium not found at T = %g, Pres = %g\n",
1457 temp, pres);
1458 }
1459 }
1460 return retn;
1461}
1462
1463
1464void ChemEquil::adjustEloc(ThermoPhase& s, span<double> elMolesGoal)
1465{
1466 if (m_eloc == npos) {
1467 return;
1468 }
1469 if (fabs(elMolesGoal[m_eloc]) > 1.0E-20) {
1470 return;
1471 }
1473 size_t maxPosEloc = npos;
1474 size_t maxNegEloc = npos;
1475 double maxPosVal = -1.0;
1476 double maxNegVal = -1.0;
1477 if (m_loglevel > 0) {
1478 for (size_t k = 0; k < m_kk; k++) {
1479 if (nAtoms(k,m_eloc) > 0.0 && m_molefractions[k] > maxPosVal && m_molefractions[k] > 0.0) {
1480 maxPosVal = m_molefractions[k];
1481 maxPosEloc = k;
1482 }
1483 if (nAtoms(k,m_eloc) < 0.0 && m_molefractions[k] > maxNegVal && m_molefractions[k] > 0.0) {
1484 maxNegVal = m_molefractions[k];
1485 maxNegEloc = k;
1486 }
1487 }
1488 }
1489
1490 double sumPos = 0.0;
1491 double sumNeg = 0.0;
1492 for (size_t k = 0; k < m_kk; k++) {
1493 if (nAtoms(k,m_eloc) > 0.0) {
1494 sumPos += nAtoms(k,m_eloc) * m_molefractions[k];
1495 }
1496 if (nAtoms(k,m_eloc) < 0.0) {
1497 sumNeg += nAtoms(k,m_eloc) * m_molefractions[k];
1498 }
1499 }
1500 sumNeg = - sumNeg;
1501
1502 if (sumPos >= sumNeg) {
1503 if (sumPos <= 0.0) {
1504 return;
1505 }
1506 double factor = (elMolesGoal[m_eloc] + sumNeg) / sumPos;
1507 if (m_loglevel > 0 && factor < 0.9999999999) {
1508 writelogf("adjustEloc: adjusted %s and friends from %g to %g to ensure neutrality condition\n",
1509 s.speciesName(maxPosEloc),
1510 m_molefractions[maxPosEloc], m_molefractions[maxPosEloc]*factor);
1511 }
1512 for (size_t k = 0; k < m_kk; k++) {
1513 if (nAtoms(k,m_eloc) > 0.0) {
1514 m_molefractions[k] *= factor;
1515 }
1516 }
1517 } else {
1518 double factor = (-elMolesGoal[m_eloc] + sumPos) / sumNeg;
1519 if (m_loglevel > 0 && factor < 0.9999999999) {
1520 writelogf("adjustEloc: adjusted %s and friends from %g to %g to ensure neutrality condition\n",
1521 s.speciesName(maxNegEloc),
1522 m_molefractions[maxNegEloc], m_molefractions[maxNegEloc]*factor);
1523 }
1524 for (size_t k = 0; k < m_kk; k++) {
1525 if (nAtoms(k,m_eloc) < 0.0) {
1526 m_molefractions[k] *= factor;
1527 }
1528 }
1529 }
1530
1533}
1534
1535} // namespace
Chemical equilibrium.
Header file for class ThermoPhase, the base class for phases with thermodynamic properties,...
size_t nRows() const
Number of rows.
Definition Array.h:167
size_t nColumns() const
Number of columns.
Definition Array.h:172
vector< double > & data()
Return a reference to the data vector.
Definition Array.h:177
Base class for exceptions thrown by Cantera classes.
virtual string getMessage() const
Method overridden by derived classes to format the error message.
int setInitialMoles(ThermoPhase &s, span< double > elMoleGoal, int loglevel=0)
Estimate the initial mole numbers.
void equilResidual(ThermoPhase &s, span< const double > x, span< const double > elmtotal, span< double > resid, double xval, double yval, int loglevel=0)
Evaluates the residual vector F, of length m_mm.
int equilibrate(ThermoPhase &s, const char *XY, int loglevel=0)
Equilibrate a phase, holding the elemental composition fixed at the initial value found within the Th...
size_t m_kk
number of species in the phase
Definition ChemEquil.h:260
int m_loglevel
Verbosity of printed output.
Definition ChemEquil.h:305
size_t m_nComponents
This is equal to the rank of the stoichiometric coefficient matrix when it is computed.
Definition ChemEquil.h:265
ThermoPhase * m_phase
Pointer to the ThermoPhase object used to initialize this object.
Definition ChemEquil.h:148
double m_elementTotalSum
Current value of the sum of the element abundances given the current element potentials.
Definition ChemEquil.h:274
void update(const ThermoPhase &s)
Update internally stored state information.
int estimateElementPotentials(ThermoPhase &s, span< double > lambda, span< double > elMolesGoal, int loglevel=0)
Generate a starting estimate for the element potentials.
size_t m_eloc
Index of the element id corresponding to the electric charge of each species.
Definition ChemEquil.h:287
double calcEmoles(ThermoPhase &s, span< double > x, const double &n_t, span< const double > Xmol_i_calc, span< double > eMolesCalc, span< double > n_i_calc, double pressureConst)
Given a vector of dimensionless element abundances, this routine calculates the moles of the elements...
void initialize(ThermoPhase &s)
Prepare for equilibrium calculations.
Definition ChemEquil.cpp:89
EquilOpt options
Options controlling how the calculation is carried out.
Definition ChemEquil.h:139
vector< double > m_molefractions
Current value of the mole fractions in the single phase. length = m_kk.
Definition ChemEquil.h:270
vector< double > m_comp
Storage of the element compositions. natom(k,m) = m_comp[k*m_mm+ m];.
Definition ChemEquil.h:283
double nAtoms(size_t k, size_t m) const
number of atoms of element m in species k.
Definition ChemEquil.h:151
double m_elemFracCutoff
element fractional cutoff, below which the element will be zeroed.
Definition ChemEquil.h:298
vector< double > m_muSS_RT
Dimensionless values of the Gibbs free energy for the standard state of each species,...
Definition ChemEquil.h:294
void dampStep(span< double > oldx, span< double > step, span< double > x)
Find an acceptable step size and take it.
vector< double > m_elementmolefracs
Current value of the element mole fractions.
Definition ChemEquil.h:278
size_t m_mm
number of elements in the phase
Definition ChemEquil.h:259
void setToEquilState(ThermoPhase &s, span< const double > x, double t)
Set mixture to an equilibrium state consistent with specified element potentials and temperature.
int estimateEP_Brinkley(ThermoPhase &s, span< double > lambda, span< double > elMoles)
Do a calculation of the element potentials using the Brinkley method, p.
A class for full (non-sparse) matrices with Fortran-compatible data storage, which adds matrix operat...
Definition DenseMatrix.h:42
Multiphase chemical equilibrium solver.
A class for multiphase mixtures.
Definition MultiPhase.h:62
void init()
Process phases and build atomic composition array.
size_t nSpecies() const
Number of species, summed over all phases.
Definition MultiPhase.h:116
void addPhase(shared_ptr< ThermoPhase > p, double moles)
Add a phase to the mixture.
size_t nElements() const
Number of elements.
Definition MultiPhase.h:86
void getMoleFractions(span< double > x) const
Get the species mole fraction vector.
Definition Phase.cpp:451
size_t nSpecies() const
Returns the number of species in the phase.
Definition Phase.h:247
double temperature() const
Temperature (K).
Definition Phase.h:586
virtual void setPressure(double p)
Set the internally stored pressure (Pa) at constant temperature and composition.
Definition Phase.h:640
double atomicWeight(size_t m) const
Atomic weight of element m.
Definition Phase.cpp:69
string speciesName(size_t k) const
Name of the species with index k.
Definition Phase.cpp:143
virtual size_t stateSize() const
Return size of vector defining internal state of the phase.
Definition Phase.cpp:249
double moleFraction(size_t k) const
Return the mole fraction of a single species.
Definition Phase.cpp:457
virtual double density() const
Density (kg/m^3).
Definition Phase.h:611
double nAtoms(size_t k, size_t m) const
Number of atoms of element m in species k.
Definition Phase.cpp:101
virtual void setTemperature(double temp)
Set the internally stored temperature of the phase (K).
Definition Phase.h:647
size_t nElements() const
Number of elements.
Definition Phase.cpp:30
virtual void setMoleFractions(span< const double > x)
Set the mole fractions to the specified values.
Definition Phase.cpp:283
virtual void restoreState(span< const double > state)
Restore the state of the phase from a previously saved state vector.
Definition Phase.cpp:269
virtual double pressure() const
Return the thermodynamic pressure (Pa).
Definition Phase.h:604
string elementName(size_t m) const
Name of the element with index m.
Definition Phase.cpp:43
virtual void saveState(span< double > state) const
Write to array 'state' the current internal state.
Definition Phase.cpp:257
Base class for a phase with thermodynamic properties.
virtual void getGibbs_RT(span< double > grt) const
Get the nondimensional Gibbs functions for the species in their standard states at the current T and ...
virtual double minTemp(size_t k=npos) const
Minimum temperature for which the thermodynamic data for the species or phase are valid.
virtual double maxTemp(size_t k=npos) const
Maximum temperature for which the thermodynamic data for the species are valid.
double entropy_mass() const
Specific entropy. Units: J/kg/K.
double intEnergy_mass() const
Specific internal energy. Units: J/kg.
virtual void getChemPotentials(span< double > mu) const
Get the species chemical potentials. Units: J/kmol.
virtual void getActivityCoefficients(span< double > ac) const
Get the array of non-dimensional molar-based activity coefficients at the current solution temperatur...
double enthalpy_mass() const
Specific enthalpy. Units: J/kg.
This file contains definitions for utility functions and text for modules, inputfiles and logging,...
size_t BasisOptimize(bool &usedZeroedSpecies, bool doFormRxn, MultiPhase *mphase, span< size_t > orderVectorSpecies, span< size_t > orderVectorElements, span< double > formRxnMatrix)
Choose the optimum basis of species for the equilibrium calculations.
void ElemRearrange(size_t nComponents, span< const double > elementAbundances, MultiPhase *mphase, span< size_t > orderVectorSpecies, span< size_t > orderVectorElements)
Handles the potential rearrangement of the constraint equations represented by the Formula Matrix.
virtual void setToEquilState(span< const double > mu_RT)
This method is used by the ChemEquil equilibrium solver.
void writelogf(const char *fmt, const Args &... args)
Write a formatted message to the screen.
Definition global.h:191
void writelog(const string &fmt, const Args &... args)
Write a formatted message to the screen.
Definition global.h:171
U len(const T &container)
Get the size of a container, cast to a signed integer type.
Definition utilities.h:231
void scale(InputIter begin, InputIter end, OutputIter out, S scale_factor)
Multiply elements of an array by a scale factor.
Definition utilities.h:118
T clip(const T &value, const T &lower, const T &upper)
Clip value such that lower <= value <= upper.
Definition global.h:326
const double GasConstant
Universal Gas Constant [J/kmol/K].
Definition ct_defs.h:123
void warn_user(const string &method, const string &msg, const Args &... args)
Print a user warning raised from method as CanteraWarning.
Definition global.h:263
Namespace for the Cantera kernel.
Definition AnyMap.cpp:595
const size_t npos
index returned by functions to indicate "no position"
Definition ct_defs.h:183
void solve(DenseMatrix &A, span< double > b, size_t nrhs, size_t ldb)
Solve Ax = b. Array b is overwritten on exit with x.
MappedVector asVectorXd(vector< double > &v)
Convenience wrapper for accessing std::vector as an Eigen VectorXd.
Definition eigen_dense.h:60
const double SmallNumber
smallest number to compare to zero.
Definition ct_defs.h:161
int _equilflag(const char *xy)
map property strings to integers
Definition ChemEquil.cpp:21
const double BigNumber
largest number to compare to inf.
Definition ct_defs.h:163
Contains declarations for string manipulation functions within Cantera.
Various templated functions that carry out common vector and polynomial operations (see Templated Arr...