1from math import exp
2from pathlib import Path
3import textwrap
4
5import cantera as ct
6import numpy as np
7from . import utilities
8
9
10class TestImplicitThirdBody(utilities.CanteraTest):
11    # tests for three-body reactions with specified collision partner
12
13    @classmethod
14    def setUpClass(cls):
15        utilities.CanteraTest.setUpClass()
16        cls.gas = ct.Solution("gri30.yaml")
17
18    def test_implicit_three_body(self):
19        # check equivalency of auto-detected and explicit specification
20        yaml1 = """
21            equation: H + 2 O2 <=> HO2 + O2
22            rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
23            """
24        rxn1 = ct.Reaction.from_yaml(yaml1, self.gas)
25        self.assertEqual(rxn1.reaction_type, "three-body")
26        self.assertEqual(rxn1.default_efficiency, 0.)
27        self.assertEqual(rxn1.efficiencies, {"O2": 1})
28
29        yaml2 = """
30            equation: H + O2 + M <=> HO2 + M
31            rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
32            type: three-body
33            default-efficiency: 0
34            efficiencies: {O2: 1.0}
35            """
36        rxn2 = ct.Reaction.from_yaml(yaml2, self.gas)
37        self.assertEqual(rxn1.efficiencies, rxn2.efficiencies)
38        self.assertEqual(rxn1.default_efficiency, rxn2.default_efficiency)
39
40    def test_duplicate(self):
41        # @todo simplify this test
42        #     duplicates are currently only checked for import from file
43        gas1 = ct.Solution(thermo="IdealGas", kinetics="GasKinetics",
44                           species=self.gas.species(), reactions=[])
45
46        yaml1 = """
47            equation: H + O2 + H2O <=> HO2 + H2O
48            rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0}
49            """
50        rxn1 = ct.Reaction.from_yaml(yaml1, gas1)
51
52        yaml2 = """
53            equation: H + O2 + M <=> HO2 + M
54            rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0}
55            type: three-body
56            default-efficiency: 0
57            efficiencies: {H2O: 1}
58            """
59        rxn2 = ct.Reaction.from_yaml(yaml2, gas1)
60
61        self.assertEqual(rxn1.reaction_type, rxn2.reaction_type)
62        self.assertEqual(rxn1.reactants, rxn2.reactants)
63        self.assertEqual(rxn1.products, rxn2.products)
64        self.assertEqual(rxn1.efficiencies, rxn2.efficiencies)
65        self.assertEqual(rxn1.default_efficiency, rxn2.default_efficiency)
66
67        gas1.add_reaction(rxn1)
68        gas1.add_reaction(rxn2)
69
70        fname = "duplicate.yaml"
71        gas1.write_yaml(fname)
72
73        with self.assertRaisesRegex(Exception, "Undeclared duplicate reactions"):
74            gas2 = ct.Solution(fname)
75
76        Path(fname).unlink()
77
78    def test_short_serialization(self):
79        # check that serialized output is compact
80        yaml = """
81            equation: H + O2 + H2O <=> HO2 + H2O
82            rate-constant: {A: 1.126e+19, b: -0.76, Ea: 0.0}
83            """
84        rxn = ct.Reaction.from_yaml(yaml, self.gas)
85        input_data = rxn.input_data
86
87        self.assertNotIn("type", input_data)
88        self.assertNotIn("default-efficiency", input_data)
89        self.assertNotIn("efficiencies", input_data)
90
91    def test_non_integer_stoich(self):
92        # check that non-integer coefficients prevent automatic conversion
93        yaml = """
94            equation: 2 H + 1.5 O2 <=> H2O + O2
95            rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
96            """
97        rxn = ct.Reaction.from_yaml(yaml, self.gas)
98        self.assertEqual(rxn.reaction_type, "elementary")
99
100    def test_not_three_body(self):
101        # check that insufficient reactants prevent automatic conversion
102        yaml = """
103            equation: HCNO + H <=> H + HNCO  # Reaction 270
104            rate-constant: {A: 2.1e+15, b: -0.69, Ea: 2850.0}
105            """
106        rxn = ct.Reaction.from_yaml(yaml, self.gas)
107        self.assertEqual(rxn.reaction_type, "elementary")
108
109    def test_user_override(self):
110        # check that type specification prevents automatic conversion
111        yaml = """
112            equation: H + 2 O2 <=> HO2 + O2
113            rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
114            type: elementary
115            """
116        rxn = ct.Reaction.from_yaml(yaml, self.gas)
117        self.assertEqual(rxn.reaction_type, "elementary")
118
119
120class ReactionRateTests:
121    # test suite for reaction rate expressions
122
123    _cls = None # reaction rate object to be tested
124    _type = None # name of reaction rate
125    _uses_pressure = False # rate evaluation requires pressure
126    _index = None # index of reaction in "kineticsfromscratch.yaml"
127    _input = None # input parameters (dict corresponding to YAML)
128    _yaml = None # yaml string specifying parameters
129
130    @classmethod
131    def setUpClass(cls):
132        utilities.CanteraTest.setUpClass()
133        cls.gas = ct.Solution("kineticsfromscratch.yaml")
134        cls.gas.X = "H2:0.1, H2O:0.2, O2:0.7, O:1e-4, OH:1e-5, H:2e-5"
135        cls.gas.TP = 900, 2*ct.one_atm
136
137    def test_type(self):
138        # check reaction type
139        self.assertIn(self._cls.__name__, "{}".format(self.rate))
140
141    def test_rate_T(self):
142        # check evaluation as a function of temperature only
143        if self._uses_pressure:
144            with self.assertRaisesRegex(ct.CanteraError, "reaction type requires pressure"):
145                self.assertNear(self.rate(self.gas.T),
146                                self.gas.forward_rate_constants[self._index])
147        else:
148            self.assertNear(self.rate(self.gas.T),
149                            self.gas.forward_rate_constants[self._index])
150
151    def test_rate_TP(self):
152        # check evaluation as a function of temperature and pressure
153        self.assertNear(self.rate(self.gas.T, self.gas.P),
154                        self.gas.forward_rate_constants[self._index])
155
156    def test_input(self):
157        # check instantiation based on input_data
158        rate = self._cls(input_data=self._input)
159        self.assertIn(self._cls.__name__, "{}".format(rate))
160        self.assertNear(rate(self.gas.T, self.gas.P),
161                        self.rate(self.gas.T, self.gas.P))
162
163    def test_unconfigured(self):
164        # check behavior of unconfigured rate object
165        rate = self._cls(input_data={})
166        self.assertTrue(np.isnan(rate(self.gas.T, self.gas.P)))
167        input_data = rate.input_data
168        self.assertIsInstance(input_data, dict)
169        if input_data:
170            self.assertEqual(input_data, {"type": self._type})
171
172    def test_roundtrip(self):
173        # check round-trip instantiation via input_data
174        input_data = self.rate.input_data
175        rate = self._cls(input_data=input_data)
176        self.assertNear(rate(self.gas.T, self.gas.P),
177                        self.rate(self.gas.T, self.gas.P))
178
179    def test_from_dict(self):
180        # check instantiation from dictionary
181        input_data = self.rate.input_data
182        rate = ct.ReactionRate.from_dict(input_data)
183        self.assertNear(rate(self.gas.T, self.gas.P),
184                        self.rate(self.gas.T, self.gas.P))
185
186    def test_from_yaml(self):
187        # check instantiation from yaml string
188        rate = ct.ReactionRate.from_yaml(self._yaml)
189        self.assertNear(rate(self.gas.T, self.gas.P),
190                        self.rate(self.gas.T, self.gas.P))
191
192    def test_with_units(self):
193        units = "units: {length: cm, quantity: mol}"
194        yaml = f"{textwrap.dedent(self._yaml)}\n{units}"
195        with self.assertRaisesRegex(Exception, "not supported"):
196            rate = ct.ReactionRate.from_yaml(yaml)
197
198
199class TestArrheniusRate(ReactionRateTests, utilities.CanteraTest):
200    # test Arrhenius rate expressions
201
202    _cls = ct.ArrheniusRate
203    _type = "Arrhenius"
204    _uses_pressure = False
205    _index = 0
206    _input = {"rate-constant": {"A": 38.7, "b": 2.7, "Ea": 26191840.0}}
207    _yaml = "rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol}"
208
209    def setUp(self):
210        self.A = self.gas.reaction(self._index).rate.pre_exponential_factor
211        self.b = self.gas.reaction(self._index).rate.temperature_exponent
212        self.Ea = self.gas.reaction(self._index).rate.activation_energy
213        self.rate = ct.ArrheniusRate(self.A, self.b, self.Ea)
214
215    def test_parameters(self):
216        # test reaction rate parameters
217        self.assertEqual(self.A, self.rate.pre_exponential_factor)
218        self.assertEqual(self.b, self.rate.temperature_exponent)
219        self.assertEqual(self.Ea, self.rate.activation_energy)
220
221    def test_allow_negative_pre_exponential_factor(self):
222        # test reaction rate property
223        self.assertFalse(self.rate.allow_negative_pre_exponential_factor)
224        self.rate.allow_negative_pre_exponential_factor = True
225        self.assertTrue(self.rate.allow_negative_pre_exponential_factor)
226
227    def test_standalone(self):
228        yaml = "rate-constant: {A: 4.0e+21 cm^6/mol^2/s, b: 0.0, Ea: 1207.72688}"
229        with self.assertRaisesRegex(Exception, "not supported"):
230            rate = ct.ReactionRate.from_yaml(yaml)
231
232
233class TestPlogRate(ReactionRateTests, utilities.CanteraTest):
234    # test Plog rate expressions
235
236    _cls = ct.PlogRate
237    _type = "pressure-dependent-Arrhenius"
238    _uses_pressure = True
239    _index = 3
240    _input = {"rate-constants": [
241        {"P": 1013.25, "A": 1.2124e+16, "b": -0.5779, "Ea": 45491376.8},
242        {"P": 101325., "A": 4.9108e+31, "b": -4.8507, "Ea": 103649395.2},
243        {"P": 1013250., "A": 1.2866e+47, "b": -9.0246, "Ea": 166508556.0},
244        {"P": 10132500., "A": 5.9632e+56, "b": -11.529, "Ea": 220076726.4}]}
245    _yaml = """
246        type: pressure-dependent-Arrhenius
247        rate-constants:
248        - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04 cal/mol}
249        - {P: 1.0 atm, A: 4.9108e+31, b: -4.8507, Ea: 2.47728e+04 cal/mol}
250        - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04 cal/mol}
251        - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04 cal/mol}
252        """
253
254    def setUp(self):
255        self.rate = ct.PlogRate([(1013.25, ct.Arrhenius(1.2124e+16, -0.5779, 45491376.8)),
256                                (101325., ct.Arrhenius(4.9108e+31, -4.8507, 103649395.2)),
257                                (1013250., ct.Arrhenius(1.2866e+47, -9.0246, 166508556.0)),
258                                (10132500., ct.Arrhenius(5.9632e+56, -11.529, 220076726.4))])
259
260    def test_get_rates(self):
261        # test getter for property rates
262        rates = self.rate.rates
263        self.assertIsInstance(rates, list)
264
265        other = self._input["rate-constants"]
266        self.assertEqual(len(rates), len(other))
267        for index, item in enumerate(rates):
268            P, rate = item
269            self.assertNear(P, other[index]["P"])
270            self.assertNear(rate.pre_exponential_factor, other[index]["A"])
271            self.assertNear(rate.temperature_exponent, other[index]["b"])
272            self.assertNear(rate.activation_energy, other[index]["Ea"])
273
274    def test_set_rates(self):
275        # test setter for property rates
276        other = [
277            {"P": 100., "A": 1.2124e+16, "b": -1., "Ea": 45491376.8},
278            {"P": 10000., "A": 4.9108e+31, "b": -2., "Ea": 103649395.2},
279            {"P": 1000000., "A": 1.2866e+47, "b": -3., "Ea": 166508556.0}]
280        rate = ct.PlogRate([(o["P"], ct.Arrhenius(o["A"], o["b"], o["Ea"]))
281                            for o in other])
282        rates = rate.rates
283        self.assertEqual(len(rates), len(other))
284
285        for index, item in enumerate(rates):
286            P, rate = item
287            self.assertNear(P, other[index]["P"])
288            self.assertNear(rate.pre_exponential_factor, other[index]["A"])
289            self.assertNear(rate.temperature_exponent, other[index]["b"])
290            self.assertNear(rate.activation_energy, other[index]["Ea"])
291
292    def test_no_rates(self):
293        # test instantiation of empty rate
294        rate = ct.PlogRate()
295        self.assertIsInstance(rate.rates, list)
296
297    def test_standalone(self):
298        yaml = """
299            type: pressure-dependent-Arrhenius
300            rate-constants:
301            - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04 cal/mol}
302            - {P: 1.0 atm, A: 4.9108e+31 cm^6/mol^2/s, b: -4.8507, Ea: 2.47728e+04 cal/mol}
303            - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04 cal/mol}
304            - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04 cal/mol}
305            """
306        with self.assertRaisesRegex(Exception, "not supported"):
307            rate = ct.ReactionRate.from_yaml(yaml)
308
309
310class TestChebyshevRate(ReactionRateTests, utilities.CanteraTest):
311    # test Chebyshev rate expressions
312
313    _cls = ct.ChebyshevRate
314    _type = "Chebyshev"
315    _uses_pressure = True
316    _index = 4
317    _input = {"data": [[8.2883, -1.1397, -0.12059, 0.016034],
318                       [1.9764, 1.0037, 0.0072865, -0.030432],
319                       [0.3177, 0.26889, 0.094806, -0.0076385]],
320              "pressure-range": [1000.0, 10000000.0],
321              "temperature-range": [290.0, 3000.0]}
322    _yaml = """
323        type: Chebyshev
324        temperature-range: [290.0, 3000.0]
325        pressure-range: [9.869232667160128e-03 atm, 98.69232667160128 atm]
326        data:
327        - [8.2883, -1.1397, -0.12059, 0.016034]
328        - [1.9764, 1.0037, 7.2865e-03, -0.030432]
329        - [0.3177, 0.26889, 0.094806, -7.6385e-03]
330        """
331
332    def setUp(self):
333        self.Trange = self.gas.reaction(self._index).rate.temperature_range
334        self.Prange = self.gas.reaction(self._index).rate.pressure_range
335        self.data = self.gas.reaction(self._index).rate.data
336        self.rate = ct.ChebyshevRate(self.Trange, self.Prange, self.data)
337
338    def test_parameters(self):
339        # test getters for rate properties
340        self.assertEqual(self.Trange[0], self.rate.temperature_range[0])
341        self.assertEqual(self.Trange[1], self.rate.temperature_range[1])
342        self.assertEqual(self.Prange[0], self.rate.pressure_range[0])
343        self.assertEqual(self.Prange[1], self.rate.pressure_range[1])
344        self.assertTrue(np.all(self.data == self.rate.data))
345
346
347class ReactionTests:
348    # test suite for reaction expressions
349
350    _cls = None # reaction object to be tested
351    _type = None # name of reaction rate
352    _legacy = False # object uses legacy framework
353    _equation = None # reaction equation string
354    _rate = None # parameters for reaction rate object constructor
355    _rate_obj = None # reaction rate object
356    _kwargs = {} # additional parameters required by contructor
357    _index = None # index of reaction in "kineticsfromscratch.yaml"
358    _yaml = None # YAML parameterization
359    _input = None # input parameters (dict corresponding to YAML)
360    _deprecated_getters = {} # test legacy getters (old framework)
361    _deprecated_setters = {} # test legacy setters (old framework)
362    _deprecated_callers = {} # test legacy callers (old framework)
363
364    @classmethod
365    def setUpClass(cls):
366        utilities.CanteraTest.setUpClass()
367        cls.gas = ct.Solution("kineticsfromscratch.yaml", transport_model=None)
368        cls.gas.X = "H2:0.1, H2O:0.2, O2:0.7, O:1e-4, OH:1e-5, H:2e-5"
369        cls.gas.TP = 900, 2*ct.one_atm
370        cls.species = cls.gas.species()
371
372    def check_rxn(self, rxn, check_legacy=True):
373        # helper function that checks reaction configuration
374        ix = self._index
375        self.assertEqual(rxn.reactants, self.gas.reaction(ix).reactants)
376        self.assertEqual(rxn.products, self.gas.reaction(ix).products)
377        if check_legacy:
378            self.assertEqual(rxn.reaction_type, self._type)
379            self.assertEqual(rxn.uses_legacy, self._type.endswith("-legacy"))
380            self.assertEqual(rxn.uses_legacy, self._legacy)
381
382        gas2 = ct.Solution(thermo="IdealGas", kinetics="GasKinetics",
383                           species=self.species, reactions=[rxn])
384        gas2.TPX = self.gas.TPX
385        self.check_solution(gas2, check_legacy)
386
387    def check_solution(self, gas2, check_legacy=True):
388        # helper function that checks evaluation of reaction rates
389        ix = self._index
390        if check_legacy:
391            self.assertEqual(gas2.reaction_type_str(0), self._type)
392        self.assertNear(gas2.forward_rate_constants[0],
393                        self.gas.forward_rate_constants[ix])
394        self.assertNear(gas2.net_rates_of_progress[0],
395                        self.gas.net_rates_of_progress[ix])
396
397    def test_rate(self):
398        # check consistency of reaction rate and forward rate constant
399        if self._rate_obj is None:
400            return
401        if self._legacy:
402            self.assertNear(self._rate_obj(self.gas.T), self.gas.forward_rate_constants[self._index])
403        else:
404            self.assertNear(self._rate_obj(self.gas.T, self.gas.P),
405                            self.gas.forward_rate_constants[self._index])
406
407    def test_from_parts(self):
408        # check instantiation from parts (reactants, products, rate expression)
409        if not self._rate_obj:
410            return
411        orig = self.gas.reaction(self._index)
412        rxn = self._cls(orig.reactants, orig.products, legacy=self._legacy)
413        rxn.rate = self._rate_obj
414        self.check_rxn(rxn)
415
416    def test_from_dict1(self):
417        # check instantiation from keywords / rate defined by dictionary
418        rxn = self._cls(equation=self._equation, rate=self._rate, kinetics=self.gas,
419                        legacy=self._legacy, **self._kwargs)
420        self.check_rxn(rxn)
421
422    def test_from_yaml(self):
423        # check instantiation from yaml string
424        if self._yaml is None:
425            return
426        rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
427        self.check_rxn(rxn)
428
429    def test_deprecated_yaml(self):
430        # check instantiation from yaml string
431        if self._yaml is None:
432            return
433        with self.assertWarnsRegex(DeprecationWarning, "is renamed to 'from_yaml'"):
434            rxn = ct.Reaction.fromYaml(self._yaml, kinetics=self.gas)
435        self.check_rxn(rxn)
436
437    def test_from_dict2(self):
438        # check instantiation from a yaml dictionary
439        if self._yaml is None:
440            return
441        rxn1 = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
442        input_data = rxn1.input_data
443        rxn2 = ct.Reaction.from_dict(input_data, kinetics=self.gas)
444        # cannot compare types as input_data does not recreate legacy objects
445        self.check_rxn(rxn2, check_legacy=False)
446
447    def test_from_rate(self):
448        # check instantiation from keywords / rate provided as object
449        if self._rate_obj is None:
450            return
451        rxn = self._cls(equation=self._equation, rate=self._rate_obj, kinetics=self.gas,
452                        legacy=self._legacy, **self._kwargs)
453        self.check_rxn(rxn)
454
455    def test_add_rxn(self):
456        # check adding new reaction to solution
457        if self._rate_obj is None:
458            return
459        gas2 = ct.Solution(thermo="IdealGas", kinetics="GasKinetics",
460                           species=self.species, reactions=[])
461        gas2.TPX = self.gas.TPX
462
463        rxn = self._cls(equation=self._equation, rate=self._rate_obj, kinetics=self.gas,
464                        legacy=self._legacy, **self._kwargs)
465        gas2.add_reaction(rxn)
466        self.check_solution(gas2)
467
468    def test_raises_invalid_rate(self):
469        # check exception for instantiation from keywords / invalid rate
470        with self.assertRaises(TypeError):
471            rxn = self._cls(equation=self._equation, rate=(), kinetics=self.gas,
472                            legacy=self._legacy, **self._kwargs)
473
474    def test_no_rate(self):
475        # check behavior for instantiation from keywords / no rate
476        if self._rate_obj is None:
477            return
478        rxn = self._cls(equation=self._equation, kinetics=self.gas,
479                        legacy=self._legacy, **self._kwargs)
480        if self._legacy:
481            self.assertNear(rxn.rate(self.gas.T), 0.)
482        else:
483            self.assertTrue(np.isnan(rxn.rate(self.gas.T, self.gas.P)))
484
485        gas2 = ct.Solution(thermo="IdealGas", kinetics="GasKinetics",
486                           species=self.species, reactions=[rxn])
487        gas2.TPX = self.gas.TPX
488
489        self.assertNear(gas2.forward_rate_constants[0], 0.)
490        self.assertNear(gas2.net_rates_of_progress[0], 0.)
491
492    def test_replace_rate(self):
493        # check replacing reaction rate expression
494        if self._rate_obj is None:
495            return
496        rxn = self._cls(equation=self._equation, kinetics=self.gas,
497                        legacy=self._legacy, **self._kwargs)
498        rxn.rate = self._rate_obj
499        self.check_rxn(rxn)
500
501    def test_roundtrip(self):
502        # check round-trip instantiation via input_data
503        if self._legacy:
504            return
505        rxn = self._cls(equation=self._equation, rate=self._rate_obj, kinetics=self.gas,
506                        legacy=self._legacy, **self._kwargs)
507        input_data = rxn.rate.input_data
508        rate_obj = rxn.rate.__class__(input_data=input_data)
509        rxn2 = self._cls(equation=self._equation, rate=rate_obj, kinetics=self.gas,
510                         legacy=self._legacy, **self._kwargs)
511        self.check_rxn(rxn2)
512
513    def check_equal(self, one, two):
514        # helper function for deprecation tests
515        self.assertEqual(type(one), type(two))
516        if isinstance(one, (list, tuple, np.ndarray)):
517            self.assertArrayNear(one, two)
518        else:
519            self.assertNear(one, two)
520
521    def test_deprecated_getters(self):
522        # check property getters deprecated in new framework
523        if self._yaml is None:
524            return
525
526        ct.suppress_deprecation_warnings() # disable fatal deprecation warnings
527
528        rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
529        for attr, default in self._deprecated_getters.items():
530            if self._legacy:
531                self.check_equal(getattr(rxn, attr), default)
532            else:
533                with self.assertWarnsRegex(DeprecationWarning, "property is moved"):
534                    try:
535                        self.check_equal(getattr(rxn, attr), default)
536                    except Exception as err:
537                        print(f"Exception raised when testing getter for '{attr}'")
538                        raise err
539
540        ct.make_deprecation_warnings_fatal() # re-enable fatal deprecation warnings
541
542    def test_deprecated_setters(self):
543        # check property setters deprecated in new framework
544        if self._yaml is None:
545            return
546
547        rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
548        for attr, new in self._deprecated_setters.items():
549            if self._legacy:
550                setattr(rxn, attr, new)
551                self.check_equal(getattr(rxn, attr), new)
552            else:
553                with self.assertWarnsRegex(DeprecationWarning, "property is moved"):
554                    setattr(rxn, attr, new)
555                with self.assertWarnsRegex(DeprecationWarning, "property is moved"):
556                    self.check_equal(getattr(rxn, attr), new)
557
558    def test_deprecated_callers(self):
559        # check methods deprecated in new framework
560        if self._yaml is None:
561            return
562
563        rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
564        for state, value in self._deprecated_callers.items():
565            T, P = state
566            if self._legacy:
567                self.check_equal(rxn(T, P), value)
568            else:
569                with self.assertWarnsRegex(DeprecationWarning, "method is moved"):
570                    self.check_equal(rxn(T, P), value)
571
572
573class TestElementary(ReactionTests, utilities.CanteraTest):
574    # test updated version of elementary reaction
575
576    _cls = ct.ElementaryReaction
577    _type = "elementary"
578    _equation = "H2 + O <=> H + OH"
579    _rate = {"A": 38.7, "b": 2.7, "Ea": 2.619184e+07}
580    _index = 0
581    _yaml = """
582        equation: O + H2 <=> H + OH
583        rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol}
584        """
585    _deprecated_getters = {"allow_negative_pre_exponential_factor": False}
586    _deprecated_setters = {"allow_negative_pre_exponential_factor": True}
587
588    @classmethod
589    def setUpClass(cls):
590        ReactionTests.setUpClass()
591        if cls._legacy:
592            args = list(cls._rate.values())
593            cls._rate_obj = ct.Arrhenius(*args)
594        else:
595            cls._rate_obj = ct.ArrheniusRate(**cls._rate)
596
597    def test_arrhenius(self):
598        # test assigning Arrhenius rate
599        rate = ct.Arrhenius(self._rate["A"], self._rate["b"], self._rate["Ea"])
600        rxn = self._cls(equation=self._equation, kinetics=self.gas,
601                        legacy=self._legacy, **self._kwargs)
602        if self._legacy:
603            rxn.rate = rate
604        else:
605            with self.assertWarnsRegex(DeprecationWarning, "'Arrhenius' object is deprecated"):
606                rxn.rate = rate
607        self.check_rxn(rxn)
608
609
610class TestElementary2(TestElementary):
611    # test legacy version of elementary reaction
612
613    _cls = ct.ElementaryReaction
614    _type = "elementary-legacy"
615    _legacy = True
616    _yaml = """
617        equation: O + H2 <=> H + OH
618        type: elementary-legacy
619        rate-constant: {A: 38.7, b: 2.7, Ea: 6260.0 cal/mol}
620        """
621
622
623class TestThreeBody(TestElementary):
624    # test updated version of three-body reaction
625
626    _cls = ct.ThreeBodyReaction
627    _type = "three-body"
628    _equation = "2 O + M <=> O2 + M"
629    _rate = {"A": 1.2e11, "b": -1.0, "Ea": 0.0}
630    _kwargs = {"efficiencies": {"H2": 2.4, "H2O": 15.4, "AR": 0.83}}
631    _index = 1
632    _yaml = """
633        equation: 2 O + M <=> O2 + M
634        type: three-body
635        rate-constant: {A: 1.2e+11, b: -1.0, Ea: 0.0 cal/mol}
636        efficiencies: {H2: 2.4, H2O: 15.4, AR: 0.83}
637        """
638
639    def test_from_parts(self):
640        # overload default reaction creation from parts
641        orig = self.gas.reaction(self._index)
642        rxn = self._cls(orig.reactants, orig.products, legacy=self._legacy)
643        rxn.rate = self._rate_obj
644        rxn.efficiencies = self._kwargs["efficiencies"]
645        self.check_rxn(rxn)
646
647    def test_rate(self):
648        # rate constant contains third-body concentration
649        pass
650
651    def test_efficiencies(self):
652        # check efficiencies
653        rxn = self._cls(equation=self._equation, rate=self._rate_obj, kinetics=self.gas,
654                        legacy=self._legacy, **self._kwargs)
655
656        self.assertEqual(rxn.efficiencies, self._kwargs["efficiencies"])
657
658
659class TestThreeBody2(TestThreeBody):
660    # test legacy version of three-body reaction
661
662    _cls = ct.ThreeBodyReaction
663    _type = "three-body-legacy"
664    _legacy = True
665    _yaml = """
666        equation: 2 O + M <=> O2 + M
667        type: three-body-legacy
668        rate-constant: {A: 1.2e+11, b: -1.0, Ea: 0.0 cal/mol}
669        efficiencies: {H2: 2.4, H2O: 15.4, AR: 0.83}
670        """
671
672
673class TestImplicitThreeBody(TestThreeBody):
674    # test three-body reactions with explicit collision parther
675
676    _cls = ct.ThreeBodyReaction
677    _type = "three-body"
678    _equation = "H + 2 O2 <=> HO2 + O2"
679    _rate = {"A": 2.08e+19, "b": -1.24, "Ea": 0.0}
680    _index = 5
681    _yaml = """
682        equation: H + 2 O2 <=> HO2 + O2
683        rate-constant: {A: 2.08e+19, b: -1.24, Ea: 0.0}
684        """
685
686    def test_efficiencies(self):
687        # overload of default tester
688        rxn = self._cls(equation=self._equation, rate=self._rate_obj, kinetics=self.gas,
689                        legacy=self._legacy)
690        self.assertEqual(rxn.efficiencies, {"O2": 1.})
691        self.assertEqual(rxn.default_efficiency, 0.)
692
693    def test_from_parts(self):
694        # overload of default tester
695        orig = self.gas.reaction(self._index)
696        rxn = self._cls(orig.reactants, orig.products)
697        rxn.rate = self._rate_obj
698        rxn.efficiencies = {"O2": 1.}
699        rxn.default_efficiency = 0
700        self.check_rxn(rxn)
701
702
703class TestPlog(ReactionTests, utilities.CanteraTest):
704    # test updated version of Plog reaction
705
706    _cls = ct.PlogReaction
707    _type = "pressure-dependent-Arrhenius"
708    _legacy = False
709    _equation = "H2 + O2 <=> 2 OH"
710    _rate = [(1013.25, ct.Arrhenius(1.2124e+16, -0.5779, 45491376.8)),
711             (101325., ct.Arrhenius(4.9108e+31, -4.8507, 103649395.2)),
712             (1013250., ct.Arrhenius(1.2866e+47, -9.0246, 166508556.0)),
713             (10132500., ct.Arrhenius(5.9632e+56, -11.529, 220076726.4))]
714    _index = 3
715    _yaml = """
716        equation: H2 + O2 <=> 2 OH
717        type: pressure-dependent-Arrhenius
718        rate-constants:
719        - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04 cal/mol}
720        - {P: 1.0 atm, A: 4.9108e+31, b: -4.8507, Ea: 2.47728e+04 cal/mol}
721        - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04 cal/mol}
722        - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04 cal/mol}
723        """
724    _deprecated_callers = {(1000., ct.one_atm): 530968934612.9017}
725
726    @classmethod
727    def setUpClass(cls):
728        ReactionTests.setUpClass()
729        if not cls._legacy:
730            cls._rate_obj = ct.PlogRate(cls._rate)
731
732    def check_rates(self, rates, other):
733        # helper function used by deprecation tests
734        self.assertEqual(len(rates), len(other))
735        for index, item in enumerate(rates):
736            P, rate = item
737            self.assertNear(P, other[index][0])
738            self.assertNear(rate.pre_exponential_factor, other[index][1].pre_exponential_factor)
739            self.assertNear(rate.temperature_exponent, other[index][1].temperature_exponent)
740            self.assertNear(rate.activation_energy, other[index][1].activation_energy)
741
742    def test_deprecated_getters(self):
743        # overload default tester for deprecated property getters
744        rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
745        if self._legacy:
746            self.check_rates(rxn.rates, self._rate)
747        else:
748            with self.assertWarnsRegex(DeprecationWarning, "property is moved"):
749                self.check_rates(rxn.rates, self._rate)
750
751    def test_deprecated_setters(self):
752        # overload default tester for deprecated property setters
753        rate = ct.PlogRate(self._rate)
754        rates = rate.rates
755
756        rxn = ct.Reaction.from_yaml(self._yaml, kinetics=self.gas)
757        if self._legacy:
758            rxn.rates = rates
759            self.check_rates(rxn.rates, self._rate)
760        else:
761            with self.assertWarnsRegex(DeprecationWarning, "Setter is replaceable"):
762                rxn.rates = rates
763            with self.assertWarnsRegex(DeprecationWarning, "property is moved"):
764                self.check_rates(rxn.rates, self._rate)
765
766
767class TestPlog2(TestPlog):
768    # test legacy version of Plog reaction
769
770    _cls = ct.PlogReaction
771    _type = "pressure-dependent-Arrhenius-legacy"
772    _legacy = True
773    _rate_obj = None
774    _yaml = """
775        equation: H2 + O2 <=> 2 OH
776        type: pressure-dependent-Arrhenius-legacy
777        rate-constants:
778        - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04 cal/mol}
779        - {P: 1.0 atm, A: 4.9108e+31, b: -4.8507, Ea: 2.47728e+04 cal/mol}
780        - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04 cal/mol}
781        - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04 cal/mol}
782        """
783
784
785class TestChebyshev(ReactionTests, utilities.CanteraTest):
786    # test updated version of Chebyshev reaction
787
788    _cls = ct.ChebyshevReaction
789    _type = "Chebyshev"
790    _equation = "HO2 <=> OH + O"
791    _rate = {"temperature_range": (290., 3000.), "pressure_range": (1000., 10000000.0),
792             "data": [[ 8.2883e+00, -1.1397e+00, -1.2059e-01,  1.6034e-02],
793                      [ 1.9764e+00,  1.0037e+00,  7.2865e-03, -3.0432e-02],
794                      [ 3.1770e-01,  2.6889e-01,  9.4806e-02, -7.6385e-03]]}
795    _index = 4
796    _yaml = """
797        equation: HO2 <=> OH + O
798        type: Chebyshev
799        temperature-range: [290.0, 3000.0]
800        pressure-range: [9.869232667160128e-03 atm, 98.69232667160128 atm]
801        data:
802        - [8.2883, -1.1397, -0.12059, 0.016034]
803        - [1.9764, 1.0037, 7.2865e-03, -0.030432]
804        - [0.3177, 0.26889, 0.094806, -7.6385e-03]
805        """
806    _deprecated_getters = {"nPressure": 4, "nTemperature": 3}
807    _deprecated_callers = {(1000., ct.one_atm): 2858762454.1119065}
808
809    @classmethod
810    def setUpClass(cls):
811        ReactionTests.setUpClass()
812        if not cls._legacy:
813            cls._rate_obj = ct.ChebyshevRate(**cls._rate)
814        cls._deprecated_getters.update({"coeffs": np.array(cls._rate["data"])})
815        cls._deprecated_getters.update(
816            {k: v for k, v in cls._rate.items()
817                if k not in ["data", "temperature_range", "pressure_range"]})
818
819
820class TestChebyshev2(TestChebyshev):
821    # test legacy version of Chebyshev reaction
822
823    _cls = ct.ChebyshevReaction
824    _type = "Chebyshev-legacy"
825    _legacy = True
826    _rate_obj = None
827    _yaml = """
828        equation: HO2 <=> OH + O
829        type: Chebyshev-legacy
830        temperature-range: [290.0, 3000.0]
831        pressure-range: [9.869232667160128e-03 atm, 98.69232667160128 atm]
832        data:
833        - [8.2883, -1.1397, -0.12059, 0.016034]
834        - [1.9764, 1.0037, 7.2865e-03, -0.030432]
835        - [0.3177, 0.26889, 0.094806, -7.6385e-03]
836        """
837
838
839class TestCustom(ReactionTests, utilities.CanteraTest):
840    # test Custom reaction
841
842    # probe O + H2 <=> H + OH
843    _cls = ct.CustomReaction
844    _type = "custom-rate-function"
845    _legacy = False
846    _equation = "H2 + O <=> H + OH"
847    _rate_obj = ct.CustomRate(lambda T: 38.7 * T**2.7 * exp(-3150.15428/T))
848    _index = 0
849    _yaml = None
850
851    def setUp(self):
852        # need to overwrite rate to ensure correct type ("method" is not compatible with Func1)
853        self._rate = lambda T: 38.7 * T**2.7 * exp(-3150.15428/T)
854
855    def test_roundtrip(self):
856        # overload default tester for round trip
857        pass
858
859    def test_from_func1(self):
860        # check instantiation from keywords / rate provided as func1
861        f = ct.Func1(self._rate)
862        rxn = ct.CustomReaction(equation=self._equation, rate=f, kinetics=self.gas)
863        self.check_rxn(rxn)
864
865    def test_rate_func(self):
866        # check result of rate expression
867        f = ct.Func1(self._rate)
868        rate = ct.CustomRate(f)
869        self.assertNear(rate(self.gas.T), self.gas.forward_rate_constants[self._index])
870
871    def test_custom_lambda(self):
872        # check instantiation from keywords / rate provided as lambda function
873        rxn = ct.CustomReaction(equation=self._equation,
874                                rate=lambda T: 38.7 * T**2.7 * exp(-3150.15428/T),
875                                kinetics=self.gas)
876        self.check_rxn(rxn)
877