Cantera  3.3.0a1
Loading...
Searching...
No Matches
Reactor.cpp
Go to the documentation of this file.
1//! @file Reactor.cpp A zero-dimensional reactor
2
3// This file is part of Cantera. See License.txt in the top-level directory or
4// at https://cantera.org/license.txt for license and copyright information.
5
16
17#include <boost/math/tools/roots.hpp>
18
19using namespace std;
20namespace bmt = boost::math::tools;
21
22namespace Cantera
23{
24
25Reactor::Reactor(shared_ptr<Solution> sol, const string& name)
26 : Reactor(sol, true, name)
27{
28}
29
30Reactor::Reactor(shared_ptr<Solution> sol, bool clone, const string& name)
31 : ReactorBase(sol, clone, name)
32{
33 m_kin = m_solution->kinetics().get();
34 setChemistryEnabled(m_kin->nReactions() > 0);
35 m_vol = 1.0; // By default, the volume is set to 1.0 m^3.
36}
37
38void Reactor::setDerivativeSettings(AnyMap& settings)
39{
40 m_kin->setDerivativeSettings(settings);
41 // translate settings to surfaces
42 for (auto S : m_surfaces) {
43 S->kinetics()->setDerivativeSettings(settings);
44 }
45}
46
47void Reactor::getState(double* y)
48{
49 if (m_thermo == 0) {
50 throw CanteraError("Reactor::getState",
51 "Error: reactor is empty.");
52 }
53 m_thermo->restoreState(m_state);
54
55 // set the first component to the total mass
56 m_mass = m_thermo->density() * m_vol;
57 y[0] = m_mass;
58
59 // set the second component to the total volume
60 y[1] = m_vol;
61
62 // set the third component to the total internal energy
63 y[2] = m_thermo->intEnergy_mass() * m_mass;
64
65 // set components y+3 ... y+K+2 to the mass fractions of each species
66 m_thermo->getMassFractions(y+3);
67
68 // set the remaining components to the surface species
69 // coverages on the walls
70 getSurfaceInitialConditions(y + m_nsp + 3);
71}
72
73void Reactor::getSurfaceInitialConditions(double* y)
74{
75 size_t loc = 0;
76 for (auto& S : m_surfaces) {
77 S->getCoverages(y + loc);
78 loc += S->thermo()->nSpecies();
79 }
80}
81
82void Reactor::initialize(double t0)
83{
84 if (!m_thermo || (m_chem && !m_kin)) {
85 throw CanteraError("Reactor::initialize", "Reactor contents not set"
86 " for reactor '" + m_name + "'.");
87 }
88 m_thermo->restoreState(m_state);
89 m_sdot.resize(m_nsp, 0.0);
90 m_wdot.resize(m_nsp, 0.0);
91 updateConnected(true);
92
93 for (size_t n = 0; n < m_wall.size(); n++) {
94 WallBase* W = m_wall[n];
95 W->initialize();
96 }
97
98 m_nv = m_nsp + 3;
99 m_nv_surf = 0;
100 size_t maxnt = 0;
101 for (auto& S : m_surfaces) {
102 m_nv_surf += S->thermo()->nSpecies();
103 size_t nt = S->kinetics()->nTotalSpecies();
104 maxnt = std::max(maxnt, nt);
105 }
106 m_nv += m_nv_surf;
107 m_work.resize(maxnt);
108}
109
110size_t Reactor::nSensParams() const
111{
112 size_t ns = m_sensParams.size();
113 for (auto& S : m_surfaces) {
114 ns += S->nSensParams();
115 }
116 return ns;
117}
118
119void Reactor::updateState(double* y)
120{
121 // The components of y are [0] the total mass, [1] the total volume,
122 // [2] the total internal energy, [3...K+3] are the mass fractions of each
123 // species, and [K+3...] are the coverages of surface species on each wall.
124 m_mass = y[0];
125 m_vol = y[1];
126 m_thermo->setMassFractions_NoNorm(y+3);
127
128 if (m_energy) {
129 double U = y[2];
130 // Residual function: error in internal energy as a function of T
131 auto u_err = [this, U](double T) {
132 m_thermo->setState_TD(T, m_mass / m_vol);
133 return m_thermo->intEnergy_mass() * m_mass - U;
134 };
135
136 double T = m_thermo->temperature();
137 boost::uintmax_t maxiter = 100;
138 pair<double, double> TT;
139 try {
140 TT = bmt::bracket_and_solve_root(
141 u_err, T, 1.2, true, bmt::eps_tolerance<double>(48), maxiter);
142 } catch (std::exception&) {
143 // Try full-range bisection if bracketing fails (for example, near
144 // temperature limits for the phase's equation of state)
145 try {
146 TT = bmt::bisect(u_err, m_thermo->minTemp(), m_thermo->maxTemp(),
147 bmt::eps_tolerance<double>(48), maxiter);
148 } catch (std::exception& err2) {
149 // Set m_thermo back to a reasonable state if root finding fails
150 m_thermo->setState_TD(T, m_mass / m_vol);
151 throw CanteraError("Reactor::updateState",
152 "{}\nat U = {}, rho = {}", err2.what(), U, m_mass / m_vol);
153 }
154 }
155 if (fabs(TT.first - TT.second) > 1e-7*TT.first) {
156 throw CanteraError("Reactor::updateState", "root finding failed");
157 }
158 m_thermo->setState_TD(TT.second, m_mass / m_vol);
159 } else {
160 m_thermo->setDensity(m_mass/m_vol);
161 }
162
163 updateConnected(true);
164 updateSurfaceState(y + m_nsp + 3);
165}
166
167void Reactor::updateSurfaceState(double* y)
168{
169 size_t loc = 0;
170 for (auto& S : m_surfaces) {
171 S->setCoverages(y+loc);
172 loc += S->thermo()->nSpecies();
173 }
174}
175
176void Reactor::updateConnected(bool updatePressure) {
177 // save parameters needed by other connected reactors
178 m_enthalpy = m_thermo->enthalpy_mass();
179 if (updatePressure) {
180 m_pressure = m_thermo->pressure();
181 }
182 m_thermo->saveState(m_state);
183
184 // Update the mass flow rate of connected flow devices
185 double time = 0.0;
186 if (m_net != nullptr) {
187 time = (timeIsIndependent()) ? m_net->time() : m_net->distance();
188 }
189 for (size_t i = 0; i < m_outlet.size(); i++) {
190 m_outlet[i]->setSimTime(time);
191 m_outlet[i]->updateMassFlowRate(time);
192 }
193 for (size_t i = 0; i < m_inlet.size(); i++) {
194 m_inlet[i]->setSimTime(time);
195 m_inlet[i]->updateMassFlowRate(time);
196 }
197 for (size_t i = 0; i < m_wall.size(); i++) {
198 m_wall[i]->setSimTime(time);
199 }
200}
201
202void Reactor::eval(double time, double* LHS, double* RHS)
203{
204 double& dmdt = RHS[0];
205 double* mdYdt = RHS + 3; // mass * dY/dt
206
207 evalWalls(time);
208 m_thermo->restoreState(m_state);
209 const vector<double>& mw = m_thermo->molecularWeights();
210 const double* Y = m_thermo->massFractions();
211
212 evalSurfaces(LHS + m_nsp + 3, RHS + m_nsp + 3, m_sdot.data());
213 // mass added to gas phase from surface reactions
214 double mdot_surf = dot(m_sdot.begin(), m_sdot.end(), mw.begin());
215 dmdt = mdot_surf;
216
217 // volume equation
218 RHS[1] = m_vdot;
219
220 if (m_chem) {
221 m_kin->getNetProductionRates(&m_wdot[0]); // "omega dot"
222 }
223
224 for (size_t k = 0; k < m_nsp; k++) {
225 // production in gas phase and from surfaces
226 mdYdt[k] = (m_wdot[k] * m_vol + m_sdot[k]) * mw[k];
227 // dilution by net surface mass flux
228 mdYdt[k] -= Y[k] * mdot_surf;
229 LHS[k+3] = m_mass;
230 }
231
232 // Energy equation.
233 // @f[
234 // \dot U = -P\dot V + A \dot q + \dot m_{in} h_{in} - \dot m_{out} h.
235 // @f]
236 if (m_energy) {
237 RHS[2] = - m_thermo->pressure() * m_vdot + m_Qdot;
238 } else {
239 RHS[2] = 0.0;
240 }
241
242 // add terms for outlets
243 for (auto outlet : m_outlet) {
244 double mdot = outlet->massFlowRate();
245 dmdt -= mdot; // mass flow out of system
246 if (m_energy) {
247 RHS[2] -= mdot * m_enthalpy;
248 }
249 }
250
251 // add terms for inlets
252 for (auto inlet : m_inlet) {
253 double mdot = inlet->massFlowRate();
254 dmdt += mdot; // mass flow into system
255 for (size_t n = 0; n < m_nsp; n++) {
256 double mdot_spec = inlet->outletSpeciesMassFlowRate(n);
257 // flow of species into system and dilution by other species
258 mdYdt[n] += (mdot_spec - mdot * Y[n]);
259 }
260 if (m_energy) {
261 RHS[2] += mdot * inlet->enthalpy_mass();
262 }
263 }
264}
265
266void Reactor::evalWalls(double t)
267{
268 // time is currently unused
269 m_vdot = 0.0;
270 m_Qdot = 0.0;
271 for (size_t i = 0; i < m_wall.size(); i++) {
272 int f = 2 * m_lr[i] - 1;
273 m_vdot -= f * m_wall[i]->expansionRate();
274 m_Qdot += f * m_wall[i]->heatRate();
275 }
276}
277
278void Reactor::evalSurfaces(double* LHS, double* RHS, double* sdot)
279{
280 fill(sdot, sdot + m_nsp, 0.0);
281 size_t loc = 0; // offset into ydot
282
283 for (auto S : m_surfaces) {
284 Kinetics* kin = S->kinetics();
285 SurfPhase* surf = S->thermo();
286
287 double rs0 = 1.0/surf->siteDensity();
288 size_t nk = surf->nSpecies();
289 double sum = 0.0;
290 S->restoreState();
291 kin->getNetProductionRates(&m_work[0]);
292 for (size_t k = 1; k < nk; k++) {
293 RHS[loc + k] = m_work[k] * rs0 * surf->size(k);
294 sum -= RHS[loc + k];
295 }
296 RHS[loc] = sum;
297 loc += nk;
298
299 size_t bulkloc = kin->kineticsSpeciesIndex(m_thermo->speciesName(0));
300 double wallarea = S->area();
301 for (size_t k = 0; k < m_nsp; k++) {
302 sdot[k] += m_work[bulkloc + k] * wallarea;
303 }
304 }
305}
306
307vector<size_t> Reactor::steadyConstraints() const
308{
309 if (!energyEnabled()) {
310 throw CanteraError("Reactor::steadyConstraints", "Steady state solver cannot"
311 " be used with {0} when energy equation is disabled."
312 "\nConsider using IdealGas{0} instead.\n"
313 "See https://github.com/Cantera/enhancements/issues/234", type());
314 }
315 if (nSurfs() != 0) {
316 throw CanteraError("Reactor::steadyConstraints", "Steady state solver cannot"
317 " currently be used when reactor surfaces are present.\n"
318 "See https://github.com/Cantera/enhancements/issues/234.");
319 }
320 return {1}; // volume
321}
322
323Eigen::SparseMatrix<double> Reactor::finiteDifferenceJacobian()
324{
325 if (m_nv == 0) {
326 throw CanteraError("Reactor::finiteDifferenceJacobian",
327 "Reactor must be initialized first.");
328 }
329 // clear former jacobian elements
330 m_jac_trips.clear();
331
332 Eigen::ArrayXd yCurrent(m_nv);
333 getState(yCurrent.data());
334 double time = (m_net != nullptr) ? m_net->time() : 0.0;
335
336 Eigen::ArrayXd yPerturbed = yCurrent;
337 Eigen::ArrayXd lhsPerturbed(m_nv), lhsCurrent(m_nv);
338 Eigen::ArrayXd rhsPerturbed(m_nv), rhsCurrent(m_nv);
339 lhsCurrent = 1.0;
340 rhsCurrent = 0.0;
341 updateState(yCurrent.data());
342 eval(time, lhsCurrent.data(), rhsCurrent.data());
343
344 double rel_perturb = std::sqrt(std::numeric_limits<double>::epsilon());
345 double atol = (m_net != nullptr) ? m_net->atol() : 1e-15;
346
347 for (size_t j = 0; j < m_nv; j++) {
348 yPerturbed = yCurrent;
349 double delta_y = std::max(std::abs(yCurrent[j]), 1000 * atol) * rel_perturb;
350 yPerturbed[j] += delta_y;
351
352 updateState(yPerturbed.data());
353 lhsPerturbed = 1.0;
354 rhsPerturbed = 0.0;
355 eval(time, lhsPerturbed.data(), rhsPerturbed.data());
356
357 // d ydot_i/dy_j
358 for (size_t i = 0; i < m_nv; i++) {
359 double ydotPerturbed = rhsPerturbed[i] / lhsPerturbed[i];
360 double ydotCurrent = rhsCurrent[i] / lhsCurrent[i];
361 if (ydotCurrent != ydotPerturbed) {
362 m_jac_trips.emplace_back(
363 static_cast<int>(i), static_cast<int>(j),
364 (ydotPerturbed - ydotCurrent) / delta_y);
365 }
366 }
367 }
368 updateState(yCurrent.data());
369
370 Eigen::SparseMatrix<double> jac(m_nv, m_nv);
371 jac.setFromTriplets(m_jac_trips.begin(), m_jac_trips.end());
372 return jac;
373}
374
375
376void Reactor::evalSurfaces(double* RHS, double* sdot)
377{
378 fill(sdot, sdot + m_nsp, 0.0);
379 size_t loc = 0; // offset into ydot
380
381 for (auto S : m_surfaces) {
382 Kinetics* kin = S->kinetics();
383 SurfPhase* surf = S->thermo();
384
385 double rs0 = 1.0/surf->siteDensity();
386 size_t nk = surf->nSpecies();
387 double sum = 0.0;
388 S->restoreState();
389 kin->getNetProductionRates(&m_work[0]);
390 for (size_t k = 1; k < nk; k++) {
391 RHS[loc + k] = m_work[k] * rs0 * surf->size(k);
392 sum -= RHS[loc + k];
393 }
394 RHS[loc] = sum;
395 loc += nk;
396
397 size_t bulkloc = kin->kineticsSpeciesIndex(m_thermo->speciesName(0));
398 double wallarea = S->area();
399 for (size_t k = 0; k < m_nsp; k++) {
400 sdot[k] += m_work[bulkloc + k] * wallarea;
401 }
402 }
403}
404
405void Reactor::addSensitivityReaction(size_t rxn)
406{
407 if (!m_chem || rxn >= m_kin->nReactions()) {
408 throw CanteraError("Reactor::addSensitivityReaction",
409 "Reaction number out of range ({})", rxn);
410 }
411
412 size_t p = network().registerSensitivityParameter(
413 name()+": "+m_kin->reaction(rxn)->equation(), 1.0, 1.0);
414 m_sensParams.emplace_back(
415 SensitivityParameter{rxn, p, 1.0, SensParameterType::reaction});
416}
417
418void Reactor::addSensitivitySpeciesEnthalpy(size_t k)
419{
420 if (k >= m_thermo->nSpecies()) {
421 throw CanteraError("Reactor::addSensitivitySpeciesEnthalpy",
422 "Species index out of range ({})", k);
423 }
424
425 size_t p = network().registerSensitivityParameter(
426 name() + ": " + m_thermo->speciesName(k) + " enthalpy",
427 0.0, GasConstant * 298.15);
428 m_sensParams.emplace_back(
429 SensitivityParameter{k, p, m_thermo->Hf298SS(k),
430 SensParameterType::enthalpy});
431}
432
433size_t Reactor::speciesIndex(const string& nm) const
434{
435 // check for a gas species name
436 size_t k = m_thermo->speciesIndex(nm, false);
437 if (k != npos) {
438 return k;
439 }
440
441 // check for a wall species
442 size_t offset = m_nsp;
443 for (auto& S : m_surfaces) {
444 ThermoPhase* th = S->thermo();
445 k = th->speciesIndex(nm, false);
446 if (k != npos) {
447 return k + offset;
448 } else {
449 offset += th->nSpecies();
450 }
451 }
452 throw CanteraError("Reactor::speciesIndex",
453 "Species '{}' not found", nm);
454}
455
456size_t Reactor::componentIndex(const string& nm) const
457{
458 if (nm == "mass") {
459 return 0;
460 }
461 if (nm == "volume") {
462 return 1;
463 }
464 if (nm == "int_energy") {
465 return 2;
466 }
467 try {
468 return speciesIndex(nm) + 3;
469 } catch (const CanteraError&) {
470 throw CanteraError("Reactor::componentIndex",
471 "Component '{}' not found", nm);
472 }
473}
474
475string Reactor::componentName(size_t k) {
476 if (k == 0) {
477 return "mass";
478 } else if (k == 1) {
479 return "volume";
480 } else if (k == 2) {
481 return "int_energy";
482 } else if (k >= 3 && k < neq()) {
483 k -= 3;
484 if (k < m_thermo->nSpecies()) {
485 return m_thermo->speciesName(k);
486 } else {
487 k -= m_thermo->nSpecies();
488 }
489 for (auto& S : m_surfaces) {
490 ThermoPhase* th = S->thermo();
491 if (k < th->nSpecies()) {
492 return th->speciesName(k);
493 } else {
494 k -= th->nSpecies();
495 }
496 }
497 }
498 throw IndexError("Reactor::componentName", "component", k, m_nv);
499}
500
501double Reactor::upperBound(size_t k) const {
502 if (k == 0) {
503 return BigNumber; // mass
504 } else if (k == 1) {
505 return BigNumber; // volume
506 } else if (k == 2) {
507 return BigNumber; // internal energy
508 } else if (k >= 3 && k < m_nv) {
509 return 1.0; // species mass fraction or surface coverage
510 } else {
511 throw CanteraError("Reactor::upperBound", "Index {} is out of bounds.", k);
512 }
513}
514
515double Reactor::lowerBound(size_t k) const {
516 if (k == 0) {
517 return 0; // mass
518 } else if (k == 1) {
519 return 0; // volume
520 } else if (k == 2) {
521 return -BigNumber; // internal energy
522 } else if (k >= 3 && k < m_nv) {
523 return -Tiny; // species mass fraction or surface coverage
524 } else {
525 throw CanteraError("Reactor::lowerBound", "Index {} is out of bounds.", k);
526 }
527}
528
529void Reactor::resetBadValues(double* y) {
530 for (size_t k = 3; k < m_nv; k++) {
531 y[k] = std::max(y[k], 0.0);
532 }
533}
534
535void Reactor::applySensitivity(double* params)
536{
537 if (!params) {
538 return;
539 }
540 for (auto& p : m_sensParams) {
541 if (p.type == SensParameterType::reaction) {
542 p.value = m_kin->multiplier(p.local);
543 m_kin->setMultiplier(p.local, p.value*params[p.global]);
544 } else if (p.type == SensParameterType::enthalpy) {
545 m_thermo->modifyOneHf298SS(p.local, p.value + params[p.global]);
546 }
547 }
548 for (auto& S : m_surfaces) {
549 S->setSensitivityParameters(params);
550 }
551 m_thermo->invalidateCache();
552 if (m_kin) {
553 m_kin->invalidateCache();
554 }
555}
556
557void Reactor::resetSensitivity(double* params)
558{
559 if (!params) {
560 return;
561 }
562 for (auto& p : m_sensParams) {
563 if (p.type == SensParameterType::reaction) {
564 m_kin->setMultiplier(p.local, p.value);
565 } else if (p.type == SensParameterType::enthalpy) {
566 m_thermo->resetHf298(p.local);
567 }
568 }
569 for (auto& S : m_surfaces) {
570 S->resetSensitivityParameters();
571 }
572 m_thermo->invalidateCache();
573 if (m_kin) {
574 m_kin->invalidateCache();
575 }
576}
577
578void Reactor::setAdvanceLimits(const double *limits)
579{
580 if (m_thermo == 0) {
581 throw CanteraError("Reactor::setAdvanceLimits",
582 "Error: reactor is empty.");
583 }
584 m_advancelimits.assign(limits, limits + m_nv);
585
586 // resize to zero length if no limits are set
587 if (std::none_of(m_advancelimits.begin(), m_advancelimits.end(),
588 [](double val){return val>0;})) {
589 m_advancelimits.resize(0);
590 }
591}
592
593bool Reactor::getAdvanceLimits(double *limits) const
594{
595 bool has_limit = hasAdvanceLimits();
596 if (has_limit) {
597 std::copy(m_advancelimits.begin(), m_advancelimits.end(), limits);
598 } else {
599 std::fill(limits, limits + m_nv, -1.0);
600 }
601 return has_limit;
602}
603
604void Reactor::setAdvanceLimit(const string& nm, const double limit)
605{
606 size_t k = componentIndex(nm);
607
608 if (m_thermo == 0) {
609 throw CanteraError("Reactor::setAdvanceLimit",
610 "Error: reactor is empty.");
611 }
612 if (m_nv == 0) {
613 if (m_net == 0) {
614 throw CanteraError("Reactor::setAdvanceLimit",
615 "Cannot set limit on a reactor that is not "
616 "assigned to a ReactorNet object.");
617 } else {
618 m_net->initialize();
619 }
620 } else if (k > m_nv) {
621 throw CanteraError("Reactor::setAdvanceLimit",
622 "Index out of bounds.");
623 }
624 m_advancelimits.resize(m_nv, -1.0);
625 m_advancelimits[k] = limit;
626
627 // resize to zero length if no limits are set
628 if (std::none_of(m_advancelimits.begin(), m_advancelimits.end(),
629 [](double val){return val>0;})) {
630 m_advancelimits.resize(0);
631 }
632}
633
634}
Base class for kinetics managers and also contains the kineticsmgr module documentation (see Kinetics...
Header file for class ReactorSurface.
Header for a simple thermodynamics model of a surface phase derived from ThermoPhase,...
Header file for base class WallBase.
A map of string keys to values whose type can vary at runtime.
Definition AnyMap.h:431
Base class for exceptions thrown by Cantera classes.
An array index is out of range.
Public interface for kinetics managers.
Definition Kinetics.h:126
size_t kineticsSpeciesIndex(size_t k, size_t n) const
The location of species k of phase n in species arrays.
Definition Kinetics.h:273
virtual void getNetProductionRates(double *wdot)
Species net production rates [kmol/m^3/s or kmol/m^2/s].
Definition Kinetics.cpp:425
size_t nSpecies() const
Returns the number of species in the phase.
Definition Phase.h:245
size_t speciesIndex(const string &name, bool raise=true) const
Returns the index of a species named 'name' within the Phase object.
Definition Phase.cpp:127
string speciesName(size_t k) const
Name of the species with index k.
Definition Phase.cpp:143
A simple thermodynamic model for a surface phase, assuming an ideal solution model.
Definition SurfPhase.h:114
double size(size_t k) const
Returns the number of sites occupied by one molecule of species k.
Definition SurfPhase.h:237
double siteDensity() const
Returns the site density.
Definition SurfPhase.h:232
Base class for a phase with thermodynamic properties.
Base class for 'walls' (walls, pistons, etc.) connecting reactors.
Definition Wall.h:23
virtual void initialize()
Called just before the start of integration.
Definition Wall.h:63
double dot(InputIter x_begin, InputIter x_end, InputIter2 y_begin)
Function that calculates a templated inner product.
Definition utilities.h:82
const double GasConstant
Universal Gas Constant [J/kmol/K].
Definition ct_defs.h:120
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:180
const double Tiny
Small number to compare differences of mole fractions against.
Definition ct_defs.h:173
offset
Offsets of solution components in the 1D solution array.
Definition Flow1D.h:25
const double BigNumber
largest number to compare to inf.
Definition ct_defs.h:160
Various templated functions that carry out common vector and polynomial operations (see Templated Arr...