1 /**
2  *  @file InterfaceKinetics.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 
8 #include "cantera/kinetics/InterfaceKinetics.h"
9 #include "cantera/kinetics/RateCoeffMgr.h"
10 #include "cantera/kinetics/ImplicitSurfChem.h"
11 #include "cantera/kinetics/Reaction.h"
12 #include "cantera/thermo/SurfPhase.h"
13 #include "cantera/base/utilities.h"
14 
15 using namespace std;
16 
17 namespace Cantera
18 {
19 
InterfaceKinetics(ThermoPhase * thermo)20 InterfaceKinetics::InterfaceKinetics(ThermoPhase* thermo) :
21     m_redo_rates(false),
22     m_surf(0),
23     m_integrator(0),
24     m_ROP_ok(false),
25     m_temp(0.0),
26     m_logtemp(0.0),
27     m_has_coverage_dependence(false),
28     m_has_electrochem_rxns(false),
29     m_has_exchange_current_density_formulation(false),
30     m_phaseExistsCheck(false),
31     m_ioFlag(0),
32     m_nDim(2)
33 {
34     if (thermo != 0) {
35         addPhase(*thermo);
36     }
37 }
38 
~InterfaceKinetics()39 InterfaceKinetics::~InterfaceKinetics()
40 {
41     delete m_integrator;
42 }
43 
setElectricPotential(int n,doublereal V)44 void InterfaceKinetics::setElectricPotential(int n, doublereal V)
45 {
46     thermo(n).setElectricPotential(V);
47     m_redo_rates = true;
48 }
49 
_update_rates_T()50 void InterfaceKinetics::_update_rates_T()
51 {
52     // First task is update the electrical potentials from the Phases
53     _update_rates_phi();
54     if (m_has_coverage_dependence) {
55         m_surf->getCoverages(m_actConc.data());
56         m_rates.update_C(m_actConc.data());
57         m_blowers_masel_rates.update_C(m_actConc.data());
58         m_redo_rates = true;
59     }
60 
61     // Go find the temperature from the surface
62     doublereal T = thermo(surfacePhaseIndex()).temperature();
63     m_redo_rates = true;
64     if (T != m_temp || m_redo_rates) {
65         m_logtemp = log(T);
66 
67         //  Calculate the forward rate constant by calling m_rates and store it in m_rfn[]
68         m_rates.update(T, m_logtemp, m_rfn.data());
69         for (size_t n = 0; n < nPhases(); n++) {
70             thermo(n).getPartialMolarEnthalpies(m_grt.data() + m_start[n]);
71         }
72 
73         // Use the stoichiometric manager to find deltaH for each reaction.
74         getReactionDelta(m_grt.data(), m_dH.data());
75         m_blowers_masel_rates.updateBlowersMasel(T, m_logtemp, m_rfn.data(), m_dH.data());
76         applyStickingCorrection(T, m_rfn.data());
77 
78         // If we need to do conversions between exchange current density
79         // formulation and regular formulation (either way) do it here.
80         if (m_has_exchange_current_density_formulation) {
81             convertExchangeCurrentDensityFormulation(m_rfn.data());
82         }
83         if (m_has_electrochem_rxns) {
84             applyVoltageKfwdCorrection(m_rfn.data());
85         }
86         m_temp = T;
87         updateKc();
88         m_ROP_ok = false;
89         m_redo_rates = false;
90     }
91 }
92 
_update_rates_phi()93 void InterfaceKinetics::_update_rates_phi()
94 {
95     // Store electric potentials for each phase in the array m_phi[].
96     for (size_t n = 0; n < nPhases(); n++) {
97         if (thermo(n).electricPotential() != m_phi[n]) {
98             m_phi[n] = thermo(n).electricPotential();
99             m_redo_rates = true;
100         }
101     }
102 }
103 
_update_rates_C()104 void InterfaceKinetics::_update_rates_C()
105 {
106     for (size_t n = 0; n < nPhases(); n++) {
107         const ThermoPhase* tp = m_thermo[n];
108         /*
109          * We call the getActivityConcentrations function of each ThermoPhase
110          * class that makes up this kinetics object to obtain the generalized
111          * concentrations for species within that class. This is collected in
112          * the vector m_conc. m_start[] are integer indices for that vector
113          * denoting the start of the species for each phase.
114          */
115         tp->getActivityConcentrations(m_actConc.data() + m_start[n]);
116 
117         // Get regular concentrations too
118         tp->getConcentrations(m_conc.data() + m_start[n]);
119     }
120     m_ROP_ok = false;
121 }
122 
getActivityConcentrations(doublereal * const conc)123 void InterfaceKinetics::getActivityConcentrations(doublereal* const conc)
124 {
125     _update_rates_C();
126     copy(m_actConc.begin(), m_actConc.end(), conc);
127 }
128 
updateKc()129 void InterfaceKinetics::updateKc()
130 {
131     fill(m_rkcn.begin(), m_rkcn.end(), 0.0);
132 
133     if (m_revindex.size() > 0) {
134         /*
135          * Get the vector of standard state electrochemical potentials for
136          * species in the Interfacial kinetics object and store it in m_mu0[]
137          * and m_mu0_Kc[]
138          */
139         updateMu0();
140         doublereal rrt = 1.0 / thermo(reactionPhaseIndex()).RT();
141 
142         // compute Delta mu^0 for all reversible reactions
143         getRevReactionDelta(m_mu0_Kc.data(), m_rkcn.data());
144 
145         for (size_t i = 0; i < m_revindex.size(); i++) {
146             size_t irxn = m_revindex[i];
147             if (irxn == npos || irxn >= nReactions()) {
148                 throw CanteraError("InterfaceKinetics::updateKc",
149                                    "illegal value: irxn = {}", irxn);
150             }
151             // WARNING this may overflow HKM
152             m_rkcn[irxn] = exp(m_rkcn[irxn]*rrt);
153         }
154         for (size_t i = 0; i != m_irrev.size(); ++i) {
155             m_rkcn[ m_irrev[i] ] = 0.0;
156         }
157     }
158 }
159 
updateMu0()160 void InterfaceKinetics::updateMu0()
161 {
162     // First task is update the electrical potentials from the Phases
163     _update_rates_phi();
164 
165     updateExchangeCurrentQuantities();
166     size_t ik = 0;
167     for (size_t n = 0; n < nPhases(); n++) {
168         thermo(n).getStandardChemPotentials(m_mu0.data() + m_start[n]);
169         for (size_t k = 0; k < thermo(n).nSpecies(); k++) {
170             m_mu0_Kc[ik] = m_mu0[ik] + Faraday * m_phi[n] * thermo(n).charge(k);
171             m_mu0_Kc[ik] -= thermo(reactionPhaseIndex()).RT()
172                             * thermo(n).logStandardConc(k);
173             ik++;
174         }
175     }
176 }
177 
getEquilibriumConstants(doublereal * kc)178 void InterfaceKinetics::getEquilibriumConstants(doublereal* kc)
179 {
180     updateMu0();
181     doublereal rrt = 1.0 / thermo(reactionPhaseIndex()).RT();
182     std::fill(kc, kc + nReactions(), 0.0);
183     getReactionDelta(m_mu0_Kc.data(), kc);
184     for (size_t i = 0; i < nReactions(); i++) {
185         kc[i] = exp(-kc[i]*rrt);
186     }
187 }
188 
updateExchangeCurrentQuantities()189 void InterfaceKinetics::updateExchangeCurrentQuantities()
190 {
191     // Calculate:
192     //   - m_StandardConc[]
193     //   - m_ProdStanConcReac[]
194     //   - m_deltaG0[]
195     //   - m_mu0[]
196 
197     // First collect vectors of the standard Gibbs free energies of the
198     // species and the standard concentrations
199     //   - m_mu0
200     //   - m_StandardConc
201     size_t ik = 0;
202 
203     for (size_t n = 0; n < nPhases(); n++) {
204         thermo(n).getStandardChemPotentials(m_mu0.data() + m_start[n]);
205         size_t nsp = thermo(n).nSpecies();
206         for (size_t k = 0; k < nsp; k++) {
207             m_StandardConc[ik] = thermo(n).standardConcentration(k);
208             ik++;
209         }
210     }
211 
212     getReactionDelta(m_mu0.data(), m_deltaG0.data());
213 
214     //  Calculate the product of the standard concentrations of the reactants
215     for (size_t i = 0; i < nReactions(); i++) {
216         m_ProdStanConcReac[i] = 1.0;
217     }
218     m_reactantStoich.multiply(m_StandardConc.data(), m_ProdStanConcReac.data());
219 }
220 
applyVoltageKfwdCorrection(doublereal * const kf)221 void InterfaceKinetics::applyVoltageKfwdCorrection(doublereal* const kf)
222 {
223     // Compute the electrical potential energy of each species
224     size_t ik = 0;
225     for (size_t n = 0; n < nPhases(); n++) {
226         size_t nsp = thermo(n).nSpecies();
227         for (size_t k = 0; k < nsp; k++) {
228             m_pot[ik] = Faraday * thermo(n).charge(k) * m_phi[n];
229             ik++;
230         }
231     }
232 
233     // Compute the change in electrical potential energy for each reaction. This
234     // will only be non-zero if a potential difference is present.
235     getReactionDelta(m_pot.data(), deltaElectricEnergy_.data());
236 
237     // Modify the reaction rates. Only modify those with a non-zero activation
238     // energy. Below we decrease the activation energy below zero but in some
239     // debug modes we print out a warning message about this.
240 
241     // NOTE, there is some discussion about this point. Should we decrease the
242     // activation energy below zero? I don't think this has been decided in any
243     // definitive way. The treatment below is numerically more stable, however.
244     for (size_t i = 0; i < m_beta.size(); i++) {
245         size_t irxn = m_ctrxn[i];
246 
247         // Add the voltage correction to the forward reaction rate constants.
248         double eamod = m_beta[i] * deltaElectricEnergy_[irxn];
249         if (eamod != 0.0) {
250             kf[irxn] *= exp(-eamod/thermo(reactionPhaseIndex()).RT());
251         }
252     }
253 }
254 
convertExchangeCurrentDensityFormulation(doublereal * const kfwd)255 void InterfaceKinetics::convertExchangeCurrentDensityFormulation(doublereal* const kfwd)
256 {
257     updateExchangeCurrentQuantities();
258     // Loop over all reactions which are defined to have a voltage transfer
259     // coefficient that affects the activity energy for the reaction
260     for (size_t i = 0; i < m_ctrxn.size(); i++) {
261         size_t irxn = m_ctrxn[i];
262 
263         // Determine whether the reaction rate constant is in an exchange
264         // current density formulation format.
265         int iECDFormulation = m_ctrxn_ecdf[i];
266         if (iECDFormulation) {
267             // We need to have the straight chemical reaction rate constant to
268             // come out of this calculation.
269             double tmp = exp(- m_beta[i] * m_deltaG0[irxn]
270                                 / thermo(reactionPhaseIndex()).RT());
271             tmp *= 1.0 / m_ProdStanConcReac[irxn] / Faraday;
272             kfwd[irxn] *= tmp;
273         }
274     }
275 }
276 
getFwdRateConstants(doublereal * kfwd)277 void InterfaceKinetics::getFwdRateConstants(doublereal* kfwd)
278 {
279     updateROP();
280     for (size_t i = 0; i < nReactions(); i++) {
281         // base rate coefficient multiplied by perturbation factor
282         kfwd[i] = m_rfn[i] * m_perturb[i];
283     }
284 }
285 
getRevRateConstants(doublereal * krev,bool doIrreversible)286 void InterfaceKinetics::getRevRateConstants(doublereal* krev, bool doIrreversible)
287 {
288     getFwdRateConstants(krev);
289     if (doIrreversible) {
290         getEquilibriumConstants(m_ropnet.data());
291         for (size_t i = 0; i < nReactions(); i++) {
292             krev[i] /= m_ropnet[i];
293         }
294     } else {
295         for (size_t i = 0; i < nReactions(); i++) {
296             krev[i] *= m_rkcn[i];
297         }
298     }
299 }
300 
updateROP()301 void InterfaceKinetics::updateROP()
302 {
303     // evaluate rate constants and equilibrium constants at temperature and phi
304     // (electric potential)
305     _update_rates_T();
306     // get updated activities (rates updated below)
307     _update_rates_C();
308 
309     if (m_ROP_ok) {
310         return;
311     }
312 
313     for (size_t i = 0; i < nReactions(); i++) {
314         // Scale the base forward rate coefficient by the perturbation factor
315         m_ropf[i] = m_rfn[i] * m_perturb[i];
316         // Multiply the scaled forward rate coefficient by the reciprocal of the
317         // equilibrium constant
318         m_ropr[i] = m_ropf[i] * m_rkcn[i];
319     }
320 
321     // multiply ropf by the activity concentration reaction orders to obtain
322     // the forward rates of progress.
323     m_reactantStoich.multiply(m_actConc.data(), m_ropf.data());
324 
325     // For reversible reactions, multiply ropr by the activity concentration
326     // products
327     m_revProductStoich.multiply(m_actConc.data(), m_ropr.data());
328 
329     for (size_t j = 0; j != nReactions(); ++j) {
330         m_ropnet[j] = m_ropf[j] - m_ropr[j];
331     }
332 
333     // For reactions involving multiple phases, we must check that the phase
334     // being consumed actually exists. This is particularly important for phases
335     // that are stoichiometric phases containing one species with a unity
336     // activity
337     if (m_phaseExistsCheck) {
338         for (size_t j = 0; j != nReactions(); ++j) {
339             if ((m_ropr[j] > m_ropf[j]) && (m_ropr[j] > 0.0)) {
340                 for (size_t p = 0; p < nPhases(); p++) {
341                     if (m_rxnPhaseIsProduct[j][p] && !m_phaseExists[p]) {
342                         m_ropnet[j] = 0.0;
343                         m_ropr[j] = m_ropf[j];
344                         if (m_ropf[j] > 0.0) {
345                             for (size_t rp = 0; rp < nPhases(); rp++) {
346                                 if (m_rxnPhaseIsReactant[j][rp] && !m_phaseExists[rp]) {
347                                     m_ropnet[j] = 0.0;
348                                     m_ropr[j] = m_ropf[j] = 0.0;
349                                 }
350                             }
351                         }
352                     }
353                     if (m_rxnPhaseIsReactant[j][p] && !m_phaseIsStable[p]) {
354                         m_ropnet[j] = 0.0;
355                         m_ropr[j] = m_ropf[j];
356                     }
357                 }
358             } else if ((m_ropf[j] > m_ropr[j]) && (m_ropf[j] > 0.0)) {
359                 for (size_t p = 0; p < nPhases(); p++) {
360                     if (m_rxnPhaseIsReactant[j][p] && !m_phaseExists[p]) {
361                         m_ropnet[j] = 0.0;
362                         m_ropf[j] = m_ropr[j];
363                         if (m_ropf[j] > 0.0) {
364                             for (size_t rp = 0; rp < nPhases(); rp++) {
365                                 if (m_rxnPhaseIsProduct[j][rp] && !m_phaseExists[rp]) {
366                                     m_ropnet[j] = 0.0;
367                                     m_ropf[j] = m_ropr[j] = 0.0;
368                                 }
369                             }
370                         }
371                     }
372                     if (m_rxnPhaseIsProduct[j][p] && !m_phaseIsStable[p]) {
373                         m_ropnet[j] = 0.0;
374                         m_ropf[j] = m_ropr[j];
375                     }
376                 }
377             }
378         }
379     }
380     m_ROP_ok = true;
381 }
382 
getDeltaGibbs(doublereal * deltaG)383 void InterfaceKinetics::getDeltaGibbs(doublereal* deltaG)
384 {
385     // Get the chemical potentials of the species in the all of the phases used
386     // in the kinetics mechanism
387     for (size_t n = 0; n < nPhases(); n++) {
388         m_thermo[n]->getChemPotentials(m_mu.data() + m_start[n]);
389     }
390 
391     // Use the stoichiometric manager to find deltaG for each reaction.
392     getReactionDelta(m_mu.data(), m_deltaG.data());
393     if (deltaG != 0 && (m_deltaG.data() != deltaG)) {
394         for (size_t j = 0; j < nReactions(); ++j) {
395             deltaG[j] = m_deltaG[j];
396         }
397     }
398 }
399 
getDeltaElectrochemPotentials(doublereal * deltaM)400 void InterfaceKinetics::getDeltaElectrochemPotentials(doublereal* deltaM)
401 {
402     // Get the chemical potentials of the species
403     for (size_t n = 0; n < nPhases(); n++) {
404         thermo(n).getElectrochemPotentials(m_grt.data() + m_start[n]);
405     }
406 
407     // Use the stoichiometric manager to find deltaG for each reaction.
408     getReactionDelta(m_grt.data(), deltaM);
409 }
410 
getDeltaEnthalpy(doublereal * deltaH)411 void InterfaceKinetics::getDeltaEnthalpy(doublereal* deltaH)
412 {
413     // Get the partial molar enthalpy of all species
414     for (size_t n = 0; n < nPhases(); n++) {
415         thermo(n).getPartialMolarEnthalpies(m_grt.data() + m_start[n]);
416     }
417 
418     // Use the stoichiometric manager to find deltaH for each reaction.
419     getReactionDelta(m_grt.data(), deltaH);
420 }
421 
getDeltaEntropy(doublereal * deltaS)422 void InterfaceKinetics::getDeltaEntropy(doublereal* deltaS)
423 {
424     // Get the partial molar entropy of all species in all of the phases
425     for (size_t n = 0; n < nPhases(); n++) {
426         thermo(n).getPartialMolarEntropies(m_grt.data() + m_start[n]);
427     }
428 
429     // Use the stoichiometric manager to find deltaS for each reaction.
430     getReactionDelta(m_grt.data(), deltaS);
431 }
432 
getDeltaSSGibbs(doublereal * deltaGSS)433 void InterfaceKinetics::getDeltaSSGibbs(doublereal* deltaGSS)
434 {
435     // Get the standard state chemical potentials of the species. This is the
436     // array of chemical potentials at unit activity We define these here as the
437     // chemical potentials of the pure species at the temperature and pressure
438     // of the solution.
439     for (size_t n = 0; n < nPhases(); n++) {
440         thermo(n).getStandardChemPotentials(m_mu0.data() + m_start[n]);
441     }
442 
443     // Use the stoichiometric manager to find deltaG for each reaction.
444     getReactionDelta(m_mu0.data(), deltaGSS);
445 }
446 
getDeltaSSEnthalpy(doublereal * deltaH)447 void InterfaceKinetics::getDeltaSSEnthalpy(doublereal* deltaH)
448 {
449     // Get the standard state enthalpies of the species. This is the array of
450     // chemical potentials at unit activity We define these here as the
451     // enthalpies of the pure species at the temperature and pressure of the
452     // solution.
453     for (size_t n = 0; n < nPhases(); n++) {
454         thermo(n).getEnthalpy_RT(m_grt.data() + m_start[n]);
455     }
456     for (size_t k = 0; k < m_kk; k++) {
457         m_grt[k] *= thermo(reactionPhaseIndex()).RT();
458     }
459 
460     // Use the stoichiometric manager to find deltaH for each reaction.
461     getReactionDelta(m_grt.data(), deltaH);
462 }
463 
getDeltaSSEntropy(doublereal * deltaS)464 void InterfaceKinetics::getDeltaSSEntropy(doublereal* deltaS)
465 {
466     // Get the standard state entropy of the species. We define these here as
467     // the entropies of the pure species at the temperature and pressure of the
468     // solution.
469     for (size_t n = 0; n < nPhases(); n++) {
470         thermo(n).getEntropy_R(m_grt.data() + m_start[n]);
471     }
472     for (size_t k = 0; k < m_kk; k++) {
473         m_grt[k] *= GasConstant;
474     }
475 
476     // Use the stoichiometric manager to find deltaS for each reaction.
477     getReactionDelta(m_grt.data(), deltaS);
478 }
479 
addReaction(shared_ptr<Reaction> r_base,bool resize)480 bool InterfaceKinetics::addReaction(shared_ptr<Reaction> r_base, bool resize)
481 {
482     if (!m_surf) {
483         init();
484     }
485 
486     // Check that the number of surface sites is balanced
487     double reac_sites = 0.0;
488     double prod_sites = 0.0;
489     for (const auto& reactant : r_base->reactants) {
490         size_t k = m_surf->speciesIndex(reactant.first);
491         if (k != npos) {
492             reac_sites += reactant.second * m_surf->size(k);
493         }
494     }
495     for (const auto& product : r_base->products) {
496         size_t k = m_surf->speciesIndex(product.first);
497         if (k != npos) {
498             prod_sites += product.second * m_surf->size(k);
499         }
500     }
501     if (fabs(reac_sites - prod_sites) > 1e-5 * (reac_sites + prod_sites)) {
502         throw CanteraError("InterfaceKinetics::addReaction", "Number of surface"
503             " sites not balanced in reaction {}.\nReactant sites: {}\n"
504             "Product sites: {}", r_base->equation(), reac_sites, prod_sites);
505     }
506 
507     size_t i = nReactions();
508     bool added = Kinetics::addReaction(r_base, resize);
509     if (!added) {
510         return false;
511     }
512     if (r_base->reaction_type == BMINTERFACE_RXN) {
513         BlowersMaselInterfaceReaction& r = dynamic_cast<BlowersMaselInterfaceReaction&>(*r_base);
514         BMSurfaceArrhenius rate = buildBMSurfaceArrhenius(i, r, false);
515         m_blowers_masel_rates.install(i, rate);
516 
517         // Turn on the global flag indicating surface coverage dependence
518         if (!r.coverage_deps.empty()) {
519             m_has_coverage_dependence = true;
520         }
521         if (r.reversible) {
522             m_revindex.push_back(i);
523         } else {
524             m_irrev.push_back(i);
525         }
526 
527         m_rxnPhaseIsReactant.emplace_back(nPhases(), false);
528         m_rxnPhaseIsProduct.emplace_back(nPhases(), false);
529 
530         for (const auto& sp : r.reactants) {
531             size_t k = kineticsSpeciesIndex(sp.first);
532             size_t p = speciesPhaseIndex(k);
533             m_rxnPhaseIsReactant[i][p] = true;
534         }
535         for (const auto& sp : r.products) {
536             size_t k = kineticsSpeciesIndex(sp.first);
537             size_t p = speciesPhaseIndex(k);
538             m_rxnPhaseIsProduct[i][p] = true;
539         }
540 
541     } else {
542         InterfaceReaction& r = dynamic_cast<InterfaceReaction&>(*r_base);
543         SurfaceArrhenius rate = buildSurfaceArrhenius(i, r, false);
544         m_rates.install(i, rate);
545 
546         // Turn on the global flag indicating surface coverage dependence
547         if (!r.coverage_deps.empty()) {
548             m_has_coverage_dependence = true;
549         }
550         ElectrochemicalReaction* re = dynamic_cast<ElectrochemicalReaction*>(&r);
551         if (re) {
552             m_has_electrochem_rxns = true;
553             m_beta.push_back(re->beta);
554             m_ctrxn.push_back(i);
555             if (re->exchange_current_density_formulation) {
556                 m_has_exchange_current_density_formulation = true;
557                 m_ctrxn_ecdf.push_back(1);
558             } else {
559                 m_ctrxn_ecdf.push_back(0);
560             }
561         }
562         if (r.reversible) {
563             m_revindex.push_back(i);
564         } else {
565             m_irrev.push_back(i);
566         }
567 
568         m_rxnPhaseIsReactant.emplace_back(nPhases(), false);
569         m_rxnPhaseIsProduct.emplace_back(nPhases(), false);
570 
571         for (const auto& sp : r.reactants) {
572             size_t k = kineticsSpeciesIndex(sp.first);
573             size_t p = speciesPhaseIndex(k);
574             m_rxnPhaseIsReactant[i][p] = true;
575         }
576         for (const auto& sp : r.products) {
577             size_t k = kineticsSpeciesIndex(sp.first);
578             size_t p = speciesPhaseIndex(k);
579             m_rxnPhaseIsProduct[i][p] = true;
580         }
581     }
582     deltaElectricEnergy_.push_back(0.0);
583     m_deltaG0.push_back(0.0);
584     m_deltaG.push_back(0.0);
585     m_ProdStanConcReac.push_back(0.0);
586 
587     return true;
588 }
589 
modifyReaction(size_t i,shared_ptr<Reaction> r_base)590 void InterfaceKinetics::modifyReaction(size_t i, shared_ptr<Reaction> r_base)
591 {
592     Kinetics::modifyReaction(i, r_base);
593     if (r_base->reaction_type == BMINTERFACE_RXN) {
594         BlowersMaselInterfaceReaction& r = dynamic_cast<BlowersMaselInterfaceReaction&>(*r_base);
595         BMSurfaceArrhenius rate = buildBMSurfaceArrhenius(i, r, true);
596         m_blowers_masel_rates.replace(i, rate);
597     } else {
598         InterfaceReaction& r = dynamic_cast<InterfaceReaction&>(*r_base);
599         SurfaceArrhenius rate = buildSurfaceArrhenius(i, r, true);
600         m_rates.replace(i, rate);
601     }
602     // Invalidate cached data
603     m_redo_rates = true;
604     m_temp += 0.1;
605 }
606 
buildSurfaceArrhenius(size_t i,InterfaceReaction & r,bool replace)607 SurfaceArrhenius InterfaceKinetics::buildSurfaceArrhenius(
608     size_t i, InterfaceReaction& r, bool replace)
609 {
610     if (r.is_sticking_coefficient) {
611         // Identify the interface phase
612         size_t iInterface = npos;
613         size_t min_dim = 4;
614         for (size_t n = 0; n < nPhases(); n++) {
615             if (thermo(n).nDim() < min_dim) {
616                 iInterface = n;
617                 min_dim = thermo(n).nDim();
618             }
619         }
620 
621         std::string sticking_species = r.sticking_species;
622         if (sticking_species == "") {
623             // Identify the sticking species if not explicitly given
624             bool foundStick = false;
625             for (const auto& sp : r.reactants) {
626                 size_t iPhase = speciesPhaseIndex(kineticsSpeciesIndex(sp.first));
627                 if (iPhase != iInterface) {
628                     // Non-interface species. There should be exactly one of these
629                     if (foundStick) {
630                         throw InputFileError("InterfaceKinetics::buildSurfaceArrhenius",
631                             r.input, "Multiple non-interface species ('{}' and '{}')\n"
632                             "found in sticking reaction: '{}'.\nSticking species "
633                             "must be explicitly specified.",
634                             sticking_species, sp.first, r.equation());
635                     }
636                     foundStick = true;
637                     sticking_species = sp.first;
638                 }
639             }
640             if (!foundStick) {
641                 throw InputFileError("InterfaceKinetics::buildSurfaceArrhenius",
642                     r.input, "No non-interface species found "
643                     "in sticking reaction: '{}'", r.equation());
644             }
645         }
646 
647         double surface_order = 0.0;
648         double multiplier = 1.0;
649         // Adjust the A-factor
650         for (const auto& sp : r.reactants) {
651             size_t iPhase = speciesPhaseIndex(kineticsSpeciesIndex(sp.first));
652             const ThermoPhase& p = thermo(iPhase);
653             size_t k = p.speciesIndex(sp.first);
654             if (sp.first == sticking_species) {
655                 multiplier *= sqrt(GasConstant/(2*Pi*p.molecularWeight(k)));
656             } else {
657                 // Non-sticking species. Convert from coverages used in the
658                 // sticking probability expression to the concentration units
659                 // used in the mass action rate expression. For surface phases,
660                 // the dependence on the site density is incorporated when the
661                 // rate constant is evaluated, since we don't assume that the
662                 // site density is known at this time.
663                 double order = getValue(r.orders, sp.first, sp.second);
664                 if (&p == m_surf) {
665                     multiplier *= pow(m_surf->size(k), order);
666                     surface_order += order;
667                 } else {
668                     multiplier *= pow(p.standardConcentration(k), -order);
669                 }
670             }
671         }
672 
673         if (!replace) {
674             m_stickingData.emplace_back(StickData{i, surface_order, multiplier,
675                                                   r.use_motz_wise_correction});
676         } else {
677             // Modifying an existing sticking reaction.
678             for (auto& item : m_stickingData) {
679                 if (item.index == i) {
680                     item.order = surface_order;
681                     item.multiplier = multiplier;
682                     item.use_motz_wise = r.use_motz_wise_correction;
683                     break;
684                 }
685             }
686         }
687     }
688 
689     SurfaceArrhenius rate(r.rate.preExponentialFactor(),
690                           r.rate.temperatureExponent(),
691                           r.rate.activationEnergy_R());
692 
693     // Set up coverage dependencies
694     for (const auto& sp : r.coverage_deps) {
695         size_t k = thermo(reactionPhaseIndex()).speciesIndex(sp.first);
696         rate.addCoverageDependence(k, sp.second.a, sp.second.m, sp.second.E);
697     }
698     return rate;
699 }
700 
buildBMSurfaceArrhenius(size_t i,BlowersMaselInterfaceReaction & r,bool replace)701 BMSurfaceArrhenius InterfaceKinetics::buildBMSurfaceArrhenius(
702     size_t i, BlowersMaselInterfaceReaction& r, bool replace)
703 {
704     if (r.is_sticking_coefficient) {
705         // Identify the interface phase
706         size_t iInterface = npos;
707         size_t min_dim = 4;
708         for (size_t n = 0; n < nPhases(); n++) {
709             if (thermo(n).nDim() < min_dim) {
710                 iInterface = n;
711                 min_dim = thermo(n).nDim();
712             }
713         }
714 
715         std::string sticking_species = r.sticking_species;
716         if (sticking_species == "") {
717             // Identify the sticking species if not explicitly given
718             bool foundStick = false;
719             for (const auto& sp : r.reactants) {
720                 size_t iPhase = speciesPhaseIndex(kineticsSpeciesIndex(sp.first));
721                 if (iPhase != iInterface) {
722                     // Non-interface species. There should be exactly one of these
723                     if (foundStick) {
724                         throw InputFileError("InterfaceKinetics::buildBMSurfaceArrhenius",
725                             r.input, "Multiple non-interface species ('{}' and '{}')\n"
726                             "found in sticking reaction: '{}'.\nSticking species "
727                             "must be explicitly specified.",
728                             sticking_species, sp.first, r.equation());
729                     }
730                     foundStick = true;
731                     sticking_species = sp.first;
732                 }
733             }
734             if (!foundStick) {
735                 throw InputFileError("InterfaceKinetics::buildBMSurfaceArrhenius",
736                     r.input, "No non-interface species found "
737                     "in sticking reaction: '{}'", r.equation());
738             }
739         }
740 
741         double surface_order = 0.0;
742         double multiplier = 1.0;
743         // Adjust the A-factor
744         for (const auto& sp : r.reactants) {
745             size_t iPhase = speciesPhaseIndex(kineticsSpeciesIndex(sp.first));
746             const ThermoPhase& p = thermo(iPhase);
747             size_t k = p.speciesIndex(sp.first);
748             if (sp.first == sticking_species) {
749                 multiplier *= sqrt(GasConstant/(2*Pi*p.molecularWeight(k)));
750             } else {
751                 // Non-sticking species. Convert from coverages used in the
752                 // sticking probability expression to the concentration units
753                 // used in the mass action rate expression. For surface phases,
754                 // the dependence on the site density is incorporated when the
755                 // rate constant is evaluated, since we don't assume that the
756                 // site density is known at this time.
757                 double order = getValue(r.orders, sp.first, sp.second);
758                 if (&p == m_surf) {
759                     multiplier *= pow(m_surf->size(k), order);
760                     surface_order += order;
761                 } else {
762                     multiplier *= pow(p.standardConcentration(k), -order);
763                 }
764             }
765         }
766 
767         if (!replace) {
768             m_stickingData.emplace_back(StickData{i, surface_order, multiplier,
769                                                   r.use_motz_wise_correction});
770         } else {
771             // Modifying an existing sticking reaction.
772             for (auto& item : m_stickingData) {
773                 if (item.index == i) {
774                     item.order = surface_order;
775                     item.multiplier = multiplier;
776                     item.use_motz_wise = r.use_motz_wise_correction;
777                     break;
778                 }
779             }
780         }
781     }
782 
783     BMSurfaceArrhenius rate(r.rate.preExponentialFactor(),
784                             r.rate.temperatureExponent(),
785                             r.rate.activationEnergy_R0(),
786                             r.rate.bondEnergy());
787 
788     // Set up coverage dependencies
789     for (const auto& sp : r.coverage_deps) {
790         size_t k = thermo(reactionPhaseIndex()).speciesIndex(sp.first);
791         rate.addCoverageDependence(k, sp.second.a, sp.second.m, sp.second.E);
792     }
793     return rate;
794 }
795 
setIOFlag(int ioFlag)796 void InterfaceKinetics::setIOFlag(int ioFlag)
797 {
798     m_ioFlag = ioFlag;
799     if (m_integrator) {
800         m_integrator->setIOFlag(ioFlag);
801     }
802 }
803 
addPhase(ThermoPhase & thermo)804 void InterfaceKinetics::addPhase(ThermoPhase& thermo)
805 {
806     Kinetics::addPhase(thermo);
807     m_phaseExists.push_back(true);
808     m_phaseIsStable.push_back(true);
809 }
810 
init()811 void InterfaceKinetics::init()
812 {
813     size_t ks = reactionPhaseIndex();
814     if (ks == npos) {
815         throw CanteraError("InterfaceKinetics::init",
816                            "no surface phase is present.");
817     }
818 
819     // Check to see that the interface routine has a dimension of 2
820     m_surf = (SurfPhase*)&thermo(ks);
821     if (m_surf->nDim() != m_nDim) {
822         throw CanteraError("InterfaceKinetics::init",
823                            "expected interface dimension = 2, but got dimension = {}",
824                            m_surf->nDim());
825     }
826 }
827 
resizeSpecies()828 void InterfaceKinetics::resizeSpecies()
829 {
830     size_t kOld = m_kk;
831     Kinetics::resizeSpecies();
832     if (m_kk != kOld && nReactions()) {
833         throw CanteraError("InterfaceKinetics::resizeSpecies", "Cannot add"
834             " species to InterfaceKinetics after reactions have been added.");
835     }
836     m_actConc.resize(m_kk);
837     m_conc.resize(m_kk);
838     m_StandardConc.resize(m_kk, 0.0);
839     m_mu0.resize(m_kk);
840     m_mu.resize(m_kk);
841     m_mu0_Kc.resize(m_kk);
842     m_grt.resize(m_kk);
843     m_pot.resize(m_kk, 0.0);
844     m_phi.resize(nPhases(), 0.0);
845 }
846 
electrochem_beta(size_t irxn) const847 doublereal InterfaceKinetics::electrochem_beta(size_t irxn) const
848 {
849     for (size_t i = 0; i < m_ctrxn.size(); i++) {
850         if (m_ctrxn[i] == irxn) {
851             return m_beta[i];
852         }
853     }
854     return 0.0;
855 }
856 
advanceCoverages(doublereal tstep,doublereal rtol,doublereal atol,doublereal maxStepSize,size_t maxSteps,size_t maxErrTestFails)857 void InterfaceKinetics::advanceCoverages(doublereal tstep, doublereal rtol,
858                                          doublereal atol, doublereal maxStepSize,
859                                          size_t maxSteps, size_t maxErrTestFails)
860 {
861     if (m_integrator == 0) {
862         vector<InterfaceKinetics*> k{this};
863         m_integrator = new ImplicitSurfChem(k);
864     }
865     m_integrator->setTolerances(rtol, atol);
866     m_integrator->setMaxStepSize(maxStepSize);
867     m_integrator->setMaxSteps(maxSteps);
868     m_integrator->setMaxErrTestFails(maxErrTestFails);
869     m_integrator->integrate(0.0, tstep);
870     delete m_integrator;
871     m_integrator = 0;
872 }
873 
solvePseudoSteadyStateProblem(int ifuncOverride,doublereal timeScaleOverride)874 void InterfaceKinetics::solvePseudoSteadyStateProblem(
875     int ifuncOverride, doublereal timeScaleOverride)
876 {
877     // create our own solver object
878     if (m_integrator == 0) {
879         vector<InterfaceKinetics*> k{this};
880         m_integrator = new ImplicitSurfChem(k);
881         m_integrator->initialize();
882     }
883     m_integrator->setIOFlag(m_ioFlag);
884     // New direct method to go here
885     m_integrator->solvePseudoSteadyStateProblem(ifuncOverride, timeScaleOverride);
886 }
887 
setPhaseExistence(const size_t iphase,const int exists)888 void InterfaceKinetics::setPhaseExistence(const size_t iphase, const int exists)
889 {
890     checkPhaseIndex(iphase);
891     if (exists) {
892         if (!m_phaseExists[iphase]) {
893             m_phaseExistsCheck--;
894             m_phaseExistsCheck = std::max(m_phaseExistsCheck, 0);
895             m_phaseExists[iphase] = true;
896         }
897         m_phaseIsStable[iphase] = true;
898     } else {
899         if (m_phaseExists[iphase]) {
900             m_phaseExistsCheck++;
901             m_phaseExists[iphase] = false;
902         }
903         m_phaseIsStable[iphase] = false;
904     }
905 }
906 
phaseExistence(const size_t iphase) const907 int InterfaceKinetics::phaseExistence(const size_t iphase) const
908 {
909     checkPhaseIndex(iphase);
910     return m_phaseExists[iphase];
911 }
912 
phaseStability(const size_t iphase) const913 int InterfaceKinetics::phaseStability(const size_t iphase) const
914 {
915     checkPhaseIndex(iphase);
916     return m_phaseIsStable[iphase];
917 }
918 
setPhaseStability(const size_t iphase,const int isStable)919 void InterfaceKinetics::setPhaseStability(const size_t iphase, const int isStable)
920 {
921     checkPhaseIndex(iphase);
922     if (isStable) {
923         m_phaseIsStable[iphase] = true;
924     } else {
925         m_phaseIsStable[iphase] = false;
926     }
927 }
928 
applyStickingCorrection(double T,double * kf)929 void InterfaceKinetics::applyStickingCorrection(double T, double* kf)
930 {
931     if (m_stickingData.empty()) {
932         return;
933     }
934 
935     static const int cacheId = m_cache.getId();
936     CachedArray cached = m_cache.getArray(cacheId);
937     vector_fp& factors = cached.value;
938 
939     double n0 = m_surf->siteDensity();
940     if (!cached.validate(n0)) {
941         factors.resize(m_stickingData.size());
942         for (size_t n = 0; n < m_stickingData.size(); n++) {
943             factors[n] = pow(n0, -m_stickingData[n].order);
944         }
945     }
946 
947     for (size_t n = 0; n < m_stickingData.size(); n++) {
948         const StickData& item = m_stickingData[n];
949         if (item.use_motz_wise) {
950             kf[item.index] /= 1 - 0.5 * kf[item.index];
951         }
952         kf[item.index] *= factors[n] * sqrt(T) * item.multiplier;
953     }
954 }
955 
956 }
957