Cantera  4.0.0a1
Loading...
Searching...
No Matches
MultiPhaseEquil.cpp
Go to the documentation of this file.
1/**
2 * @file MultiPhaseEquil.cpp
3 */
4
5// This file is part of Cantera. See License.txt in the top-level directory or
6// at https://cantera.org/license.txt for license and copyright information.
7
10#include "cantera/numerics/eigen_dense.h"
12
13#include <cstdio>
14#include <numeric>
15
16namespace Cantera
17{
18
19MultiPhaseEquil::MultiPhaseEquil(MultiPhase* mix, bool start, int loglevel) : m_mix(mix)
20{
21 // store some mixture parameters locally
22 m_nel_mix = mix->nElements();
23 m_nsp_mix = mix->nSpecies();
24 m_press = mix->pressure();
25 m_temp = mix->temperature();
26
27 m_incl_species.resize(m_nsp_mix,1);
28 m_incl_element.resize(m_nel_mix,1);
29 for (size_t m = 0; m < m_nel_mix; m++) {
30 string enm = mix->elementName(m);
31 // element 'E' or 'e' represents an electron; this requires special
32 // handling, so save its index for later use
33 if (enm == "E" || enm == "e") {
34 m_eloc = m;
35 }
36 // if an element other than electrons is not present in the mixture,
37 // then exclude it and all species containing it from the calculation.
38 // Electrons are a special case, since a species can have a negative
39 // number of 'atoms' of electrons (positive ions).
40 if (m_mix->elementMoles(m) <= 0.0 && m != m_eloc) {
41 m_incl_element[m] = 0;
42 for (size_t k = 0; k < m_nsp_mix; k++) {
43 if (m_mix->nAtoms(k,m) != 0.0) {
44 m_incl_species[k] = 0;
45 }
46 }
47 }
48 }
49
50 // Now build the list of elements to be included, starting with
51 // electrons, if they are present.
52 if (m_eloc < m_nel_mix) {
53 m_element.push_back(m_eloc);
54 m_nel++;
55 }
56 // add the included elements other than electrons
57 for (size_t m = 0; m < m_nel_mix; m++) {
58 if (m_incl_element[m] == 1 && m != m_eloc) {
59 m_nel++;
60 m_element.push_back(m);
61 }
62 }
63
64 // include pure single-constituent phases only if their thermo data are
65 // valid for this temperature. This is necessary, since some thermo
66 // polynomial fits are done only for a limited temperature range. For
67 // example, using the NASA polynomial fits for solid ice and liquid water,
68 // if this were not done the calculation would predict solid ice to be
69 // present far above its melting point, since the thermo polynomial fits
70 // only extend to 273.15 K, and give unphysical results above this
71 // temperature, leading (incorrectly) to Gibbs free energies at high
72 // temperature lower than for liquid water.
73 //
74 // When start=true (the default for TP equilibration), the solver computes
75 // its own initial composition from elemental totals. In that case, if an
76 // excluded species has non-zero initial moles, it is recorded here so that
77 // its elemental contribution can be redistributed to valid component
78 // species before calling setInitialMoles.
79 vector<size_t> excluded_with_nonzero_moles;
80 for (size_t k = 0; k < m_nsp_mix; k++) {
81 size_t ip = m_mix->speciesPhaseIndex(k);
82 if (!m_mix->solutionSpecies(k) &&
83 !m_mix->tempOK(ip)) {
84 m_incl_species[k] = 0;
85 if (m_mix->speciesMoles(k) > 0.0) {
86 if (!start) {
87 throw CanteraError("MultiPhaseEquil::MultiPhaseEquil",
88 "Species {} is excluded since its thermo properties are not "
89 "valid\nat this temperature, but it has non-zero moles in the "
90 "initial state.", m_mix->speciesName(k));
91 }
92 excluded_with_nonzero_moles.push_back(k);
93 }
94 }
95 }
96
97 // Now build the list of all species to be included in the calculation.
98 for (size_t k = 0; k < m_nsp_mix; k++) {
99 if (m_incl_species[k] ==1) {
100 m_nsp++;
101 m_species.push_back(k);
102 }
103 }
104
105 // some work arrays for internal use
106 m_work.resize(m_nsp);
107 m_work2.resize(m_nsp);
108 m_work3.resize(m_nsp_mix);
109 m_mu.resize(m_nsp_mix);
110
111 // number of moles of each species
112 m_moles.resize(m_nsp);
113 m_lastmoles.resize(m_nsp);
114 m_dxi.resize(nFree());
115
116 // initialize the mole numbers to the mixture composition
117 for (size_t ik = 0; ik < m_nsp; ik++) {
118 m_moles[ik] = m_mix->speciesMoles(m_species[ik]);
119 }
120
121 // Delta G / RT for each reaction
122 m_deltaG_RT.resize(nFree(), 0.0);
123 m_majorsp.resize(m_nsp);
124 m_sortindex.resize(m_nsp,0);
125 m_lastsort.resize(m_nel);
126 m_solnrxn.resize(nFree());
127 m_A.resize(m_nel, m_nsp, 0.0);
128 m_N.resize(m_nsp, nFree());
129 m_order.resize(std::max(m_nsp, m_nel), 0);
130 iota(m_order.begin(), m_order.begin() + m_nsp, 0);
131
132 // if the 'start' flag is set, estimate the initial mole numbers by doing a
133 // linear Gibbs minimization. In this case, only the elemental composition
134 // of the initial mixture state matters.
135 if (start) {
136 if (!excluded_with_nonzero_moles.empty()) {
137 // Adjust m_moles to restore the element contributions of excluded
138 // condensed-phase species. computeN() selects a set of linearly independent
139 // component species; the change in element totals from adding moles of
140 // these components is B*delta, where B is the component formula matrix
141 // B(i,j) = nAtoms(component_j, element_i). To restore exactly the missing
142 // element totals b_missing, we solve B*delta = b_missing and add delta[j]
143 // moles of component j. Note that b_missing must be computed after
144 // computeN(), since computeN() may reorder m_element or reduce m_nel.
145 computeN();
146 Eigen::VectorXd b_missing = Eigen::VectorXd::Zero(m_nel);
147 for (size_t k : excluded_with_nonzero_moles) {
148 for (size_t m = 0; m < m_nel; m++) {
149 b_missing[m] += m_mix->speciesMoles(k) *
150 m_mix->nAtoms(k, m_element[m]);
151 }
152 }
153 Eigen::MatrixXd B(m_nel, m_nel);
154 for (size_t i = 0; i < m_nel; i++) {
155 for (size_t j = 0; j < m_nel; j++) {
156 B(i, j) = m_mix->nAtoms(m_species[m_order[j]], m_element[i]);
157 }
158 }
159 Eigen::VectorXd delta = B.colPivHouseholderQr().solve(b_missing);
160 for (size_t j = 0; j < m_nel; j++) {
161 m_moles[m_order[j]] += delta[j];
162 }
163 updateMixMoles();
164 }
165 setInitialMoles(loglevel-1);
166 }
167 computeN();
168
169 // Take a very small step in composition space, so that no
170 // species has precisely zero moles.
171 vector<double> dxi(nFree(), 1.0e-20);
172 if (!dxi.empty()) {
173 multiply(m_N, dxi, m_work);
174 unsort(m_work);
175 }
176
177 for (size_t k = 0; k < m_nsp; k++) {
178 m_moles[k] += m_work[k];
179 m_lastmoles[k] = m_moles[k];
180 if (m_mix->solutionSpecies(m_species[k])) {
181 m_dsoln.push_back(1);
182 } else {
183 m_dsoln.push_back(0);
184 }
185 }
186 m_force = false;
187 updateMixMoles();
188
189 // At this point, the instance has been created, the species to be included
190 // have been determined, and an initial composition has been selected that
191 // has all non-zero mole numbers for the included species.
192}
193
194double MultiPhaseEquil::equilibrate(int XY, double err, int maxsteps, int loglevel)
195{
196 int i;
197 m_iter = 0;
198 for (i = 0; i < maxsteps; i++) {
199 stepComposition(loglevel-1);
200 if (error() < err) {
201 break;
202 }
203 }
204 if (i >= maxsteps) {
205 throw CanteraError("MultiPhaseEquil::equilibrate",
206 "no convergence in {} iterations. Error = {}",
207 maxsteps, error());
208 }
209 finish();
210 return error();
211}
212
213void MultiPhaseEquil::updateMixMoles()
214{
215 fill(m_work3.begin(), m_work3.end(), 0.0);
216 for (size_t k = 0; k < m_nsp; k++) {
217 m_work3[m_species[k]] = m_moles[k];
218 }
219 m_mix->setMoles(m_work3);
220}
221
223{
224 fill(m_work3.begin(), m_work3.end(), 0.0);
225 for (size_t k = 0; k < m_nsp; k++) {
226 m_work3[m_species[k]] = (m_moles[k] > 0.0 ? m_moles[k] : 0.0);
227 }
228 m_mix->setMoles(m_work3);
229}
230
232{
233 double not_mu = 1.0e12;
234 m_mix->getValidChemPotentials(not_mu, m_mu, true);
235 double dxi_min = 1.0e10;
236 bool redo = true;
237 int iter = 0;
238
239 while (redo) {
240 // choose a set of components based on the current composition
241 computeN();
242 redo = false;
243 iter++;
244 if (iter > 4) {
245 break;
246 }
247
248 // loop over all reactions
249 for (size_t j = 0; j < nFree(); j++) {
250 double dg_rt = 0.0;
251 dxi_min = 1.0e10;
252 for (size_t ik = 0; ik < m_nsp; ik++) {
253 dg_rt += mu(ik) * m_N(ik,j);
254 }
255
256 // fwd or rev direction
257 int idir = (dg_rt < 0.0 ? 1 : -1);
258
259 for (size_t ik = 0; ik < m_nsp; ik++) {
260 double nu = m_N(ik, j);
261
262 // set max change in progress variable by
263 // non-negativity requirement
264 // -> Note, 0.99 factor is so that difference of 2 numbers
265 // isn't zero. This causes differences between
266 // optimized and debug versions of the code
267 if (nu*idir < 0) {
268 double delta_xi = fabs(0.99*moles(ik)/nu);
269 // if a component has nearly zero moles, redo
270 // with a new set of components
271 if (!redo && delta_xi < 1.0e-10 && ik < m_nel) {
272 redo = true;
273 }
274 dxi_min = std::min(dxi_min, delta_xi);
275 }
276 }
277 // step the composition by dxi_min
278 for (size_t ik = 0; ik < m_nsp; ik++) {
279 moles(ik) += m_N(ik, j) * idir*dxi_min;
280 }
281 }
282 // set the moles of the phase objects to match
283 updateMixMoles();
284 }
285 return 0;
286}
287
288void MultiPhaseEquil::getComponents(span<const size_t> order)
289{
290 // if the input species array has the wrong size, ignore it
291 // and consider the species for components in declaration order.
292 if (order.size() != m_nsp) {
293 for (size_t k = 0; k < m_nsp; k++) {
294 m_order[k] = k;
295 }
296 } else {
297 for (size_t k = 0; k < m_nsp; k++) {
298 m_order[k] = order[k];
299 }
300 }
301
302 size_t nRows = m_nel;
303 size_t nColumns = m_nsp;
304
305 // set up the atomic composition matrix
306 for (size_t m = 0; m < nRows; m++) {
307 for (size_t k = 0; k < nColumns; k++) {
308 m_A(m, k) = m_mix->nAtoms(m_species[m_order[k]], m_element[m]);
309 }
310 }
311
312 // Do Gaussian elimination
313 for (size_t m = 0; m < nRows; m++) {
314 // Check for rows that are zero
315 bool isZeroRow = true;
316 for (size_t k = m; k < nColumns; k++) {
317 if (fabs(m_A(m,k)) > sqrt(Tiny)) {
318 isZeroRow = false;
319 break;
320 }
321 }
322 if (isZeroRow) {
323 // Find the last non-zero row
324 size_t n = nRows - 1;
325 bool foundSwapCandidate = false;
326 for (; n > m; n--) {
327 for (size_t k = m; k < nColumns; k++) {
328 if (fabs(m_A(n,k)) > sqrt(Tiny)) {
329 foundSwapCandidate = true;
330 break;
331 }
332 }
333 if (foundSwapCandidate) {
334 break;
335 }
336 }
337 if (m != n) {
338 // Swap this row with the last non-zero row, and keep m_element
339 // in sync so that the element index matches its A matrix row.
340 for (size_t k = 0; k < nColumns; k++) {
341 std::swap(m_A(n,k), m_A(m,k));
342 }
343 std::swap(m_element[m], m_element[n]);
344 } else {
345 // All remaining rows are zero. Elimination is complete.
346 // The rank of the element matrix is m, which may be less than
347 // m_nel when some element constraints are linearly dependent
348 // (e.g., all species share a fixed H/C ratio). Update m_nel
349 // and resize arrays that depend on nFree().
350 if (m < m_nel) {
351 m_nel = m;
352 m_dxi.resize(nFree());
353 m_deltaG_RT.assign(nFree(), 0.0);
354 m_solnrxn.resize(nFree());
355 m_N.resize(m_nsp, nFree());
356 m_lastsort.resize(m_nel);
357 }
358 break;
359 }
360 }
361
362 // If a pivot is zero, exchange columns. This occurs when a species has
363 // an elemental composition that is not linearly independent of the
364 // component species that have already been assigned
365 if (m < nColumns && m_A(m,m) == 0.0) {
366 // First, we need to find a good candidate for a component species
367 // to swap in for the one that has zero pivot. It must contain
368 // element m, be linearly independent of the components processed so
369 // far (m_A(m,k) != 0), and should be a major species if possible.
370 // We'll choose the species with greatest mole fraction that
371 // satisfies these criteria.
372 double maxmoles = -999.0;
373 size_t kmax = 0;
374 for (size_t k = m+1; k < nColumns; k++) {
375 if (m_A(m,k) != 0.0 && fabs(m_moles[m_order[k]]) > maxmoles) {
376 kmax = k;
377 maxmoles = fabs(m_moles[m_order[k]]);
378 }
379 }
380
381 // Now exchange the column with zero pivot with the
382 // column for this major species
383 for (size_t n = 0; n < nRows; n++) {
384 std::swap(m_A(n, m), m_A(n, kmax));
385 }
386
387 // exchange the species labels on the columns
388 std::swap(m_order[m], m_order[kmax]);
389 }
390
391 // scale row m so that the diagonal element is unity
392 double fctr = 1.0/m_A(m,m);
393 for (size_t k = 0; k < nColumns; k++) {
394 m_A(m,k) *= fctr;
395 }
396
397 // For all rows below the diagonal, subtract A(n,m)/A(m,m)
398 // * (row m) from row n, so that A(n,m) = 0.
399 for (size_t n = m+1; n < m_nel; n++) {
400 fctr = m_A(n,m)/m_A(m,m);
401 for (size_t k = 0; k < m_nsp; k++) {
402 m_A(n,k) -= m_A(m,k)*fctr;
403 }
404 }
405 }
406
407 // The left m_nel columns of A are now upper-diagonal. Now
408 // reduce the m_nel columns to diagonal form by back-solving
409 for (size_t m = std::min(nRows,nColumns)-1; m > 0; m--) {
410 for (size_t n = m-1; n != npos; n--) {
411 if (m_A(n,m) != 0.0) {
412 double fctr = m_A(n,m);
413 for (size_t k = m; k < m_nsp; k++) {
414 m_A(n,k) -= fctr*m_A(m,k);
415 }
416 }
417 }
418 }
419
420 // create stoichiometric coefficient matrix.
421 for (size_t n = 0; n < m_nsp; n++) {
422 if (n < m_nel) {
423 for (size_t k = 0; k < nFree(); k++) {
424 m_N(n, k) = -m_A(n, k + m_nel);
425 }
426 } else {
427 for (size_t k = 0; k < nFree(); k++) {
428 m_N(n, k) = 0.0;
429 }
430 m_N(n, n - m_nel) = 1.0;
431 }
432 }
433
434 // find reactions involving solution phase species
435 for (size_t j = 0; j < nFree(); j++) {
436 m_solnrxn[j] = false;
437 for (size_t k = 0; k < m_nsp; k++) {
438 if (m_N(k, j) != 0 && m_mix->solutionSpecies(m_species[m_order[k]])) {
439 m_solnrxn[j] = true;
440 }
441 }
442 }
443}
444
445void MultiPhaseEquil::unsort(span<double> x)
446{
447 checkArraySize("MultiPhaseEquil::unsort", x.size(), m_nsp);
448 m_work2.assign(x.begin(), x.end());
449 for (size_t k = 0; k < m_nsp; k++) {
450 x[m_order[k]] = m_work2[k];
451 }
452}
453
454void MultiPhaseEquil::step(double omega, span<double> deltaN, int loglevel)
455{
456 if (omega < 0.0) {
457 throw CanteraError("MultiPhaseEquil::step","negative omega");
458 }
459 if (deltaN.size() != m_nsp) {
460 throw CanteraError("MultiPhaseEquil::step",
461 "Expected deltaN size {}, got {}", m_nsp, deltaN.size());
462 }
463
464 for (size_t ik = 0; ik < m_nel; ik++) {
465 size_t k = m_order[ik];
466 m_lastmoles[k] = m_moles[k];
467 m_moles[k] += omega * deltaN[k];
468 }
469
470 for (size_t ik = m_nel; ik < m_nsp; ik++) {
471 size_t k = m_order[ik];
472 m_lastmoles[k] = m_moles[k];
473 if (m_majorsp[k]) {
474 m_moles[k] += omega * deltaN[k];
475 } else {
476 // Minor non-component species use a non-linear update:
477 // moles_new = |m_moles[k]| * exp(-dG/RT), capped at 10x growth for
478 // stability. When m_moles[k] is exactly zero, this degenerates to 0
479 // regardless of dG; the species stays at 0 and the component
480 // correction below suppresses the would-be formation reaction.
481 size_t j = ik - m_nel;
482 double moles_new = fabs(m_moles[k])
483 * std::min(10.0, exp(-m_deltaG_RT[j]));
484 // The components were already updated above by omega*deltaN, which
485 // assumes each noncomponent species k changes by omega*deltaN[k].
486 // The actual change here is moles_new - m_moles[k], so element
487 // balance has an "excess" error that should be corrected on the
488 // components. We only apply the correction when the imbalance is
489 // catastrophic — i.e., the Newton step expects a large change in
490 // this species but the exponential formula caps it at a small
491 // fraction. For modestly-trace species the imbalance is O(m_moles[k])
492 // and applying the correction routinely would destabilize convergence
493 // by perturbing trace component species.
494 double excess = (moles_new - m_moles[k]) - omega * deltaN[k];
495 if (fabs(excess) > 10.0 * fabs(moles_new - m_moles[k]) &&
496 fabs(excess) > SmallNumber) {
497 for (size_t n = 0; n < m_nel; n++) {
498 m_moles[m_order[n]] += m_N(n, j) * excess;
499 }
500 }
501 m_moles[k] = moles_new;
502 }
503 }
504 updateMixMoles();
505}
506
508{
509 m_iter++;
510 double grad0 = computeReactionSteps(m_dxi);
511
512 // Drop trace stoichiometric (single-species) condensed phases that the current step
513 // would consume. Such a phase, with a mole number near the numerical floor left
514 // over from initialization, would otherwise pin the step size omega to a
515 // vanishingly small value (omega <= -moles/deltaN in the loop below), stalling
516 // progress on every other reaction. Because the step is removing it (dxi < 0, i.e.
517 // dG/RT for its formation reaction is positive), the phase should be absent. We
518 // zero its mole number and suppress its formation reaction here.
519 double total_moles = 0.0;
520 for (size_t k = 0; k < m_nsp; k++) {
521 total_moles += std::max(0.0, m_moles[k]);
522 }
523 double trace = Tiny * total_moles;
524 bool dropped = false;
525 for (size_t j = 0; j < nFree(); j++) {
526 size_t k = m_order[j + m_nel];
527 if (!m_dsoln[k] && m_moles[k] > 0.0 && m_moles[k] < trace && m_dxi[j] < 0.0) {
528 m_dxi[j] = 0.0;
529 m_moles[k] = 0.0;
530 dropped = true;
531 }
532 }
533 if (dropped) {
534 updateMixMoles();
535 }
536
537 // compute the mole fraction changes.
538 if (nFree()) {
539 multiply(m_N, m_dxi, m_work);
540 }
541
542 // change to sequential form
543 unsort(m_work);
544
545 // scale omega to keep the major species non-negative
546 double FCTR = 0.99;
547 const double MAJOR_THRESHOLD = 1.0e-12;
548 double omegamax = 1.0;
549 for (size_t ik = 0; ik < m_nsp; ik++) {
550 size_t k = m_order[ik];
551 if (ik < m_nel) {
552 FCTR = 0.99;
553 if (m_moles[k] < MAJOR_THRESHOLD) {
554 m_force = true;
555 }
556 } else {
557 FCTR = 0.9;
558 }
559 // if species k is in a multi-species solution phase, then its mole
560 // number must remain positive, unless the entire phase goes away. First
561 // we'll determine an upper bound on omega, such that all
562 if (m_dsoln[k] == 1) {
563 if ((m_moles[k] > MAJOR_THRESHOLD) || (ik < m_nel)) {
564 if (m_moles[k] < MAJOR_THRESHOLD) {
565 m_force = true;
566 }
567 double omax = m_moles[k]*FCTR/(fabs(m_work[k]) + Tiny);
568 if (m_work[k] < 0.0 && omax < omegamax) {
569 omegamax = omax;
570 if (omegamax < 1.0e-5) {
571 m_force = true;
572 }
573 }
574 m_majorsp[k] = true;
575 } else {
576 m_majorsp[k] = false;
577 }
578 } else {
579 if (m_work[k] < 0.0 && m_moles[k] > 0.0) {
580 double omax = -m_moles[k]/m_work[k];
581 if (omax < omegamax) {
582 omegamax = omax;
583 if (omegamax < 1.0e-5) {
584 m_force = true;
585 }
586 }
587 }
588 m_majorsp[k] = true;
589 }
590 }
591
592 // now take a step with this scaled omega
593 step(omegamax, m_work, loglevel);
594 // compute the gradient of G at this new position in the current direction.
595 // If it is positive, then we have overshot the minimum. In this case,
596 // interpolate back.
597 double not_mu = 1.0e12;
598 m_mix->getValidChemPotentials(not_mu, m_mu);
599 double grad1 = 0.0;
600 for (size_t k = 0; k < m_nsp; k++) {
601 grad1 += m_work[k] * m_mu[m_species[k]];
602 }
603
604 double omega = omegamax;
605 if (grad1 > 0.0) {
606 omega *= fabs(grad0) / (grad1 + fabs(grad0));
607 for (size_t k = 0; k < m_nsp; k++) {
608 m_moles[k] = m_lastmoles[k];
609 }
610 step(omega, m_work);
611 }
612 return omega;
613}
614
616{
617 checkArraySize("MultiPhaseEquil::computeReactionSteps", dxi.size(), nFree());
618 vector<double> nu(m_nsp, 0.0);
619 double grad = 0.0;
620 computeN();
621 double not_mu = 1.0e12;
622 m_mix->getValidChemPotentials(not_mu, m_mu);
623
624 for (size_t j = 0; j < nFree(); j++) {
625 // get stoichiometric vector
626 getStoichVector(j, nu);
627
628 // compute Delta G
629 double dg_rt = 0.0;
630 for (size_t k = 0; k < m_nsp; k++) {
631 dg_rt += m_mu[m_species[k]] * nu[k];
632 }
633 dg_rt /= (m_temp * GasConstant);
634
635 m_deltaG_RT[j] = dg_rt;
636 double fctr = 1.0;
637
638 // if this is a formation reaction for a single-component phase,
639 // check whether reaction should be included
640 size_t k = m_order[j + m_nel];
641 if (!m_dsoln[k]) {
642 if (m_moles[k] <= 0.0 && dg_rt > 0.0) {
643 fctr = 0.0;
644 } else {
645 fctr = 0.5;
646 }
647 } else if (!m_solnrxn[j]) {
648 fctr = 1.0;
649 } else {
650 // component sum
651 double csum = 0.0;
652 for (k = 0; k < m_nel; k++) {
653 size_t kc = m_order[k];
654 double nmoles = fabs(m_mix->speciesMoles(m_species[kc])) + Tiny;
655 csum += pow(nu[kc], 2)*m_dsoln[kc]/nmoles;
656 }
657
658 // noncomponent term
659 size_t kc = m_order[j + m_nel];
660 size_t ip_kc = m_mix->speciesPhaseIndex(m_species[kc]);
661 double nmoles_kc = fabs(m_mix->speciesMoles(m_species[kc])) + Tiny;
662 double pm_kc = fabs(m_mix->phaseMoles(ip_kc)) + Tiny;
663
664 // Phase correction: subtract (sum_{k in phase} nu_k)^2 / n_phase for
665 // each solution phase, which is the Hessian term for an ideal phase.
666 //
667 // For the phase containing kc, combine term1 = dsoln/nmoles_kc with
668 // the phase correction -nu_sum^2/pm_kc into a single fraction. This is
669 // algebraically equivalent but avoids catastrophic cancellation when
670 // pm_kc ≈ nmoles_kc (a trace single-species phase).
671 double nu_sum_kc = 0.0;
672 double sum = 0.0;
673 for (size_t ip = 0; ip < m_mix->nPhases(); ip++) {
674 double pm = fabs(m_mix->phaseMoles(ip));
675 if (m_mix->phase(ip).nSpecies() > 1 && pm > 0.0) {
676 double nu_sum = 0.0;
677 for (k = 0; k < m_nsp; k++) {
678 kc = m_species[k];
679 if (m_mix->speciesPhaseIndex(kc) == ip) {
680 nu_sum += nu[k];
681 }
682 }
683 if (ip == ip_kc) {
684 nu_sum_kc = nu_sum;
685 } else {
686 sum -= nu_sum * nu_sum / (pm + Tiny);
687 }
688 }
689 }
690 kc = m_order[j + m_nel];
691 double term1 = (m_dsoln[kc]*pm_kc - nu_sum_kc*nu_sum_kc*nmoles_kc)
692 / (nmoles_kc*pm_kc);
693 double rfctr = term1 + csum + sum;
694 if (fabs(rfctr) < Tiny) {
695 fctr = 1.0;
696 } else {
697 fctr = 1.0/rfctr;
698 }
699 }
700 dxi[j] = -fctr*dg_rt;
701
702 for (size_t m = 0; m < m_nel; m++) {
703 if (m_moles[m_order[m]] <= 0.0 && (m_N(m, j)*dxi[j] < 0.0)) {
704 dxi[j] = 0.0;
705 }
706 }
707 grad += dxi[j]*dg_rt;
708
709 }
710 return grad*GasConstant*m_temp;
711}
712
713void MultiPhaseEquil::computeN()
714{
715 // Sort the list of species by mole fraction (decreasing order)
716 vector<pair<double, size_t>> moleFractions(m_nsp);
717 for (size_t k = 0; k < m_nsp; k++) {
718 // use -Xk to generate reversed sort order
719 moleFractions[k] = {-m_mix->speciesMoles(m_species[k]), k};
720 }
721 std::sort(moleFractions.begin(), moleFractions.end());
722 for (size_t k = 0; k < m_nsp; k++) {
723 m_sortindex[k] = moleFractions[k].second;
724 }
725
726 bool reselect = m_force;
727 for (size_t m = 0; m < m_nel && !reselect; m++) {
728 size_t k = 0;
729 for (size_t ik = 0; ik < m_nsp; ik++) {
730 k = m_sortindex[ik];
731 if (m_mix->nAtoms(m_species[k],m_element[m]) != 0) {
732 break;
733 }
734 }
735 bool ok = false;
736 for (size_t ij = 0; ij < m_nel; ij++) {
737 if (k == m_order[ij]) {
738 ok = true;
739 }
740 }
741 if (!ok) {
742 reselect = true;
743 }
744 }
745
746 // Reselect the component basis if any current component has been depleted to a
747 // negligible mole number. A near-empty basis species throttles every reaction that
748 // must consume it (the step size is bounded by moles/|deltaN|), which stalls the
749 // solver in a limit cycle without converging. Reselecting from the
750 // mole-fraction-sorted list replaces the depleted species with an abundant,
751 // linearly-independent one when possible.
752 if (!reselect) {
753 double total_moles = 0.0;
754 for (size_t k = 0; k < m_nsp; k++) {
755 total_moles += std::max(0.0, m_moles[k]);
756 }
757 for (size_t m = 0; m < m_nel; m++) {
758 if (m_moles[m_order[m]] < 1.0e-3 * total_moles) {
759 reselect = true;
760 break;
761 }
762 }
763 }
764
765 if (reselect) {
766 getComponents(m_sortindex);
767 m_force = true;
768 }
769}
770
771double MultiPhaseEquil::error()
772{
773 double err, maxerr = 0.0;
774
775 // Total moles in the mixture; used to scale the "negligible reaction" check
776 // below: a reaction whose maximum possible Gibbs reduction is far below the
777 // mixture scale cannot meaningfully change the composition and should not
778 // block convergence.
779 double total_moles = 0.0;
780 for (size_t k = 0; k < m_nsp; k++) {
781 total_moles += std::max(0.0, m_moles[k]);
782 }
783
784 // examine every reaction
785 for (size_t j = 0; j < nFree(); j++) {
786 size_t ik = j + m_nel;
787
788 // don't require formation reactions for solution species
789 // present in trace amounts to be equilibrated
790 if (!isStoichPhase(ik) && fabs(moles(ik)) <= Tiny) {
791 err = 0.0;
792 } else if (isStoichPhase(ik) && moles(ik) <= 0.0 &&
793 m_deltaG_RT[j] >= 0.0) {
794 // for stoichiometric phase species, no error if not present and
795 // delta G for the formation reaction is positive
796 err = 0.0;
797 } else {
798 err = fabs(m_deltaG_RT[j]);
799 // For an unfavorable (dG > 0) solution-phase formation reaction,
800 // the maximum extent achievable in a single step is bounded by
801 // the noncomponent's own moles (which the reaction is consuming)
802 // and by the components that the reaction would produce
803 // (m_N(n,j) > 0 in the same direction). If this maximum extent is
804 // far below the mixture scale, the reaction cannot meaningfully
805 // change the composition and its dG/RT — which can be set by
806 // floating-point noise in trace mole fractions — should not block
807 // convergence. We do not apply the same bound to favorable
808 // reactions because they can still grow a species from trace
809 // through repeated 10× steps of the exponential update formula.
810 if (!isStoichPhase(ik) && m_deltaG_RT[j] > 0.0) {
811 double max_extent = fabs(moles(ik));
812 for (size_t n = 0; n < m_nel; n++) {
813 double nu = m_N(n, j);
814 if (nu > 0.0) {
815 double mc = std::max(0.0, m_moles[m_order[n]]);
816 max_extent = std::min(max_extent, mc / nu);
817 }
818 }
819 if (max_extent * err < 1.0e-15 * total_moles) {
820 err = 0.0;
821 }
822 }
823 }
824 maxerr = std::max(maxerr, err);
825 }
826 return maxerr;
827}
828
829double MultiPhaseEquil::phaseMoles(size_t iph) const
830{
831 return m_mix->phaseMoles(iph);
832}
833
834void MultiPhaseEquil::reportCSV(const string& reportFile)
835{
836 FILE* FP = fopen(reportFile.c_str(), "w");
837 if (!FP) {
838 throw CanteraError("MultiPhaseEquil::reportCSV", "Failure to open file");
839 }
840 vector<double> mf(m_nsp_mix, 1.0);
841 vector<double> fe(m_nsp_mix, 0.0);
842 vector<double> VolPM;
843 vector<double> activity;
844 vector<double> ac;
845 vector<double> mu;
846 vector<double> mu0;
847 vector<double> molalities;
848
849 double vol = 0.0;
850 for (size_t iphase = 0; iphase < m_mix->nPhases(); iphase++) {
851 size_t istart = m_mix->speciesIndex(0, iphase);
852 ThermoPhase& tref = m_mix->phase(iphase);
853 size_t nSpecies = tref.nSpecies();
854 VolPM.resize(nSpecies, 0.0);
855 tref.getMoleFractions(span<double>(&mf[istart], tref.nSpecies()));
856 tref.getPartialMolarVolumes(VolPM);
857
858 double TMolesPhase = phaseMoles(iphase);
859 double VolPhaseVolumes = 0.0;
860 for (size_t k = 0; k < nSpecies; k++) {
861 VolPhaseVolumes += VolPM[k] * mf[istart + k];
862 }
863 VolPhaseVolumes *= TMolesPhase;
864 vol += VolPhaseVolumes;
865 }
866 fprintf(FP,"--------------------- VCS_MULTIPHASE_EQUIL FINAL REPORT"
867 " -----------------------------\n");
868 fprintf(FP,"Temperature = %11.5g kelvin\n", m_mix->temperature());
869 fprintf(FP,"Pressure = %11.5g Pascal\n", m_mix->pressure());
870 fprintf(FP,"Total Volume = %11.5g m**3\n", vol);
871
872 for (size_t iphase = 0; iphase < m_mix->nPhases(); iphase++) {
873 size_t istart = m_mix->speciesIndex(0, iphase);
874 ThermoPhase& tref = m_mix->phase(iphase);
875 ThermoPhase* tp = &tref;
876 tp->getMoleFractions(span<double>(&mf[istart], tp->nSpecies()));
877 string phaseName = tref.name();
878 double TMolesPhase = phaseMoles(iphase);
879 size_t nSpecies = tref.nSpecies();
880 activity.resize(nSpecies, 0.0);
881 ac.resize(nSpecies, 0.0);
882 mu0.resize(nSpecies, 0.0);
883 mu.resize(nSpecies, 0.0);
884 VolPM.resize(nSpecies, 0.0);
885 molalities.resize(nSpecies, 0.0);
886 int actConvention = tp->activityConvention();
887 tp->getActivities(activity);
888 tp->getActivityCoefficients(ac);
889 tp->getStandardChemPotentials(mu0);
890 tp->getPartialMolarVolumes(VolPM);
891 tp->getChemPotentials(mu);
892 double VolPhaseVolumes = 0.0;
893 for (size_t k = 0; k < nSpecies; k++) {
894 VolPhaseVolumes += VolPM[k] * mf[istart + k];
895 }
896 VolPhaseVolumes *= TMolesPhase;
897 vol += VolPhaseVolumes;
898 if (actConvention == 1) {
899 MolalityVPSSTP* mTP = static_cast<MolalityVPSSTP*>(tp);
900 mTP->getMolalities(molalities);
901 tp->getChemPotentials(mu);
902
903 if (iphase == 0) {
904 fprintf(FP," Name, Phase, PhaseMoles, Mole_Fract, "
905 "Molalities, ActCoeff, Activity,"
906 "ChemPot_SS0, ChemPot, mole_num, PMVol, Phase_Volume\n");
907
908 fprintf(FP," , , (kmol), , "
909 ", , ,"
910 " (kJ/gmol), (kJ/gmol), (kmol), (m**3/kmol), (m**3)\n");
911 }
912 for (size_t k = 0; k < nSpecies; k++) {
913 string sName = tp->speciesName(k);
914 fprintf(FP,"%12s, %11s, %11.3e, %11.3e, %11.3e, %11.3e, %11.3e,"
915 "%11.3e, %11.3e, %11.3e, %11.3e, %11.3e\n",
916 sName.c_str(),
917 phaseName.c_str(), TMolesPhase,
918 mf[istart + k], molalities[k], ac[k], activity[k],
919 mu0[k]*1.0E-6, mu[k]*1.0E-6,
920 mf[istart + k] * TMolesPhase,
921 VolPM[k], VolPhaseVolumes);
922 }
923 } else {
924 if (iphase == 0) {
925 fprintf(FP," Name, Phase, PhaseMoles, Mole_Fract, "
926 "Molalities, ActCoeff, Activity,"
927 " ChemPotSS0, ChemPot, mole_num, PMVol, Phase_Volume\n");
928
929 fprintf(FP," , , (kmol), , "
930 ", , ,"
931 " (kJ/gmol), (kJ/gmol), (kmol), (m**3/kmol), (m**3)\n");
932 }
933 for (size_t k = 0; k < nSpecies; k++) {
934 molalities[k] = 0.0;
935 }
936 for (size_t k = 0; k < nSpecies; k++) {
937 string sName = tp->speciesName(k);
938 fprintf(FP,"%12s, %11s, %11.3e, %11.3e, %11.3e, %11.3e, %11.3e, "
939 "%11.3e, %11.3e,% 11.3e, %11.3e, %11.3e\n",
940 sName.c_str(),
941 phaseName.c_str(), TMolesPhase,
942 mf[istart + k], molalities[k], ac[k],
943 activity[k], mu0[k]*1.0E-6, mu[k]*1.0E-6,
944 mf[istart + k] * TMolesPhase,
945 VolPM[k], VolPhaseVolumes);
946 }
947 }
948 }
949 fclose(FP);
950}
951
952}
Header for intermediate ThermoPhase object for phases which employ molality based activity coefficien...
Base class for exceptions thrown by Cantera classes.
void resize(size_t n, size_t m, double v=0.0) override
Resize the matrix.
void getComponents(span< const size_t > order)
This method finds a set of component species and a complete set of formation reactions for the non-co...
double stepComposition(int loglevel=0)
Take one step in composition, given the gradient of G at the starting point, and a vector of reaction...
void unsort(span< double > x)
Re-arrange a vector of species properties in sorted form (components first) into unsorted,...
void finish()
Clean up the composition.
size_t nFree() const
Number of degrees of freedom.
MultiPhaseEquil(MultiPhase *mix, bool start=true, int loglevel=0)
Construct a multiphase equilibrium manager for a multiphase mixture.
double computeReactionSteps(span< double > dxi)
Compute the change in extent of reaction for each reaction.
int setInitialMoles(int loglevel=0)
Estimate the initial mole numbers.
A class for multiphase mixtures.
Definition MultiPhase.h:62
size_t speciesIndex(size_t k, size_t p) const
Return the global index of the species belonging to phase number p with local index k within the phas...
Definition MultiPhase.h:206
bool solutionSpecies(size_t kGlob) const
Return true is species kGlob is a species in a multicomponent solution phase.
double nAtoms(const size_t kGlob, const size_t mGlob) const
Returns the Number of atoms of global element mGlob in global species kGlob.
void setMoles(span< const double > n)
Sets all of the global species mole numbers.
double speciesMoles(size_t kGlob) const
Returns the moles of global species k. units = kmol.
size_t nSpecies() const
Number of species, summed over all phases.
Definition MultiPhase.h:116
double pressure() const
Pressure [Pa].
Definition MultiPhase.h:352
size_t nPhases() const
Number of phases.
Definition MultiPhase.h:389
double temperature() const
Temperature [K].
Definition MultiPhase.h:308
string speciesName(size_t kGlob) const
Name of species with global index kGlob.
bool tempOK(size_t p) const
Return true if the phase p has valid thermo data for the current temperature.
size_t speciesPhaseIndex(const size_t kGlob) const
Returns the phase index of the Kth "global" species.
void getValidChemPotentials(double not_mu, span< double > mu, bool standard=false) const
Returns a vector of Valid chemical potentials.
double phaseMoles(const size_t n) const
Return the number of moles in phase n.
size_t nElements() const
Number of elements.
Definition MultiPhase.h:86
ThermoPhase & phase(size_t n)
Return a reference to phase n.
string elementName(size_t m) const
Returns the name of the global element m.
double elementMoles(size_t m) const
Total moles of global element m, summed over all phases.
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
const double GasConstant
Universal Gas Constant [J/kmol/K].
Definition ct_defs.h:123
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
const double Tiny
Small number to compare differences of mole fractions against.
Definition ct_defs.h:176
void multiply(const DenseMatrix &A, span< const double > b, span< double > prod)
Multiply A*b and return the result in prod. Uses BLAS routine DGEMV.
const double SmallNumber
smallest number to compare to zero.
Definition ct_defs.h:161
void checkArraySize(const char *procedure, size_t available, size_t required)
Wrapper for throwing ArraySizeError.
Contains declarations for string manipulation functions within Cantera.