1# This file is part of Cantera. See License.txt in the top-level directory or
2# at https://cantera.org/license.txt for license and copyright information.
3
4from ctypes import c_int
5
6# NOTE: These cdef functions cannot be members of Kinetics because they would
7# cause "layout conflicts" when creating derived classes with multiple bases,
8# e.g. class Solution. [Cython 0.16]
9cdef np.ndarray get_species_array(Kinetics kin, kineticsMethod1d method):
10    cdef np.ndarray[np.double_t, ndim=1] data = np.empty(kin.n_total_species)
11    method(kin.kinetics, &data[0])
12    # @TODO: Fix _selected_species to work with interface kinetics
13    if kin._selected_species.size:
14        return data[kin._selected_species]
15    else:
16        return data
17
18cdef np.ndarray get_reaction_array(Kinetics kin, kineticsMethod1d method):
19    cdef np.ndarray[np.double_t, ndim=1] data = np.empty(kin.n_reactions)
20    method(kin.kinetics, &data[0])
21    return data
22
23cdef np.ndarray get_dense(Kinetics kin, kineticsMethodSparse method):
24    cdef CxxSparseMatrix smat = method(kin.kinetics)
25    cdef size_t length = smat.nonZeros()
26    if length == 0:
27        return np.zeros((kin.n_reactions, 0))
28
29    # index/value triplets
30    cdef np.ndarray[int, ndim=1, mode="c"] rows = np.empty(length, dtype=c_int)
31    cdef np.ndarray[int, ndim=1, mode="c"] cols = np.empty(length, dtype=c_int)
32    cdef np.ndarray[np.double_t, ndim=1] data = np.empty(length)
33
34    size = CxxSparseTriplets(smat, &rows[0], &cols[0], &data[0], length)
35    out = np.zeros((smat.rows(), smat.cols()))
36    for i in xrange(length):
37        out[rows[i], cols[i]] = data[i]
38    return out
39
40cdef tuple get_sparse(Kinetics kin, kineticsMethodSparse method):
41    # retrieve sparse matrix
42    cdef CxxSparseMatrix smat = method(kin.kinetics)
43
44    # pointers to values and inner indices of CSC storage
45    cdef size_t length = smat.nonZeros()
46    cdef np.ndarray[np.double_t, ndim=1] value = np.empty(length)
47    cdef np.ndarray[int, ndim=1, mode="c"] inner = np.empty(length, dtype=c_int)
48
49    # pointers outer indices of CSC storage
50    cdef size_t ncols = smat.outerSize()
51    cdef np.ndarray[int, ndim=1, mode="c"] outer = np.empty(ncols + 1, dtype=c_int)
52
53    CxxSparseCscData(smat, &value[0], &inner[0], &outer[0])
54    return value, inner, outer
55
56
57cdef class Kinetics(_SolutionBase):
58    """
59    Instances of class `Kinetics` are responsible for evaluating reaction rates
60    of progress, species production rates, and other quantities pertaining to
61    a reaction mechanism.
62    """
63
64    property kinetics_model:
65        """
66        Return type of kinetics.
67        """
68        def __get__(self):
69            return pystr(self.kinetics.kineticsType())
70
71    property n_total_species:
72        """
73        Total number of species in all phases participating in the kinetics
74        mechanism.
75        """
76        def __get__(self):
77            return self.kinetics.nTotalSpecies()
78
79    property n_reactions:
80        """Number of reactions in the reaction mechanism."""
81        def __get__(self):
82            return self.kinetics.nReactions()
83
84    property n_phases:
85        """Number of phases in the reaction mechanism."""
86        def __get__(self):
87            return self.kinetics.nPhases()
88
89    property reaction_phase_index:
90        """The index of the phase where the reactions occur."""
91        def __get__(self):
92            return self.kinetics.reactionPhaseIndex()
93
94    def _check_phase_index(self, n):
95        if not 0 <= n < self.n_phases:
96            raise ValueError("Phase index ({0}) out of range".format(n))
97
98    def _check_reaction_index(self, n):
99        if not 0 <= n < self.n_reactions:
100            raise ValueError("Reaction index ({0}) out of range".format(n))
101
102    def _check_kinetics_species_index(self, n):
103        if not 0 <= n < self.n_total_species:
104            raise ValueError("Kinetics Species index ({0}) out of range".format(n))
105
106    def kinetics_species_index(self, species, int phase=0):
107        """
108        The index of species *species* of phase *phase* within arrays returned
109        by methods of class `Kinetics`. If *species* is a string, the *phase*
110        argument is unused.
111        """
112        cdef int k
113        if isinstance(species, (str, bytes)):
114            return self.kinetics.kineticsSpeciesIndex(stringify(species))
115        else:
116            k = species
117            self._check_kinetics_species_index(k)
118            self._check_phase_index(k)
119            return self.kinetics.kineticsSpeciesIndex(k, phase)
120
121    def kinetics_species_name(self, k):
122        """
123        Name of the species with index *k* in the arrays returned by methods
124        of class `Kinetics`.
125        """
126        return pystr(self.kinetics.kineticsSpeciesName(k))
127
128    property kinetics_species_names:
129        """
130        A list of all species names, corresponding to the arrays returned by
131        methods of class `Kinetics`.
132        """
133        def __get__(self):
134            return [self.kinetics_species_name(k)
135                    for k in range(self.n_total_species)]
136
137    def reaction(self, int i_reaction):
138        """
139        Return a `Reaction` object representing the reaction with index
140        ``i_reaction``. Changes to this object do not affect the `Kinetics` or
141        `Solution` object until the `modify_reaction` function is called.
142        """
143        return Reaction.wrap(self.kinetics.reaction(i_reaction))
144
145    def reactions(self):
146        """
147        Return a list of all `Reaction` objects. Changes to these objects do not
148        affect the `Kinetics` or `Solution` object until the `modify_reaction`
149        function is called.
150        """
151        return [self.reaction(i) for i in range(self.n_reactions)]
152
153    def modify_reaction(self, int irxn, Reaction rxn):
154        """
155        Modify the `Reaction` with index ``irxn`` to have the same rate
156        parameters as ``rxn``. ``rxn`` must have the same reactants and products
157        and be of the same type (i.e. `ElementaryReaction`, `FalloffReaction`,
158        `PlogReaction`, etc.) as the existing reaction. This method does not
159        modify the third-body efficiencies, reaction orders, or reversibility of
160        the reaction.
161        """
162        self.kinetics.modifyReaction(irxn, rxn._reaction)
163
164    def add_reaction(self, Reaction rxn):
165        """ Add a new reaction to this phase. """
166        self.kinetics.addReaction(rxn._reaction)
167
168    def is_reversible(self, int i_reaction):
169        """True if reaction `i_reaction` is reversible."""
170        self._check_reaction_index(i_reaction)
171        return self.kinetics.isReversible(i_reaction)
172
173    def multiplier(self, int i_reaction):
174        """
175        A scaling factor applied to the rate coefficient for reaction
176        *i_reaction*. Can be used to carry out sensitivity analysis or to
177        selectively disable a particular reaction. See `set_multiplier`.
178        """
179        self._check_reaction_index(i_reaction)
180        return self.kinetics.multiplier(i_reaction)
181
182    def set_multiplier(self, double value, int i_reaction=-1):
183        """
184        Set the multiplier for for reaction *i_reaction* to *value*.
185        If *i_reaction* is not specified, then the multiplier for all reactions
186        is set to *value*. See `multiplier`.
187        """
188        if i_reaction == -1:
189            for i_reaction in range(self.n_reactions):
190                self.kinetics.setMultiplier(i_reaction, value)
191        else:
192            self._check_reaction_index(i_reaction)
193            self.kinetics.setMultiplier(i_reaction, value)
194
195    def reaction_type(self, int i_reaction):
196        """
197        Type code of reaction *i_reaction*.
198
199        .. deprecated:: 2.6
200        """
201        warnings.warn("Behavior changes after Cantera 2.6, "
202                      "when function will return a type string. "
203                      "Use method 'reaction_type_str' during "
204                      "transition instead.", DeprecationWarning)
205        self._check_reaction_index(i_reaction)
206        return self.kinetics.reactionType(i_reaction)
207
208    def reaction_type_str(self, int i_reaction):
209        """Type of reaction *i_reaction*."""
210        self._check_reaction_index(i_reaction)
211        return pystr(self.kinetics.reactionTypeStr(i_reaction))
212
213    def reaction_equation(self, int i_reaction):
214        """The equation for the specified reaction. See also `reaction_equations`."""
215        self._check_reaction_index(i_reaction)
216        return pystr(self.kinetics.reactionString(i_reaction))
217
218    def reactants(self, int i_reaction):
219        """The reactants portion of the reaction equation"""
220        self._check_reaction_index(i_reaction)
221        return pystr(self.kinetics.reactantString(i_reaction))
222
223    def products(self, int i_reaction):
224        """The products portion of the reaction equation"""
225        self._check_reaction_index(i_reaction)
226        return pystr(self.kinetics.productString(i_reaction))
227
228    def reaction_equations(self, indices=None):
229        """
230        Returns a list containing the reaction equation for all reactions in the
231        mechanism (if *indices* is unspecified) or the equations for each
232        reaction in the sequence *indices*. For example::
233
234            >>> gas.reaction_equations()
235            ['2 O + M <=> O2 + M', 'O + H + M <=> OH + M', 'O + H2 <=> H + OH', ...]
236            >>> gas.reaction_equations([2,3])
237            ['O + H + M <=> OH + M', 'O + H2 <=> H + OH']
238
239        See also `reaction_equation`.
240        """
241        if indices is None:
242            return [self.reaction_equation(i) for i in range(self.n_reactions)]
243        else:
244            return [self.reaction_equation(i) for i in indices]
245
246    def reactant_stoich_coeff(self, k_spec, int i_reaction):
247        """
248        The stoichiometric coefficient of species ``k_spec`` as a reactant in
249        reaction ``i_reaction``.
250        """
251        cdef int k
252        if isinstance(k_spec, (str, bytes)):
253            k = self.kinetics_species_index(k_spec)
254        else:
255            k = k_spec
256            self._check_kinetics_species_index(k_spec)
257
258        self._check_reaction_index(i_reaction)
259        return self.kinetics.reactantStoichCoeff(k, i_reaction)
260
261    def product_stoich_coeff(self, k_spec, int i_reaction):
262        """
263        The stoichiometric coefficient of species ``k_spec`` as a product in
264        reaction ``i_reaction``.
265        """
266        cdef int k
267        if isinstance(k_spec, (str, bytes)):
268            k = self.kinetics_species_index(k_spec)
269        else:
270            k = k_spec
271            self._check_kinetics_species_index(k_spec)
272
273        self._check_reaction_index(i_reaction)
274        return self.kinetics.productStoichCoeff(k, i_reaction)
275
276    def reactant_stoich_coeffs(self):
277        """
278        The array of reactant stoichiometric coefficients. Element *[k,i]* of
279        this array is the reactant stoichiometric coefficient of species *k* in
280        reaction *i*.
281
282        .. deprecated:: 2.6
283
284            Behavior to change after Cantera 2.6; for new behavior, see property
285            `Kinetics.reactant_stoich_coeffs3`.
286        """
287        warnings.warn("Behavior to change after Cantera 2.6; for new behavior, see "
288                      "property 'reactant_stoich_coeffs3'.", DeprecationWarning)
289        return self.reactant_stoich_coeffs3
290
291    property reactant_stoich_coeffs3:
292        """
293        The array of reactant stoichiometric coefficients. Element ``[k,i]`` of
294        this array is the reactant stoichiometric coefficient of species ``k`` in
295        reaction ``i``.
296
297        For sparse output, set ``ct.use_sparse(True)``.
298        """
299        def __get__(self):
300            if __use_sparse__:
301                tup = get_sparse(self, kin_reactantStoichCoeffs)
302                shape = self.n_total_species, self.n_reactions
303                return _scipy_sparse.csc_matrix(tup, shape=shape)
304            return get_dense(self, kin_reactantStoichCoeffs)
305
306    def product_stoich_coeffs(self):
307        """
308        The array of product stoichiometric coefficients. Element *[k,i]* of
309        this array is the product stoichiometric coefficient of species *k* in
310        reaction *i*.
311
312        .. deprecated:: 2.6
313
314            Behavior to change after Cantera 2.6; for new behavior, see property
315            `Kinetics.reactant_stoich_coeffs3`.
316        """
317        warnings.warn("Behavior to change after Cantera 2.6; for new behavior, see "
318                      "property 'product_stoich_coeffs3'.", DeprecationWarning)
319        return self.product_stoich_coeffs3
320
321    property product_stoich_coeffs3:
322        """
323        The array of product stoichiometric coefficients. Element ``[k,i]`` of
324        this array is the product stoichiometric coefficient of species ``k`` in
325        reaction ``i``.
326
327        For sparse output, set ``ct.use_sparse(True)``.
328        """
329        def __get__(self):
330            if __use_sparse__:
331                tup = get_sparse(self, kin_productStoichCoeffs)
332                shape = self.n_total_species, self.n_reactions
333                return _scipy_sparse.csc_matrix(tup, shape=shape)
334            return get_dense(self, kin_productStoichCoeffs)
335
336    property product_stoich_coeffs_reversible:
337        """
338        The array of product stoichiometric coefficients of reversible reactions.
339        Element ``[k,i]`` of this array is the product stoichiometric coefficient
340        of species ``k`` in reaction ``i``.
341
342        For sparse output, set ``ct.use_sparse(True)``.
343        """
344        def __get__(self):
345            if __use_sparse__:
346                tup = get_sparse(self, kin_revProductStoichCoeffs)
347                shape = self.n_total_species, self.n_reactions
348                return _scipy_sparse.csc_matrix(tup, shape=shape)
349            return get_dense(self, kin_revProductStoichCoeffs)
350
351    property forward_rates_of_progress:
352        """
353        Forward rates of progress for the reactions. [kmol/m^3/s] for bulk
354        phases or [kmol/m^2/s] for surface phases.
355        """
356        def __get__(self):
357            return get_reaction_array(self, kin_getFwdRatesOfProgress)
358
359    property reverse_rates_of_progress:
360        """
361        Reverse rates of progress for the reactions. [kmol/m^3/s] for bulk
362        phases or [kmol/m^2/s] for surface phases.
363        """
364        def __get__(self):
365            return get_reaction_array(self, kin_getRevRatesOfProgress)
366
367    property net_rates_of_progress:
368        """
369        Net rates of progress for the reactions. [kmol/m^3/s] for bulk phases
370        or [kmol/m^2/s] for surface phases.
371        """
372        def __get__(self):
373            return get_reaction_array(self, kin_getNetRatesOfProgress)
374
375    property equilibrium_constants:
376        """Equilibrium constants in concentration units for all reactions."""
377        def __get__(self):
378            return get_reaction_array(self, kin_getEquilibriumConstants)
379
380    property forward_rate_constants:
381        """
382        Forward rate constants for all reactions. The computed values include
383        all temperature-dependent, pressure-dependent, and third body
384        contributions. Units are a combination of kmol, m^3 and s, that depend
385        on the rate expression for the reaction.
386
387        .. deprecated:: 2.6
388
389            Behavior to change after Cantera 2.6; for Cantera 2.6, rate constants of
390            three-body reactions are multiplied with third-body concentrations
391            (no change to legacy behavior). After Cantera 2.6, results will no longer
392            include third-body concentrations and be consistent with conventional
393            definitions (see Eq. 9.75 in Kee, Coltrin and Glarborg, 'Chemically
394            Reacting Flow', Wiley Interscience, 2003).
395            To switch to new behavior, run 'cantera.use_legacy_rate_constants(False)'.
396        """
397        def __get__(self):
398            return get_reaction_array(self, kin_getFwdRateConstants)
399
400    property reverse_rate_constants:
401        """
402        Reverse rate constants for all reactions. The computed values include
403        all temperature-dependent, pressure-dependent, and third body
404        contributions. Units are a combination of kmol, m^3 and s, that depend
405        on the rate expression for the reaction.
406
407        .. deprecated:: 2.6
408
409            Behavior to change after Cantera 2.6; for Cantera 2.6, rate constants of
410            three-body reactions are multiplied with third-body concentrations
411            (no change to legacy behavior). After Cantera 2.6, results will no longer
412            include third-body concentrations and be consistent with conventional
413            definitions (see Eq. 9.75 in Kee, Coltrin and Glarborg, 'Chemically
414            Reacting Flow', Wiley Interscience, 2003).
415            To switch to new behavior, run 'cantera.use_legacy_rate_constants(False)'.
416        """
417        def __get__(self):
418            return get_reaction_array(self, kin_getRevRateConstants)
419
420    property creation_rates:
421        """
422        Creation rates for each species. [kmol/m^3/s] for bulk phases or
423        [kmol/m^2/s] for surface phases.
424        """
425        def __get__(self):
426            return get_species_array(self, kin_getCreationRates)
427
428    property destruction_rates:
429        """
430        Destruction rates for each species. [kmol/m^3/s] for bulk phases or
431        [kmol/m^2/s] for surface phases.
432        """
433        def __get__(self):
434            return get_species_array(self, kin_getDestructionRates)
435
436    property net_production_rates:
437        """
438        Net production rates for each species. [kmol/m^3/s] for bulk phases or
439        [kmol/m^2/s] for surface phases.
440        """
441        def __get__(self):
442            return get_species_array(self, kin_getNetProductionRates)
443
444    property delta_enthalpy:
445        """Change in enthalpy for each reaction [J/kmol]."""
446        def __get__(self):
447            return get_reaction_array(self, kin_getDeltaEnthalpy)
448
449    property delta_gibbs:
450        """Change in Gibbs free energy for each reaction [J/kmol]."""
451        def __get__(self):
452            return get_reaction_array(self, kin_getDeltaGibbs)
453
454    property delta_entropy:
455        """Change in entropy for each reaction [J/kmol/K]."""
456        def __get__(self):
457            return get_reaction_array(self, kin_getDeltaEntropy)
458
459    property delta_standard_enthalpy:
460        """
461        Change in standard-state enthalpy (independent of composition) for
462        each reaction [J/kmol].
463        """
464        def __get__(self):
465            return get_reaction_array(self, kin_getDeltaSSEnthalpy)
466
467    property delta_standard_gibbs:
468        """
469        Change in standard-state Gibbs free energy (independent of composition)
470        for each reaction [J/kmol].
471        """
472        def __get__(self):
473            return get_reaction_array(self, kin_getDeltaSSGibbs)
474
475    property delta_standard_entropy:
476        """
477        Change in standard-state entropy (independent of composition) for
478        each reaction [J/kmol/K].
479        """
480        def __get__(self):
481            return get_reaction_array(self, kin_getDeltaSSEntropy)
482
483    property heat_release_rate:
484        """
485        Get the total volumetric heat release rate [W/m^3].
486        """
487        def __get__(self):
488            return - np.sum(self.partial_molar_enthalpies *
489                            self.net_production_rates, 0)
490
491    property heat_production_rates:
492        """
493        Get the volumetric heat production rates [W/m^3] on a per-reaction
494        basis. The sum over all reactions results in the total volumetric heat
495        release rate.
496        Example: C. K. Law: Combustion Physics (2006), Fig. 7.8.6
497
498        >>> gas.heat_production_rates[1]  # heat production rate of the 2nd reaction
499        """
500        def __get__(self):
501            return - self.net_rates_of_progress * self.delta_enthalpy
502
503
504cdef class InterfaceKinetics(Kinetics):
505    """
506    A kinetics manager for heterogeneous reaction mechanisms. The
507    reactions are assumed to occur at an interface between bulk phases.
508    """
509    def __init__(self, infile='', name='', adjacent=(), *args, **kwargs):
510        super().__init__(infile, name, adjacent, *args, **kwargs)
511        if pystr(self.kinetics.kineticsType()) not in ("Surf", "Edge"):
512            raise TypeError("Underlying Kinetics class is not of the correct type.")
513
514        self._phase_indices = {}
515        for phase in [self] + list(adjacent):
516            i = self.kinetics.phaseIndex(stringify(phase.name))
517            self._phase_indices[phase] = i
518            self._phase_indices[phase.name] = i
519            self._phase_indices[i] = i
520
521    def advance_coverages(self, double dt, double rtol=1e-7, double atol=1e-14,
522                          double max_step_size=0, int max_steps=20000,
523                          int max_error_test_failures=7):
524        """
525        This method carries out a time-accurate advancement of the surface
526        coverages for a specified amount of time.
527        """
528        (<CxxInterfaceKinetics*>self.kinetics).advanceCoverages(
529            dt, rtol, atol, max_step_size, max_steps, max_error_test_failures)
530
531    def advance_coverages_to_steady_state(self):
532        """
533        This method advances the surface coverages to steady state.
534        """
535        (<CxxInterfaceKinetics*>self.kinetics).solvePseudoSteadyStateProblem()
536
537    def phase_index(self, phase):
538        """
539        Get the index of the phase *phase*, where *phase* may specified using
540        the phase object, the name, or the index itself.
541        """
542        return self._phase_indices[phase]
543
544    def _phase_slice(self, phase):
545        p = self.phase_index(phase)
546        k1 = self.kinetics_species_index(0, p)
547
548        if p == self.n_phases - 1:
549            k2 = self.n_total_species
550        else:
551            k2 = self.kinetics_species_index(0, p+1)
552
553        return slice(k1,k2)
554
555    def get_creation_rates(self, phase):
556        """
557        Creation rates for each species in phase *phase*. Use the
558        `creation_rates` property to get the creation rates for species in all
559        phases.
560        """
561        return self.creation_rates[self._phase_slice(phase)]
562
563    def get_destruction_rates(self, phase):
564        """
565        Destruction rates for each species in phase *phase*. Use the
566        `destruction_rates` property to get the destruction rates for species
567        in all phases.
568        """
569        return self.destruction_rates[self._phase_slice(phase)]
570
571    def get_net_production_rates(self, phase):
572        """
573        Net production rates for each species in phase *phase*. Use the
574        `net_production_rates` property to get the net_production rates for
575        species in all phases.
576        """
577        return self.net_production_rates[self._phase_slice(phase)]
578
579    def write_yaml(self, filename, phases=None, units=None, precision=None,
580                   skip_user_defined=None):
581        """
582        See `_SolutionBase.write_yaml`.
583        """
584        if phases is not None:
585            phases = list(phases)
586        else:
587            phases = []
588
589        for phase in self._phase_indices:
590            if isinstance(phase, _SolutionBase) and phase is not self:
591                phases.append(phase)
592
593        super().write_yaml(filename, phases, units, precision, skip_user_defined)
594