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