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