1import itertools
2from pathlib import Path
3import logging
4import io
5
6from . import utilities
7import cantera as ct
8from cantera import ck2cti, ck2yaml, cti2yaml, ctml2yaml
9
10
11class converterTestCommon:
12    def convert(self, inputFile, thermo=None, transport=None,
13                surface=None, output=None, extra=None,
14                quiet=True, permissive=None):
15        if output is None:
16            output = Path(inputFile).stem  # strip '.inp'
17        if inputFile is not None:
18            inputFile = self.test_data_path / inputFile
19        if thermo is not None:
20            thermo = self.test_data_path / thermo
21        if transport is not None:
22            transport = self.test_data_path / transport
23        if surface is not None:
24            surface = self.test_data_path / surface
25        if extra is not None:
26            extra = self.test_data_path / extra
27        output = self.test_work_path / (output + self.ext)
28        # In Python >= 3.8, this can be replaced by the missing_ok argument
29        if output.is_file():
30            output.unlink()
31        self._convert(inputFile, thermo=thermo, transport=transport,
32            surface=surface, output=output, extra=extra,
33            quiet=quiet, permissive=permissive)
34        return output
35
36    def checkConversion(self, refFile, testFile):
37        ref = ct.Solution(refFile)
38        gas = ct.Solution(testFile)
39
40        self.assertEqual(ref.element_names, gas.element_names)
41        self.assertEqual(ref.species_names, gas.species_names)
42        coeffs_ref = ref.reactant_stoich_coeffs3
43        coeffs_gas = gas.reactant_stoich_coeffs3
44        self.assertEqual(coeffs_gas.shape, coeffs_ref.shape)
45        self.assertTrue((coeffs_gas == coeffs_ref).all())
46
47        compositionA = [[ref.n_atoms(i,j) for j in range(ref.n_elements)]
48                        for i in range(ref.n_species)]
49        compositionB = [[gas.n_atoms(i,j) for j in range(gas.n_elements)]
50                        for i in range(gas.n_species)]
51        self.assertEqual(compositionA, compositionB)
52
53        return ref, gas
54
55    def checkThermo(self, ref, gas, temperatures):
56        for T in temperatures:
57            ref.TP = T, ct.one_atm
58            gas.TP = T, ct.one_atm
59            ref_cp = ref.standard_cp_R
60            gas_cp = gas.standard_cp_R
61            ref_h = ref.standard_enthalpies_RT
62            gas_h = gas.standard_enthalpies_RT
63            ref_s = ref.standard_entropies_R
64            gas_s = gas.standard_entropies_R
65            for i in range(gas.n_species):
66                message = ' for species {0} at T = {1}'.format(i, T)
67                self.assertNear(ref_cp[i], gas_cp[i], 1e-7, msg='cp'+message)
68                self.assertNear(ref_h[i], gas_h[i], 1e-7, msg='h'+message)
69                self.assertNear(ref_s[i], gas_s[i], 1e-7, msg='s'+message)
70
71    def checkKinetics(self, ref, gas, temperatures, pressures, tol=1e-8):
72        for T,P in itertools.product(temperatures, pressures):
73            ref.TP = T, P
74            gas.TP = T, P
75            ref_kf = ref.forward_rate_constants
76            ref_kr = ref.reverse_rate_constants
77            gas_kf = gas.forward_rate_constants
78            gas_kr = gas.reverse_rate_constants
79            for i in range(gas.n_reactions):
80                message = ' for reaction {0} at T = {1}, P = {2}'.format(i, T, P)
81                self.assertNear(ref_kf[i], gas_kf[i], rtol=tol, msg='kf' + message)
82                self.assertNear(ref_kr[i], gas_kr[i], rtol=tol, msg='kr' + message)
83
84    @utilities.slow_test
85    def test_gri30(self):
86        output = self.convert('gri30.inp', thermo='gri30_thermo.dat',
87                              transport='gri30_tran.dat', output='gri30_test')
88
89        ref, gas = self.checkConversion('gri30.xml', output)
90        self.checkKinetics(ref, gas, [300, 1500], [5e3, 1e5, 2e6])
91
92    def test_soot(self):
93        output = self.convert('soot.inp', thermo='soot-therm.dat', output='soot_test')
94        ref, gas = self.checkConversion('soot.xml', output)
95        self.checkThermo(ref, gas, [300, 1100])
96        self.checkKinetics(ref, gas, [300, 1100], [5e3, 1e5, 2e6])
97
98    def test_pdep(self):
99        output = self.convert('pdep-test.inp')
100        ref, gas = self.checkConversion('pdep-test.yaml', output)
101        # Chebyshev coefficients in XML are truncated to 6 digits, limiting accuracy
102        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6],
103                           tol=2e-4)
104
105    def test_species_only(self):
106        self.convert(None, thermo='dummy-thermo.dat', output='dummy-thermo')
107        if self.ext == ".cti":
108            cti = "ideal_gas(elements='C H', species='dummy-thermo:R1A R1B P1')"
109            gas = ct.Solution(source=cti)
110        elif self.ext == ".yaml":
111            yaml = ("{phases: [{name: gas, species: "
112                    "[{dummy-thermo.yaml/species: [R1A, R1B, P1]}], "
113                    "thermo: ideal-gas}]}")
114            gas = ct.Solution(yaml=yaml)
115        self.assertEqual(gas.n_species, 3)
116        self.assertEqual(gas.n_reactions, 0)
117
118    def test_missingThermo(self):
119        with self.assertRaisesRegex(self.InputError, 'No thermo data'):
120            self.convert('h2o2_missingThermo.inp')
121
122    def test_duplicate_thermo(self):
123        with self.assertRaisesRegex(self.InputError, 'additional thermo'):
124            self.convert('duplicate-thermo.inp')
125
126        output = self.convert('duplicate-thermo.inp', permissive=True)
127
128        gas = ct.Solution(output)
129        self.assertEqual(gas.n_species, 3)
130        self.assertEqual(gas.n_reactions, 2)
131
132    def test_duplicate_species(self):
133        with self.assertRaisesRegex(self.InputError, 'additional declaration'):
134            self.convert('duplicate-species.inp')
135
136        output = self.convert('duplicate-species.inp', permissive=True)
137
138        gas = ct.Solution(output)
139        self.assertEqual(gas.species_names, ['foo','bar','baz'])
140
141    def test_pathologicalSpeciesNames(self):
142        output = self.convert('species-names.inp')
143        gas = ct.Solution(output)
144
145        self.assertEqual(gas.n_species, 9)
146        self.assertEqual(gas.species_name(0), '(Parens)')
147        self.assertEqual(gas.species_name(1), '@#$%^-2')
148        self.assertEqual(gas.species_index('co:lons:'), 2)
149        self.assertEqual(gas.species_name(3), '[xy2]*{.}')
150        self.assertEqual(gas.species_name(4), 'plus+')
151        self.assertEqual(gas.species_name(5), 'eq=uals')
152        self.assertEqual(gas.species_name(6), 'plus')
153        self.assertEqual(gas.species_name(7), 'trans_butene')
154        self.assertEqual(gas.species_name(8), 'co')
155
156        self.assertEqual(gas.n_reactions, 12)
157        nu = gas.product_stoich_coeffs3 - gas.reactant_stoich_coeffs3
158        self.assertEqual(list(nu[:,0]), [-1, -1, 0, 2, 0, 0, 0, 0, 0])
159        self.assertEqual(list(nu[:,1]), [-2, 3, 0, -1, 0, 0, 0, 0, 0])
160        self.assertEqual(list(nu[:,2]), [-1, 0, 0, 0, 1, 0, 0, 0, 0])
161        self.assertEqual(list(nu[:,3]), [3, 0, 0, 0, -2, -1, 0, 0, 0])
162        self.assertEqual(list(nu[:,4]), [2, 0, 0, 0, -1, 0, -1, 0, 0])
163        self.assertEqual(list(nu[:,5]), [1, 0, 0, 0, 1, -1, -1, 0, 0])
164        self.assertEqual(list(nu[:,6]), [2, 0, -1, 0, 0, -1, 0, 0, 0])
165        self.assertEqual(list(nu[:,7]), [0, 0, 0, 0, -1, 1, 0, 0, 0])
166        self.assertEqual(list(nu[:,8]), [0, 0, 0, 0, -1, 1, 0, 0, 0])
167        self.assertEqual(list(nu[:,9]), [0, 0, 0, 0, -1, 1, 0, 0, 0])
168        self.assertEqual(list(nu[:,10]), [0, 0, -1, 0, 2, 0, 0, -1, 0])
169        self.assertEqual(list(nu[:,11]), [0, 0, -1, 0, 2, 0, 0, 0, -1])
170
171    def test_unterminatedSections(self):
172        with self.assertRaisesRegex(self.InputError, 'implicitly ended'):
173            self.convert('unterminated-sections.inp')
174
175        output = self.convert('unterminated-sections.inp', permissive=True)
176        gas = ct.Solution(output)
177        self.assertEqual(gas.n_species, 3)
178        self.assertEqual(gas.n_reactions, 2)
179
180    def test_unterminatedSections2(self):
181        with self.assertRaisesRegex(self.InputError, 'implicitly ended'):
182            self.convert('unterminated-sections2.inp')
183
184        output = self.convert('unterminated-sections2.inp', permissive=True)
185        gas = ct.Solution(output)
186        self.assertEqual(gas.n_species, 3)
187        self.assertEqual(gas.n_reactions, 2)
188
189    def test_unrecognized_section(self):
190        with self.assertRaisesRegex(self.InputError, 'SPAM'):
191            self.convert('unrecognized-section.inp', thermo='dummy-thermo.dat',
192                         permissive=True)
193
194    def test_nasa9(self):
195        output = self.convert('nasa9-test.inp', thermo='nasa9-test-therm.dat')
196        ref, gas = self.checkConversion('nasa9-test.xml', output)
197        self.checkThermo(ref, gas, [300, 500, 1200, 5000])
198
199    def test_nasa9_subset(self):
200        output = self.convert('nasa9-test-subset.inp', thermo='nasa9-test-therm.dat')
201        ref, gas = self.checkConversion('nasa9-test-subset.xml', output)
202        self.checkThermo(ref, gas, [300, 500, 1200, 5000])
203
204    def test_sri_falloff(self):
205        output = self.convert('sri-falloff.inp', thermo='dummy-thermo.dat')
206        ref, gas = self.checkConversion('sri-falloff.xml', output)
207        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6])
208
209    def test_chemically_activated(self):
210        output = self.convert('chemically-activated-reaction.inp')
211        ref, gas = self.checkConversion('chemically-activated-reaction.xml',
212                                        output)
213        # pre-exponential factor in XML is truncated to 7 sig figs, limiting accuracy
214        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6, 1e7],
215                           tol=1e-7)
216
217    def test_explicit_third_bodies(self):
218        output = self.convert('explicit-third-bodies.inp', thermo='dummy-thermo.dat')
219        ref, gas = self.checkConversion('explicit-third-bodies.xml', output)
220        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6])
221
222    def test_explicit_reverse_rate(self):
223        output = self.convert('explicit-reverse-rate.inp', thermo='dummy-thermo.dat')
224        ref, gas = self.checkConversion('explicit-reverse-rate.xml', output)
225        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6])
226
227        # Reactions with explicit reverse rate constants are transformed into
228        # two irreversible reactions with reactants and products swapped, unless
229        # the explicit reverse rate is zero so only the forward reaction is used.
230        Rr = gas.reverse_rate_constants
231        self.assertEqual(Rr[0], 0.0)
232        self.assertEqual(Rr[1], 0.0)
233        self.assertEqual(Rr[2], 0.0)
234        self.assertEqual(Rr[3], 0.0)
235        self.assertEqual(Rr[4], 0.0)
236        Rstoich = gas.reactant_stoich_coeffs3
237        Pstoich = gas.product_stoich_coeffs3
238        self.assertEqual(list(Rstoich[:, 0]), list(Pstoich[:, 1]))
239        self.assertEqual(list(Rstoich[:, 1]), list(Pstoich[:, 0]))
240        self.assertEqual(list(Rstoich[:, 2]), list(Pstoich[:, 3]))
241        self.assertEqual(list(Rstoich[:, 3]), list(Pstoich[:, 2]))
242
243        self.assertEqual(gas.n_reactions, 5)
244
245    def test_explicit_forward_order(self):
246        output = self.convert('explicit-forward-order.inp', thermo='dummy-thermo.dat')
247        ref, gas = self.checkConversion('explicit-forward-order.xml', output)
248        # pre-exponential factor in XML is truncated to 7 sig figs, limiting accuracy
249        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6],
250                           tol=2e-7)
251
252    def test_negative_order(self):
253        with self.assertRaisesRegex(self.InputError, 'Negative reaction order'):
254            self.convert('negative-order.inp', thermo='dummy-thermo.dat')
255
256    def test_negative_order_permissive(self):
257        output = self.convert('negative-order.inp', thermo='dummy-thermo.dat',
258                              permissive=True)
259        ref, gas = self.checkConversion('negative-order.cti', output)
260        # pre-exponential factor in XML is truncated to 7 sig figs, limiting accuracy
261        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6],
262                           tol=2e-7)
263
264    def test_negative_A_factor(self):
265        output = self.convert('negative-rate.inp', thermo='dummy-thermo.dat')
266        gas = ct.Solution(output)  # Validate the mechanism
267        self.assertLess(gas.reaction(4).rate.pre_exponential_factor, 0)
268        self.assertLess(gas.reaction(1).rate.pre_exponential_factor, 0)
269        self.assertLess(gas.reaction(2).rate.pre_exponential_factor, 0)
270        self.assertLess(gas.forward_rate_constants[5], 0)
271
272    def test_bad_troe_value(self):
273        with self.assertRaises(ValueError):
274            self.convert('bad-troe.inp', thermo='dummy-thermo.dat')
275
276    def test_invalid_reaction_equation(self):
277        with self.assertRaisesRegex(self.InputError, 'Unparsable'):
278            self.convert('invalid-equation.inp', thermo='dummy-thermo.dat')
279
280    @utilities.slow_test
281    def test_reaction_units(self):
282        out_def = self.convert('units-default.inp', thermo='dummy-thermo.dat')
283        out_cus = self.convert('units-custom.inp', thermo='dummy-thermo.dat')
284        default, custom = self.checkConversion(out_def, out_cus)
285        self.checkKinetics(default, custom,
286                           [300, 800, 1450, 2800], [5e0, 5e3, 1e5, 2e6, 1e8], 1e-7)
287
288    def test_float_stoich_coeffs(self):
289        output = self.convert('float-stoich.inp', thermo='dummy-thermo.dat')
290        gas = ct.Solution(output)
291
292        R = gas.reactant_stoich_coeffs3
293        P = gas.product_stoich_coeffs3
294        self.assertArrayNear(R[:,0], [0, 1.5, 0.5, 0])
295        self.assertArrayNear(P[:,0], [1, 0, 0, 1])
296        self.assertArrayNear(R[:,1], [1, 0, 0, 1])
297        self.assertArrayNear(P[:,1], [0, 0.33, 1.67, 0])
298
299    def test_photon(self):
300        output = self.convert('photo-reaction.inp', thermo='dummy-thermo.dat',
301                              permissive=True)
302
303        ref, gas = self.checkConversion('photo-reaction.xml', output)
304        self.checkKinetics(ref, gas, [300, 800, 1450, 2800], [5e3, 1e5, 2e6])
305
306    def test_transport_normal(self):
307        output = self.convert('h2o2.inp', transport='gri30_tran.dat',
308                              output='h2o2_transport_normal')
309
310        gas = ct.Solution(output)
311        gas.TPX = 300, 101325, 'H2:1.0, O2:1.0'
312        self.assertAlmostEqual(gas.thermal_conductivity, 0.07663, 4)
313
314    def test_transport_embedded(self):
315        output = self.convert('with-transport.inp')
316        gas = ct.Solution(output)
317        gas.X = [0.2, 0.3, 0.5]
318        D = gas.mix_diff_coeffs
319        for d in D:
320            self.assertTrue(d > 0.0)
321
322    def test_transport_missing_species(self):
323        with self.assertRaisesRegex(self.InputError, 'No transport data'):
324            self.convert('h2o2.inp', transport='h2o2-missing-species-tran.dat',
325                output='h2o2_transport_missing_species')
326
327    def test_transport_extra_column_entries(self):
328        with self.assertRaisesRegex(self.InputError, '572.400'):
329            self.convert('h2o2.inp',
330                transport='h2o2-extra-column-entries-tran.dat',
331                output='h2o2_extra-column-entries-tran')
332
333    def test_transport_duplicate_species(self):
334        with self.assertRaisesRegex(self.InputError, 'duplicate transport'):
335            self.convert('h2o2.inp',
336                transport='h2o2-duplicate-species-tran.dat',
337                output='h2o2_transport_duplicate_species.cti')
338
339        self.convert('h2o2.inp',
340            transport='h2o2-duplicate-species-tran.dat',
341            output='h2o2_transport_duplicate_species', permissive=True)
342
343    def test_transport_bad_geometry(self):
344        with self.assertRaisesRegex(self.InputError, 'geometry flag'):
345            self.convert('h2o2.inp',
346                transport='h2o2-bad-geometry-tran.dat',
347                output='h2o2_transport_bad_geometry')
348
349    def test_transport_float_geometry(self):
350        with self.assertRaisesRegex(self.InputError, 'geometry flag'):
351            self.convert('h2o2.inp',
352                transport='h2o2-float-geometry-tran.dat',
353                output='h2o2_transport_float_geometry')
354
355    def test_empty_reaction_section(self):
356        output = self.convert('h2o2_emptyReactions.inp')
357        gas = ct.Solution(output)
358        self.assertEqual(gas.n_species, 9)
359        self.assertEqual(gas.n_reactions, 0)
360
361    def test_reaction_comments1(self):
362        output = self.convert('pdep-test.inp')
363        text = output.read_text()
364        self.assertIn('Generic mechanism header', text)
365        self.assertIn('Single PLOG reaction', text)
366        self.assertIn('Multiple PLOG expressions at the same pressure', text)
367
368    def test_reaction_comments2(self):
369        output = self.convert('explicit-third-bodies.inp', thermo='dummy-thermo.dat')
370        text = output.read_text()
371        self.assertIn('An end of line comment', text)
372        self.assertIn('A comment after the last reaction', text)
373
374    def test_custom_element(self):
375        output = self.convert('custom-elements.inp')
376        gas = ct.Solution(output)
377        self.assertEqual(gas.n_elements, 4)
378        self.assertNear(gas.atomic_weight(2), 13.003)
379        self.assertEqual(gas.n_atoms('ethane', 'C'), 2)
380        self.assertEqual(gas.n_atoms('CC', 'C'), 1)
381        self.assertEqual(gas.n_atoms('CC', 'Ci'), 1)
382
383    def test_surface_mech(self):
384        output = self.convert('surface1-gas.inp', surface='surface1.inp',
385                              output='surface1')
386
387        gas = ct.Solution(output, 'gas')
388        surf = ct.Interface(output, 'PT_SURFACE', [gas])
389
390        self.assertEqual(gas.n_reactions, 11)
391        self.assertEqual(surf.n_reactions, 15)
392        self.assertEqual(surf.species('O2_Pt').size, 3)
393
394        # Different units for rate constants in each input file
395        # 62.1 kJ/gmol = 6.21e7 J/kmol
396        self.assertNear(gas.reaction(0).rate.activation_energy, 6.21e7)
397        # 67400 J/mol = 6.74e7 J/kmol
398        self.assertNear(surf.reaction(1).rate.activation_energy, 6.74e7)
399
400        # Sticking coefficients
401        self.assertFalse(surf.reaction(1).is_sticking_coefficient)
402        self.assertTrue(surf.reaction(2).is_sticking_coefficient)
403        self.assertTrue(surf.reaction(2).use_motz_wise_correction)
404        self.assertTrue(surf.reaction(4).is_sticking_coefficient)
405        self.assertFalse(surf.reaction(4).use_motz_wise_correction)
406        self.assertTrue(surf.reaction(4).duplicate)
407        self.assertTrue(surf.reaction(6).use_motz_wise_correction)
408
409        # Coverage dependencies
410        covdeps = surf.reaction(1).coverage_deps
411        self.assertEqual(len(covdeps), 2)
412        self.assertIn('H_Pt', covdeps)
413        self.assertEqual(covdeps['OH_Pt'][1], 1.0)
414        self.assertNear(covdeps['H_Pt'][2], -6e6) # 6000 J/gmol = 6e6 J/kmol
415
416    def test_surface_mech2(self):
417        output = self.convert('surface1-gas-noreac.inp', surface='surface1.inp',
418                              output='surface1-nogasreac')
419
420        gas = ct.Solution(output, 'gas')
421        surf = ct.Interface(output, 'PT_SURFACE', [gas])
422
423        self.assertEqual(gas.n_reactions, 0)
424        self.assertEqual(surf.n_reactions, 15)
425
426        # Coverage dependencies
427        covdeps = surf.reaction(1).coverage_deps
428        self.assertIn('H_Pt', covdeps)
429        self.assertEqual(covdeps['OH_Pt'][1], 1.0)
430        self.assertNear(covdeps['H_Pt'][2], -6e6)
431
432    def test_third_body_plus_falloff_reactions(self):
433        self.convert('third_body_plus_falloff_reaction.inp')
434        gas = ct.Solution('third_body_plus_falloff_reaction' + self.ext)
435        self.assertEqual(gas.n_reactions, 2)
436
437    def test_blank_line_in_header(self):
438        self.convert('blank_line_in_header.inp')
439        gas = ct.Solution('blank_line_in_header' + self.ext)
440        self.assertEqual(gas.n_reactions, 1)
441
442
443class ck2ctiTest(converterTestCommon, utilities.CanteraTest):
444    ext = '.cti'
445    InputError = ck2cti.InputParseError
446
447    def _convert(self, inputFile, *, thermo, transport, surface, output, extra,
448                 quiet, permissive):
449        ck2cti.convertMech(inputFile, thermoFile=thermo,
450            transportFile=transport, surfaceFile=surface, outName=output,
451            quiet=quiet, permissive=permissive)
452
453    def test_missingElement(self):
454        with self.assertRaisesRegex(self.InputError, 'Undefined elements'):
455            self.convert('h2o2_missingElement.inp', output='h2o2_missingElement')
456
457
458class ck2yamlTest(converterTestCommon, utilities.CanteraTest):
459    ext = '.yaml'
460    InputError = ck2yaml.InputError
461
462    def _convert(self, inputFile, *, thermo, transport, surface, output, extra,
463                 quiet, permissive):
464        ck2yaml.convert_mech(inputFile, thermo_file=thermo,
465            transport_file=transport, surface_file=surface, out_name=output,
466            extra_file=extra, quiet=quiet, permissive=permissive)
467
468    @utilities.slow_test
469    def test_extra(self):
470        self.convert('gri30.inp', thermo='gri30_thermo.dat',
471                     transport='gri30_tran.dat', output='gri30_extra',
472                     extra='extra.yaml')
473
474        output = self.test_work_path / ("gri30_extra" + self.ext)
475        yml = utilities.load_yaml(output)
476
477        desc = yml['description'].split('\n')[-1]
478        self.assertEqual(desc, 'This is an alternative description.')
479        for key in ['foo', 'bar']:
480            self.assertIn(key, yml.keys())
481
482    def test_sri_zero(self):
483        # This test tests it can convert the SRI parameters when D or E equal to 0
484        self.convert('sri_convert_test.txt')
485        output = self.test_work_path / ("sri_convert_test" + self.ext)
486        mech = utilities.load_yaml(output)
487        D = mech['reactions'][0]['SRI']['D']
488        E = mech['reactions'][0]['SRI']['E']
489        self.assertEqual(D, 0)
490        self.assertEqual(E, 0)
491
492    def test_duplicate_reactions(self):
493        # Running a test this way instead of using the convertMech function
494        # tests the behavior of the ck2yaml.main function and the mechanism
495        # validation step.
496
497        # clear global handler created by logging.basicConfig() in ck2cti
498        logging.getLogger().handlers.clear()
499
500        # Replace the ck2yaml logger with our own in order to capture the output
501        log_stream = io.StringIO()
502        logger = logging.getLogger('cantera.ck2yaml')
503        original_handler = logger.handlers.pop()
504        logformatter = logging.Formatter('%(message)s')
505        handler = logging.StreamHandler(log_stream)
506        handler.setFormatter(logformatter)
507        logger.addHandler(handler)
508
509        with self.assertRaises(SystemExit):
510            ck2yaml.main([
511                f"--input={self.test_data_path}/undeclared-duplicate-reactions.inp",
512                f"--thermo={self.test_data_path}/dummy-thermo.dat"])
513
514        # Put the original logger back in place
515        logger.handlers.clear()
516        logger.addHandler(original_handler)
517
518        message = log_stream.getvalue()
519        for token in ('FAILED', 'lines 12 and 14', 'R1A', 'R1B'):
520            self.assertIn(token, message)
521
522    def test_error_for_big_element_number(self):
523        with self.assertRaisesRegex(self.InputError, 'Element amounts can have no more than 3 digits.'):
524            self.convert('big_element_num_err.inp')
525
526class CtmlConverterTest(utilities.CanteraTest):
527    def test_sofc(self):
528        gas_a, anode_bulk, oxide_a = ct.import_phases(
529            'sofc.cti',
530            ['gas', 'metal', 'oxide_bulk'])
531
532        self.assertNear(gas_a.P, ct.one_atm)
533        self.assertNear(anode_bulk['electron'].X, 1.0)
534        self.assertNear(oxide_a.density_mole, 17.6)
535
536    @utilities.slow_test
537    def test_diamond(self):
538        gas, solid = ct.import_phases('diamond.cti', ['gas','diamond'])
539        face = ct.Interface('diamond.cti', 'diamond_100', [gas, solid])
540
541        self.assertNear(face.site_density, 3e-8)
542
543    def test_invalid(self):
544        try:
545            gas = ct.Solution('invalid.cti')
546        except ct.CanteraError as e:
547            err = e
548
549        self.assertIn('already contains', err.args[0])
550
551    def test_noninteger_atomicity(self):
552        gas = ct.Solution('noninteger-atomicity.cti')
553        self.assertNear(gas.molecular_weights[gas.species_index('CnHm')],
554                        10.65*gas.atomic_weight('C') + 21.8*gas.atomic_weight('H'))
555
556    def test_reaction_orders(self):
557        gas = ct.Solution('reaction-orders.cti')
558        self.assertEqual(gas.n_reactions, 1)
559        R = gas.reaction(0)
560        self.assertTrue(R.allow_nonreactant_orders)
561        self.assertNear(R.orders.get('OH'), 0.15)
562        self.assertTrue(R.allow_negative_orders)
563        self.assertNear(R.orders.get('H2'), -0.25)
564
565    def test_long_source_input(self):
566        """
567        Here we are testing if passing a very long string will result in a
568        Solution object. This should result in a temp file creation in most OS's
569        """
570
571        gas = ct.Solution('pdep-test.yaml')
572
573        data = (self.test_data_path / "pdep-test.cti").read_text()
574        data_size_2048kB = data + ' '*2048*1024
575        gas2 = ct.Solution(source=data_size_2048kB)
576
577        self.assertEqual(gas.n_reactions, gas2.n_reactions)
578
579    def test_short_source_input(self):
580        """
581        Here we are testing if passing a short string will result in a Solution
582        object. This should not result in a temp file creation in most OS's
583        """
584
585        gas = ct.Solution('pdep-test.yaml')
586
587        data = (self.test_data_path / "pdep-test.cti").read_text()
588        data_size_32kB = data + ' '*18000
589        gas2 = ct.Solution(source=data_size_32kB)
590
591        self.assertEqual(gas.n_reactions, gas2.n_reactions)
592
593
594class cti2yamlTest(utilities.CanteraTest):
595    def checkConversion(self, basename, cls=ct.Solution, ctiphases=(),
596                        yamlphases=(), **kwargs):
597        ctiPhase = cls(basename + '.cti', adjacent=ctiphases, **kwargs)
598        yamlPhase = cls(basename + '.yaml', adjacent=yamlphases, **kwargs)
599
600        self.assertEqual(ctiPhase.element_names, yamlPhase.element_names)
601        self.assertEqual(ctiPhase.species_names, yamlPhase.species_names)
602        self.assertEqual(ctiPhase.n_reactions, yamlPhase.n_reactions)
603        for C, Y in zip(ctiPhase.species(), yamlPhase.species()):
604            self.assertEqual(C.composition, Y.composition)
605
606        for C, Y in zip(ctiPhase.reactions(), yamlPhase.reactions()):
607            self.assertEqual(C.__class__, Y.__class__)
608            self.assertEqual(C.reactants, Y.reactants)
609            self.assertEqual(C.products, Y.products)
610            self.assertEqual(C.duplicate, Y.duplicate)
611
612        for i, sp in zip(range(ctiPhase.n_reactions), ctiPhase.kinetics_species_names):
613            self.assertEqual(ctiPhase.reactant_stoich_coeff(sp, i),
614                             yamlPhase.reactant_stoich_coeff(sp, i))
615
616        return ctiPhase, yamlPhase
617
618    def checkThermo(self, ctiPhase, yamlPhase, temperatures, tol=1e-7):
619        for T in temperatures:
620            ctiPhase.TP = T, ct.one_atm
621            yamlPhase.TP = T, ct.one_atm
622            cp_cti = ctiPhase.partial_molar_cp
623            cp_yaml = yamlPhase.partial_molar_cp
624            h_cti = ctiPhase.partial_molar_enthalpies
625            h_yaml = yamlPhase.partial_molar_enthalpies
626            s_cti = ctiPhase.partial_molar_entropies
627            s_yaml = yamlPhase.partial_molar_entropies
628            self.assertNear(ctiPhase.density, yamlPhase.density)
629            for i in range(ctiPhase.n_species):
630                message = ' for species {0} at T = {1}'.format(i, T)
631                self.assertNear(cp_cti[i], cp_yaml[i], tol, msg='cp'+message)
632                self.assertNear(h_cti[i], h_yaml[i], tol, msg='h'+message)
633                self.assertNear(s_cti[i], s_yaml[i], tol, msg='s'+message)
634
635    def checkKinetics(self, ctiPhase, yamlPhase, temperatures, pressures, tol=1e-7):
636        for T,P in itertools.product(temperatures, pressures):
637            ctiPhase.TP = T, P
638            yamlPhase.TP = T, P
639            kf_cti = ctiPhase.forward_rate_constants
640            kr_cti = ctiPhase.reverse_rate_constants
641            kf_yaml = yamlPhase.forward_rate_constants
642            kr_yaml = yamlPhase.reverse_rate_constants
643            for i in range(yamlPhase.n_reactions):
644                message = ' for reaction {0} at T = {1}, P = {2}'.format(i, T, P)
645                self.assertNear(kf_cti[i], kf_yaml[i], rtol=tol, msg='kf '+message)
646                self.assertNear(kr_cti[i], kr_yaml[i], rtol=tol, msg='kr '+message)
647
648    def checkTransport(self, ctiPhase, yamlPhase, temperatures,
649                       model='mixture-averaged'):
650        ctiPhase.transport_model = model
651        yamlPhase.transport_model = model
652        for T in temperatures:
653            ctiPhase.TP = T, ct.one_atm
654            yamlPhase.TP = T, ct.one_atm
655            self.assertNear(ctiPhase.viscosity, yamlPhase.viscosity)
656            self.assertNear(ctiPhase.thermal_conductivity,
657                            yamlPhase.thermal_conductivity)
658            Dkm_cti = ctiPhase.mix_diff_coeffs
659            Dkm_yaml = yamlPhase.mix_diff_coeffs
660            for i in range(ctiPhase.n_species):
661                message = 'dkm for species {0} at T = {1}'.format(i, T)
662                self.assertNear(Dkm_cti[i], Dkm_yaml[i], msg=message)
663
664    @utilities.slow_test
665    def test_gri30(self):
666        cti2yaml.convert(
667            self.cantera_data_path / "gri30.cti",
668            self.test_work_path / "gri30.yaml",
669        )
670        ctiPhase, yamlPhase = self.checkConversion('gri30')
671        X = {'O2': 0.3, 'H2': 0.1, 'CH4': 0.2, 'CO2': 0.4}
672        ctiPhase.X = X
673        yamlPhase.X = X
674        self.checkThermo(ctiPhase, yamlPhase, [300, 500, 1300, 2000])
675        self.checkKinetics(ctiPhase, yamlPhase, [900, 1800], [2e5, 20e5])
676        self.checkTransport(ctiPhase, yamlPhase, [298, 1001, 2400])
677
678    def test_pdep(self):
679        cti2yaml.convert(
680            self.test_data_path / "pdep-test.cti",
681            self.test_work_path / "pdep-test.yaml",
682        )
683        ctiPhase, yamlPhase = self.checkConversion('pdep-test')
684        self.checkKinetics(ctiPhase, yamlPhase, [300, 1000, 2200],
685                           [100, ct.one_atm, 2e5, 2e6, 9.9e6])
686
687    def test_ptcombust(self):
688        cti2yaml.convert(
689            self.cantera_data_path / "ptcombust.cti",
690            self.test_work_path / "ptcombust.yaml",
691        )
692        ctiGas, yamlGas = self.checkConversion('ptcombust')
693        ctiSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface,
694            name='Pt_surf', ctiphases=[ctiGas], yamlphases=[yamlGas])
695
696        self.checkKinetics(ctiGas, yamlGas, [500, 1200], [1e4, 3e5])
697        self.checkThermo(ctiSurf, yamlSurf, [400, 800, 1600])
698        self.checkKinetics(ctiSurf, yamlSurf, [500, 1200], [1e4, 3e5])
699
700    @utilities.slow_test
701    def test_ptcombust_motzwise(self):
702        cti2yaml.convert(
703            self.test_data_path / "ptcombust-motzwise.cti",
704            self.test_work_path / "ptcombust-motzwise.yaml",
705        )
706        ctiGas, yamlGas = self.checkConversion('ptcombust-motzwise')
707        ctiSurf, yamlSurf = self.checkConversion('ptcombust-motzwise', ct.Interface,
708            name='Pt_surf', ctiphases=[ctiGas], yamlphases=[yamlGas])
709
710
711        self.checkKinetics(ctiGas, yamlGas, [500, 1200], [1e4, 3e5])
712        self.checkThermo(ctiSurf, yamlSurf, [400, 800, 1600])
713        self.checkKinetics(ctiSurf, yamlSurf, [900], [101325])
714
715    def test_sofc(self):
716        cti2yaml.convert(
717            self.cantera_data_path / "sofc.cti",
718            self.test_work_path / "sofc.yaml",
719        )
720        ctiGas, yamlGas = self.checkConversion('sofc')
721        ctiMetal, yamlMetal = self.checkConversion('sofc', name='metal')
722        ctiOxide, yamlOxide = self.checkConversion('sofc', name='oxide_bulk')
723        ctiMSurf, yamlMSurf = self.checkConversion('sofc', ct.Interface,
724            name='metal_surface', ctiphases=[ctiGas, ctiMetal],
725            yamlphases=[yamlGas, yamlMetal])
726        ctiOSurf, yamlOSurf = self.checkConversion('sofc', ct.Interface,
727            name='oxide_surface', ctiphases=[ctiGas, ctiOxide],
728            yamlphases=[yamlGas, yamlOxide])
729        cti_tpb, yaml_tpb = self.checkConversion('sofc', ct.Interface,
730            name='tpb', ctiphases=[ctiMetal, ctiMSurf, ctiOSurf],
731            yamlphases=[yamlMetal, yamlMSurf, yamlOSurf])
732
733        self.checkThermo(ctiMSurf, yamlMSurf, [900, 1000, 1100])
734        self.checkThermo(ctiOSurf, yamlOSurf, [900, 1000, 1100])
735        ctiMetal.electric_potential = yamlMetal.electric_potential = 2
736        self.checkKinetics(cti_tpb, yaml_tpb, [900, 1000, 1100], [1e5])
737        ctiMetal.electric_potential = yamlMetal.electric_potential = 4
738        self.checkKinetics(cti_tpb, yaml_tpb, [900, 1000, 1100], [1e5])
739
740    @utilities.slow_test
741    def test_liquidvapor(self):
742        output_file = self.test_work_path / "liquidvapor.yaml"
743        cti2yaml.convert(
744            self.cantera_data_path / "liquidvapor.cti",
745            output_file,
746        )
747        for name in ['water', 'nitrogen', 'methane', 'hydrogen', 'oxygen',
748                     'hfc134a', 'carbondioxide', 'heptane']:
749            ctiPhase, yamlPhase = self.checkConversion('liquidvapor', name=name)
750            self.checkThermo(ctiPhase, yamlPhase,
751                             [1.3 * ctiPhase.min_temp, 0.7 * ctiPhase.max_temp])
752        # The output file must be removed after this test because it shadows
753        # a file distributed with Cantera that is used in other tests.
754        output_file.unlink()
755
756    def test_Redlich_Kwong_CO2(self):
757        cti2yaml.convert(
758            self.test_data_path / "co2_RK_example.cti",
759            self.test_work_path / "co2_RK_example.yaml",
760        )
761        ctiGas, yamlGas = self.checkConversion('co2_RK_example')
762        for P in [1e5, 2e6, 1.3e7]:
763            yamlGas.TP = ctiGas.TP = 300, P
764            self.checkThermo(ctiGas, yamlGas, [300, 400, 500])
765
766    @utilities.slow_test
767    def test_Redlich_Kwong_ndodecane(self):
768        cti2yaml.convert(
769            self.cantera_data_path / "nDodecane_Reitz.cti",
770            self.test_work_path / "nDodecane_Reitz.yaml",
771        )
772        ctiGas, yamlGas = self.checkConversion('nDodecane_Reitz')
773        self.checkThermo(ctiGas, yamlGas, [300, 400, 500])
774        self.checkKinetics(ctiGas, yamlGas, [300, 500, 1300], [1e5, 2e6, 1.4e7],
775                           1e-6)
776
777    def test_diamond(self):
778        cti2yaml.convert(
779            self.cantera_data_path / "diamond.cti",
780            self.test_work_path / "diamond.yaml",
781        )
782        ctiGas, yamlGas = self.checkConversion('diamond', name='gas')
783        ctiSolid, yamlSolid = self.checkConversion('diamond', name='diamond')
784        ctiSurf, yamlSurf = self.checkConversion('diamond',
785            ct.Interface, name='diamond_100', ctiphases=[ctiGas, ctiSolid],
786            yamlphases=[yamlGas, yamlSolid])
787        self.checkThermo(ctiSolid, yamlSolid, [300, 500])
788        self.checkThermo(ctiSurf, yamlSurf, [330, 490])
789        self.checkKinetics(ctiSurf, yamlSurf, [400, 800], [2e5])
790
791    def test_lithium_ion_battery(self):
792        cti2yaml.convert(
793            self.cantera_data_path / "lithium_ion_battery.cti",
794            self.test_work_path / "lithium_ion_battery.yaml",
795        )
796        name = 'lithium_ion_battery'
797        ctiAnode, yamlAnode = self.checkConversion(name, name='anode')
798        ctiCathode, yamlCathode = self.checkConversion(name, name='cathode')
799        ctiMetal, yamlMetal = self.checkConversion(name, name='electron')
800        ctiElyt, yamlElyt = self.checkConversion(name, name='electrolyte')
801        ctiAnodeInt, yamlAnodeInt = self.checkConversion(name,
802            name='edge_anode_electrolyte',
803            ctiphases=[ctiAnode, ctiMetal, ctiElyt],
804            yamlphases=[yamlAnode, yamlMetal, yamlElyt])
805        ctiCathodeInt, yamlCathodeInt = self.checkConversion(name,
806            name='edge_cathode_electrolyte',
807            ctiphases=[ctiCathode, ctiMetal, ctiElyt],
808            yamlphases=[yamlCathode, yamlMetal, yamlElyt])
809
810        self.checkThermo(ctiAnode, yamlAnode, [300, 330])
811        self.checkThermo(ctiCathode, yamlCathode, [300, 330])
812
813        ctiAnode.X = yamlAnode.X = [0.7, 0.3]
814        self.checkThermo(ctiAnode, yamlAnode, [300, 330])
815        ctiCathode.X = yamlCathode.X = [0.2, 0.8]
816        self.checkThermo(ctiCathode, yamlCathode, [300, 330])
817
818        for phase in [ctiAnode, yamlAnode, ctiCathode, yamlCathode, ctiMetal,
819                      yamlMetal, ctiElyt, yamlElyt, ctiAnodeInt, yamlAnodeInt,
820                      ctiCathodeInt, yamlCathodeInt]:
821            phase.TP = 300, 1e5
822        ctiMetal.electric_potential = yamlMetal.electric_potential = 0
823        ctiElyt.electric_potential = yamlElyt.electric_potential = 1.9
824        self.checkKinetics(ctiAnodeInt, yamlAnodeInt, [300], [1e5])
825
826        ctiMetal.electric_potential = yamlMetal.electric_potential = 2.2
827        ctiElyt.electric_potential = yamlElyt.electric_potential = 0
828        self.checkKinetics(ctiCathodeInt, yamlCathodeInt, [300], [1e5])
829
830    def test_ch4_ion(self):
831        cti2yaml.convert(
832            self.test_data_path / "ch4_ion.cti",
833            self.test_work_path / "ch4_ion.yaml",
834        )
835        ctiGas, yamlGas = self.checkConversion("ch4_ion")
836        self.checkThermo(ctiGas, yamlGas, [300, 500, 1300, 2000])
837        self.checkKinetics(ctiGas, yamlGas, [900, 1800], [2e5, 20e5])
838        self.checkTransport(ctiGas, yamlGas, [298, 1001, 2400])
839
840class ctml2yamlTest(utilities.CanteraTest):
841
842    def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(),
843                        yamlphases=(), **kwargs):
844        ctmlPhase = cls(basename + '.xml', adjacent=ctmlphases, **kwargs)
845        yamlPhase = cls(basename + '.yaml', adjacent=yamlphases, **kwargs)
846
847        self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
848        self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
849        self.assertEqual(ctmlPhase.n_reactions, yamlPhase.n_reactions)
850        for C, Y in zip(ctmlPhase.species(), yamlPhase.species()):
851            self.assertEqual(C.composition, Y.composition)
852
853        for C, Y in zip(ctmlPhase.reactions(), yamlPhase.reactions()):
854            self.assertEqual(C.__class__, Y.__class__)
855            self.assertEqual(C.reactants, Y.reactants)
856            self.assertEqual(C.products, Y.products)
857            self.assertEqual(C.duplicate, Y.duplicate)
858
859        for i, sp in zip(range(ctmlPhase.n_reactions), ctmlPhase.kinetics_species_names):
860            self.assertEqual(ctmlPhase.reactant_stoich_coeff(sp, i),
861                             yamlPhase.reactant_stoich_coeff(sp, i))
862
863        return ctmlPhase, yamlPhase
864
865    def checkThermo(self, ctmlPhase, yamlPhase, temperatures, pressure=ct.one_atm, tol=1e-7):
866        for T in temperatures:
867            ctmlPhase.TP = T, pressure
868            yamlPhase.TP = T, pressure
869            cp_ctml = ctmlPhase.partial_molar_cp
870            cp_yaml = yamlPhase.partial_molar_cp
871            h_ctml = ctmlPhase.partial_molar_enthalpies
872            h_yaml = yamlPhase.partial_molar_enthalpies
873            s_ctml = ctmlPhase.partial_molar_entropies
874            s_yaml = yamlPhase.partial_molar_entropies
875            self.assertNear(ctmlPhase.density, yamlPhase.density)
876            for i in range(ctmlPhase.n_species):
877                message = ' for species {0} at T = {1}'.format(ctmlPhase.species_names[i], T)
878                self.assertNear(cp_ctml[i], cp_yaml[i], tol, msg='cp'+message)
879                self.assertNear(h_ctml[i], h_yaml[i], tol, msg='h'+message)
880                self.assertNear(s_ctml[i], s_yaml[i], tol, msg='s'+message)
881
882    def checkKinetics(self, ctmlPhase, yamlPhase, temperatures, pressures, tol=1e-7):
883        for T,P in itertools.product(temperatures, pressures):
884            ctmlPhase.TP = T, P
885            yamlPhase.TP = T, P
886            kf_ctml = ctmlPhase.forward_rate_constants
887            kr_ctml = ctmlPhase.reverse_rate_constants
888            kf_yaml = yamlPhase.forward_rate_constants
889            kr_yaml = yamlPhase.reverse_rate_constants
890            for i in range(yamlPhase.n_reactions):
891                message = ' for reaction {0} at T = {1}, P = {2}'.format(i, T, P)
892                self.assertNear(kf_ctml[i], kf_yaml[i], rtol=tol, msg='kf '+message)
893                self.assertNear(kr_ctml[i], kr_yaml[i], rtol=tol, msg='kr '+message)
894
895    def checkTransport(self, ctmlPhase, yamlPhase, temperatures,
896                       model='mixture-averaged'):
897        ctmlPhase.transport_model = model
898        yamlPhase.transport_model = model
899        for T in temperatures:
900            ctmlPhase.TP = T, ct.one_atm
901            yamlPhase.TP = T, ct.one_atm
902            self.assertNear(ctmlPhase.viscosity, yamlPhase.viscosity)
903            self.assertNear(ctmlPhase.thermal_conductivity,
904                            yamlPhase.thermal_conductivity)
905            Dkm_ctml = ctmlPhase.mix_diff_coeffs
906            Dkm_yaml = yamlPhase.mix_diff_coeffs
907            for i in range(ctmlPhase.n_species):
908                message = 'dkm for species {0} at T = {1}'.format(i, T)
909                self.assertNear(Dkm_ctml[i], Dkm_yaml[i], msg=message)
910
911    @utilities.slow_test
912    def test_gri30(self):
913        ctml2yaml.convert(
914            self.cantera_data_path / "gri30.xml",
915            self.test_work_path / "gri30.yaml",
916        )
917        ctmlPhase, yamlPhase = self.checkConversion('gri30')
918        X = {'O2': 0.3, 'H2': 0.1, 'CH4': 0.2, 'CO2': 0.4}
919        ctmlPhase.X = X
920        yamlPhase.X = X
921        self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000])
922        self.checkKinetics(ctmlPhase, yamlPhase, [900, 1800], [2e5, 20e5])
923        self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2400])
924
925    def test_pdep(self):
926        ctml2yaml.convert(
927            self.test_data_path / "pdep-test.xml",
928            self.test_work_path / "pdep-test.yaml",
929        )
930        ctmlPhase, yamlPhase = self.checkConversion('pdep-test')
931        self.checkKinetics(ctmlPhase, yamlPhase, [300, 1000, 2200],
932                           [100, ct.one_atm, 2e5, 2e6, 9.9e6])
933
934    def test_ptcombust(self):
935        ctml2yaml.convert(
936            self.cantera_data_path / "ptcombust.xml",
937            self.test_work_path / "ptcombust.yaml",
938        )
939        ctmlGas, yamlGas = self.checkConversion('ptcombust')
940        ctmlSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface,
941            name='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas])
942
943        self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5])
944        self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600])
945        self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5])
946
947    def test_ptcombust_motzwise(self):
948        ctml2yaml.convert(
949            self.test_data_path / "ptcombust-motzwise.xml",
950            self.test_work_path / "ptcombust-motzwise.yaml",
951        )
952        ctmlGas, yamlGas = self.checkConversion('ptcombust-motzwise')
953        ctmlSurf, yamlSurf = self.checkConversion('ptcombust-motzwise', ct.Interface,
954            name='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas])
955
956        self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5])
957        self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600])
958        self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5])
959
960    def test_sofc(self):
961        ctml2yaml.convert(
962            self.cantera_data_path / "sofc.xml",
963            self.test_work_path / "sofc.yaml",
964        )
965        ctmlGas, yamlGas = self.checkConversion('sofc')
966        ctmlMetal, yamlMetal = self.checkConversion('sofc', name='metal')
967        ctmlOxide, yamlOxide = self.checkConversion('sofc', name='oxide_bulk')
968        ctmlMSurf, yamlMSurf = self.checkConversion('sofc', ct.Interface,
969            name='metal_surface', ctmlphases=[ctmlGas, ctmlMetal],
970            yamlphases=[yamlGas, yamlMetal])
971        ctmlOSurf, yamlOSurf = self.checkConversion('sofc', ct.Interface,
972            name='oxide_surface', ctmlphases=[ctmlGas, ctmlOxide],
973            yamlphases=[yamlGas, yamlOxide])
974        ctml_tpb, yaml_tpb = self.checkConversion('sofc', ct.Interface,
975            name='tpb', ctmlphases=[ctmlMetal, ctmlMSurf, ctmlOSurf],
976            yamlphases=[yamlMetal, yamlMSurf, yamlOSurf])
977
978        self.checkThermo(ctmlMSurf, yamlMSurf, [900, 1000, 1100])
979        self.checkThermo(ctmlOSurf, yamlOSurf, [900, 1000, 1100])
980        ctmlMetal.electric_potential = yamlMetal.electric_potential = 2
981        self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5])
982        ctmlMetal.electric_potential = yamlMetal.electric_potential = 4
983        self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5])
984
985    def test_liquidvapor(self):
986        output_file = self.test_work_path / "liquidvapor.yaml"
987        ctml2yaml.convert(
988            self.cantera_data_path / "liquidvapor.xml", output_file,
989        )
990        for name in ['water', 'nitrogen', 'methane', 'hydrogen', 'oxygen',
991                     'hfc134a', 'carbondioxide', 'heptane']:
992            ctmlPhase, yamlPhase = self.checkConversion('liquidvapor', name=name)
993            self.checkThermo(ctmlPhase, yamlPhase,
994                             [1.3 * ctmlPhase.min_temp, 0.7 * ctmlPhase.max_temp])
995        # The output file must be removed after this test because it shadows
996        # a file distributed with Cantera that is used in other tests.
997        output_file.unlink()
998
999    def test_Redlich_Kwong_CO2(self):
1000        ctml2yaml.convert(
1001            self.test_data_path / "co2_RK_example.xml",
1002            self.test_work_path / "co2_RK_example.yaml",
1003        )
1004        ctmlGas, yamlGas = self.checkConversion('co2_RK_example')
1005        for P in [1e5, 2e6, 1.3e7]:
1006            yamlGas.TP = ctmlGas.TP = 300, P
1007            self.checkThermo(ctmlGas, yamlGas, [300, 400, 500])
1008
1009    @utilities.slow_test
1010    def test_Redlich_Kwong_ndodecane(self):
1011        ctml2yaml.convert(
1012            self.cantera_data_path / "nDodecane_Reitz.xml",
1013            self.test_work_path / "nDodecane_Reitz.yaml",
1014        )
1015        ctmlGas, yamlGas = self.checkConversion('nDodecane_Reitz')
1016        self.checkThermo(ctmlGas, yamlGas, [300, 400, 500])
1017        self.checkKinetics(ctmlGas, yamlGas, [300, 500, 1300], [1e5, 2e6, 1.4e7],
1018                           1e-6)
1019
1020    def test_diamond(self):
1021        ctml2yaml.convert(
1022            self.cantera_data_path / "diamond.xml",
1023            self.test_work_path / "diamond.yaml",
1024        )
1025        ctmlGas, yamlGas = self.checkConversion('diamond', name='gas')
1026        ctmlSolid, yamlSolid = self.checkConversion('diamond', name='diamond')
1027        ctmlSurf, yamlSurf = self.checkConversion('diamond',
1028            ct.Interface, name='diamond_100', ctmlphases=[ctmlGas, ctmlSolid],
1029            yamlphases=[yamlGas, yamlSolid])
1030        self.checkThermo(ctmlSolid, yamlSolid, [300, 500])
1031        self.checkThermo(ctmlSurf, yamlSurf, [330, 490])
1032        self.checkKinetics(ctmlSurf, yamlSurf, [400, 800], [2e5])
1033
1034    def test_lithium_ion_battery(self):
1035        name = 'lithium_ion_battery'
1036        ctml2yaml.convert(
1037            self.cantera_data_path / (name + ".xml"),
1038            self.test_work_path / (name + ".yaml"),
1039        )
1040        ctmlAnode, yamlAnode = self.checkConversion(name, name='anode')
1041        ctmlCathode, yamlCathode = self.checkConversion(name, name='cathode')
1042        ctmlMetal, yamlMetal = self.checkConversion(name, name='electron')
1043        ctmlElyt, yamlElyt = self.checkConversion(name, name='electrolyte')
1044        ctmlAnodeInt, yamlAnodeInt = self.checkConversion(name,
1045            name='edge_anode_electrolyte',
1046            ctmlphases=[ctmlAnode, ctmlMetal, ctmlElyt],
1047            yamlphases=[yamlAnode, yamlMetal, yamlElyt])
1048        ctmlCathodeInt, yamlCathodeInt = self.checkConversion(name,
1049            name='edge_cathode_electrolyte',
1050            ctmlphases=[ctmlCathode, ctmlMetal, ctmlElyt],
1051            yamlphases=[yamlCathode, yamlMetal, yamlElyt])
1052
1053        self.checkThermo(ctmlAnode, yamlAnode, [300, 330])
1054        self.checkThermo(ctmlCathode, yamlCathode, [300, 330])
1055
1056        ctmlAnode.X = yamlAnode.X = [0.7, 0.3]
1057        self.checkThermo(ctmlAnode, yamlAnode, [300, 330])
1058        ctmlCathode.X = yamlCathode.X = [0.2, 0.8]
1059        self.checkThermo(ctmlCathode, yamlCathode, [300, 330])
1060
1061        for phase in [ctmlAnode, yamlAnode, ctmlCathode, yamlCathode, ctmlMetal,
1062                      yamlMetal, ctmlElyt, yamlElyt, ctmlAnodeInt, yamlAnodeInt,
1063                      ctmlCathodeInt, yamlCathodeInt]:
1064            phase.TP = 300, 1e5
1065        ctmlMetal.electric_potential = yamlMetal.electric_potential = 0
1066        ctmlElyt.electric_potential = yamlElyt.electric_potential = 1.9
1067        self.checkKinetics(ctmlAnodeInt, yamlAnodeInt, [300], [1e5])
1068
1069        ctmlMetal.electric_potential = yamlMetal.electric_potential = 2.2
1070        ctmlElyt.electric_potential = yamlElyt.electric_potential = 0
1071        self.checkKinetics(ctmlCathodeInt, yamlCathodeInt, [300], [1e5])
1072
1073    def test_noxNeg(self):
1074        ctml2yaml.convert(
1075            self.test_data_path / "noxNeg.xml",
1076            self.test_work_path / "noxNeg.yaml",
1077        )
1078        ctmlGas, yamlGas = self.checkConversion('noxNeg')
1079        self.checkThermo(ctmlGas, yamlGas, [300, 1000])
1080        self.checkKinetics(ctmlGas, yamlGas, [300, 1000], [1e5])
1081
1082    def test_ch4_ion(self):
1083        ctml2yaml.convert(
1084            self.test_data_path / "ch4_ion.xml",
1085            self.test_work_path / "ch4_ion.yaml",
1086        )
1087        ctmlGas, yamlGas = self.checkConversion("ch4_ion")
1088        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1089        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1090        self.checkTransport(ctmlGas, yamlGas, [298, 1001, 2400])
1091
1092    def test_nasa9(self):
1093        ctml2yaml.convert(
1094            self.test_data_path / "nasa9-test.xml",
1095            self.test_work_path / "nasa9-test.yaml",
1096        )
1097        ctmlGas, yamlGas = self.checkConversion("nasa9-test")
1098        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1099
1100    def test_chemically_activated(self):
1101        ctml2yaml.convert(
1102            self.test_data_path / "chemically-activated-reaction.xml",
1103            self.test_work_path / "chemically-activated-reaction.yaml",
1104        )
1105        ctmlGas, yamlGas = self.checkConversion("chemically-activated-reaction")
1106        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1107        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1108
1109    def test_explicit_forward_order(self):
1110        ctml2yaml.convert(
1111            self.test_data_path / "explicit-forward-order.xml",
1112            self.test_work_path / "explicit-forward-order.yaml",
1113        )
1114        ctmlGas, yamlGas = self.checkConversion("explicit-forward-order")
1115        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1116        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1117
1118    def test_explicit_reverse_rate(self):
1119        ctml2yaml.convert(
1120            self.test_data_path / "explicit-reverse-rate.xml",
1121            self.test_work_path / "explicit-reverse-rate.yaml",
1122        )
1123        ctmlGas, yamlGas = self.checkConversion("explicit-reverse-rate")
1124        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1125        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1126
1127    def test_explicit_third_bodies(self):
1128        ctml2yaml.convert(
1129            self.test_data_path / "explicit-third-bodies.xml",
1130            self.test_work_path / "explicit-third-bodies.yaml",
1131        )
1132        ctmlGas, yamlGas = self.checkConversion("explicit-third-bodies")
1133        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1134        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1135
1136    def test_fractional_stoich_coeffs(self):
1137        ctml2yaml.convert(
1138            self.test_data_path / "frac.xml",
1139            self.test_work_path / "frac.yaml",
1140        )
1141        ctmlGas, yamlGas = self.checkConversion("frac")
1142        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1143        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1144
1145    def test_water_IAPWS95_thermo(self):
1146        ctml2yaml.convert(
1147            self.test_data_path / "liquid-water.xml",
1148            self.test_work_path / "liquid-water.yaml",
1149        )
1150        ctmlWater, yamlWater = self.checkConversion("liquid-water")
1151        self.checkThermo(ctmlWater, yamlWater, [300, 500, 1300, 2000], pressure=22064000.0)
1152        self.assertEqual(ctmlWater.transport_model, yamlWater.transport_model)
1153        dens = ctmlWater.density
1154        for T in [298, 1001, 2400]:
1155            ctmlWater.TD = T, dens
1156            yamlWater.TD = T, dens
1157            self.assertNear(ctmlWater.viscosity, yamlWater.viscosity)
1158            self.assertNear(ctmlWater.thermal_conductivity,
1159                            yamlWater.thermal_conductivity)
1160
1161    def test_hmw_nacl_phase(self):
1162        basename = "HMW_NaCl_sp1977_alt"
1163        xml_file = self.test_data_path / (basename + ".xml")
1164        yaml_file = self.test_data_path / (basename + ".yaml")
1165        ctml2yaml.convert(xml_file, yaml_file)
1166
1167        # Can only be loaded by ThermoPhase due to a bug in TransportFactory
1168        # ThermoPhase does not have reactions (neither does the input file)
1169        # so we can't use checkConversion
1170        ctmlPhase = ct.ThermoPhase(str(xml_file))
1171        yamlPhase = ct.ThermoPhase(str(yaml_file))
1172        self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
1173        self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
1174        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1175
1176    def test_NaCl_solid_phase(self):
1177        ctml2yaml.convert(
1178            self.test_data_path / "NaCl_Solid.xml",
1179            self.test_work_path / "NaCl_Solid.yaml",
1180        )
1181        ctmlPhase, yamlPhase = self.checkConversion("NaCl_Solid")
1182        self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000])
1183
1184    def test_DH_NaCl_phase(self):
1185        ctml2yaml.convert(
1186            self.test_data_path / "debye-huckel-all.xml",
1187            self.test_work_path / "debye-huckel-all.yaml",
1188        )
1189        for name in [
1190            "debye-huckel-dilute",
1191            "debye-huckel-B-dot-ak",
1192            "debye-huckel-B-dot-a",
1193            "debye-huckel-pitzer-beta_ij",
1194            "debye-huckel-beta_ij",
1195        ]:
1196            # Can only be loaded by ThermoPhase due to a bug in TransportFactory
1197            # ThermoPhase does not have reactions (neither does the input file)
1198            # so we can't use checkConversion
1199            ctmlPhase = ct.ThermoPhase("debye-huckel-all.xml", name=name)
1200            yamlPhase = ct.ThermoPhase("debye-huckel-all.yaml", name=name)
1201            self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
1202            self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
1203            self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1204
1205    def test_Maskell_solid_soln(self):
1206        ctml2yaml.convert(
1207            self.test_data_path / "MaskellSolidSolnPhase_valid.xml",
1208            self.test_work_path / "MaskellSolidSolnPhase_valid.yaml",
1209        )
1210
1211        ctmlPhase, yamlPhase = self.checkConversion("MaskellSolidSolnPhase_valid")
1212        # Maskell phase doesn't support partial molar properties, so just check density
1213        for T in [300, 500, 1300, 2000]:
1214            ctmlPhase.TP = T, ct.one_atm
1215            yamlPhase.TP = T, ct.one_atm
1216            self.assertNear(ctmlPhase.density, yamlPhase.density)
1217
1218    def test_mock_ion(self):
1219        ctml2yaml.convert(
1220            self.test_data_path / "mock_ion.xml",
1221            self.test_work_path / "mock_ion.yaml",
1222        )
1223        ctmlPhase = ct.ThermoPhase("mock_ion.xml")
1224        yamlPhase = ct.ThermoPhase("mock_ion.yaml")
1225        # Due to changes in how the species elements are specified, the composition
1226        # of the species differs from XML to YAML (electrons are used to specify charge
1227        # in YAML while the charge node is used in XML). Therefore, checkConversion
1228        # won't work and we have to check a few things manually. There are also no
1229        # reactions specified for these phases so don't need to do any checks for that.
1230        self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
1231        self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
1232        # ions-from-neutral-molecule phase doesn't support partial molar properties,
1233        # so just check density
1234        for T in [300, 500, 1300, 2000]:
1235            ctmlPhase.TP = T, ct.one_atm
1236            yamlPhase.TP = T, ct.one_atm
1237            self.assertNear(ctmlPhase.density, yamlPhase.density)
1238
1239    def test_Redlich_Kister(self):
1240        ctml2yaml.convert(
1241            self.test_data_path / "RedlichKisterVPSSTP_valid.xml",
1242            self.test_work_path / "RedlichKisterVPSSTP_valid.yaml",
1243        )
1244
1245        ctmlPhase, yamlPhase = self.checkConversion("RedlichKisterVPSSTP_valid")
1246        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1247
1248    def test_species_names(self):
1249        ctml2yaml.convert(
1250            self.test_data_path / "species-names.xml",
1251            self.test_work_path / "species-names.yaml",
1252        )
1253        ctmlGas, yamlGas = self.checkConversion('species-names')
1254        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1255
1256    def test_sri_falloff_reaction(self):
1257        ctml2yaml.convert(
1258            self.test_data_path / "sri-falloff.xml",
1259            self.test_work_path / "sri-falloff.yaml",
1260        )
1261        ctmlGas, yamlGas = self.checkConversion("sri-falloff")
1262        self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000])
1263        self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5])
1264
1265    def test_vpss_and_hkft(self):
1266        ctml2yaml.convert(
1267            self.test_data_path / "pdss_hkft.xml",
1268            self.test_work_path / "pdss_hkft.yaml",
1269        )
1270        ctmlPhase = ct.ThermoPhase("pdss_hkft.xml")
1271        yamlPhase = ct.ThermoPhase("pdss_hkft.yaml")
1272        # Due to changes in how the species elements are specified, the
1273        # composition of the species differs from XML to YAML (electrons are used
1274        # to specify charge in YAML while the charge node is used in XML).
1275        # Therefore, checkConversion won't work and we have to check a few things
1276        # manually. There are also no reactions specified for these phases so don't
1277        # need to do any checks for that.
1278        self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
1279        self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
1280        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1281
1282    def test_lattice_solid(self):
1283        ctml2yaml.convert(
1284            self.test_data_path / "Li7Si3_ls.xml",
1285            self.test_work_path / "Li7Si3_ls.yaml",
1286        )
1287        # Use ThermoPhase to avoid constructing a default Transport object which
1288        # throws an error for the LatticeSolidPhase
1289        basename = "Li7Si3_ls"
1290        name = "Li7Si3_and_Interstitials(S)"
1291        ctmlPhase = ct.ThermoPhase(basename + ".xml", name=name)
1292        yamlPhase = ct.ThermoPhase(basename + ".yaml", name=name)
1293        self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
1294        self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
1295        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1296
1297    def test_margules(self):
1298        ctml2yaml.convert(
1299            self.test_data_path / "LiKCl_liquid.xml",
1300            self.test_work_path / "LiKCl_liquid.yaml",
1301        )
1302        ctmlPhase, yamlPhase = self.checkConversion("LiKCl_liquid")
1303        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1304
1305    def test_idealsolidsoln(self):
1306        with self.assertWarnsRegex(UserWarning, "SolidKinetics type is not implemented"):
1307            ctml2yaml.convert(
1308                self.test_data_path / "IdealSolidSolnPhaseExample.xml",
1309                self.test_work_path / "IdealSolidSolnPhaseExample.yaml",
1310            )
1311
1312        # SolidKinetics is not implemented, so can't create a Kinetics class instance.
1313        basename = "IdealSolidSolnPhaseExample"
1314        ctmlPhase = ct.ThermoPhase(basename + ".xml")
1315        yamlPhase = ct.ThermoPhase(basename + ".yaml")
1316
1317        self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names)
1318        self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names)
1319        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1320
1321    def test_idealmolalsoln(self):
1322        ctml2yaml.convert(
1323            self.test_data_path / "IdealMolalSolnPhaseExample.xml",
1324            self.test_work_path / "IdealMolalSolnPhaseExample.yaml",
1325        )
1326
1327        ctmlPhase, yamlPhase = self.checkConversion("IdealMolalSolnPhaseExample")
1328        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1329
1330    def test_transport_models(self):
1331        ctml2yaml.convert(
1332            self.test_data_path / "transport_models_test.xml",
1333            self.test_work_path / "transport_models_test.yaml",
1334        )
1335        for name in ["UnityLewis", "CK_Mix", "CK_Multi", "HighP"]:
1336            ctmlPhase, yamlPhase = self.checkConversion("transport_models_test", name=name)
1337            self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2500])
1338
1339    def test_nonreactant_orders(self):
1340        ctml2yaml.convert(
1341            self.test_data_path / "reaction-orders.xml",
1342            self.test_work_path / "reaction-orders.yaml",
1343        )
1344
1345        ctmlPhase, yamlPhase = self.checkConversion("reaction-orders")
1346        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1347        self.checkKinetics(ctmlPhase, yamlPhase, [300, 1001, 2500], [1e5, 10e5])
1348
1349    def test_species_ss_temperature_polynomials(self):
1350        ctml2yaml.convert(
1351            self.test_data_path / "Li_Liquid.xml",
1352            self.test_work_path / "Li_Liquid.yaml",
1353        )
1354
1355        ctmlPhase, yamlPhase = self.checkConversion("Li_Liquid")
1356        self.checkThermo(ctmlPhase, yamlPhase, [300, 500])
1357
1358    def test_duplicate_section_ids(self):
1359        with self.assertWarnsRegex(UserWarning, "Duplicate 'speciesData' id"):
1360                ctml2yaml.convert(
1361                    self.test_data_path / "duplicate-speciesData-ids.xml",
1362                    self.test_work_path / "duplicate-speciesData-ids.yaml",
1363                )
1364        with self.assertWarnsRegex(UserWarning, "Duplicate 'reactionData' id"):
1365                ctml2yaml.convert(
1366                    self.test_data_path / "duplicate-reactionData-ids.xml",
1367                    self.test_work_path / "duplicate-reactionData-ids.yaml",
1368                )
1369