From b49359f18c8caecf1c3b62a4414f62f2dddb7a73 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 14 Aug 2019 15:02:55 -0400 Subject: [PATCH 01/99] [ctml2yaml] Add first working version of ctml2yaml Correctly converts GRI-3.0 from CTML to YAML format. --- interfaces/cython/cantera/ctml2yaml.py | 252 ++++++++++++++++++ .../cython/cantera/test/test_convert.py | 90 ++++++- 2 files changed, 341 insertions(+), 1 deletion(-) create mode 100644 interfaces/cython/cantera/ctml2yaml.py diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py new file mode 100644 index 00000000000..646625c6233 --- /dev/null +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -0,0 +1,252 @@ +""" +This file will convert CTML format files to YAML. +""" + +from pathlib import Path +import sys + +import xml.etree.ElementTree as etree + +try: + import ruamel_yaml as yaml +except ImportError: + from ruamel import yaml + +thermo_model_mapping = {"IdealGas": "ideal-gas"} +kinetics_model_mapping = {"GasKinetics": "gas"} +transport_model_mapping = { + "Mix": "mixture-averaged", + "Multi": "multi-component", + "None": None, +} +species_thermo_mapping = {"NASA": "NASA7"} +species_transport_mapping = {"gas_transport": "gas"} + + +def process_three_body(rate_coeff): + """Process a three-body reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attribs = {"type": "three-body"} + reaction_attribs["rate-constant"] = process_arrhenius_parameters( + rate_coeff.find("Arrhenius") + ) + reaction_attribs["efficiencies"] = process_efficiencies( + rate_coeff.find("efficiencies") + ) + + return reaction_attribs + + +def process_lindemann(rate_coeff): + """Process a Lindemann falloff reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attribs = {"type": "falloff"} + for arr_coeff in rate_coeff.iterfind("Arrhenius"): + if arr_coeff.get("name") is not None and arr_coeff.get("name") == "k0": + reaction_attribs["low-P-rate-constant"] = process_arrhenius_parameters( + arr_coeff + ) + elif arr_coeff.get("name") is None: + reaction_attribs["high-P-rate-constant"] = process_arrhenius_parameters( + arr_coeff + ) + else: + raise TypeError("Too many Arrhenius nodes") + reaction_attribs["efficiencies"] = process_efficiencies( + rate_coeff.find("efficiencies") + ) + + return reaction_attribs + + +def process_troe(rate_coeff): + """Process a Troe falloff reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + # This gets the low-p and high-p rate constants and the efficiencies + reaction_attribs = process_lindemann(rate_coeff) + + troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() + troe_names = ["A", "T3", "T1", "T2"] + reaction_attribs["Troe"] = {} + # zip stops when the shortest iterable is exhausted. If T2 is not present + # in the Troe parameters (i.e., troe_params is three elements long), it + # will be omitted here as well. + for name, param in zip(troe_names, troe_params): + reaction_attribs["Troe"].update({name: float(param)}) + + return reaction_attribs + + +def process_arrhenius(rate_coeff): + """Process a standard Arrhenius-type reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + return {"rate-constant": process_arrhenius_parameters(rate_coeff.find("Arrhenius"))} + + +def process_arrhenius_parameters(arr_node): + """Process the parameters from an Arrhenius child of a rateCoeff node.""" + rate_constant = {} + A = arr_node.find("A") + rate_constant["A"] = A.text.strip() + if A.get("units") is not None: + rate_constant["A"] += " {}".format(A.get("units")) + + # Can units for b ever be specified? I don't think so... + rate_constant["b"] = arr_node.find("b").text.strip() + + Ea = arr_node.find("E") + rate_constant["Ea"] = Ea.text.strip() + if Ea.get("units") is not None: + rate_constant["Ea"] += " {}".format(Ea.get("units")) + + return rate_constant + + +def process_efficiencies(eff_node): + """Process the efficiency information about a reaction.""" + efficiencies = {} + effs = eff_node.text.replace("\n", " ").strip().split() + # Is there any way to do this with a comprehension? + for eff in effs: + s, e = eff.split(":") + efficiencies[s] = float(e) + + return efficiencies + + +reaction_type_mapping = { + "threeBody": process_three_body, + None: process_arrhenius, + "Lindemann": process_lindemann, + "Troe": process_troe, +} + + +def process_NASA7_thermo(thermo): + """Process a NASA 7 thermo entry from XML to a dictionary.""" + thermo_attribs = {"model": "NASA7", "data": []} + temperature_ranges = set() + for model in thermo.iterfind("NASA"): + temperature_ranges.add(float(model.get("Tmin"))) + temperature_ranges.add(float(model.get("Tmax"))) + coeffs = model.find("floatArray").text.replace("\n", " ").strip().split(",") + thermo_attribs["data"].append(list(map(float, coeffs))) + assert ( + len(temperature_ranges) == 3 + ), "The midpoint temperature is not consistent between NASA7 entries" + thermo_attribs["temperature-ranges"] = sorted(list(temperature_ranges)) + return thermo_attribs + + +def convert(inpfile, outfile): + """Convert an input CTML file to a YAML file.""" + inpfile = Path(inpfile) + ctml_tree = etree.parse(str(inpfile)).getroot() + phases = [] + for phase in ctml_tree.iterfind("phase"): + phase_attribs = {"name": phase.get("id")} + phase_attribs["thermo"] = thermo_model_mapping[ + phase.find("thermo").get("model") + ] + phase_attribs["elements"] = phase.find("elementArray").text.strip().split() + phase_attribs["species"] = ( + phase.find("speciesArray").text.replace("\n", " ").strip().split() + ) + phase_attribs["kinetics"] = kinetics_model_mapping[ + phase.find("kinetics").get("model") + ] + transport_model = transport_model_mapping[phase.find("transport").get("model")] + if transport_model is not None: + phase_attribs["transport-model"] = transport_model + phases.append(phase_attribs) + + species_data = [] + for species in ctml_tree.find("speciesData").iterfind("species"): + species_attribs = {"name": species.get("name")} + composition = {} + for element_amount in species.find("atomArray").text.strip().split(): + element, num = element_amount.split(":") + composition[element] = num + + species_attribs["composition"] = composition + species_attribs["note"] = species.find("note").text + + thermo = species.find("thermo") + if thermo[0].tag == "NASA": + species_attribs["thermo"] = process_NASA7_thermo(thermo) + else: + raise TypeError("Unknown thermo model type: {}".format(thermo[0].tag)) + + transport = species.find("transport") + if transport is not None: + transport_attribs = {} + transport_attribs["model"] = species_transport_mapping[ + transport.get("model") + ] + transport_attribs["geometry"] = transport.findtext( + "string[@title='geometry']" + ) + transport_attribs["well-depth"] = transport.findtext("LJ_welldepth") + transport_attribs["diameter"] = transport.findtext("LJ_diameter") + transport_attribs["polarizability"] = transport.findtext("polarizability") + transport_attribs["rotational-relaxation"] = transport.findtext("rotRelax") + transport_attribs["dipole"] = transport.findtext("dipoleMoment") + transport.findtext("dispersion_coefficient") + transport.findtext("quadrupole_polarizability") + + species_attribs["transport"] = transport_attribs + + species_data.append(species_attribs) + + reaction_data = [] + for reaction in ctml_tree.find("reactionData").iterfind("reaction"): + reaction_attribs = {} + reaction_type = reaction.get("type") + rate_coeff = reaction.find("rateCoeff") + if reaction_type in [None, "threeBody"]: + reaction_attribs.update(reaction_type_mapping[reaction_type](rate_coeff)) + elif reaction_type in ["falloff"]: + sub_type = rate_coeff.find("falloff").get("type") + if sub_type not in ["Lindemann", "Troe"]: + raise TypeError( + "Unknown falloff type '{}' for reaction id {}".format( + sub_type, reaction.get("id") + ) + ) + else: + reaction_attribs.update(reaction_type_mapping[sub_type](rate_coeff)) + else: + raise TypeError( + "Unknown reaction type '{}' for reaction id {}".format( + reaction_type, reaction.get("id") + ) + ) + + reaction_attribs["equation"] = ( + reaction.find("equation").text.replace("[", "<").replace("]", ">") + ) + + if reaction.get("duplicate", "") == "yes": + reaction_attribs["duplicate"] = True + + reaction_data.append(reaction_attribs) + + yaml_doc = {"phases": phases, "species": species_data, "reactions": reaction_data} + yaml_obj = yaml.YAML(typ="safe") + yaml_obj.dump(yaml_doc, Path(outfile)) + + +if __name__ == "__main__": + convert(sys.argv[1], sys.argv[2]) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 37541c12a55..ec016391c40 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -5,7 +5,8 @@ from . import utilities import cantera as ct -from cantera import ck2cti, ck2yaml, cti2yaml +from cantera import ck2cti, ck2yaml, cti2yaml, ctml2yaml + class converterTestCommon: def convert(self, inputFile, thermo=None, transport=None, @@ -729,3 +730,90 @@ def test_ch4_ion(self): self.checkThermo(ctiGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctiGas, yamlGas, [900, 1800], [2e5, 20e5]) self.checkTransport(ctiGas, yamlGas, [298, 1001, 2400]) + + +class ctml2yamlTest(utilities.CanteraTest): + @classmethod + def setUpClass(cls): + super().setUpClass() + ctml2yaml.convert(Path(cls.cantera_data).joinpath('gri30.xml'), + Path(cls.test_work_dir).joinpath('gri30.yaml')) + + def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(), + yamlphases=(), **kwargs): + ctmlPhase = cls(basename + '.xml', phases=ctmlphases, **kwargs) + yamlPhase = cls(basename + '.yaml', phases=yamlphases, **kwargs) + + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + self.assertEqual(ctmlPhase.n_reactions, yamlPhase.n_reactions) + for C, Y in zip(ctmlPhase.species(), yamlPhase.species()): + self.assertEqual(C.composition, Y.composition) + + for i, (C, Y) in enumerate(zip(ctmlPhase.reactions(), + yamlPhase.reactions())): + self.assertEqual(C.__class__, Y.__class__) + self.assertEqual(C.reactants, Y.reactants) + self.assertEqual(C.products, Y.products) + self.assertEqual(C.duplicate, Y.duplicate) + + for i, sp in zip(range(ctmlPhase.n_reactions), ctmlPhase.kinetics_species_names): + self.assertEqual(ctmlPhase.reactant_stoich_coeff(sp, i), + yamlPhase.reactant_stoich_coeff(sp, i)) + + return ctmlPhase, yamlPhase + + def checkThermo(self, ctmlPhase, yamlPhase, temperatures, tol=1e-7): + for T in temperatures: + ctmlPhase.TP = T, ct.one_atm + yamlPhase.TP = T, ct.one_atm + cp_ctml = ctmlPhase.partial_molar_cp + cp_yaml = yamlPhase.partial_molar_cp + h_ctml = ctmlPhase.partial_molar_enthalpies + h_yaml = yamlPhase.partial_molar_enthalpies + s_ctml = ctmlPhase.partial_molar_entropies + s_yaml = yamlPhase.partial_molar_entropies + self.assertNear(ctmlPhase.density, yamlPhase.density) + for i in range(ctmlPhase.n_species): + message = ' for species {0} at T = {1}'.format(i, T) + self.assertNear(cp_ctml[i], cp_yaml[i], tol, msg='cp'+message) + self.assertNear(h_ctml[i], h_yaml[i], tol, msg='h'+message) + self.assertNear(s_ctml[i], s_yaml[i], tol, msg='s'+message) + + def checkKinetics(self, ctmlPhase, yamlPhase, temperatures, pressures, tol=1e-7): + for T,P in itertools.product(temperatures, pressures): + ctmlPhase.TP = T, P + yamlPhase.TP = T, P + kf_ctml = ctmlPhase.forward_rate_constants + kr_ctml = ctmlPhase.reverse_rate_constants + kf_yaml = yamlPhase.forward_rate_constants + kr_yaml = yamlPhase.reverse_rate_constants + for i in range(yamlPhase.n_reactions): + message = ' for reaction {0} at T = {1}, P = {2}'.format(i, T, P) + self.assertNear(kf_ctml[i], kf_yaml[i], rtol=tol, msg='kf '+message) + self.assertNear(kr_ctml[i], kr_yaml[i], rtol=tol, msg='kr '+message) + + def checkTransport(self, ctmlPhase, yamlPhase, temperatures, + model='mixture-averaged'): + ctmlPhase.transport_model = model + yamlPhase.transport_model = model + for T in temperatures: + ctmlPhase.TP = T, ct.one_atm + yamlPhase.TP = T, ct.one_atm + self.assertNear(ctmlPhase.viscosity, yamlPhase.viscosity) + self.assertNear(ctmlPhase.thermal_conductivity, + yamlPhase.thermal_conductivity) + Dkm_ctml = ctmlPhase.mix_diff_coeffs + Dkm_yaml = yamlPhase.mix_diff_coeffs + for i in range(ctmlPhase.n_species): + message = 'dkm for species {0} at T = {1}'.format(i, T) + self.assertNear(Dkm_ctml[i], Dkm_yaml[i], msg=message) + + def test_gri30(self): + ctmlPhase, yamlPhase = self.checkConversion('gri30') + X = {'O2': 0.3, 'H2': 0.1, 'CH4': 0.2, 'CO2': 0.4} + ctmlPhase.X = X + yamlPhase.X = X + self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlPhase, yamlPhase, [900, 1800], [2e5, 20e5]) + self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2400]) From 4c8050de4bf58d6211c3a291b18c7fb9daa2c7f7 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Thu, 15 Aug 2019 11:44:33 -0400 Subject: [PATCH 02/99] [ctml2yaml] Simplify handling of species transport properties Check that values for properties are not equal to zero and only include them in that case. --- interfaces/cython/cantera/ctml2yaml.py | 53 ++++++++++++++++++++------ 1 file changed, 42 insertions(+), 11 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 646625c6233..0e4a88b8a43 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -12,6 +12,8 @@ except ImportError: from ruamel import yaml +import numpy as np + thermo_model_mapping = {"IdealGas": "ideal-gas"} kinetics_model_mapping = {"GasKinetics": "gas"} transport_model_mapping = { @@ -21,6 +23,15 @@ } species_thermo_mapping = {"NASA": "NASA7"} species_transport_mapping = {"gas_transport": "gas"} +transport_properties_mapping = { + "LJ_welldepth": "well-depth", + "LJ_diameter": "diameter", + "polarizability": "polarizability", + "rotRelax": "rotational-relaxation", + "dipoleMoment": "dipole", + "dispersion_coefficient": "dispersion-coefficient", + "quadrupole_polarizability": "quadrupole-polarizability", +} def process_three_body(rate_coeff): @@ -150,6 +161,20 @@ def process_NASA7_thermo(thermo): return thermo_attribs +def check_float_neq_zero(value, name): + """Check that the text value associated with a tag is non-zero. + + If the value is not zero, return a dictionary with the key ``name`` + and the value. If the value is zero, return an empty dictionary. + Calling functions can use this function to update a dictionary of + attributes without adding keys whose values are zero. + """ + if not np.isclose(value, 0.0): + return {name: value} + else: + return {} + + def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) @@ -187,24 +212,30 @@ def convert(inpfile, outfile): if thermo[0].tag == "NASA": species_attribs["thermo"] = process_NASA7_thermo(thermo) else: - raise TypeError("Unknown thermo model type: {}".format(thermo[0].tag)) + raise TypeError( + "Unknown thermo model type: '{}' for species '{}'".format( + thermo[0].tag, species.get("name") + ) + ) transport = species.find("transport") if transport is not None: transport_attribs = {} - transport_attribs["model"] = species_transport_mapping[ - transport.get("model") - ] + transport_attribs["model"] = species_transport_mapping.get( + transport.get("model"), False + ) + if not transport_attribs["model"]: + raise TypeError( + "Unknown transport model type: '{}' for species '{}'".format( + transport.get("model"), species.get("name") + ) + ) transport_attribs["geometry"] = transport.findtext( "string[@title='geometry']" ) - transport_attribs["well-depth"] = transport.findtext("LJ_welldepth") - transport_attribs["diameter"] = transport.findtext("LJ_diameter") - transport_attribs["polarizability"] = transport.findtext("polarizability") - transport_attribs["rotational-relaxation"] = transport.findtext("rotRelax") - transport_attribs["dipole"] = transport.findtext("dipoleMoment") - transport.findtext("dispersion_coefficient") - transport.findtext("quadrupole_polarizability") + for tag, name in transport_properties_mapping.items(): + value = float(transport.findtext(tag, default=0.0)) + transport_attribs.update(check_float_neq_zero(value, name)) species_attribs["transport"] = transport_attribs From 9f958a51038a453d329c8935d2f603ef74421ec2 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 16 Aug 2019 05:36:57 -0400 Subject: [PATCH 03/99] [ctml2yaml] Add pressure-dependent reactions --- interfaces/cython/cantera/ctml2yaml.py | 117 +++++++++++++++--- .../cython/cantera/test/test_convert.py | 7 ++ 2 files changed, 109 insertions(+), 15 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 0e4a88b8a43..5fb09a0f750 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -4,6 +4,7 @@ from pathlib import Path import sys +import re import xml.etree.ElementTree as etree @@ -33,6 +34,51 @@ "quadrupole_polarizability": "quadrupole-polarizability", } +# Improved float formatting requires Numpy >= 1.14 +if hasattr(np, "format_float_positional"): + + def float2string(data): + if data == 0: + return "0.0" + elif 0.01 <= abs(data) < 10000: + return np.format_float_positional(data, trim="0") + else: + return np.format_float_scientific(data, trim="0") + + +else: + + def float2string(data): + return repr(data) + + +def represent_float(self, data): + # type: (Any) -> Any + if data != data: + value = ".nan" + elif data == self.inf_value: + value = ".inf" + elif data == -self.inf_value: + value = "-.inf" + else: + value = float2string(data) + + return self.represent_scalar("tag:yaml.org,2002:float", value) + + +yaml.RoundTripRepresenter.add_representer(float, represent_float) + + +def get_float_or_units(node): + value = float(node.text.strip()) + if node.get("units") is not None: + units = node.get("units") + units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", units) + units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", units) + return "{} {}".format(float2string(value), units) + else: + return value + def process_three_body(rate_coeff): """Process a three-body reaction. @@ -97,6 +143,54 @@ def process_troe(rate_coeff): return reaction_attribs +def process_plog(rate_coeff): + """Process a PLOG reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attributes = {"type": "pressure-dependent-Arrhenius"} + rate_constants = [] + for arr_coeff in rate_coeff.iterfind("Arrhenius"): + rate_constant = process_arrhenius_parameters(arr_coeff) + rate_constant["P"] = get_float_or_units(arr_coeff.find("P")) + rate_constants.append(rate_constant) + reaction_attributes["rate-constants"] = rate_constants + + return reaction_attributes + + +def process_chebyshev(rate_coeff): + """Process a Chebyshev reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attributes = { + "type": "Chebyshev", + "temperature-range": [ + get_float_or_units(rate_coeff.find("Tmin")), + get_float_or_units(rate_coeff.find("Tmax")), + ], + "pressure-range": [ + get_float_or_units(rate_coeff.find("Pmin")), + get_float_or_units(rate_coeff.find("Pmax")), + ], + } + data_node = rate_coeff.find("floatArray") + n_p_values = int(data_node.get("degreeP")) + n_T_values = int(data_node.get("degreeT")) + data_text = list(map(float, data_node.text.replace("\n", " ").strip().split(","))) + data = [data_text[i : i + n_p_values] for i in range(0, len(data_text), n_p_values)] + if len(data) != n_T_values: + raise ValueError( + "The number of rows of the data do not match the specified temperature degree." + ) + reaction_attributes["data"] = data + + return reaction_attributes + + def process_arrhenius(rate_coeff): """Process a standard Arrhenius-type reaction. @@ -109,19 +203,9 @@ def process_arrhenius(rate_coeff): def process_arrhenius_parameters(arr_node): """Process the parameters from an Arrhenius child of a rateCoeff node.""" rate_constant = {} - A = arr_node.find("A") - rate_constant["A"] = A.text.strip() - if A.get("units") is not None: - rate_constant["A"] += " {}".format(A.get("units")) - - # Can units for b ever be specified? I don't think so... - rate_constant["b"] = arr_node.find("b").text.strip() - - Ea = arr_node.find("E") - rate_constant["Ea"] = Ea.text.strip() - if Ea.get("units") is not None: - rate_constant["Ea"] += " {}".format(Ea.get("units")) - + rate_constant["A"] = get_float_or_units(arr_node.find("A")) + rate_constant["b"] = get_float_or_units(arr_node.find("b")) + rate_constant["Ea"] = get_float_or_units(arr_node.find("E")) return rate_constant @@ -142,6 +226,8 @@ def process_efficiencies(eff_node): None: process_arrhenius, "Lindemann": process_lindemann, "Troe": process_troe, + "plog": process_plog, + "chebyshev": process_chebyshev, } @@ -206,7 +292,8 @@ def convert(inpfile, outfile): composition[element] = num species_attribs["composition"] = composition - species_attribs["note"] = species.find("note").text + if species.findtext("note") is not None: + species_attribs["note"] = species.findtext("note") thermo = species.find("thermo") if thermo[0].tag == "NASA": @@ -246,7 +333,7 @@ def convert(inpfile, outfile): reaction_attribs = {} reaction_type = reaction.get("type") rate_coeff = reaction.find("rateCoeff") - if reaction_type in [None, "threeBody"]: + if reaction_type in [None, "threeBody", "plog", "chebyshev"]: reaction_attribs.update(reaction_type_mapping[reaction_type](rate_coeff)) elif reaction_type in ["falloff"]: sub_type = rate_coeff.find("falloff").get("type") diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index ec016391c40..05b44a3db71 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -817,3 +817,10 @@ def test_gri30(self): self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000]) self.checkKinetics(ctmlPhase, yamlPhase, [900, 1800], [2e5, 20e5]) self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2400]) + + def test_pdep(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath('pdep-test.xml'), + Path(self.test_work_dir).joinpath('pdep-test.yaml')) + ctmlPhase, yamlPhase = self.checkConversion('pdep-test') + self.checkKinetics(ctmlPhase, yamlPhase, [300, 1000, 2200], + [100, ct.one_atm, 2e5, 2e6, 9.9e6]) From 9af113131a539fbc86db1f83094080b52b6684e6 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 16 Aug 2019 14:07:56 -0400 Subject: [PATCH 04/99] [ctml2yaml] Include reaction orders in output --- interfaces/cython/cantera/ctml2yaml.py | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 5fb09a0f750..781d0cba3b1 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -356,6 +356,24 @@ def convert(inpfile, outfile): reaction.find("equation").text.replace("[", "<").replace("]", ">") ) + reactants = { + a.split(":")[0]: float(a.split(":")[1]) + for a in reaction.findtext("reactants").replace("\n", " ").strip().split() + } + # products = { + # a.split(":")[0]: float(a.split(":")[1]) + # for a in reaction.findtext("products").replace("\n", " ").strip().split() + # } + orders = {} + # Need to make this more general, for non-reactant orders + for order_node in reaction.iterfind("order"): + species = order_node.get("species") + order = float(order_node.text) + if not np.isclose(reactants[species], order): + orders[species] = order + if orders: + reaction_attribs["orders"] = orders + if reaction.get("duplicate", "") == "yes": reaction_attribs["duplicate"] = True From 6327208f6db43af05688c31488ccef26bedebd01 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 16 Aug 2019 14:16:41 -0400 Subject: [PATCH 05/99] [ctml2yaml] Handle more complicated speciesArrays Can now handle undeclared element options and species arrays located in another file. --- interfaces/cython/cantera/ctml2yaml.py | 25 ++++++++++++++++++++++--- 1 file changed, 22 insertions(+), 3 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 781d0cba3b1..b04ff280a9d 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -261,6 +261,21 @@ def check_float_neq_zero(value, name): return {} +def get_species_array(speciesArray_node): + """Process a list of species from a speciesArray node.""" + species_list = speciesArray_node.text.replace("\n", " ").strip().split() + datasrc = speciesArray_node.get("datasrc", "") + if datasrc.startswith("#"): + return species_list + else: + filename, location = datasrc.split("#", 1) + name = str(Path(filename).with_suffix(".yaml")) + if location == "species_data": + location = "species" + datasrc = "{}/{}".format(name, location) + return [{datasrc: species_list}] + + def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) @@ -272,9 +287,13 @@ def convert(inpfile, outfile): phase.find("thermo").get("model") ] phase_attribs["elements"] = phase.find("elementArray").text.strip().split() - phase_attribs["species"] = ( - phase.find("speciesArray").text.replace("\n", " ").strip().split() - ) + phase_attribs["species"] = get_species_array(phase.find("speciesArray")) + species_skip = phase.find("speciesArray").find("skip") + if species_skip is not None: + element_skip = species_skip.get("element", "") + if element_skip == "undeclared": + phase_attribs["skip-undeclared-elements"] = True + phase_attribs["kinetics"] = kinetics_model_mapping[ phase.find("kinetics").get("model") ] From cf4cc43b5c471abd1e41c91265e3b13bf27d1f9a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 16 Aug 2019 14:25:41 -0400 Subject: [PATCH 06/99] [ctml2yaml] Add state information from phase --- interfaces/cython/cantera/ctml2yaml.py | 34 ++++++++++++++++++++++++++ 1 file changed, 34 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index b04ff280a9d..c507ecfaebc 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -33,6 +33,13 @@ "dispersion_coefficient": "dispersion-coefficient", "quadrupole_polarizability": "quadrupole-polarizability", } +state_properties_mapping = { + "moleFractions": "X", + "massFractions": "Y", + "temperature": "T", + "pressure": "P", + "coverages": "coverages", +} # Improved float formatting requires Numpy >= 1.14 if hasattr(np, "format_float_positional"): @@ -276,6 +283,20 @@ def get_species_array(speciesArray_node): return [{datasrc: species_list}] +def split_species_value_string(text, sep=" "): + """Split a string of species:value pairs into a dictionary. + + The keys of the dictionary are species names and the values are the + number associated with each species. This is useful for things like + elemental composition, mole fraction mappings, coverage mappings, etc. + + The keyword argument sep is used to determine how the pairs are split, + typically either " " or ",". + """ + pairs = list(map(str.strip, text.replace("\n", " ").strip().split(sep))) + return {a.split(":")[0]: float(a.split(":")[1]) for a in pairs} + + def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) @@ -300,6 +321,19 @@ def convert(inpfile, outfile): transport_model = transport_model_mapping[phase.find("transport").get("model")] if transport_model is not None: phase_attribs["transport-model"] = transport_model + state_node = phase.find("state") + if state_node is not None: + phase_state = {} + for prop in state_node: + property_name = state_properties_mapping[prop.tag] + if prop.tag in ["moleFractions", "massFractions", "coverages"]: + value = split_species_value_string(prop.text, sep=",") + else: + value = get_float_or_units(prop) + phase_state[property_name] = value + if phase_state: + phase_attribs["state"] = phase_state + phases.append(phase_attribs) species_data = [] From 349857ef6575634da7e757f8c3f11f59ee851d5b Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 16 Aug 2019 15:54:27 -0400 Subject: [PATCH 07/99] [ctml2yaml] Process reactionArray node --- interfaces/cython/cantera/ctml2yaml.py | 24 ++++++++++++++++++++++++ 1 file changed, 24 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c507ecfaebc..29bc8aa4d5a 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -283,6 +283,27 @@ def get_species_array(speciesArray_node): return [{datasrc: species_list}] +def get_reaction_array(reactionArray_node): + """Process reactions from a reactionArray node in a phase definition.""" + datasrc = reactionArray_node.get("datasrc", "") + if not datasrc.startswith("#"): + filename, location = datasrc.split("#", 1) + name = str(Path(filename).with_suffix(".yaml")) + if location == "reaction_data": + location = "reactions" + datasrc = "{}/{}".format(name, location) + skip = reactionArray_node.find("skip") + if skip is not None: + species_skip = skip.get("species", "") + if species_skip == "undeclared": + reactions = {datasrc: "declared-species"} + return {"reactions": [reactions]} + elif datasrc == "#reaction_data": + return {"reactions": "all"} + else: + return {} + + def split_species_value_string(text, sep=" "): """Split a string of species:value pairs into a dictionary. @@ -321,6 +342,9 @@ def convert(inpfile, outfile): transport_model = transport_model_mapping[phase.find("transport").get("model")] if transport_model is not None: phase_attribs["transport-model"] = transport_model + + phase_attribs.update(get_reaction_array(phase.find("reactionArray"))) + state_node = phase.find("state") if state_node is not None: phase_state = {} From d6a5d41da9ce7ab8d5fda4f6c868044ebbfcc6d0 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 15:24:00 -0500 Subject: [PATCH 08/99] [ctml2yaml] Add ptcombust test Adds surface phase types --- interfaces/cython/cantera/ctml2yaml.py | 47 ++++++++++++++++--- .../cython/cantera/test/test_convert.py | 11 +++++ 2 files changed, 52 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 29bc8aa4d5a..b166e995062 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -15,8 +15,8 @@ import numpy as np -thermo_model_mapping = {"IdealGas": "ideal-gas"} -kinetics_model_mapping = {"GasKinetics": "gas"} +thermo_model_mapping = {"IdealGas": "ideal-gas", "Surface": "ideal-surface"} +kinetics_model_mapping = {"GasKinetics": "gas", "Interface": "surface"} transport_model_mapping = { "Mix": "mixture-averaged", "Multi": "multi-component", @@ -198,6 +198,33 @@ def process_chebyshev(rate_coeff): return reaction_attributes +def process_surface_reaction(rate_coeff): + """Process a surface reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + arr_node = rate_coeff.find("Arrhenius") + sticking = arr_node.get("type", "") == "stick" + if sticking: + reaction_attributes = { + "sticking-coefficient": process_arrhenius_parameters(arr_node) + } + else: + reaction_attributes = {"rate-constant": process_arrhenius_parameters(arr_node)} + cov_node = arr_node.find("coverage") + if cov_node is not None: + cov_species = cov_node.get("species") + cov_a = get_float_or_units(cov_node.find("a")) + cov_m = get_float_or_units(cov_node.find("m")) + cov_e = get_float_or_units(cov_node.find("e")) + reaction_attributes["coverage-dependencies"] = { + cov_species: {"a": cov_a, "m": cov_m, "E": cov_e} + } + + return reaction_attributes + + def process_arrhenius(rate_coeff): """Process a standard Arrhenius-type reaction. @@ -235,6 +262,7 @@ def process_efficiencies(eff_node): "Troe": process_troe, "plog": process_plog, "chebyshev": process_chebyshev, + "surface": process_surface_reaction, } @@ -322,12 +350,17 @@ def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) ctml_tree = etree.parse(str(inpfile)).getroot() + + # Phases phases = [] for phase in ctml_tree.iterfind("phase"): phase_attribs = {"name": phase.get("id")} - phase_attribs["thermo"] = thermo_model_mapping[ - phase.find("thermo").get("model") - ] + phase_thermo = phase.find("thermo") + phase_attribs["thermo"] = thermo_model_mapping[phase_thermo.get("model")] + for node in phase_thermo: + if node.tag == "site_density": + phase_attribs["site-density"] = get_float_or_units(node) + phase_attribs["elements"] = phase.find("elementArray").text.strip().split() phase_attribs["species"] = get_species_array(phase.find("speciesArray")) species_skip = phase.find("speciesArray").find("skip") @@ -360,6 +393,7 @@ def convert(inpfile, outfile): phases.append(phase_attribs) + # Species species_data = [] for species in ctml_tree.find("speciesData").iterfind("species"): species_attribs = {"name": species.get("name")} @@ -405,12 +439,13 @@ def convert(inpfile, outfile): species_data.append(species_attribs) + # Reactions reaction_data = [] for reaction in ctml_tree.find("reactionData").iterfind("reaction"): reaction_attribs = {} reaction_type = reaction.get("type") rate_coeff = reaction.find("rateCoeff") - if reaction_type in [None, "threeBody", "plog", "chebyshev"]: + if reaction_type in [None, "threeBody", "plog", "chebyshev", "surface"]: reaction_attribs.update(reaction_type_mapping[reaction_type](rate_coeff)) elif reaction_type in ["falloff"]: sub_type = rate_coeff.find("falloff").get("type") diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 05b44a3db71..985a6dbba61 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -824,3 +824,14 @@ def test_pdep(self): ctmlPhase, yamlPhase = self.checkConversion('pdep-test') self.checkKinetics(ctmlPhase, yamlPhase, [300, 1000, 2200], [100, ct.one_atm, 2e5, 2e6, 9.9e6]) + + def test_ptcombust(self): + ctml2yaml.convert(Path(self.cantera_data).joinpath('ptcombust.xml'), + Path(self.test_work_dir).joinpath('ptcombust.yaml')) + ctmlGas, yamlGas = self.checkConversion('ptcombust') + ctmlSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface, + phaseid='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) + + self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5]) + self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600]) + self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5]) From 6af0ad016bdfa86c7934e234820aa040bec45403 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 14:56:01 -0400 Subject: [PATCH 09/99] [ctml2yaml] Fix composition string processing Composition strings can be separated by comma or by space with no differences. Also, the value could be an int or a float, so check both. Use updated function to set the elemental composition of species. --- interfaces/cython/cantera/ctml2yaml.py | 25 ++++++++++++++++--------- 1 file changed, 16 insertions(+), 9 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index b166e995062..5c26e6d2cd1 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -332,7 +332,7 @@ def get_reaction_array(reactionArray_node): return {} -def split_species_value_string(text, sep=" "): +def split_species_value_string(text): """Split a string of species:value pairs into a dictionary. The keys of the dictionary are species names and the values are the @@ -342,8 +342,15 @@ def split_species_value_string(text, sep=" "): The keyword argument sep is used to determine how the pairs are split, typically either " " or ",". """ - pairs = list(map(str.strip, text.replace("\n", " ").strip().split(sep))) - return {a.split(":")[0]: float(a.split(":")[1]) for a in pairs} + pairs = {} + for t in text.replace("\n", " ").replace(",", " ").strip().split(): + key, value = t.split(":") + try: + pairs[key] = int(value) + except ValueError: + pairs[key] = float(value) + + return pairs def convert(inpfile, outfile): @@ -384,7 +391,7 @@ def convert(inpfile, outfile): for prop in state_node: property_name = state_properties_mapping[prop.tag] if prop.tag in ["moleFractions", "massFractions", "coverages"]: - value = split_species_value_string(prop.text, sep=",") + value = split_species_value_string(prop.text) else: value = get_float_or_units(prop) phase_state[property_name] = value @@ -397,12 +404,12 @@ def convert(inpfile, outfile): species_data = [] for species in ctml_tree.find("speciesData").iterfind("species"): species_attribs = {"name": species.get("name")} - composition = {} - for element_amount in species.find("atomArray").text.strip().split(): - element, num = element_amount.split(":") - composition[element] = num + atom_array = species.find("atomArray") + if atom_array.text is not None: + species_attribs["composition"] = split_species_value_string(atom_array.text) + else: + species_attribs["composition"] = {} - species_attribs["composition"] = composition if species.findtext("note") is not None: species_attribs["note"] = species.findtext("note") From 2a870710f4d73e9759b512203d163eeddf4a9dcc Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 14:57:39 -0400 Subject: [PATCH 10/99] [ctml2yaml] Use lowercase in property mappings --- interfaces/cython/cantera/ctml2yaml.py | 36 +++++++++++++++++++------- 1 file changed, 27 insertions(+), 9 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 5c26e6d2cd1..0b9bac0d7be 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -15,12 +15,23 @@ import numpy as np -thermo_model_mapping = {"IdealGas": "ideal-gas", "Surface": "ideal-surface"} -kinetics_model_mapping = {"GasKinetics": "gas", "Interface": "surface"} +thermo_model_mapping = { + "idealgas": "ideal-gas", + "surface": "ideal-surface", + "metal": "electron-cloud", + "lattice": "lattice", + "edge": "edge", +} +kinetics_model_mapping = { + "gaskinetics": "gas", + "interface": "surface", + "none": None, + "edge": "edge", +} transport_model_mapping = { - "Mix": "mixture-averaged", - "Multi": "multi-component", - "None": None, + "mix": "mixture-averaged", + "multi": "multi-component", + "none": None, } species_thermo_mapping = {"NASA": "NASA7"} species_transport_mapping = {"gas_transport": "gas"} @@ -363,7 +374,9 @@ def convert(inpfile, outfile): for phase in ctml_tree.iterfind("phase"): phase_attribs = {"name": phase.get("id")} phase_thermo = phase.find("thermo") - phase_attribs["thermo"] = thermo_model_mapping[phase_thermo.get("model")] + phase_attribs["thermo"] = thermo_model_mapping[ + phase_thermo.get("model").lower() + ] for node in phase_thermo: if node.tag == "site_density": phase_attribs["site-density"] = get_float_or_units(node) @@ -376,10 +389,15 @@ def convert(inpfile, outfile): if element_skip == "undeclared": phase_attribs["skip-undeclared-elements"] = True - phase_attribs["kinetics"] = kinetics_model_mapping[ - phase.find("kinetics").get("model") + kinetics_model = kinetics_model_mapping[ + phase.find("kinetics").get("model").lower() + ] + if kinetics_model is not None: + phase_attribs["kinetics"] = kinetics_model + + transport_model = transport_model_mapping[ + phase.find("transport").get("model").lower() ] - transport_model = transport_model_mapping[phase.find("transport").get("model")] if transport_model is not None: phase_attribs["transport-model"] = transport_model From 0a397dd1c922a40029e97e47f4dba26e21393364 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 14:58:34 -0400 Subject: [PATCH 11/99] [ctml2yaml] Allow density as a thermo property In phase definitions, density can be specified for certain thermo models --- interfaces/cython/cantera/ctml2yaml.py | 2 ++ 1 file changed, 2 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 0b9bac0d7be..41dbd4ed48a 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -380,6 +380,8 @@ def convert(inpfile, outfile): for node in phase_thermo: if node.tag == "site_density": phase_attribs["site-density"] = get_float_or_units(node) + elif node.tag == "density": + phase_attribs["density"] = get_float_or_units(node) phase_attribs["elements"] = phase.find("elementArray").text.strip().split() phase_attribs["species"] = get_species_array(phase.find("speciesArray")) From 8479d42e086bcc04ea0370e9bda899cb07a95738 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 15:00:10 -0400 Subject: [PATCH 12/99] [ctml2yaml] Allow empty/nonexistant reactionArrays --- interfaces/cython/cantera/ctml2yaml.py | 1 + 1 file changed, 1 insertion(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 41dbd4ed48a..3b48ad20193 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -403,6 +403,7 @@ def convert(inpfile, outfile): if transport_model is not None: phase_attribs["transport-model"] = transport_model + if phase.find("reactionArray") is not None: phase_attribs.update(get_reaction_array(phase.find("reactionArray"))) state_node = phase.find("state") From adaae4233e700d53c5b6baa024b1c8f785079e33 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 15:00:31 -0400 Subject: [PATCH 13/99] [ctml2yaml] Add reaction ID to output --- interfaces/cython/cantera/ctml2yaml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 3b48ad20193..0ea719e4d70 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -471,6 +471,9 @@ def convert(inpfile, outfile): reaction_data = [] for reaction in ctml_tree.find("reactionData").iterfind("reaction"): reaction_attribs = {} + reaction_id = reaction.get("id", False) + if reaction_id: + reaction_attribs["id"] = reaction_id reaction_type = reaction.get("type") rate_coeff = reaction.find("rateCoeff") if reaction_type in [None, "threeBody", "plog", "chebyshev", "surface"]: From 9a34719a7d892b52a5a58c43cd12ca47037619c1 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 15:01:11 -0400 Subject: [PATCH 14/99] [ctml2yaml] Add constant-cp species thermo --- interfaces/cython/cantera/ctml2yaml.py | 12 ++++++++++++ 1 file changed, 12 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 0ea719e4d70..c06153d0bef 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -293,6 +293,16 @@ def process_NASA7_thermo(thermo): return thermo_attribs +def process_const_cp_thermo(thermo): + """Process a constant c_p thermo entry from XML to a dictionary.""" + thermo_attribs = {"model": "constant-cp"} + for node in thermo.find("const_cp"): + value = get_float_or_units(node) + thermo_attribs[node.tag] = value + + return thermo_attribs + + def check_float_neq_zero(value, name): """Check that the text value associated with a tag is non-zero. @@ -437,6 +447,8 @@ def convert(inpfile, outfile): thermo = species.find("thermo") if thermo[0].tag == "NASA": species_attribs["thermo"] = process_NASA7_thermo(thermo) + elif thermo[0].tag == "const_cp": + species_attribs["thermo"] = process_const_cp_thermo(thermo) else: raise TypeError( "Unknown thermo model type: '{}' for species '{}'".format( From ed684d743cbf7a66fcd57205f65b427cf08ca4f9 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 15:01:31 -0400 Subject: [PATCH 15/99] [ctml2yaml] Add edge reaction types --- interfaces/cython/cantera/ctml2yaml.py | 19 +++++++++++++++++-- 1 file changed, 17 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c06153d0bef..5fe5ef7e2c6 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -236,6 +236,20 @@ def process_surface_reaction(rate_coeff): return reaction_attributes +def process_edge_reaction(rate_coeff): + """Process an edge reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + arr_node = rate_coeff.find("Arrhenius") + reaction_attributes = { + "rate-constant": process_arrhenius_parameters(arr_node), + "beta": float(rate_coeff.find("electrochem").get("beta")), + } + return reaction_attributes + + def process_arrhenius(rate_coeff): """Process a standard Arrhenius-type reaction. @@ -274,6 +288,7 @@ def process_efficiencies(eff_node): "plog": process_plog, "chebyshev": process_chebyshev, "surface": process_surface_reaction, + "edge": process_edge_reaction, } @@ -414,7 +429,7 @@ def convert(inpfile, outfile): phase_attribs["transport-model"] = transport_model if phase.find("reactionArray") is not None: - phase_attribs.update(get_reaction_array(phase.find("reactionArray"))) + phase_attribs.update(get_reaction_array(phase.find("reactionArray"))) state_node = phase.find("state") if state_node is not None: @@ -488,7 +503,7 @@ def convert(inpfile, outfile): reaction_attribs["id"] = reaction_id reaction_type = reaction.get("type") rate_coeff = reaction.find("rateCoeff") - if reaction_type in [None, "threeBody", "plog", "chebyshev", "surface"]: + if reaction_type in [None, "threeBody", "plog", "chebyshev", "surface", "edge"]: reaction_attribs.update(reaction_type_mapping[reaction_type](rate_coeff)) elif reaction_type in ["falloff"]: sub_type = rate_coeff.find("falloff").get("type") From 4096b7c7186b2f5045bad2d7e1ef5cba998c6912 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 18:02:17 -0400 Subject: [PATCH 16/99] [ctml2yaml] Allow reactions to be filtered Reaction entries can now be filtered as specified in the include node of the reactionArray node. Fix typo in transport key name at phase level. --- interfaces/cython/cantera/ctml2yaml.py | 62 +++++++++++++++++++++----- 1 file changed, 52 insertions(+), 10 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 5fe5ef7e2c6..3a2a79bc193 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -350,7 +350,12 @@ def get_species_array(speciesArray_node): def get_reaction_array(reactionArray_node): """Process reactions from a reactionArray node in a phase definition.""" datasrc = reactionArray_node.get("datasrc", "") + has_filter = reactionArray_node.find("include") is not None if not datasrc.startswith("#"): + if has_filter: + raise ValueError( + "Filtering reaction lists is not possible with external data sources" + ) filename, location = datasrc.split("#", 1) name = str(Path(filename).with_suffix(".yaml")) if location == "reaction_data": @@ -361,9 +366,18 @@ def get_reaction_array(reactionArray_node): species_skip = skip.get("species", "") if species_skip == "undeclared": reactions = {datasrc: "declared-species"} + else: + raise ValueError("Unknown value in skip parameter for reactionArray") + else: + raise ValueError( + "Missing skip node in reactionArray with external data source" + ) return {"reactions": [reactions]} elif datasrc == "#reaction_data": - return {"reactions": "all"} + if has_filter: + return {"reactions": []} + else: + return {"reactions": "all"} else: return {} @@ -396,8 +410,10 @@ def convert(inpfile, outfile): # Phases phases = [] + reaction_filters = [] for phase in ctml_tree.iterfind("phase"): - phase_attribs = {"name": phase.get("id")} + phase_name = phase.get("id") + phase_attribs = {"name": phase_name} phase_thermo = phase.find("thermo") phase_attribs["thermo"] = thermo_model_mapping[ phase_thermo.get("model").lower() @@ -416,20 +432,30 @@ def convert(inpfile, outfile): if element_skip == "undeclared": phase_attribs["skip-undeclared-elements"] = True - kinetics_model = kinetics_model_mapping[ - phase.find("kinetics").get("model").lower() - ] - if kinetics_model is not None: - phase_attribs["kinetics"] = kinetics_model - transport_model = transport_model_mapping[ phase.find("transport").get("model").lower() ] if transport_model is not None: - phase_attribs["transport-model"] = transport_model + phase_attribs["transport"] = transport_model if phase.find("reactionArray") is not None: + # The kinetics model should only be specified if reactions + # are associated with the phase + kinetics_model = kinetics_model_mapping[ + phase.find("kinetics").get("model").lower() + ] + if kinetics_model is not None: + phase_attribs["kinetics"] = kinetics_model + phase_attribs.update(get_reaction_array(phase.find("reactionArray"))) + reaction_filter = phase.find("reactionArray").find("include") + if reaction_filter is not None: + phase_attribs["reactions"].append("{}-reactions".format(phase_name)) + if reaction_filter.get("min") != reaction_filter.get("max"): + raise ValueError("Can't handle differing reaction filter criteria") + reaction_filters.append( + ("{}-reactions".format(phase_name), reaction_filter.get("min")) + ) state_node = phase.find("state") if state_node is not None: @@ -549,7 +575,23 @@ def convert(inpfile, outfile): reaction_data.append(reaction_attribs) - yaml_doc = {"phases": phases, "species": species_data, "reactions": reaction_data} + output_reactions = {} + for phase_name, pattern in reaction_filters: + pattern = re.compile(pattern.replace("*", ".*")) + hits = [] + misses = [] + for reaction in reaction_data: + if pattern.match(reaction.get("id", "")): + hits.append(reaction) + else: + misses.append(reaction) + reaction_data = misses + output_reactions[phase_name] = hits + if reaction_data: + output_reactions["reactions"] = reaction_data + + yaml_doc = {"phases": phases, "species": species_data} + yaml_doc.update(output_reactions) yaml_obj = yaml.YAML(typ="safe") yaml_obj.dump(yaml_doc, Path(outfile)) From f3e7569ec0ad6f900d219977afb796ea54cc9c39 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 19 Aug 2019 18:02:38 -0400 Subject: [PATCH 17/99] [ctml2yaml] Add test_sofc to ctml2yaml tests --- .../cython/cantera/test/test_convert.py | 24 ++++++++++++++++++- 1 file changed, 23 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 985a6dbba61..68ca6c32386 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -731,7 +731,6 @@ def test_ch4_ion(self): self.checkKinetics(ctiGas, yamlGas, [900, 1800], [2e5, 20e5]) self.checkTransport(ctiGas, yamlGas, [298, 1001, 2400]) - class ctml2yamlTest(utilities.CanteraTest): @classmethod def setUpClass(cls): @@ -835,3 +834,26 @@ def test_ptcombust(self): self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5]) self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600]) self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5]) + + def test_sofc(self): + ctml2yaml.convert(Path(self.cantera_data).joinpath('sofc.xml'), + Path(self.test_work_dir).joinpath('sofc.yaml')) + ctmlGas, yamlGas = self.checkConversion('sofc') + ctmlMetal, yamlMetal = self.checkConversion('sofc', phaseid='metal') + ctmlOxide, yamlOxide = self.checkConversion('sofc', phaseid='oxide_bulk') + ctmlMSurf, yamlMSurf = self.checkConversion('sofc', ct.Interface, + phaseid='metal_surface', ctmlphases=[ctmlGas, ctmlMetal], + yamlphases=[yamlGas, yamlMetal]) + ctmlOSurf, yamlOSurf = self.checkConversion('sofc', ct.Interface, + phaseid='oxide_surface', ctmlphases=[ctmlGas, ctmlOxide], + yamlphases=[yamlGas, yamlOxide]) + ctml_tpb, yaml_tpb = self.checkConversion('sofc', ct.Interface, + phaseid='tpb', ctmlphases=[ctmlMetal, ctmlMSurf, ctmlOSurf], + yamlphases=[yamlMetal, yamlMSurf, yamlOSurf]) + + self.checkThermo(ctmlMSurf, yamlMSurf, [900, 1000, 1100]) + self.checkThermo(ctmlOSurf, yamlOSurf, [900, 1000, 1100]) + ctmlMetal.electric_potential = yamlMetal.electric_potential = 2 + self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5]) + ctmlMetal.electric_potential = yamlMetal.electric_potential = 4 + self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5]) From d3b0c2963e19b83da2e8710b582fed3fbba33d6d Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Thu, 22 Aug 2019 20:54:37 -0400 Subject: [PATCH 18/99] [ctml2yaml] Fix integer-like reaction IDs error reaction_id nodes that look like integers (e.g., '0001') are being read as integers by the parser and converting them to strings throws errors. The fix is to only include the reaction_id if it doesn't look like an integer. --- interfaces/cython/cantera/ctml2yaml.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 3a2a79bc193..a788e84af4c 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -526,7 +526,13 @@ def convert(inpfile, outfile): reaction_attribs = {} reaction_id = reaction.get("id", False) if reaction_id: - reaction_attribs["id"] = reaction_id + # If the reaction_id can be converted to an integer, it was likely + # added automatically, so there's no need to include it in the + # output. + try: + reaction_id = int(reaction_id) + except ValueError: + reaction_attribs["id"] = reaction_id reaction_type = reaction.get("type") rate_coeff = reaction.find("rateCoeff") if reaction_type in [None, "threeBody", "plog", "chebyshev", "surface", "edge"]: From f2485cb03bd9aa9ada2240b7a4b6c22548999061 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 6 Sep 2019 17:50:18 -0400 Subject: [PATCH 19/99] [ctml2yaml] Add Flow classes to format YAML output --- interfaces/cython/cantera/ctml2yaml.py | 15 +++++++++++++++ 1 file changed, 15 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index a788e84af4c..9dadf562f8d 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -15,6 +15,21 @@ import numpy as np +BlockMap = yaml.comments.CommentedMap + + +def FlowMap(*args, **kwargs): + m = yaml.comments.CommentedMap(*args, **kwargs) + m.fa.set_flow_style() + return m + + +def FlowList(*args, **kwargs): + lst = yaml.comments.CommentedSeq(*args, **kwargs) + lst.fa.set_flow_style() + return lst + + thermo_model_mapping = { "idealgas": "ideal-gas", "surface": "ideal-surface", From dde55466f626eb46e1368f39d6ef501005a785e6 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 7 Sep 2019 11:33:07 -0400 Subject: [PATCH 20/99] [ctml2yaml] Refactor reaction processing to class --- interfaces/cython/cantera/ctml2yaml.py | 512 +++++++++++++------------ 1 file changed, 269 insertions(+), 243 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 9dadf562f8d..f1ce20015af 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -113,198 +113,300 @@ def get_float_or_units(node): return value -def process_three_body(rate_coeff): - """Process a three-body reaction. - - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - reaction_attribs = {"type": "three-body"} - reaction_attribs["rate-constant"] = process_arrhenius_parameters( - rate_coeff.find("Arrhenius") - ) - reaction_attribs["efficiencies"] = process_efficiencies( - rate_coeff.find("efficiencies") - ) - - return reaction_attribs - +def split_species_value_string(text): + """Split a string of species:value pairs into a dictionary. -def process_lindemann(rate_coeff): - """Process a Lindemann falloff reaction. + The keys of the dictionary are species names and the values are the + number associated with each species. This is useful for things like + elemental composition, mole fraction mappings, coverage mappings, etc. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + The keyword argument sep is used to determine how the pairs are split, + typically either " " or ",". """ - reaction_attribs = {"type": "falloff"} - for arr_coeff in rate_coeff.iterfind("Arrhenius"): - if arr_coeff.get("name") is not None and arr_coeff.get("name") == "k0": - reaction_attribs["low-P-rate-constant"] = process_arrhenius_parameters( - arr_coeff - ) - elif arr_coeff.get("name") is None: - reaction_attribs["high-P-rate-constant"] = process_arrhenius_parameters( - arr_coeff - ) - else: - raise TypeError("Too many Arrhenius nodes") - reaction_attribs["efficiencies"] = process_efficiencies( - rate_coeff.find("efficiencies") - ) + pairs = {} + for t in text.replace("\n", " ").replace(",", " ").strip().split(): + key, value = t.split(":") + try: + pairs[key] = int(value) + except ValueError: + pairs[key] = float(value) - return reaction_attribs + return pairs -def process_troe(rate_coeff): - """Process a Troe falloff reaction. +class Reaction: + """Represents a reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param reaction: + An ETree Element node with the reaction information """ - # This gets the low-p and high-p rate constants and the efficiencies - reaction_attribs = process_lindemann(rate_coeff) - troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() - troe_names = ["A", "T3", "T1", "T2"] - reaction_attribs["Troe"] = {} - # zip stops when the shortest iterable is exhausted. If T2 is not present - # in the Troe parameters (i.e., troe_params is three elements long), it - # will be omitted here as well. - for name, param in zip(troe_names, troe_params): - reaction_attribs["Troe"].update({name: float(param)}) - - return reaction_attribs + def __init__(self, reaction): + reaction_attribs = BlockMap({}) + reaction_id = reaction.get("id", False) + if reaction_id: + # If the reaction_id can be converted to an integer, it was likely + # added automatically, so there's no need to include it in the + # output. Including an integer-like reaction ID will generate an error + # when reading the YAML file. + try: + reaction_id = int(reaction_id) + except ValueError: + reaction_attribs["id"] = reaction_id + reaction_type = reaction.get("type") + rate_coeff = reaction.find("rateCoeff") + if reaction_type not in [ + None, + "threeBody", + "plog", + "chebyshev", + "surface", + "edge", + "falloff", + ]: + raise TypeError( + "Unknown reaction type '{}' for reaction id {}".format( + reaction_type, reaction.get("id") + ) + ) + if reaction_type is None: + # The default type is an Arrhenius reaction + reaction_type = "arrhenius" + elif reaction_type in ["falloff"]: + falloff_type = rate_coeff.find("falloff").get("type") + if falloff_type not in ["Lindemann", "Troe"]: + raise TypeError( + "Unknown falloff type '{}' for reaction id {}".format( + falloff_type, reaction.get("id") + ) + ) + else: + reaction_type = falloff_type -def process_plog(rate_coeff): - """Process a PLOG reaction. + func = getattr(self, reaction_type.lower()) + reaction_attribs.update(func(rate_coeff)) - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - reaction_attributes = {"type": "pressure-dependent-Arrhenius"} - rate_constants = [] - for arr_coeff in rate_coeff.iterfind("Arrhenius"): - rate_constant = process_arrhenius_parameters(arr_coeff) - rate_constant["P"] = get_float_or_units(arr_coeff.find("P")) - rate_constants.append(rate_constant) - reaction_attributes["rate-constants"] = rate_constants + reaction_attribs["equation"] = ( + reaction.find("equation").text.replace("[", "<").replace("]", ">") + ) - return reaction_attributes + reactants = split_species_value_string(reaction.findtext("reactants")) + # products = { + # a.split(":")[0]: float(a.split(":")[1]) + # for a in reaction.findtext("products").replace("\n", " ").strip().split() + # } + orders = {} + # Need to make this more general, for non-reactant orders + for order_node in reaction.iterfind("order"): + species = order_node.get("species") + order = float(order_node.text) + if not np.isclose(reactants[species], order): + orders[species] = order + if orders: + reaction_attribs["orders"] = orders + if reaction.get("duplicate", "") == "yes": + reaction_attribs["duplicate"] = True -def process_chebyshev(rate_coeff): - """Process a Chebyshev reaction. + self.reaction_attribs = reaction_attribs - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - reaction_attributes = { - "type": "Chebyshev", - "temperature-range": [ - get_float_or_units(rate_coeff.find("Tmin")), - get_float_or_units(rate_coeff.find("Tmax")), - ], - "pressure-range": [ - get_float_or_units(rate_coeff.find("Pmin")), - get_float_or_units(rate_coeff.find("Pmax")), - ], - } - data_node = rate_coeff.find("floatArray") - n_p_values = int(data_node.get("degreeP")) - n_T_values = int(data_node.get("degreeT")) - data_text = list(map(float, data_node.text.replace("\n", " ").strip().split(","))) - data = [data_text[i : i + n_p_values] for i in range(0, len(data_text), n_p_values)] - if len(data) != n_T_values: - raise ValueError( - "The number of rows of the data do not match the specified temperature degree." - ) - reaction_attributes["data"] = data + @classmethod + def to_yaml(cls, representer, data): + return representer.represent_dict(data.reaction_attribs) - return reaction_attributes + def threebody(self, rate_coeff): + """Process a three-body reaction. + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attribs = FlowMap({"type": "three-body"}) + reaction_attribs["rate-constant"] = self.process_arrhenius_parameters( + rate_coeff.find("Arrhenius") + ) + reaction_attribs["efficiencies"] = self.process_efficiencies( + rate_coeff.find("efficiencies") + ) -def process_surface_reaction(rate_coeff): - """Process a surface reaction. + return reaction_attribs + + def lindemann(self, rate_coeff): + """Process a Lindemann falloff reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attribs = FlowMap({"type": "falloff"}) + for arr_coeff in rate_coeff.iterfind("Arrhenius"): + if arr_coeff.get("name") is not None and arr_coeff.get("name") == "k0": + reaction_attribs[ + "low-P-rate-constant" + ] = self.process_arrhenius_parameters(arr_coeff) + elif arr_coeff.get("name") is None: + reaction_attribs[ + "high-P-rate-constant" + ] = self.process_arrhenius_parameters(arr_coeff) + else: + raise TypeError("Too many Arrhenius nodes") + reaction_attribs["efficiencies"] = self.process_efficiencies( + rate_coeff.find("efficiencies") + ) - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - arr_node = rate_coeff.find("Arrhenius") - sticking = arr_node.get("type", "") == "stick" - if sticking: - reaction_attributes = { - "sticking-coefficient": process_arrhenius_parameters(arr_node) - } - else: - reaction_attributes = {"rate-constant": process_arrhenius_parameters(arr_node)} - cov_node = arr_node.find("coverage") - if cov_node is not None: - cov_species = cov_node.get("species") - cov_a = get_float_or_units(cov_node.find("a")) - cov_m = get_float_or_units(cov_node.find("m")) - cov_e = get_float_or_units(cov_node.find("e")) - reaction_attributes["coverage-dependencies"] = { - cov_species: {"a": cov_a, "m": cov_m, "E": cov_e} + return reaction_attribs + + def troe(self, rate_coeff): + """Process a Troe falloff reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + # This gets the low-p and high-p rate constants and the efficiencies + reaction_attribs = self.lindemann(rate_coeff) + + troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() + troe_names = ["A", "T3", "T1", "T2"] + reaction_attribs["Troe"] = {} + # zip stops when the shortest iterable is exhausted. If T2 is not present + # in the Troe parameters (i.e., troe_params is three elements long), it + # will be omitted here as well. + for name, param in zip(troe_names, troe_params): + reaction_attribs["Troe"].update({name: float(param)}) + + return reaction_attribs + + def plog(self, rate_coeff): + """Process a PLOG reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attributes = FlowMap({"type": "pressure-dependent-Arrhenius"}) + rate_constants = [] + for arr_coeff in rate_coeff.iterfind("Arrhenius"): + rate_constant = self.process_arrhenius_parameters(arr_coeff) + rate_constant["P"] = get_float_or_units(arr_coeff.find("P")) + rate_constants.append(rate_constant) + reaction_attributes["rate-constants"] = rate_constants + + return reaction_attributes + + def chebyshev(self, rate_coeff): + """Process a Chebyshev reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attributes = FlowMap( + { + "type": "Chebyshev", + "temperature-range": FlowList( + [ + get_float_or_units(rate_coeff.find("Tmin")), + get_float_or_units(rate_coeff.find("Tmax")), + ] + ), + "pressure-range": FlowList( + [ + get_float_or_units(rate_coeff.find("Pmin")), + get_float_or_units(rate_coeff.find("Pmax")), + ] + ), } + ) + data_node = rate_coeff.find("floatArray") + n_p_values = int(data_node.get("degreeP")) + n_T_values = int(data_node.get("degreeT")) + data_text = list( + map(float, data_node.text.replace("\n", " ").strip().split(",")) + ) + data = [] + for i in range(0, len(data_text), n_p_values): + data.append(FlowList(data_text[i : i + n_p_values])) - return reaction_attributes + if len(data) != n_T_values: + raise ValueError( + "The number of rows of the data do not match the specified temperature degree." + ) + reaction_attributes["data"] = data + return reaction_attributes -def process_edge_reaction(rate_coeff): - """Process an edge reaction. + def surface(self, rate_coeff): + """Process a surface reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - arr_node = rate_coeff.find("Arrhenius") - reaction_attributes = { - "rate-constant": process_arrhenius_parameters(arr_node), - "beta": float(rate_coeff.find("electrochem").get("beta")), - } - return reaction_attributes + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + arr_node = rate_coeff.find("Arrhenius") + sticking = arr_node.get("type", "") == "stick" + if sticking: + reaction_attributes = FlowMap( + {"sticking-coefficient": self.process_arrhenius_parameters(arr_node)} + ) + else: + reaction_attributes = FlowMap( + {"rate-constant": self.process_arrhenius_parameters(arr_node)} + ) + cov_node = arr_node.find("coverage") + if cov_node is not None: + cov_species = cov_node.get("species") + cov_a = get_float_or_units(cov_node.find("a")) + cov_m = get_float_or_units(cov_node.find("m")) + cov_e = get_float_or_units(cov_node.find("e")) + reaction_attributes["coverage-dependencies"] = { + cov_species: {"a": cov_a, "m": cov_m, "E": cov_e} + } + + return reaction_attributes + + def edge(self, rate_coeff): + """Process an edge reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + arr_node = rate_coeff.find("Arrhenius") + reaction_attributes = FlowMap( + { + "rate-constant": self.process_arrhenius_parameters(arr_node), + "beta": float(rate_coeff.find("electrochem").get("beta")), + } + ) + return reaction_attributes + + def arrhenius(self, rate_coeff): + """Process a standard Arrhenius-type reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + return FlowMap( + { + "rate-constant": self.process_arrhenius_parameters( + rate_coeff.find("Arrhenius") + ) + } + ) + def process_arrhenius_parameters(self, arr_node): + """Process the parameters from an Arrhenius child of a rateCoeff node.""" + rate_constant = FlowMap({}) + rate_constant["A"] = get_float_or_units(arr_node.find("A")) + rate_constant["b"] = get_float_or_units(arr_node.find("b")) + rate_constant["Ea"] = get_float_or_units(arr_node.find("E")) + return rate_constant -def process_arrhenius(rate_coeff): - """Process a standard Arrhenius-type reaction. + def process_efficiencies(self, eff_node): + """Process the efficiency information about a reaction.""" + efficiencies = FlowMap({}) + effs = eff_node.text.replace("\n", " ").strip().split() + # Is there any way to do this with a comprehension? + for eff in effs: + s, e = eff.split(":") + efficiencies[s] = float(e) - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - return {"rate-constant": process_arrhenius_parameters(rate_coeff.find("Arrhenius"))} - - -def process_arrhenius_parameters(arr_node): - """Process the parameters from an Arrhenius child of a rateCoeff node.""" - rate_constant = {} - rate_constant["A"] = get_float_or_units(arr_node.find("A")) - rate_constant["b"] = get_float_or_units(arr_node.find("b")) - rate_constant["Ea"] = get_float_or_units(arr_node.find("E")) - return rate_constant - - -def process_efficiencies(eff_node): - """Process the efficiency information about a reaction.""" - efficiencies = {} - effs = eff_node.text.replace("\n", " ").strip().split() - # Is there any way to do this with a comprehension? - for eff in effs: - s, e = eff.split(":") - efficiencies[s] = float(e) - - return efficiencies - - -reaction_type_mapping = { - "threeBody": process_three_body, - None: process_arrhenius, - "Lindemann": process_lindemann, - "Troe": process_troe, - "plog": process_plog, - "chebyshev": process_chebyshev, - "surface": process_surface_reaction, - "edge": process_edge_reaction, -} + return efficiencies def process_NASA7_thermo(thermo): @@ -397,27 +499,6 @@ def get_reaction_array(reactionArray_node): return {} -def split_species_value_string(text): - """Split a string of species:value pairs into a dictionary. - - The keys of the dictionary are species names and the values are the - number associated with each species. This is useful for things like - elemental composition, mole fraction mappings, coverage mappings, etc. - - The keyword argument sep is used to determine how the pairs are split, - typically either " " or ",". - """ - pairs = {} - for t in text.replace("\n", " ").replace(",", " ").strip().split(): - key, value = t.split(":") - try: - pairs[key] = int(value) - except ValueError: - pairs[key] = float(value) - - return pairs - - def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) @@ -538,63 +619,7 @@ def convert(inpfile, outfile): # Reactions reaction_data = [] for reaction in ctml_tree.find("reactionData").iterfind("reaction"): - reaction_attribs = {} - reaction_id = reaction.get("id", False) - if reaction_id: - # If the reaction_id can be converted to an integer, it was likely - # added automatically, so there's no need to include it in the - # output. - try: - reaction_id = int(reaction_id) - except ValueError: - reaction_attribs["id"] = reaction_id - reaction_type = reaction.get("type") - rate_coeff = reaction.find("rateCoeff") - if reaction_type in [None, "threeBody", "plog", "chebyshev", "surface", "edge"]: - reaction_attribs.update(reaction_type_mapping[reaction_type](rate_coeff)) - elif reaction_type in ["falloff"]: - sub_type = rate_coeff.find("falloff").get("type") - if sub_type not in ["Lindemann", "Troe"]: - raise TypeError( - "Unknown falloff type '{}' for reaction id {}".format( - sub_type, reaction.get("id") - ) - ) - else: - reaction_attribs.update(reaction_type_mapping[sub_type](rate_coeff)) - else: - raise TypeError( - "Unknown reaction type '{}' for reaction id {}".format( - reaction_type, reaction.get("id") - ) - ) - - reaction_attribs["equation"] = ( - reaction.find("equation").text.replace("[", "<").replace("]", ">") - ) - - reactants = { - a.split(":")[0]: float(a.split(":")[1]) - for a in reaction.findtext("reactants").replace("\n", " ").strip().split() - } - # products = { - # a.split(":")[0]: float(a.split(":")[1]) - # for a in reaction.findtext("products").replace("\n", " ").strip().split() - # } - orders = {} - # Need to make this more general, for non-reactant orders - for order_node in reaction.iterfind("order"): - species = order_node.get("species") - order = float(order_node.text) - if not np.isclose(reactants[species], order): - orders[species] = order - if orders: - reaction_attribs["orders"] = orders - - if reaction.get("duplicate", "") == "yes": - reaction_attribs["duplicate"] = True - - reaction_data.append(reaction_attribs) + reaction_data.append(Reaction(reaction)) output_reactions = {} for phase_name, pattern in reaction_filters: @@ -602,7 +627,7 @@ def convert(inpfile, outfile): hits = [] misses = [] for reaction in reaction_data: - if pattern.match(reaction.get("id", "")): + if pattern.match(reaction.reaction_attribs.get("id", "")): hits.append(reaction) else: misses.append(reaction) @@ -614,6 +639,7 @@ def convert(inpfile, outfile): yaml_doc = {"phases": phases, "species": species_data} yaml_doc.update(output_reactions) yaml_obj = yaml.YAML(typ="safe") + yaml_obj.register_class(Reaction) yaml_obj.dump(yaml_doc, Path(outfile)) From ece6ebe728b1bc6e26e6377b6bf8d704a33a1d61 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 7 Sep 2019 16:18:41 -0400 Subject: [PATCH 21/99] [ctml2yaml] Improve formatting of output file Add metadata to the output. Add newlines between each top-level key. --- interfaces/cython/cantera/ctml2yaml.py | 33 +++++++++++++++++++++----- 1 file changed, 27 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index f1ce20015af..63cdb7164ce 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -7,6 +7,7 @@ import re import xml.etree.ElementTree as etree +from email.utils import formatdate try: import ruamel_yaml as yaml @@ -621,7 +622,7 @@ def convert(inpfile, outfile): for reaction in ctml_tree.find("reactionData").iterfind("reaction"): reaction_data.append(Reaction(reaction)) - output_reactions = {} + output_reactions = BlockMap() for phase_name, pattern in reaction_filters: pattern = re.compile(pattern.replace("*", ".*")) hits = [] @@ -632,15 +633,35 @@ def convert(inpfile, outfile): else: misses.append(reaction) reaction_data = misses - output_reactions[phase_name] = hits + if hits: + output_reactions[phase_name] = hits + output_reactions.yaml_set_comment_before_after_key(phase_name, before="\n") if reaction_data: output_reactions["reactions"] = reaction_data + output_reactions.yaml_set_comment_before_after_key("reactions", before="\n") - yaml_doc = {"phases": phases, "species": species_data} - yaml_doc.update(output_reactions) - yaml_obj = yaml.YAML(typ="safe") + output_phases = BlockMap({"phases": phases}) + output_phases.yaml_set_comment_before_after_key("phases", before="\n") + + output_species = BlockMap({"species": species_data}) + output_species.yaml_set_comment_before_after_key("species", before="\n") + + yaml_obj = yaml.YAML() yaml_obj.register_class(Reaction) - yaml_obj.dump(yaml_doc, Path(outfile)) + metadata = BlockMap( + { + "generator": "ctml2yaml", + "cantera-version": "2.5.0a3", + "date": formatdate(localtime=True), + } + ) + if inpfile is not None: + metadata["input-files"] = FlowList([str(inpfile)]) + with Path(outfile).open("w") as output_file: + yaml_obj.dump(metadata, output_file) + yaml_obj.dump(output_phases, output_file) + yaml_obj.dump(output_species, output_file) + yaml_obj.dump(output_reactions, output_file) if __name__ == "__main__": From c76c53e65cbc9c4eea9f78c2b35b33e02298befa Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 15:35:52 -0500 Subject: [PATCH 22/99] [ctml2yaml] Simplify handling of NumPy functions Use a global flag to determine the available formatting functions from NumPy. Move some functions around for readability. --- interfaces/cython/cantera/ctml2yaml.py | 54 +++++++++++++------------- 1 file changed, 27 insertions(+), 27 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 63cdb7164ce..38d2473986b 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -9,6 +9,8 @@ import xml.etree.ElementTree as etree from email.utils import formatdate +from typing import Any + try: import ruamel_yaml as yaml except ImportError: @@ -69,25 +71,23 @@ def FlowList(*args, **kwargs): } # Improved float formatting requires Numpy >= 1.14 -if hasattr(np, "format_float_positional"): - - def float2string(data): - if data == 0: - return "0.0" - elif 0.01 <= abs(data) < 10000: - return np.format_float_positional(data, trim="0") - else: - return np.format_float_scientific(data, trim="0") - +HAS_FMT_FLT_POS = hasattr(np, "format_float_positional") -else: - def float2string(data): +def float2string(data): + if not HAS_FMT_FLT_POS: return repr(data) + if data == 0: + return "0.0" + elif 0.01 <= abs(data) < 10000: + return np.format_float_positional(data, trim="0") + else: + return np.format_float_scientific(data, trim="0") + def represent_float(self, data): - # type: (Any) -> Any + # type: (Any, Any) -> Any if data != data: value = ".nan" elif data == self.inf_value: @@ -114,6 +114,20 @@ def get_float_or_units(node): return value +def check_float_neq_zero(value, name): + """Check that the text value associated with a tag is non-zero. + + If the value is not zero, return a dictionary with the key ``name`` + and the value. If the value is zero, return an empty dictionary. + Calling functions can use this function to ``update`` a dictionary of + attributes without adding keys whose values are zero. + """ + if not np.isclose(value, 0.0): + return {name: value} + else: + return {} + + def split_species_value_string(text): """Split a string of species:value pairs into a dictionary. @@ -436,20 +450,6 @@ def process_const_cp_thermo(thermo): return thermo_attribs -def check_float_neq_zero(value, name): - """Check that the text value associated with a tag is non-zero. - - If the value is not zero, return a dictionary with the key ``name`` - and the value. If the value is zero, return an empty dictionary. - Calling functions can use this function to update a dictionary of - attributes without adding keys whose values are zero. - """ - if not np.isclose(value, 0.0): - return {name: value} - else: - return {} - - def get_species_array(speciesArray_node): """Process a list of species from a speciesArray node.""" species_list = speciesArray_node.text.replace("\n", " ").strip().split() From 78260378cf13cebdffd8de3d738847c900faf102 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 7 Sep 2019 16:39:36 -0400 Subject: [PATCH 23/99] [ctml2yaml] Refactor phase definition to a class --- interfaces/cython/cantera/ctml2yaml.py | 297 ++++++++++++++----------- 1 file changed, 163 insertions(+), 134 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 38d2473986b..7fe357f12f5 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -33,24 +33,6 @@ def FlowList(*args, **kwargs): return lst -thermo_model_mapping = { - "idealgas": "ideal-gas", - "surface": "ideal-surface", - "metal": "electron-cloud", - "lattice": "lattice", - "edge": "edge", -} -kinetics_model_mapping = { - "gaskinetics": "gas", - "interface": "surface", - "none": None, - "edge": "edge", -} -transport_model_mapping = { - "mix": "mixture-averaged", - "multi": "multi-component", - "none": None, -} species_thermo_mapping = {"NASA": "NASA7"} species_transport_mapping = {"gas_transport": "gas"} transport_properties_mapping = { @@ -62,13 +44,6 @@ def FlowList(*args, **kwargs): "dispersion_coefficient": "dispersion-coefficient", "quadrupole_polarizability": "quadrupole-polarizability", } -state_properties_mapping = { - "moleFractions": "X", - "massFractions": "Y", - "temperature": "T", - "pressure": "P", - "coverages": "coverages", -} # Improved float formatting requires Numpy >= 1.14 HAS_FMT_FLT_POS = hasattr(np, "format_float_positional") @@ -149,6 +124,155 @@ def split_species_value_string(text): return pairs +class Phase: + """Represents a phase. + + :param phase: + ElementTree Element node with a phase definition. + """ + + _thermo_model_mapping = { + "idealgas": "ideal-gas", + "surface": "ideal-surface", + "metal": "electron-cloud", + "lattice": "lattice", + "edge": "edge", + } + _kinetics_model_mapping = { + "gaskinetics": "gas", + "interface": "surface", + "none": None, + "edge": "edge", + } + _transport_model_mapping = { + "mix": "mixture-averaged", + "multi": "multi-component", + "none": None, + } + + _state_properties_mapping = { + "moleFractions": "X", + "massFractions": "Y", + "temperature": "T", + "pressure": "P", + "coverages": "coverages", + } + + def __init__(self, phase): + phase_name = phase.get("id") + phase_attribs = BlockMap({"name": phase_name}) + phase_thermo = phase.find("thermo") + phase_attribs["thermo"] = self._thermo_model_mapping[ + phase_thermo.get("model").lower() + ] + for node in phase_thermo: + if node.tag == "site_density": + phase_attribs["site-density"] = get_float_or_units(node) + elif node.tag == "density": + phase_attribs["density"] = get_float_or_units(node) + + phase_attribs["elements"] = FlowList( + phase.find("elementArray").text.strip().split() + ) + phase_attribs["species"] = self.get_species_array(phase.find("speciesArray")) + species_skip = phase.find("speciesArray").find("skip") + if species_skip is not None: + element_skip = species_skip.get("element", "") + if element_skip == "undeclared": + phase_attribs["skip-undeclared-elements"] = True + + transport_model = self._transport_model_mapping[ + phase.find("transport").get("model").lower() + ] + if transport_model is not None: + phase_attribs["transport"] = transport_model + + if phase.find("reactionArray") is not None: + # The kinetics model should only be specified if reactions + # are associated with the phase + kinetics_model = self._kinetics_model_mapping[ + phase.find("kinetics").get("model").lower() + ] + if kinetics_model is not None: + phase_attribs["kinetics"] = kinetics_model + + phase_attribs.update(self.get_reaction_array(phase.find("reactionArray"))) + reaction_filter = phase.find("reactionArray").find("include") + if reaction_filter is not None: + phase_attribs["reactions"].append("{}-reactions".format(phase_name)) + + state_node = phase.find("state") + if state_node is not None: + phase_state = FlowMap({}) + for prop in state_node: + property_name = self._state_properties_mapping[prop.tag] + if prop.tag in ["moleFractions", "massFractions", "coverages"]: + value = split_species_value_string(prop.text) + else: + value = get_float_or_units(prop) + phase_state[property_name] = value + if phase_state: + phase_attribs["state"] = phase_state + + self.phase_attribs = phase_attribs + + def get_species_array(self, speciesArray_node): + """Process a list of species from a speciesArray node.""" + species_list = FlowList( + speciesArray_node.text.replace("\n", " ").strip().split() + ) + datasrc = speciesArray_node.get("datasrc", "") + if datasrc.startswith("#"): + return species_list + else: + filename, location = datasrc.split("#", 1) + name = str(Path(filename).with_suffix(".yaml")) + if location == "species_data": + location = "species" + datasrc = "{}/{}".format(name, location) + return [{datasrc: species_list}] + + def get_reaction_array(self, reactionArray_node): + """Process reactions from a reactionArray node in a phase definition.""" + datasrc = reactionArray_node.get("datasrc", "") + has_filter = reactionArray_node.find("include") is not None + if not datasrc.startswith("#"): + if has_filter: + raise ValueError( + "Filtering reaction lists is not possible with external data sources" + ) + filename, location = datasrc.split("#", 1) + name = str(Path(filename).with_suffix(".yaml")) + if location == "reaction_data": + location = "reactions" + datasrc = "{}/{}".format(name, location) + skip = reactionArray_node.find("skip") + if skip is not None: + species_skip = skip.get("species", "") + if species_skip == "undeclared": + reactions = {datasrc: "declared-species"} + else: + raise ValueError( + "Unknown value in skip parameter for reactionArray" + ) + else: + raise ValueError( + "Missing skip node in reactionArray with external data source" + ) + return {"reactions": FlowList([reactions])} + elif datasrc == "#reaction_data": + if has_filter: + return {"reactions": FlowList([])} + else: + return {"reactions": "all"} + else: + return {} + + @classmethod + def to_yaml(cls, representer, data): + return representer.represent_dict(data.phase_attribs) + + class Reaction: """Represents a reaction. @@ -450,56 +574,6 @@ def process_const_cp_thermo(thermo): return thermo_attribs -def get_species_array(speciesArray_node): - """Process a list of species from a speciesArray node.""" - species_list = speciesArray_node.text.replace("\n", " ").strip().split() - datasrc = speciesArray_node.get("datasrc", "") - if datasrc.startswith("#"): - return species_list - else: - filename, location = datasrc.split("#", 1) - name = str(Path(filename).with_suffix(".yaml")) - if location == "species_data": - location = "species" - datasrc = "{}/{}".format(name, location) - return [{datasrc: species_list}] - - -def get_reaction_array(reactionArray_node): - """Process reactions from a reactionArray node in a phase definition.""" - datasrc = reactionArray_node.get("datasrc", "") - has_filter = reactionArray_node.find("include") is not None - if not datasrc.startswith("#"): - if has_filter: - raise ValueError( - "Filtering reaction lists is not possible with external data sources" - ) - filename, location = datasrc.split("#", 1) - name = str(Path(filename).with_suffix(".yaml")) - if location == "reaction_data": - location = "reactions" - datasrc = "{}/{}".format(name, location) - skip = reactionArray_node.find("skip") - if skip is not None: - species_skip = skip.get("species", "") - if species_skip == "undeclared": - reactions = {datasrc: "declared-species"} - else: - raise ValueError("Unknown value in skip parameter for reactionArray") - else: - raise ValueError( - "Missing skip node in reactionArray with external data source" - ) - return {"reactions": [reactions]} - elif datasrc == "#reaction_data": - if has_filter: - return {"reactions": []} - else: - return {"reactions": "all"} - else: - return {} - - def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) @@ -508,66 +582,20 @@ def convert(inpfile, outfile): # Phases phases = [] reaction_filters = [] - for phase in ctml_tree.iterfind("phase"): - phase_name = phase.get("id") - phase_attribs = {"name": phase_name} - phase_thermo = phase.find("thermo") - phase_attribs["thermo"] = thermo_model_mapping[ - phase_thermo.get("model").lower() - ] - for node in phase_thermo: - if node.tag == "site_density": - phase_attribs["site-density"] = get_float_or_units(node) - elif node.tag == "density": - phase_attribs["density"] = get_float_or_units(node) - - phase_attribs["elements"] = phase.find("elementArray").text.strip().split() - phase_attribs["species"] = get_species_array(phase.find("speciesArray")) - species_skip = phase.find("speciesArray").find("skip") - if species_skip is not None: - element_skip = species_skip.get("element", "") - if element_skip == "undeclared": - phase_attribs["skip-undeclared-elements"] = True - - transport_model = transport_model_mapping[ - phase.find("transport").get("model").lower() - ] - if transport_model is not None: - phase_attribs["transport"] = transport_model - - if phase.find("reactionArray") is not None: - # The kinetics model should only be specified if reactions - # are associated with the phase - kinetics_model = kinetics_model_mapping[ - phase.find("kinetics").get("model").lower() - ] - if kinetics_model is not None: - phase_attribs["kinetics"] = kinetics_model - - phase_attribs.update(get_reaction_array(phase.find("reactionArray"))) - reaction_filter = phase.find("reactionArray").find("include") - if reaction_filter is not None: - phase_attribs["reactions"].append("{}-reactions".format(phase_name)) - if reaction_filter.get("min") != reaction_filter.get("max"): - raise ValueError("Can't handle differing reaction filter criteria") - reaction_filters.append( - ("{}-reactions".format(phase_name), reaction_filter.get("min")) + for phase_node in ctml_tree.iterfind("phase"): + this_phase = Phase(phase_node) + phases.append(this_phase) + + reaction_filter = phase_node.find("./reactionArray/include") + if reaction_filter is not None: + if reaction_filter.get("min") != reaction_filter.get("max"): + raise ValueError("Can't handle differing reaction filter criteria") + reaction_filters.append( + ( + "{}-reactions".format(this_phase.phase_attribs["name"]), + reaction_filter.get("min"), ) - - state_node = phase.find("state") - if state_node is not None: - phase_state = {} - for prop in state_node: - property_name = state_properties_mapping[prop.tag] - if prop.tag in ["moleFractions", "massFractions", "coverages"]: - value = split_species_value_string(prop.text) - else: - value = get_float_or_units(prop) - phase_state[property_name] = value - if phase_state: - phase_attribs["state"] = phase_state - - phases.append(phase_attribs) + ) # Species species_data = [] @@ -647,6 +675,7 @@ def convert(inpfile, outfile): output_species.yaml_set_comment_before_after_key("species", before="\n") yaml_obj = yaml.YAML() + yaml_obj.register_class(Phase) yaml_obj.register_class(Reaction) metadata = BlockMap( { From ac808829ff34bc424663637304b784043d9fae25 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 7 Sep 2019 17:30:16 -0400 Subject: [PATCH 24/99] [ctml2yaml] Refactor species into a class --- interfaces/cython/cantera/ctml2yaml.py | 123 ++++++++++++++----------- 1 file changed, 68 insertions(+), 55 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 7fe357f12f5..042be6d1cee 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -34,16 +34,6 @@ def FlowList(*args, **kwargs): species_thermo_mapping = {"NASA": "NASA7"} -species_transport_mapping = {"gas_transport": "gas"} -transport_properties_mapping = { - "LJ_welldepth": "well-depth", - "LJ_diameter": "diameter", - "polarizability": "polarizability", - "rotRelax": "rotational-relaxation", - "dipoleMoment": "dipole", - "dispersion_coefficient": "dispersion-coefficient", - "quadrupole_polarizability": "quadrupole-polarizability", -} # Improved float formatting requires Numpy >= 1.14 HAS_FMT_FLT_POS = hasattr(np, "format_float_positional") @@ -113,7 +103,7 @@ def split_species_value_string(text): The keyword argument sep is used to determine how the pairs are split, typically either " " or ",". """ - pairs = {} + pairs = FlowMap({}) for t in text.replace("\n", " ").replace(",", " ").strip().split(): key, value = t.split(":") try: @@ -273,6 +263,71 @@ def to_yaml(cls, representer, data): return representer.represent_dict(data.phase_attribs) +class Species: + """Represents a species.""" + + _species_transport_mapping = {"gas_transport": "gas"} + _transport_properties_mapping = { + "LJ_welldepth": "well-depth", + "LJ_diameter": "diameter", + "polarizability": "polarizability", + "rotRelax": "rotational-relaxation", + "dipoleMoment": "dipole", + "dispersion_coefficient": "dispersion-coefficient", + "quadrupole_polarizability": "quadrupole-polarizability", + } + + def __init__(self, species): + species_attribs = BlockMap({"name": species.get("name")}) + atom_array = species.find("atomArray") + if atom_array.text is not None: + species_attribs["composition"] = split_species_value_string(atom_array.text) + else: + species_attribs["composition"] = {} + + if species.findtext("note") is not None: + species_attribs["note"] = species.findtext("note") + + thermo = species.find("thermo") + if thermo[0].tag == "NASA": + species_attribs["thermo"] = process_NASA7_thermo(thermo) + elif thermo[0].tag == "const_cp": + species_attribs["thermo"] = process_const_cp_thermo(thermo) + else: + raise TypeError( + "Unknown thermo model type: '{}' for species '{}'".format( + thermo[0].tag, species.get("name") + ) + ) + + transport = species.find("transport") + if transport is not None: + transport_attribs = {} + transport_attribs["model"] = self._species_transport_mapping.get( + transport.get("model"), False + ) + if not transport_attribs["model"]: + raise TypeError( + "Unknown transport model type: '{}' for species '{}'".format( + transport.get("model"), species.get("name") + ) + ) + transport_attribs["geometry"] = transport.findtext( + "string[@title='geometry']" + ) + for tag, name in self._transport_properties_mapping.items(): + value = float(transport.findtext(tag, default=0.0)) + transport_attribs.update(check_float_neq_zero(value, name)) + + species_attribs["transport"] = transport_attribs + + self.species_attribs = species_attribs + + @classmethod + def to_yaml(cls, representer, data): + return representer.represent_dict(data.species_attribs) + + class Reaction: """Represents a reaction. @@ -600,50 +655,7 @@ def convert(inpfile, outfile): # Species species_data = [] for species in ctml_tree.find("speciesData").iterfind("species"): - species_attribs = {"name": species.get("name")} - atom_array = species.find("atomArray") - if atom_array.text is not None: - species_attribs["composition"] = split_species_value_string(atom_array.text) - else: - species_attribs["composition"] = {} - - if species.findtext("note") is not None: - species_attribs["note"] = species.findtext("note") - - thermo = species.find("thermo") - if thermo[0].tag == "NASA": - species_attribs["thermo"] = process_NASA7_thermo(thermo) - elif thermo[0].tag == "const_cp": - species_attribs["thermo"] = process_const_cp_thermo(thermo) - else: - raise TypeError( - "Unknown thermo model type: '{}' for species '{}'".format( - thermo[0].tag, species.get("name") - ) - ) - - transport = species.find("transport") - if transport is not None: - transport_attribs = {} - transport_attribs["model"] = species_transport_mapping.get( - transport.get("model"), False - ) - if not transport_attribs["model"]: - raise TypeError( - "Unknown transport model type: '{}' for species '{}'".format( - transport.get("model"), species.get("name") - ) - ) - transport_attribs["geometry"] = transport.findtext( - "string[@title='geometry']" - ) - for tag, name in transport_properties_mapping.items(): - value = float(transport.findtext(tag, default=0.0)) - transport_attribs.update(check_float_neq_zero(value, name)) - - species_attribs["transport"] = transport_attribs - - species_data.append(species_attribs) + species_data.append(Species(species)) # Reactions reaction_data = [] @@ -676,6 +688,7 @@ def convert(inpfile, outfile): yaml_obj = yaml.YAML() yaml_obj.register_class(Phase) + yaml_obj.register_class(Species) yaml_obj.register_class(Reaction) metadata = BlockMap( { From f0804e05c00c692f6a6c6e83d0d02e3507f7f1fa Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 7 Sep 2019 22:22:09 -0400 Subject: [PATCH 25/99] [ctml2yaml] Refactor SpeciesThermo to a class --- interfaces/cython/cantera/ctml2yaml.py | 80 ++++++++++++++------------ 1 file changed, 42 insertions(+), 38 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 042be6d1cee..9f177fc1e21 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -33,8 +33,6 @@ def FlowList(*args, **kwargs): return lst -species_thermo_mapping = {"NASA": "NASA7"} - # Improved float formatting requires Numpy >= 1.14 HAS_FMT_FLT_POS = hasattr(np, "format_float_positional") @@ -263,6 +261,46 @@ def to_yaml(cls, representer, data): return representer.represent_dict(data.phase_attribs) +class SpeciesThermo: + """Represents a species thermodynamic model.""" + + def __init__(self, thermo): + thermo_type = thermo[0].tag + if thermo_type not in ["NASA", "const_cp"]: + raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) + func = getattr(self, thermo_type) + self.thermo_attribs = func(thermo) + + def NASA(self, thermo): + """Process a NASA 7 thermo entry from XML to a dictionary.""" + thermo_attribs = BlockMap({"model": "NASA7", "data": []}) + temperature_ranges = set() + for model in thermo.iterfind("NASA"): + temperature_ranges.add(float(model.get("Tmin"))) + temperature_ranges.add(float(model.get("Tmax"))) + coeffs = model.find("floatArray").text.replace("\n", " ").strip().split(",") + thermo_attribs["data"].append(FlowList(map(float, coeffs))) + if len(temperature_ranges) != 3: + raise ValueError( + "The midpoint temperature is not consistent between NASA7 entries" + ) + thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) + return thermo_attribs + + def const_cp(self, thermo): + """Process a constant c_p thermo entry from XML to a dictionary.""" + thermo_attribs = BlockMap({"model": "constant-cp"}) + for node in thermo.find("const_cp"): + value = get_float_or_units(node) + thermo_attribs[node.tag] = value + + return thermo_attribs + + @classmethod + def to_yaml(cls, representer, data): + return representer.represent_dict(data.thermo_attribs) + + class Species: """Represents a species.""" @@ -289,16 +327,7 @@ def __init__(self, species): species_attribs["note"] = species.findtext("note") thermo = species.find("thermo") - if thermo[0].tag == "NASA": - species_attribs["thermo"] = process_NASA7_thermo(thermo) - elif thermo[0].tag == "const_cp": - species_attribs["thermo"] = process_const_cp_thermo(thermo) - else: - raise TypeError( - "Unknown thermo model type: '{}' for species '{}'".format( - thermo[0].tag, species.get("name") - ) - ) + species_attribs["thermo"] = SpeciesThermo(thermo) transport = species.find("transport") if transport is not None: @@ -603,32 +632,6 @@ def process_efficiencies(self, eff_node): return efficiencies -def process_NASA7_thermo(thermo): - """Process a NASA 7 thermo entry from XML to a dictionary.""" - thermo_attribs = {"model": "NASA7", "data": []} - temperature_ranges = set() - for model in thermo.iterfind("NASA"): - temperature_ranges.add(float(model.get("Tmin"))) - temperature_ranges.add(float(model.get("Tmax"))) - coeffs = model.find("floatArray").text.replace("\n", " ").strip().split(",") - thermo_attribs["data"].append(list(map(float, coeffs))) - assert ( - len(temperature_ranges) == 3 - ), "The midpoint temperature is not consistent between NASA7 entries" - thermo_attribs["temperature-ranges"] = sorted(list(temperature_ranges)) - return thermo_attribs - - -def process_const_cp_thermo(thermo): - """Process a constant c_p thermo entry from XML to a dictionary.""" - thermo_attribs = {"model": "constant-cp"} - for node in thermo.find("const_cp"): - value = get_float_or_units(node) - thermo_attribs[node.tag] = value - - return thermo_attribs - - def convert(inpfile, outfile): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) @@ -689,6 +692,7 @@ def convert(inpfile, outfile): yaml_obj = yaml.YAML() yaml_obj.register_class(Phase) yaml_obj.register_class(Species) + yaml_obj.register_class(SpeciesThermo) yaml_obj.register_class(Reaction) metadata = BlockMap( { From af71fed1e758ef59c8b367527672598bfbf2d95e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 7 Sep 2019 22:29:53 -0400 Subject: [PATCH 26/99] [ctml2yaml] Refactor SpeciesTransport to a class --- interfaces/cython/cantera/ctml2yaml.py | 47 +++++++++++++++----------- 1 file changed, 27 insertions(+), 20 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 9f177fc1e21..7e9f6b54873 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -301,8 +301,8 @@ def to_yaml(cls, representer, data): return representer.represent_dict(data.thermo_attribs) -class Species: - """Represents a species.""" +class SpeciesTransport: + """Represents the transport properties of a species.""" _species_transport_mapping = {"gas_transport": "gas"} _transport_properties_mapping = { @@ -315,6 +315,29 @@ class Species: "quadrupole_polarizability": "quadrupole-polarizability", } + def __init__(self, transport): + transport_attribs = BlockMap({}) + transport_model = transport.get("model") + if transport_model not in self._species_transport_mapping: + raise TypeError( + "Unknown transport model type: '{}'".format(transport.get("model")) + ) + transport_attribs["model"] = self._species_transport_mapping[transport_model] + transport_attribs["geometry"] = transport.findtext("string[@title='geometry']") + for tag, name in self._transport_properties_mapping.items(): + value = float(transport.findtext(tag, default=0.0)) + transport_attribs.update(check_float_neq_zero(value, name)) + + self.transport_attribs = transport_attribs + + @classmethod + def to_yaml(cls, representer, data): + return representer.represent_dict(data.transport_attribs) + + +class Species: + """Represents a species.""" + def __init__(self, species): species_attribs = BlockMap({"name": species.get("name")}) atom_array = species.find("atomArray") @@ -331,24 +354,7 @@ def __init__(self, species): transport = species.find("transport") if transport is not None: - transport_attribs = {} - transport_attribs["model"] = self._species_transport_mapping.get( - transport.get("model"), False - ) - if not transport_attribs["model"]: - raise TypeError( - "Unknown transport model type: '{}' for species '{}'".format( - transport.get("model"), species.get("name") - ) - ) - transport_attribs["geometry"] = transport.findtext( - "string[@title='geometry']" - ) - for tag, name in self._transport_properties_mapping.items(): - value = float(transport.findtext(tag, default=0.0)) - transport_attribs.update(check_float_neq_zero(value, name)) - - species_attribs["transport"] = transport_attribs + species_attribs["transport"] = SpeciesTransport(transport) self.species_attribs = species_attribs @@ -693,6 +699,7 @@ def convert(inpfile, outfile): yaml_obj.register_class(Phase) yaml_obj.register_class(Species) yaml_obj.register_class(SpeciesThermo) + yaml_obj.register_class(SpeciesTransport) yaml_obj.register_class(Reaction) metadata = BlockMap( { From 3f484e2c63c609f3f3bd443fe3e15821a3bf58af Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 16:03:37 -0500 Subject: [PATCH 27/99] [ctml2yaml] Simplify YAML output section This change simplifies how classes are registered with the YAML emitter. --- interfaces/cython/cantera/ctml2yaml.py | 18 ++++++++---------- 1 file changed, 8 insertions(+), 10 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 7e9f6b54873..2592bd12476 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -695,12 +695,10 @@ def convert(inpfile, outfile): output_species = BlockMap({"species": species_data}) output_species.yaml_set_comment_before_after_key("species", before="\n") - yaml_obj = yaml.YAML() - yaml_obj.register_class(Phase) - yaml_obj.register_class(Species) - yaml_obj.register_class(SpeciesThermo) - yaml_obj.register_class(SpeciesTransport) - yaml_obj.register_class(Reaction) + emitter = yaml.YAML() + for cl in [Phase, Species, SpeciesThermo, SpeciesTransport, Reaction]: + emitter.register_class(cl) + metadata = BlockMap( { "generator": "ctml2yaml", @@ -711,10 +709,10 @@ def convert(inpfile, outfile): if inpfile is not None: metadata["input-files"] = FlowList([str(inpfile)]) with Path(outfile).open("w") as output_file: - yaml_obj.dump(metadata, output_file) - yaml_obj.dump(output_phases, output_file) - yaml_obj.dump(output_species, output_file) - yaml_obj.dump(output_reactions, output_file) + emitter.dump(metadata, output_file) + emitter.dump(output_phases, output_file) + emitter.dump(output_species, output_file) + emitter.dump(output_reactions, output_file) if __name__ == "__main__": From 8cd35b6a31f068f4c67a36c47400d9b9b846d075 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 9 Sep 2019 09:39:01 -0400 Subject: [PATCH 28/99] [ctml2yaml] Add handling of liquidvapor phases --- interfaces/cython/cantera/ctml2yaml.py | 33 +++++++++++++++---- .../cython/cantera/test/test_convert.py | 13 ++++++-- 2 files changed, 38 insertions(+), 8 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 2592bd12476..b320ce5cc44 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -125,6 +125,7 @@ class Phase: "metal": "electron-cloud", "lattice": "lattice", "edge": "edge", + "purefluid": "pure-fluid", } _kinetics_model_mapping = { "gaskinetics": "gas", @@ -146,6 +147,17 @@ class Phase: "coverages": "coverages", } + _pure_fluid_mapping = { + "0": "water", + "1": "nitrogen", + "2": "methane", + "3": "hydrogen", + "4": "oxygen", + "5": "HFC134a", + "7": "carbondioxide", + "8": "heptane", + } + def __init__(self, phase): phase_name = phase.get("id") phase_attribs = BlockMap({"name": phase_name}) @@ -153,6 +165,12 @@ def __init__(self, phase): phase_attribs["thermo"] = self._thermo_model_mapping[ phase_thermo.get("model").lower() ] + # Convert pure fluid type integer into the name + if phase_thermo.get("model") == "PureFluid": + phase_attribs["pure-fluid-name"] = self._pure_fluid_mapping[ + phase_thermo.get("fluid_type") + ] + for node in phase_thermo: if node.tag == "site_density": phase_attribs["site-density"] = get_float_or_units(node) @@ -169,11 +187,13 @@ def __init__(self, phase): if element_skip == "undeclared": phase_attribs["skip-undeclared-elements"] = True - transport_model = self._transport_model_mapping[ - phase.find("transport").get("model").lower() - ] - if transport_model is not None: - phase_attribs["transport"] = transport_model + transport_node = phase.find("transport") + if transport_node is not None: + transport_model = self._transport_model_mapping[ + transport_node.get("model").lower() + ] + if transport_model is not None: + phase_attribs["transport"] = transport_model if phase.find("reactionArray") is not None: # The kinetics model should only be specified if reactions @@ -712,7 +732,8 @@ def convert(inpfile, outfile): emitter.dump(metadata, output_file) emitter.dump(output_phases, output_file) emitter.dump(output_species, output_file) - emitter.dump(output_reactions, output_file) + if output_reactions: + emitter.dump(output_reactions, output_file) if __name__ == "__main__": diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 68ca6c32386..278355ea8e5 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -826,7 +826,7 @@ def test_pdep(self): def test_ptcombust(self): ctml2yaml.convert(Path(self.cantera_data).joinpath('ptcombust.xml'), - Path(self.test_work_dir).joinpath('ptcombust.yaml')) + Path(self.test_work_dir).joinpath('ptcombust.yaml')) ctmlGas, yamlGas = self.checkConversion('ptcombust') ctmlSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface, phaseid='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) @@ -837,7 +837,7 @@ def test_ptcombust(self): def test_sofc(self): ctml2yaml.convert(Path(self.cantera_data).joinpath('sofc.xml'), - Path(self.test_work_dir).joinpath('sofc.yaml')) + Path(self.test_work_dir).joinpath('sofc.yaml')) ctmlGas, yamlGas = self.checkConversion('sofc') ctmlMetal, yamlMetal = self.checkConversion('sofc', phaseid='metal') ctmlOxide, yamlOxide = self.checkConversion('sofc', phaseid='oxide_bulk') @@ -857,3 +857,12 @@ def test_sofc(self): self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5]) ctmlMetal.electric_potential = yamlMetal.electric_potential = 4 self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5]) + + def test_liquidvapor(self): + ctml2yaml.convert(Path(self.cantera_data).joinpath('liquidvapor.xml'), + Path(self.test_work_dir).joinpath('liquidvapor.yaml')) + for name in ['water', 'nitrogen', 'methane', 'hydrogen', 'oxygen', + 'hfc134a', 'carbondioxide', 'heptane']: + ctmlPhase, yamlPhase = self.checkConversion('liquidvapor', phaseid=name) + self.checkThermo(ctmlPhase, yamlPhase, + [1.3 * ctmlPhase.min_temp, 0.7 * ctmlPhase.max_temp]) From fa4eed6258d200874a3ae96f57ba4603c092ebe3 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 9 Sep 2019 09:43:48 -0400 Subject: [PATCH 29/99] [ctml2yaml] Check for empty species section Species can be specified in another data source. --- interfaces/cython/cantera/ctml2yaml.py | 9 ++++++--- 1 file changed, 6 insertions(+), 3 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index b320ce5cc44..bc593c45273 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -712,8 +712,10 @@ def convert(inpfile, outfile): output_phases = BlockMap({"phases": phases}) output_phases.yaml_set_comment_before_after_key("phases", before="\n") - output_species = BlockMap({"species": species_data}) - output_species.yaml_set_comment_before_after_key("species", before="\n") + output_species = BlockMap() + if species_data: + output_species["species"] = species_data + output_species.yaml_set_comment_before_after_key("species", before="\n") emitter = yaml.YAML() for cl in [Phase, Species, SpeciesThermo, SpeciesTransport, Reaction]: @@ -731,7 +733,8 @@ def convert(inpfile, outfile): with Path(outfile).open("w") as output_file: emitter.dump(metadata, output_file) emitter.dump(output_phases, output_file) - emitter.dump(output_species, output_file) + if output_species: + emitter.dump(output_species, output_file) if output_reactions: emitter.dump(output_reactions, output_file) From 5be317c351332a5fd6c145da84474b32641ccfe7 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 9 Sep 2019 22:22:57 -0400 Subject: [PATCH 30/99] [ctml2yaml] Add R-K equation of state The XML file for this test wasn't already present in the repo, so convert the CTI file to XML first. --- interfaces/cython/cantera/ctml2yaml.py | 109 +++++++++++++++++- .../cython/cantera/test/test_convert.py | 13 ++- 2 files changed, 117 insertions(+), 5 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index bc593c45273..6934deb63bf 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -8,6 +8,8 @@ import xml.etree.ElementTree as etree from email.utils import formatdate +from itertools import chain +from collections import defaultdict from typing import Any @@ -126,6 +128,7 @@ class Phase: "lattice": "lattice", "edge": "edge", "purefluid": "pure-fluid", + "redlichkwongmftp": "Redlich-Kwong", } _kinetics_model_mapping = { "gaskinetics": "gas", @@ -135,7 +138,7 @@ class Phase: } _transport_model_mapping = { "mix": "mixture-averaged", - "multi": "multi-component", + "multi": "multicomponent", "none": None, } @@ -358,8 +361,10 @@ def to_yaml(cls, representer, data): class Species: """Represents a species.""" - def __init__(self, species): - species_attribs = BlockMap({"name": species.get("name")}) + def __init__(self, species, activity_coefficients=None): + species_attribs = BlockMap() + species_name = species.get("name") + species_attribs["name"] = species_name atom_array = species.find("atomArray") if atom_array.text is not None: species_attribs["composition"] = split_species_value_string(atom_array.text) @@ -372,12 +377,66 @@ def __init__(self, species): thermo = species.find("thermo") species_attribs["thermo"] = SpeciesThermo(thermo) + if activity_coefficients: + species_attribs["equation-of-state"] = self.process_act_coeff( + species_name, activity_coefficients + ) + transport = species.find("transport") if transport is not None: species_attribs["transport"] = SpeciesTransport(transport) self.species_attribs = species_attribs + def process_act_coeff(self, species_name, activity_coefficients): + """If a species has activity coefficients, create an equation-of-state mapping.""" + eq_of_state = BlockMap({"model": activity_coefficients["model"]}) + pure_params = activity_coefficients["pure_params"] + pure_a = pure_params.findtext("a_coeff") + pure_a = list(map(float, pure_a.replace("\n", " ").strip().split(","))) + pure_a_units = pure_params.find("a_coeff").get("units") + if pure_a_units is not None: + pure_a_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_a_units) + pure_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_a_units) + pure_a[0] = "{} {}".format(float2string(pure_a[0]), pure_a_units + "*K^0.5") + pure_a[1] = "{} {}".format(float2string(pure_a[1]), pure_a_units + "/K^0.5") + pure_b = float(pure_params.findtext("b_coeff").strip()) + pure_b_units = pure_params.find("b_coeff").get("units") + if pure_b_units is not None: + pure_b_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_b_units) + pure_b_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_b_units) + pure_b = "{} {}".format(float2string(pure_b), pure_b_units) + eq_of_state["a"] = FlowList(pure_a) + eq_of_state["b"] = pure_b + + cross_params = activity_coefficients.get("cross_params") + if cross_params is not None: + related_species = [ + cross_params.get("species1"), + cross_params.get("species2"), + ] + if species_name == related_species[0]: + other_species = related_species[1] + else: + other_species = related_species[0] + cross_a = cross_params.findtext("a_coeff") + cross_a = list(map(float, cross_a.replace("\n", " ").strip().split(","))) + cross_a_units = cross_params.find("a_coeff").get("units") + if cross_a_units is not None: + cross_a_units = re.sub( + r"([A-Za-z])-([A-Za-z])", r"\1*\2", cross_a_units + ) + cross_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", cross_a_units) + cross_a[0] = "{} {}".format( + float2string(cross_a[0]), cross_a_units + "*K^0.5" + ) + cross_a[1] = "{} {}".format( + float2string(cross_a[1]), cross_a_units + "/K^0.5" + ) + eq_of_state["binary-a"] = {other_species: FlowList(cross_a)} + + return eq_of_state + @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.species_attribs) @@ -666,6 +725,8 @@ def convert(inpfile, outfile): # Phases phases = [] reaction_filters = [] + act_pure_params = defaultdict(list) + act_cross_params = defaultdict(list) for phase_node in ctml_tree.iterfind("phase"): this_phase = Phase(phase_node) phases.append(this_phase) @@ -680,11 +741,51 @@ def convert(inpfile, outfile): reaction_filter.get("min"), ) ) + # Collect all of the activityCoefficients nodes from all of the phase + # definitions. This allows us to check that each species has only one + # definition of pure fluid parameters. Note that activityCoefficients are + # only defined for some phase thermo models. + ac_coeff_node = phase_node.find("./thermo/activityCoefficients") + if ac_coeff_node is not None: + act_pure_params[this_phase.phase_attribs["thermo"]].extend( + list(ac_coeff_node.iterfind("pureFluidParameters")) + ) + act_cross_params[this_phase.phase_attribs["thermo"]].extend( + list(ac_coeff_node.iterfind("crossFluidParameters")) + ) # Species species_data = [] for species in ctml_tree.find("speciesData").iterfind("species"): - species_data.append(Species(species)) + species_name = species.get("name") + activity_parameters = {} + for phase_thermo, params_list in act_pure_params.items(): + for params in params_list: + if params.get("species") != species_name: + continue + if activity_parameters: + raise ValueError( + "Multiple sets of pureFluidParameters found for species " + "'{}'".format(species_name) + ) + activity_parameters["model"] = phase_thermo + activity_parameters["pure_params"] = params + + for phase_thermo, params_list in act_cross_params.items(): + for params in params_list: + related_species = [params.get("species1"), params.get("species2")] + if species_name in related_species: + if phase_thermo != activity_parameters["model"]: + raise ValueError( + "crossFluidParameters found for phase thermo '{}' with " + "pureFluidParameters found for phase thermo '{}' " + "for species '{}'".format( + phase_thermo, activity_parameters["model"], species_name + ) + ) + activity_parameters["cross_params"] = params + + species_data.append(Species(species, activity_parameters)) # Reactions reaction_data = [] diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 278355ea8e5..e29118a98c1 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -5,7 +5,7 @@ from . import utilities import cantera as ct -from cantera import ck2cti, ck2yaml, cti2yaml, ctml2yaml +from cantera import ck2cti, ck2yaml, cti2yaml, ctml2yaml, ctml_writer class converterTestCommon: @@ -866,3 +866,14 @@ def test_liquidvapor(self): ctmlPhase, yamlPhase = self.checkConversion('liquidvapor', phaseid=name) self.checkThermo(ctmlPhase, yamlPhase, [1.3 * ctmlPhase.min_temp, 0.7 * ctmlPhase.max_temp]) + + def test_Redlich_Kwong_CO2(self): + ctml_writer.convert(str(Path(self.test_data_dir).joinpath('co2_RK_example.cti')), + str(Path(self.test_work_dir).joinpath('co2_RK_example.xml'))) + ctml2yaml.convert(Path(self.test_work_dir).joinpath('co2_RK_example.xml'), + Path(self.test_work_dir).joinpath('co2_RK_example.yaml')) + ctmlGas, yamlGas = self.checkConversion('co2_RK_example') + for P in [1e5, 2e6, 1.3e7]: + yamlGas.TP = ctmlGas.TP = 300, P + self.checkThermo(ctmlGas, yamlGas, [300, 400, 500]) + From 635d592710d8b95da06e594dad0280db5fae20bb Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 9 Sep 2019 22:30:22 -0400 Subject: [PATCH 31/99] [ctml2yaml] Add Reitz n-dodecane RK test --- interfaces/cython/cantera/ctml2yaml.py | 12 ++++++------ interfaces/cython/cantera/test/test_convert.py | 7 +++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 6934deb63bf..53c8e093c3f 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -533,9 +533,9 @@ def threebody(self, rate_coeff): reaction_attribs["rate-constant"] = self.process_arrhenius_parameters( rate_coeff.find("Arrhenius") ) - reaction_attribs["efficiencies"] = self.process_efficiencies( - rate_coeff.find("efficiencies") - ) + eff_node = rate_coeff.find("efficiencies") + if eff_node is not None: + reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) return reaction_attribs @@ -557,9 +557,9 @@ def lindemann(self, rate_coeff): ] = self.process_arrhenius_parameters(arr_coeff) else: raise TypeError("Too many Arrhenius nodes") - reaction_attribs["efficiencies"] = self.process_efficiencies( - rate_coeff.find("efficiencies") - ) + eff_node = rate_coeff.find("efficiencies") + if eff_node is not None: + reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) return reaction_attribs diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index e29118a98c1..b532fb2373e 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -877,3 +877,10 @@ def test_Redlich_Kwong_CO2(self): yamlGas.TP = ctmlGas.TP = 300, P self.checkThermo(ctmlGas, yamlGas, [300, 400, 500]) + def test_Redlich_Kwong_ndodecane(self): + ctml2yaml.convert(Path(self.cantera_data).joinpath('nDodecane_Reitz.xml'), + Path(self.test_work_dir).joinpath('nDodecane_Reitz.yaml')) + ctmlGas, yamlGas = self.checkConversion('nDodecane_Reitz') + self.checkThermo(ctmlGas, yamlGas, [300, 400, 500]) + self.checkKinetics(ctmlGas, yamlGas, [300, 500, 1300], [1e5, 2e6, 1.4e7], + 1e-6) From 3b8b86e11ff7c374d021d24a56ce9776b520e0f1 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 11 Sep 2019 15:40:43 -0400 Subject: [PATCH 32/99] [ctml2yaml] Add diamond conversion test This commit includes some work-around for the fact that electron-cloud phases include the density in the phase definition but fixed-stoichiometry phases put the density in the species definition. --- interfaces/cython/cantera/ctml2yaml.py | 62 ++++++++++++++----- .../cython/cantera/test/test_convert.py | 18 +++++- 2 files changed, 63 insertions(+), 17 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 53c8e093c3f..f2c90f45d61 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -129,6 +129,8 @@ class Phase: "edge": "edge", "purefluid": "pure-fluid", "redlichkwongmftp": "Redlich-Kwong", + "stoichsubstance": "fixed-stoichiometry", + "surface": "ideal-surface", } _kinetics_model_mapping = { "gaskinetics": "gas", @@ -178,7 +180,8 @@ def __init__(self, phase): if node.tag == "site_density": phase_attribs["site-density"] = get_float_or_units(node) elif node.tag == "density": - phase_attribs["density"] = get_float_or_units(node) + if phase_attribs["thermo"] == "electron-cloud": + phase_attribs["density"] = get_float_or_units(node) phase_attribs["elements"] = FlowList( phase.find("elementArray").text.strip().split() @@ -361,7 +364,7 @@ def to_yaml(cls, representer, data): class Species: """Represents a species.""" - def __init__(self, species, activity_coefficients=None): + def __init__(self, species, **kwargs): species_attribs = BlockMap() species_name = species.get("name") species_attribs["name"] = species_name @@ -377,11 +380,24 @@ def __init__(self, species, activity_coefficients=None): thermo = species.find("thermo") species_attribs["thermo"] = SpeciesThermo(thermo) - if activity_coefficients: + activity_parameters = kwargs.get("activity_parameters", False) + if activity_parameters: species_attribs["equation-of-state"] = self.process_act_coeff( - species_name, activity_coefficients + species_name, activity_parameters ) + const_dens = kwargs.get("const_dens") + if const_dens is not None: + const_prop = { + "density": "density", + "molarDensity": "molar-density", + "molarVolume": "molar-volume", + }[const_dens.tag] + species_attribs["equation-of-state"] = { + "model": "constant-volume", + const_prop: get_float_or_units(const_dens), + } + transport = species.find("transport") if transport is not None: species_attribs["transport"] = SpeciesTransport(transport) @@ -727,6 +743,7 @@ def convert(inpfile, outfile): reaction_filters = [] act_pure_params = defaultdict(list) act_cross_params = defaultdict(list) + const_density_specs = {} for phase_node in ctml_tree.iterfind("phase"): this_phase = Phase(phase_node) phases.append(this_phase) @@ -753,39 +770,56 @@ def convert(inpfile, outfile): act_cross_params[this_phase.phase_attribs["thermo"]].extend( list(ac_coeff_node.iterfind("crossFluidParameters")) ) + phase_thermo_node = phase_node.find("thermo") + if phase_thermo_node.get("model") == "StoichSubstance": + for den_node in phase_thermo_node: + if den_node.tag == "density": + for spec in this_phase.phase_attribs["species"]: + const_density_specs[spec] = den_node # Species species_data = [] - for species in ctml_tree.find("speciesData").iterfind("species"): - species_name = species.get("name") - activity_parameters = {} + for species_node in ctml_tree.find("speciesData").iterfind("species"): + species_name = species_node.get("name") + # Does it make more sense to modify the object after construction + # with these equation-of-state type parameters? Right now, all of this + # is done during construction. + activity_params = {} for phase_thermo, params_list in act_pure_params.items(): for params in params_list: if params.get("species") != species_name: continue - if activity_parameters: + if activity_params: raise ValueError( "Multiple sets of pureFluidParameters found for species " "'{}'".format(species_name) ) - activity_parameters["model"] = phase_thermo - activity_parameters["pure_params"] = params + activity_params["model"] = phase_thermo + activity_params["pure_params"] = params for phase_thermo, params_list in act_cross_params.items(): for params in params_list: related_species = [params.get("species1"), params.get("species2")] if species_name in related_species: - if phase_thermo != activity_parameters["model"]: + if phase_thermo != activity_params["model"]: raise ValueError( "crossFluidParameters found for phase thermo '{}' with " "pureFluidParameters found for phase thermo '{}' " "for species '{}'".format( - phase_thermo, activity_parameters["model"], species_name + phase_thermo, activity_params["model"], species_name ) ) - activity_parameters["cross_params"] = params + activity_params["cross_params"] = params + + const_dens_params = const_density_specs.get(species_name) + if activity_params: + this_species = Species(species_node, activity_parameters=activity_params) + elif const_dens_params is not None: + this_species = Species(species_node, const_dens=const_dens_params) + else: + this_species = Species(species_node) - species_data.append(Species(species, activity_parameters)) + species_data.append(this_species) # Reactions reaction_data = [] diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index b532fb2373e..3cdaea14b3e 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -735,8 +735,6 @@ class ctml2yamlTest(utilities.CanteraTest): @classmethod def setUpClass(cls): super().setUpClass() - ctml2yaml.convert(Path(cls.cantera_data).joinpath('gri30.xml'), - Path(cls.test_work_dir).joinpath('gri30.yaml')) def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(), yamlphases=(), **kwargs): @@ -809,6 +807,8 @@ def checkTransport(self, ctmlPhase, yamlPhase, temperatures, self.assertNear(Dkm_ctml[i], Dkm_yaml[i], msg=message) def test_gri30(self): + ctml2yaml.convert(Path(self.cantera_data).joinpath('gri30.xml'), + Path(self.test_work_dir).joinpath('gri30.yaml')) ctmlPhase, yamlPhase = self.checkConversion('gri30') X = {'O2': 0.3, 'H2': 0.1, 'CH4': 0.2, 'CO2': 0.4} ctmlPhase.X = X @@ -879,8 +879,20 @@ def test_Redlich_Kwong_CO2(self): def test_Redlich_Kwong_ndodecane(self): ctml2yaml.convert(Path(self.cantera_data).joinpath('nDodecane_Reitz.xml'), - Path(self.test_work_dir).joinpath('nDodecane_Reitz.yaml')) + Path(self.test_work_dir).joinpath('nDodecane_Reitz.yaml')) ctmlGas, yamlGas = self.checkConversion('nDodecane_Reitz') self.checkThermo(ctmlGas, yamlGas, [300, 400, 500]) self.checkKinetics(ctmlGas, yamlGas, [300, 500, 1300], [1e5, 2e6, 1.4e7], 1e-6) + + def test_diamond(self): + ctml2yaml.convert(Path(self.cantera_data).joinpath('diamond.xml'), + Path(self.test_work_dir).joinpath('diamond.yaml')) + ctmlGas, yamlGas = self.checkConversion('diamond', phaseid='gas') + ctmlSolid, yamlSolid = self.checkConversion('diamond', phaseid='diamond') + ctmlSurf, yamlSurf = self.checkConversion('diamond', + ct.Interface, phaseid='diamond_100', ctmlphases=[ctmlGas, ctmlSolid], + yamlphases=[yamlGas, yamlSolid]) + self.checkThermo(ctmlSolid, yamlSolid, [300, 500]) + self.checkThermo(ctmlSurf, yamlSurf, [330, 490]) + self.checkKinetics(ctmlSurf, yamlSurf, [400, 800], [2e5]) From 08c567ec266cd3af4ad157a238aee4d88669284f Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 11 Sep 2019 16:34:05 -0400 Subject: [PATCH 33/99] [ctml2yaml] Add lithium_ion_battery test --- interfaces/cython/cantera/ctml2yaml.py | 105 ++++++++++++++---- .../cython/cantera/test/test_convert.py | 38 +++++++ 2 files changed, 123 insertions(+), 20 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index f2c90f45d61..2ab41aee74e 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -131,6 +131,8 @@ class Phase: "redlichkwongmftp": "Redlich-Kwong", "stoichsubstance": "fixed-stoichiometry", "surface": "ideal-surface", + "binarysolutiontabulatedthermo": "binary-solution-tabulated", + "idealsolidsolution": "ideal-condensed", } _kinetics_model_mapping = { "gaskinetics": "gas", @@ -182,10 +184,16 @@ def __init__(self, phase): elif node.tag == "density": if phase_attribs["thermo"] == "electron-cloud": phase_attribs["density"] = get_float_or_units(node) - - phase_attribs["elements"] = FlowList( - phase.find("elementArray").text.strip().split() - ) + elif node.tag == "tabulatedSpecies": + phase_attribs["tabulated-species"] = node.get("name") + elif node.tag == "tabulatedThermo": + phase_attribs["tabulated-thermo"] = self.get_tabulated_thermo(node) + + elements = phase.find("elementArray").text + if elements is not None: + phase_attribs["elements"] = FlowList( + elements.strip().split() + ) phase_attribs["species"] = self.get_species_array(phase.find("speciesArray")) species_skip = phase.find("speciesArray").find("skip") if species_skip is not None: @@ -210,10 +218,12 @@ def __init__(self, phase): if kinetics_model is not None: phase_attribs["kinetics"] = kinetics_model - phase_attribs.update(self.get_reaction_array(phase.find("reactionArray"))) - reaction_filter = phase.find("reactionArray").find("include") - if reaction_filter is not None: - phase_attribs["reactions"].append("{}-reactions".format(phase_name)) + phase_attribs.update( + self.get_reaction_array(phase.find("reactionArray")) + ) + reaction_filter = phase.find("reactionArray").find("include") + if reaction_filter is not None: + phase_attribs["reactions"].append("{}-reactions".format(phase_name)) state_node = phase.find("state") if state_node is not None: @@ -228,6 +238,10 @@ def __init__(self, phase): if phase_state: phase_attribs["state"] = phase_state + std_conc_node = phase.find("standardConc") + if std_conc_node is not None: + phase_attribs["standard-concentration-basis"] = std_conc_node.get("model") + self.phase_attribs = phase_attribs def get_species_array(self, speciesArray_node): @@ -250,6 +264,8 @@ def get_reaction_array(self, reactionArray_node): """Process reactions from a reactionArray node in a phase definition.""" datasrc = reactionArray_node.get("datasrc", "") has_filter = reactionArray_node.find("include") is not None + # if has_filter and reactionArray_node.find("include").get("min") == "None": + # return {} if not datasrc.startswith("#"): if has_filter: raise ValueError( @@ -282,6 +298,42 @@ def get_reaction_array(self, reactionArray_node): else: return {} + def get_tabulated_thermo(self, tab_thermo_node): + tab_thermo = BlockMap() + enthalpy_node = tab_thermo_node.find("enthalpy") + enthalpy_units = enthalpy_node.get("units").split("/") + entropy_node = tab_thermo_node.find("entropy") + entropy_units = entropy_node.get("units").split("/") + if enthalpy_units[:2] != entropy_units[:2]: + raise ValueError("Tabulated thermo must have the same units.") + tab_thermo["units"] = FlowMap( + {"energy": entropy_units[0], "quantity": entropy_units[1]} + ) + enthalpy = enthalpy_node.text.replace("\n", " ").split(",") + if len(enthalpy) != int(enthalpy_node.get("size")): + raise ValueError( + "The number of entries in the enthalpy list is different from the " + "indicated size." + ) + tab_thermo["enthalpy"] = FlowList(map(float, enthalpy)) + entropy = entropy_node.text.replace("\n", " ").split(",") + tab_thermo["entropy"] = FlowList(map(float, entropy)) + if len(entropy) != int(entropy_node.get("size")): + raise ValueError( + "The number of entries in the entropy list is different from the " + "indicated size." + ) + mole_fraction_node = tab_thermo_node.find("moleFraction") + mole_fraction = mole_fraction_node.text.replace("\n", " ").split(",") + tab_thermo["mole-fractions"] = FlowList(map(float, mole_fraction)) + if len(mole_fraction) != int(mole_fraction_node.get("size")): + raise ValueError( + "The number of entries in the mole_fraction list is different from the " + "indicated size." + ) + + return tab_thermo + @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.phase_attribs) @@ -402,6 +454,13 @@ def __init__(self, species, **kwargs): if transport is not None: species_attribs["transport"] = SpeciesTransport(transport) + std_state = species.find("standardState") + if std_state is not None: + species_attribs["equation-of-state"] = { + "model": "constant-volume", + "molar-volume": get_float_or_units(std_state.find("molarVolume")), + } + self.species_attribs = species_attribs def process_act_coeff(self, species_name, activity_coefficients): @@ -512,7 +571,9 @@ def __init__(self, reaction): reaction_attribs.update(func(rate_coeff)) reaction_attribs["equation"] = ( - reaction.find("equation").text.replace("[", "<").replace("]", ">") + # This has to replace the reaction direction symbols separately because + # species names can have [ or ] in them + reaction.find("equation").text.replace("[=]", "<=>").replace("=]", "=>") ) reactants = split_species_value_string(reaction.findtext("reactants")) @@ -691,12 +752,14 @@ def edge(self, rate_coeff): used to update the parent reaction entry dictionary. """ arr_node = rate_coeff.find("Arrhenius") - reaction_attributes = FlowMap( + reaction_attributes = BlockMap( { "rate-constant": self.process_arrhenius_parameters(arr_node), "beta": float(rate_coeff.find("electrochem").get("beta")), } ) + if rate_coeff.get("type", "") == "exchangecurrentdensity": + reaction_attributes["exchange-current-density-formulation"] = True return reaction_attributes def arrhenius(self, rate_coeff): @@ -715,11 +778,11 @@ def arrhenius(self, rate_coeff): def process_arrhenius_parameters(self, arr_node): """Process the parameters from an Arrhenius child of a rateCoeff node.""" - rate_constant = FlowMap({}) - rate_constant["A"] = get_float_or_units(arr_node.find("A")) - rate_constant["b"] = get_float_or_units(arr_node.find("b")) - rate_constant["Ea"] = get_float_or_units(arr_node.find("E")) - return rate_constant + return FlowMap({ + "A": get_float_or_units(arr_node.find("A")), + "b": get_float_or_units(arr_node.find("b")), + "Ea": get_float_or_units(arr_node.find("E")), + }) def process_efficiencies(self, eff_node): """Process the efficiency information about a reaction.""" @@ -752,12 +815,14 @@ def convert(inpfile, outfile): if reaction_filter is not None: if reaction_filter.get("min") != reaction_filter.get("max"): raise ValueError("Can't handle differing reaction filter criteria") - reaction_filters.append( - ( - "{}-reactions".format(this_phase.phase_attribs["name"]), - reaction_filter.get("min"), + filter_value = reaction_filter.get("min") + if filter_value != "None": + reaction_filters.append( + ( + "{}-reactions".format(this_phase.phase_attribs["name"]), + filter_value, + ) ) - ) # Collect all of the activityCoefficients nodes from all of the phase # definitions. This allows us to check that each species has only one # definition of pure fluid parameters. Note that activityCoefficients are diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 3cdaea14b3e..6606a3e1488 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -896,3 +896,41 @@ def test_diamond(self): self.checkThermo(ctmlSolid, yamlSolid, [300, 500]) self.checkThermo(ctmlSurf, yamlSurf, [330, 490]) self.checkKinetics(ctmlSurf, yamlSurf, [400, 800], [2e5]) + + def test_lithium_ion_battery(self): + name = 'lithium_ion_battery' + ctml2yaml.convert(Path(self.cantera_data).joinpath(name + ".xml"), + Path(self.test_work_dir).joinpath(name + ".yaml")) + ctmlAnode, yamlAnode = self.checkConversion(name, phaseid='anode') + ctmlCathode, yamlCathode = self.checkConversion(name, phaseid='cathode') + ctmlMetal, yamlMetal = self.checkConversion(name, phaseid='electron') + ctmlElyt, yamlElyt = self.checkConversion(name, phaseid='electrolyte') + ctmlAnodeInt, yamlAnodeInt = self.checkConversion(name, + phaseid='edge_anode_electrolyte', + ctmlphases=[ctmlAnode, ctmlMetal, ctmlElyt], + yamlphases=[yamlAnode, yamlMetal, yamlElyt]) + ctmlCathodeInt, yamlCathodeInt = self.checkConversion(name, + phaseid='edge_cathode_electrolyte', + ctmlphases=[ctmlCathode, ctmlMetal, ctmlElyt], + yamlphases=[yamlCathode, yamlMetal, yamlElyt]) + + self.checkThermo(ctmlAnode, yamlAnode, [300, 330]) + self.checkThermo(ctmlCathode, yamlCathode, [300, 330]) + + ctmlAnode.X = yamlAnode.X = [0.7, 0.3] + self.checkThermo(ctmlAnode, yamlAnode, [300, 330]) + ctmlCathode.X = yamlCathode.X = [0.2, 0.8] + self.checkThermo(ctmlCathode, yamlCathode, [300, 330]) + + for phase in [ctmlAnode, yamlAnode, ctmlCathode, yamlCathode, ctmlMetal, + yamlMetal, ctmlElyt, yamlElyt, ctmlAnodeInt, yamlAnodeInt, + ctmlCathodeInt, yamlCathodeInt]: + phase.TP = 300, 1e5 + ctmlMetal.electric_potential = yamlMetal.electric_potential = 0 + ctmlElyt.electric_potential = yamlElyt.electric_potential = 1.9 + self.checkKinetics(ctmlAnodeInt, yamlAnodeInt, [300], [1e5]) + + ctmlMetal.electric_potential = yamlMetal.electric_potential = 2.2 + ctmlElyt.electric_potential = yamlElyt.electric_potential = 0 + self.checkKinetics(ctmlCathodeInt, yamlCathodeInt, [300], [1e5]) + From 2f7667d0098dfa9c9336e8b1ec7f9e30035c7715 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 13 Sep 2019 20:57:07 -0400 Subject: [PATCH 34/99] [ctml2yaml] Add negative-A handling for reactions --- interfaces/cython/cantera/ctml2yaml.py | 100 ++++++++++-------- .../cython/cantera/test/test_convert.py | 6 ++ 2 files changed, 60 insertions(+), 46 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 2ab41aee74e..c19bbed09df 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -191,9 +191,7 @@ def __init__(self, phase): elements = phase.find("elementArray").text if elements is not None: - phase_attribs["elements"] = FlowList( - elements.strip().split() - ) + phase_attribs["elements"] = FlowList(elements.strip().split()) phase_attribs["species"] = self.get_species_array(phase.find("speciesArray")) species_skip = phase.find("speciesArray").find("skip") if species_skip is not None: @@ -573,8 +571,12 @@ def __init__(self, reaction): reaction_attribs["equation"] = ( # This has to replace the reaction direction symbols separately because # species names can have [ or ] in them - reaction.find("equation").text.replace("[=]", "<=>").replace("=]", "=>") + reaction.find("equation") + .text.replace("[=]", "<=>") + .replace("=]", "=>") ) + if reaction.get("negative_A", "").lower() == "yes": + reaction_attribs["negative-A"] = True reactants = split_species_value_string(reaction.findtext("reactants")) # products = { @@ -778,11 +780,13 @@ def arrhenius(self, rate_coeff): def process_arrhenius_parameters(self, arr_node): """Process the parameters from an Arrhenius child of a rateCoeff node.""" - return FlowMap({ - "A": get_float_or_units(arr_node.find("A")), - "b": get_float_or_units(arr_node.find("b")), - "Ea": get_float_or_units(arr_node.find("E")), - }) + return FlowMap( + { + "A": get_float_or_units(arr_node.find("A")), + "b": get_float_or_units(arr_node.find("b")), + "Ea": get_float_or_units(arr_node.find("E")), + } + ) def process_efficiencies(self, eff_node): """Process the efficiency information about a reaction.""" @@ -844,47 +848,51 @@ def convert(inpfile, outfile): # Species species_data = [] - for species_node in ctml_tree.find("speciesData").iterfind("species"): - species_name = species_node.get("name") - # Does it make more sense to modify the object after construction - # with these equation-of-state type parameters? Right now, all of this - # is done during construction. - activity_params = {} - for phase_thermo, params_list in act_pure_params.items(): - for params in params_list: - if params.get("species") != species_name: - continue - if activity_params: - raise ValueError( - "Multiple sets of pureFluidParameters found for species " - "'{}'".format(species_name) - ) - activity_params["model"] = phase_thermo - activity_params["pure_params"] = params - - for phase_thermo, params_list in act_cross_params.items(): - for params in params_list: - related_species = [params.get("species1"), params.get("species2")] - if species_name in related_species: - if phase_thermo != activity_params["model"]: + species_data_node = ctml_tree.find("speciesData") + if species_data_node is not None: + for species_node in species_data_node.iterfind("species"): + species_name = species_node.get("name") + # Does it make more sense to modify the object after construction + # with these equation-of-state type parameters? Right now, all of this + # is done during construction. + activity_params = {} + for phase_thermo, params_list in act_pure_params.items(): + for params in params_list: + if params.get("species") != species_name: + continue + if activity_params: raise ValueError( - "crossFluidParameters found for phase thermo '{}' with " - "pureFluidParameters found for phase thermo '{}' " - "for species '{}'".format( - phase_thermo, activity_params["model"], species_name - ) + "Multiple sets of pureFluidParameters found for species " + "'{}'".format(species_name) ) - activity_params["cross_params"] = params + activity_params["model"] = phase_thermo + activity_params["pure_params"] = params + + for phase_thermo, params_list in act_cross_params.items(): + for params in params_list: + related_species = [params.get("species1"), params.get("species2")] + if species_name in related_species: + if phase_thermo != activity_params["model"]: + raise ValueError( + "crossFluidParameters found for phase thermo '{}' with " + "pureFluidParameters found for phase thermo '{}' " + "for species '{}'".format( + phase_thermo, activity_params["model"], species_name + ) + ) + activity_params["cross_params"] = params - const_dens_params = const_density_specs.get(species_name) - if activity_params: - this_species = Species(species_node, activity_parameters=activity_params) - elif const_dens_params is not None: - this_species = Species(species_node, const_dens=const_dens_params) - else: - this_species = Species(species_node) + const_dens_params = const_density_specs.get(species_name) + if activity_params: + this_species = Species( + species_node, activity_parameters=activity_params + ) + elif const_dens_params is not None: + this_species = Species(species_node, const_dens=const_dens_params) + else: + this_species = Species(species_node) - species_data.append(this_species) + species_data.append(this_species) # Reactions reaction_data = [] diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 6606a3e1488..d1b3fffad47 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -934,3 +934,9 @@ def test_lithium_ion_battery(self): ctmlElyt.electric_potential = yamlElyt.electric_potential = 0 self.checkKinetics(ctmlCathodeInt, yamlCathodeInt, [300], [1e5]) + def test_noxNeg(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath('noxNeg.xml'), + Path(self.test_work_dir).joinpath('noxNeg.yaml')) + ctmlGas, yamlGas = self.checkConversion('noxNeg') + self.checkThermo(ctmlGas, yamlGas, [300, 1000]) + self.checkKinetics(ctmlGas, yamlGas, [300, 1000], [1e5]) From d1e1fcf31e7f4769cae84f87d6427dbd7e172d8e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 18 Sep 2019 20:46:22 -0400 Subject: [PATCH 35/99] [ctml_writer] Fix warning invalid escape sequence The docstring for the reaction class has some math formatting. This was causing an invalid escape sequence warning during build. The fix is to make the docstring a raw string. --- interfaces/cython/cantera/ctml_writer.py | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml_writer.py b/interfaces/cython/cantera/ctml_writer.py index afe51ab3029..7e6f77017d8 100644 --- a/interfaces/cython/cantera/ctml_writer.py +++ b/interfaces/cython/cantera/ctml_writer.py @@ -1129,7 +1129,7 @@ def __init__(self, id = '', order = '', options = []): - """ + r""" :param equation: A string specifying the chemical equation. :param kf: From bfaa1a2a52601c444852fe34b896cc0efb3e8b57 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 18 Sep 2019 20:47:34 -0400 Subject: [PATCH 36/99] [SCons] Remove test/work directory during cleaning --- SConstruct | 1 + 1 file changed, 1 insertion(+) diff --git a/SConstruct b/SConstruct index 4c9f9d31207..d76bf29d627 100644 --- a/SConstruct +++ b/SConstruct @@ -59,6 +59,7 @@ if 'clean' in COMMAND_LINE_TARGETS: removeDirectory('build') removeDirectory('stage') removeDirectory('.sconf_temp') + removeDirectory('test/work') removeFile('.sconsign.dblite') removeFile('include/cantera/base/config.h') removeFile('src/pch/system.h.gch') From 6046cc48e77dc63a21d8c49765fe7c8621fcfda8 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 18 Sep 2019 21:10:32 -0400 Subject: [PATCH 37/99] [ctml2yaml] Add ch4_ion test Requires multipel reactionArray nodes, multiple speciesArray nodes, and the option to skip undeclared third body species. --- interfaces/cython/cantera/ctml2yaml.py | 131 ++++-- .../cython/cantera/test/test_convert.py | 8 + test/data/ch4_ion.xml | 430 ++++++++++++++++++ 3 files changed, 527 insertions(+), 42 deletions(-) create mode 100644 test/data/ch4_ion.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c19bbed09df..60c1098011a 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -144,6 +144,7 @@ class Phase: "mix": "mixture-averaged", "multi": "multicomponent", "none": None, + "ion": "ionized-gas", } _state_properties_mapping = { @@ -192,12 +193,21 @@ def __init__(self, phase): elements = phase.find("elementArray").text if elements is not None: phase_attribs["elements"] = FlowList(elements.strip().split()) - phase_attribs["species"] = self.get_species_array(phase.find("speciesArray")) - species_skip = phase.find("speciesArray").find("skip") - if species_skip is not None: - element_skip = species_skip.get("element", "") - if element_skip == "undeclared": - phase_attribs["skip-undeclared-elements"] = True + + species = [] + for sA_node in phase.findall("speciesArray"): + species.append(self.get_species_array(sA_node)) + + species_skip = sA_node.find("skip") + if species_skip is not None: + element_skip = species_skip.get("element", "") + if element_skip == "undeclared": + phase_attribs["skip-undeclared-elements"] = True + if species: + if len(species) == 1 and "species" in species[0]: + phase_attribs.update(species[0]) + else: + phase_attribs["species"] = species transport_node = phase.find("transport") if transport_node is not None: @@ -207,21 +217,53 @@ def __init__(self, phase): if transport_model is not None: phase_attribs["transport"] = transport_model - if phase.find("reactionArray") is not None: - # The kinetics model should only be specified if reactions - # are associated with the phase + # The phase requires both a kinetics model and a set of + # reactions to include the kinetics + kinetics_node = phase.find("kinetics") + has_reactionArray = phase.find("reactionArray") is not None + if kinetics_node is not None and has_reactionArray: kinetics_model = self._kinetics_model_mapping[ - phase.find("kinetics").get("model").lower() + kinetics_node.get("model", "").lower() ] - if kinetics_model is not None: + reactionArray_nodes = phase.findall("reactionArray") + reactions = [] + for rA_node in reactionArray_nodes: + filter = rA_node.find("include") + if filter is not None: + if filter.get("min").lower() == "none": + continue + else: + has_filter = True + else: + has_filter = False + skip_node = rA_node.find("skip") + if skip_node is not None and skip_node.get("third_bodies") is not None: + if skip_node.get("third_bodies") == "undeclared": + phase_attribs["skip-undeclared-third-bodies"] = True + else: + raise ValueError( + "Undefined value '{}' for third_bodies skip " + "parameter".format(skip_node.get("third_bodies")) + ) + this_reactions = self.get_reaction_array(rA_node) + if has_filter: + section_name = "{}-reactions".format(phase_name) + reactions.append({section_name: this_reactions["reactions"]}) + else: + reactions.append(this_reactions) + # The reactions list may be empty, don't include it if it is + if reactions: phase_attribs["kinetics"] = kinetics_model - - phase_attribs.update( - self.get_reaction_array(phase.find("reactionArray")) - ) - reaction_filter = phase.find("reactionArray").find("include") - if reaction_filter is not None: - phase_attribs["reactions"].append("{}-reactions".format(phase_name)) + internal_source = "reactions" in reactions[0] + # If there is one reactionArray node, no reaction filter + # has been specified, and the reactions are all from + # within this file, the output should be reactions: all, + # so we use update. Otherwise, there needs to be a list + # of mappings. + if len(reactions) == 1 and not has_filter and internal_source: + phase_attribs.update(reactions[0]) + else: + phase_attribs["reactions"] = reactions state_node = phase.find("state") if state_node is not None: @@ -249,52 +291,57 @@ def get_species_array(self, speciesArray_node): ) datasrc = speciesArray_node.get("datasrc", "") if datasrc.startswith("#"): - return species_list + return {"species": species_list} else: filename, location = datasrc.split("#", 1) name = str(Path(filename).with_suffix(".yaml")) if location == "species_data": location = "species" datasrc = "{}/{}".format(name, location) - return [{datasrc: species_list}] + return {datasrc: species_list} def get_reaction_array(self, reactionArray_node): """Process reactions from a reactionArray node in a phase definition.""" datasrc = reactionArray_node.get("datasrc", "") has_filter = reactionArray_node.find("include") is not None - # if has_filter and reactionArray_node.find("include").get("min") == "None": - # return {} + skip_node = reactionArray_node.find("skip") + if skip_node is not None: + species_skip = skip_node.get("species") + if species_skip is not None and species_skip == "undeclared": + reaction_option = "declared-species" + else: + raise ValueError( + "Unknown value in species skip parameter: " + "'{}'".format(species_skip) + ) + else: + reaction_option = "all" + if not datasrc.startswith("#"): if has_filter: raise ValueError( - "Filtering reaction lists is not possible with external data sources" + "Filtering reaction lists is not possible with external data " + "sources" + ) + if skip_node is None: + raise ValueError( + "Must include skip node for external data sources: " + "'{}'".format(datasrc) ) + # This code does not handle the # character in a filename filename, location = datasrc.split("#", 1) name = str(Path(filename).with_suffix(".yaml")) if location == "reaction_data": location = "reactions" datasrc = "{}/{}".format(name, location) - skip = reactionArray_node.find("skip") - if skip is not None: - species_skip = skip.get("species", "") - if species_skip == "undeclared": - reactions = {datasrc: "declared-species"} - else: - raise ValueError( - "Unknown value in skip parameter for reactionArray" - ) - else: - raise ValueError( - "Missing skip node in reactionArray with external data source" - ) - return {"reactions": FlowList([reactions])} elif datasrc == "#reaction_data": - if has_filter: - return {"reactions": FlowList([])} - else: - return {"reactions": "all"} + datasrc = "reactions" else: - return {} + raise ValueError( + "Unable to parse the reaction data source: '{}'".format(datasrc) + ) + + return {datasrc: reaction_option} def get_tabulated_thermo(self, tab_thermo_node): tab_thermo = BlockMap() diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index d1b3fffad47..48d40ce551d 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -940,3 +940,11 @@ def test_noxNeg(self): ctmlGas, yamlGas = self.checkConversion('noxNeg') self.checkThermo(ctmlGas, yamlGas, [300, 1000]) self.checkKinetics(ctmlGas, yamlGas, [300, 1000], [1e5]) + + def test_ch4_ion(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("ch4_ion.xml"), + Path(self.test_work_dir).joinpath("ch4_ion.yaml")) + ctmlGas, yamlGas = self.checkConversion("ch4_ion") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + self.checkTransport(ctmlGas, yamlGas, [298, 1001, 2400]) diff --git a/test/data/ch4_ion.xml b/test/data/ch4_ion.xml new file mode 100644 index 00000000000..d2be3035221 --- /dev/null +++ b/test/data/ch4_ion.xml @@ -0,0 +1,430 @@ + + + + + + + O H C N E + + H O OH HO2 H2O2 C CH + CH2 CH2(S) CH3 HCO CH2O CH3O + + H2 O2 H2O CH4 CO CO2 N2 + HCO+ H3O+ E O2- + + + + + + + + 300.0 + 101325.0 + + + + + + + + + + + + H:2 + TPIS78 + + + + 2.344331120E+00, 7.980520750E-03, -1.947815100E-05, 2.015720940E-08, + -7.376117610E-12, -9.179351730E+02, 6.830102380E-01 + + + + 3.337279200E+00, -4.940247310E-05, 4.994567780E-07, -1.795663940E-10, + 2.002553760E-14, -9.501589220E+02, -3.205023310E+00 + + + + linear + 38.000 + 2.920 + 0.000 + 0.455 + 280.000 + 0.000 + 0.000 + + + + + + O:2 + TPIS89 + + + + 3.782456360E+00, -2.996734160E-03, 9.847302010E-06, -9.681295090E-09, + 3.243728370E-12, -1.063943560E+03, 3.657675730E+00 + + + + 3.282537840E+00, 1.483087540E-03, -7.579666690E-07, 2.094705550E-10, + -2.167177940E-14, -1.088457720E+03, 5.453231290E+00 + + + + linear + 107.400 + 3.460 + 0.000 + 1.131 + 3.800 + 0.000 + 0.000 + + + + + + H:2 O:1 + L 8/89 + + + + 4.198640560E+00, -2.036434100E-03, 6.520402110E-06, -5.487970620E-09, + 1.771978170E-12, -3.029372670E+04, -8.490322080E-01 + + + + 3.033992490E+00, 2.176918040E-03, -1.640725180E-07, -9.704198700E-11, + 1.682009920E-14, -3.000429710E+04, 4.966770100E+00 + + + + nonlinear + 572.400 + 2.600 + 1.840 + 1.053 + 4.000 + 0.000 + 0.000 + + + + + + C:1 H:4 + L 8/88 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + nonlinear + 141.400 + 3.750 + 0.000 + 2.600 + 13.000 + 0.000 + 0.000 + + + + + + C:1 O:1 + TPIS79 + + + + 3.579533470E+00, -6.103536800E-04, 1.016814330E-06, 9.070058840E-10, + -9.044244990E-13, -1.434408600E+04, 3.508409280E+00 + + + + 2.715185610E+00, 2.062527430E-03, -9.988257710E-07, 2.300530080E-10, + -2.036477160E-14, -1.415187240E+04, 7.818687720E+00 + + + + linear + 98.100 + 3.650 + 0.000 + 1.950 + 1.800 + 0.000 + 0.000 + + + + + + C:1 O:2 + L 7/88 + + + + 2.356773520E+00, 8.984596770E-03, -7.123562690E-06, 2.459190220E-09, + -1.436995480E-13, -4.837196970E+04, 9.901052220E+00 + + + + 3.857460290E+00, 4.414370260E-03, -2.214814040E-06, 5.234901880E-10, + -4.720841640E-14, -4.875916600E+04, 2.271638060E+00 + + + + linear + 244.000 + 3.760 + 0.000 + 2.650 + 2.100 + 0.000 + 0.000 + + + + + + N:2 + 121286 + + + + 3.298677000E+00, 1.408240400E-03, -3.963222000E-06, 5.641515000E-09, + -2.444854000E-12, -1.020899900E+03, 3.950372000E+00 + + + + 2.926640000E+00, 1.487976800E-03, -5.684760000E-07, 1.009703800E-10, + -6.753351000E-15, -9.227977000E+02, 5.980528000E+00 + + + + linear + 97.530 + 3.620 + 0.000 + 1.760 + 4.000 + 2.995 + 3.602 + + + + + + H:1 C:1 O:1 E:-1 + J12/70 + 1 + + + + 2.473973600E+00, 8.671559000E-03, -1.003150000E-05, 6.717052700E-09, + -1.787267400E-12, 9.914660800E+04, 8.175711870E+00 + + + + 3.741188000E+00, 3.344151700E-03, -1.239712100E-06, 2.118938800E-10, + -1.370415000E-14, 9.888407800E+04, 2.078613570E+00 + + + + linear + 498.000 + 3.590 + 0.000 + 1.356 + 0.000 + 0.416 + 0.000 + + + + + + H:3 O:1 E:-1 + TPIS89 + 1 + + + + 3.792952700E+00, -9.108540000E-04, 1.163635490E-05, -1.213648870E-08, + 4.261596630E-12, 7.075124010E+04, 1.471568560E+00 + + + + 2.496477160E+00, 5.728449200E-03, -1.839532810E-06, 2.735774390E-10, + -1.540939850E-14, 7.097291130E+04, 7.458507790E+00 + + + + nonlinear + 106.200 + 3.150 + 1.417 + 0.897 + 0.000 + 0.000 + 0.000 + + + + + + E:1 O:2 + L4/89 + -1 + + + + 3.664425220E+00, -9.287411380E-04, 6.454770820E-06, -7.747033800E-09, + 2.933326620E-12, -6.870769830E+03, 4.351406810E+00 + + + + 3.956662940E+00, 5.981418230E-04, -2.121339050E-07, 3.632675810E-11, + -2.249892280E-15, -7.062872290E+03, 2.278710170E+00 + + + + linear + 136.500 + 3.330 + 0.000 + 1.424 + 0.000 + 0.000 + 0.000 + + + + + + E:1 + gas L10/92 + -1 + + + + 2.500000000E+00, 0.000000000E+00, 0.000000000E+00, 0.000000000E+00, + 0.000000000E+00, -7.453750000E+02, -1.172469020E+01 + + + + 2.500000000E+00, 0.000000000E+00, 0.000000000E+00, 0.000000000E+00, + 0.000000000E+00, -7.453750000E+02, -1.172469020E+01 + + + + atom + 145.000 + 2.050 + 0.000 + 0.667 + 0.000 + 0.000 + 0.000 + + + + + + + + CH + O =] HCO+ + E + + + 2.510000E+08 + 0.0 + 1700.000000 + + + CH:1.0 O:1 + HCO+:1.0 E:1 + + + + + HCO+ + H2O =] H3O+ + CO + + + 1.510000E+12 + 0.0 + 0.000000 + + + HCO+:1.0 H2O:1 + H3O+:1.0 CO:1 + + + + + H3O+ + E =] H2O + H + + + 2.290000E+15 + -0.5 + 0.000000 + + + H3O+:1.0 E:1 + H2O:1.0 H:1 + + + + + H3O+ + E =] OH + H + H + + + 7.950000E+18 + -1.4 + 0.000000 + + + H3O+:1.0 E:1 + OH:1.0 H:2 + + + + + H3O+ + E =] H2 + OH + + + 1.250000E+16 + -0.5 + 0.000000 + + + H3O+:1.0 E:1 + H2:1.0 OH:1 + + + + + H3O+ + E =] O + H2 + H + + + 6.000000E+14 + -0.3 + 0.000000 + + + H3O+:1.0 E:1 + O:1.0 H2:1 H:1 + + + From 7ae7f75b9f9f938219a28a903858919359b1480e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Thu, 19 Sep 2019 09:41:32 -0400 Subject: [PATCH 38/99] [ctml2yaml] Add NASA9 species thermo --- interfaces/cython/cantera/ctml2yaml.py | 46 ++++++++++++++++++- .../cython/cantera/test/test_convert.py | 6 +++ 2 files changed, 51 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 60c1098011a..b2c323c04a3 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -389,7 +389,7 @@ class SpeciesThermo: def __init__(self, thermo): thermo_type = thermo[0].tag - if thermo_type not in ["NASA", "const_cp"]: + if thermo_type not in ["NASA", "NASA9", "const_cp"]: raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) func = getattr(self, thermo_type) self.thermo_attribs = func(thermo) @@ -410,6 +410,23 @@ def NASA(self, thermo): thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) return thermo_attribs + def NASA9(self, thermo): + """Process a NASA 9 thermo entry from XML to a dictionary.""" + thermo_attribs = BlockMap({"model": "NASA9", "data": []}) + temperature_ranges = set() + model_nodes = thermo.findall("NASA9") + for node in model_nodes: + temperature_ranges.add(float(node.get("Tmin"))) + temperature_ranges.add(float(node.get("Tmax"))) + coeffs = node.find("floatArray").text.replace("\n", " ").strip().split(",") + thermo_attribs["data"].append(FlowList(map(float, coeffs))) + if len(temperature_ranges) != len(model_nodes) + 1: + raise ValueError( + "The midpoint temperature is not consistent between NASA9 entries" + ) + thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) + return thermo_attribs + def const_cp(self, thermo): """Process a constant c_p thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "constant-cp"}) @@ -964,6 +981,33 @@ def convert(inpfile, outfile): output_reactions["reactions"] = reaction_data output_reactions.yaml_set_comment_before_after_key("reactions", before="\n") + # If there are no reactions to put into the local file, then we need to delete + # the sections of the phase entry that specify reactions are present in the + # local file. + if not output_reactions: + for this_phase in phases: + phase_reactions = this_phase.phase_attribs.get("reactions", "") + if phase_reactions == "all": + del this_phase.phase_attribs["reactions"] + del this_phase.phase_attribs["kinetics"] + elif isinstance(phase_reactions, list): + sources_to_remove = [] + for i, reac_source in enumerate(phase_reactions): + # reac_source is a dictionary. If reactions is the + # key in that dictionary, the source is from the local + # file and should be removed because there are no + # reactions listed in the local file. + if "reactions" in reac_source: + sources_to_remove.append(i) + for i in sources_to_remove: + del phase_reactions[i] + # If there are no more reaction sources in the list, + # delete the reactions and kinetics entries from the + # phase object + if len(phase_reactions) == 0: + del this_phase.phase_attribs["reactions"] + del this_phase.phase_attribs["kinetics"] + output_phases = BlockMap({"phases": phases}) output_phases.yaml_set_comment_before_after_key("phases", before="\n") diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 48d40ce551d..a1af37ff6f6 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -948,3 +948,9 @@ def test_ch4_ion(self): self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) self.checkTransport(ctmlGas, yamlGas, [298, 1001, 2400]) + + def test_nasa9(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("nasa9-test.xml"), + Path(self.test_work_dir).joinpath("nasa9-test.yaml")) + ctmlGas, yamlGas = self.checkConversion("nasa9-test") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) From 88634a9964b927100383363c93eff0a22719c0aa Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 4 Oct 2019 15:41:52 -0400 Subject: [PATCH 39/99] [ctml2yaml] Add chemically activated reaction type --- interfaces/cython/cantera/ctml2yaml.py | 44 ++++++++++++++++++- .../cython/cantera/test/test_convert.py | 10 +++-- 2 files changed, 50 insertions(+), 4 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index b2c323c04a3..d5bf96568c2 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -609,6 +609,7 @@ def __init__(self, reaction): "surface", "edge", "falloff", + "chemAct", ]: raise TypeError( "Unknown reaction type '{}' for reaction id {}".format( @@ -628,6 +629,14 @@ def __init__(self, reaction): ) else: reaction_type = falloff_type + elif reaction_type == "chemAct": + falloff_type = rate_coeff.find("falloff").get("type") + if falloff_type != "Troe": + raise TypeError( + "Unknown activation type '{}' for reaction id {}".format( + falloff_type, reaction.get("id") + ) + ) func = getattr(self, reaction_type.lower()) reaction_attribs.update(func(rate_coeff)) @@ -717,7 +726,40 @@ def troe(self, rate_coeff): troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() troe_names = ["A", "T3", "T1", "T2"] - reaction_attribs["Troe"] = {} + reaction_attribs["Troe"] = FlowMap() + # zip stops when the shortest iterable is exhausted. If T2 is not present + # in the Troe parameters (i.e., troe_params is three elements long), it + # will be omitted here as well. + for name, param in zip(troe_names, troe_params): + reaction_attribs["Troe"].update({name: float(param)}) + + return reaction_attribs + + def chemact(self, rate_coeff): + """Process a chemically activated falloff reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attribs = FlowMap({"type": "chemically-activated"}) + for arr_coeff in rate_coeff.iterfind("Arrhenius"): + if arr_coeff.get("name") is not None and arr_coeff.get("name") == "kHigh": + reaction_attribs[ + "high-P-rate-constant" + ] = self.process_arrhenius_parameters(arr_coeff) + elif arr_coeff.get("name") is None: + reaction_attribs[ + "low-P-rate-constant" + ] = self.process_arrhenius_parameters(arr_coeff) + else: + raise TypeError("Too many Arrhenius nodes") + eff_node = rate_coeff.find("efficiencies") + if eff_node is not None: + reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) + + troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() + troe_names = ["A", "T3", "T1", "T2"] + reaction_attribs["Troe"] = FlowMap() # zip stops when the shortest iterable is exhausted. If T2 is not present # in the Troe parameters (i.e., troe_params is three elements long), it # will be omitted here as well. diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index a1af37ff6f6..1c249500832 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -732,9 +732,6 @@ def test_ch4_ion(self): self.checkTransport(ctiGas, yamlGas, [298, 1001, 2400]) class ctml2yamlTest(utilities.CanteraTest): - @classmethod - def setUpClass(cls): - super().setUpClass() def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(), yamlphases=(), **kwargs): @@ -954,3 +951,10 @@ def test_nasa9(self): Path(self.test_work_dir).joinpath("nasa9-test.yaml")) ctmlGas, yamlGas = self.checkConversion("nasa9-test") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + + def test_chemically_activated(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("chemically-activated-reaction.xml"), + Path(self.test_work_dir).joinpath("chemically-activated-reaction.yaml")) + ctmlGas, yamlGas = self.checkConversion('chemically-activated-reaction') + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) From 3e4d64abb05b679214e19a171cc9fa2dbfaa64ee Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 4 Oct 2019 15:51:48 -0400 Subject: [PATCH 40/99] [ctml2yaml] Add negative reaction orders option --- interfaces/cython/cantera/ctml2yaml.py | 3 ++ .../cython/cantera/test/test_convert.py | 30 ++++++++++++++++++- 2 files changed, 32 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d5bf96568c2..d414714194a 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -666,6 +666,9 @@ def __init__(self, reaction): if orders: reaction_attribs["orders"] = orders + if reaction.get("negative_orders") == "yes": + reaction_attribs["negative-orders"] = True + if reaction.get("duplicate", "") == "yes": reaction_attribs["duplicate"] = True diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 1c249500832..036777b1dae 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -955,6 +955,34 @@ def test_nasa9(self): def test_chemically_activated(self): ctml2yaml.convert(Path(self.test_data_dir).joinpath("chemically-activated-reaction.xml"), Path(self.test_work_dir).joinpath("chemically-activated-reaction.yaml")) - ctmlGas, yamlGas = self.checkConversion('chemically-activated-reaction') + ctmlGas, yamlGas = self.checkConversion("chemically-activated-reaction") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + + def test_explicit_forward_order(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("explicit-forward-order.xml"), + Path(self.test_work_dir).joinpath("explicit-forward-order.yaml")) + ctmlGas, yamlGas = self.checkConversion("explicit-forward-order") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + + def test_explicit_reverse_rate(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("explicit-reverse-rate.xml"), + Path(self.test_work_dir).joinpath("explicit-reverse-rate.yaml")) + ctmlGas, yamlGas = self.checkConversion("explicit-reverse-rate") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + + def test_explicit_third_bodies(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("explicit-third-bodies.xml"), + Path(self.test_work_dir).joinpath("explicit-third-bodies.yaml")) + ctmlGas, yamlGas = self.checkConversion("explicit-third-bodies") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + + def test_fractional_stoich_coeffs(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("frac.xml"), + Path(self.test_work_dir).joinpath("frac.yaml")) + ctmlGas, yamlGas = self.checkConversion("frac") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) From 32399406f56d54172e4449310424bcc06db88507 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 4 Oct 2019 16:20:24 -0400 Subject: [PATCH 41/99] [ctml2yaml] Fixed chemical potential phase thermo --- interfaces/cython/cantera/ctml2yaml.py | 12 +++++++++--- interfaces/cython/cantera/test/test_convert.py | 6 ++++++ 2 files changed, 15 insertions(+), 3 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d414714194a..9f464a6089d 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -133,6 +133,7 @@ class Phase: "surface": "ideal-surface", "binarysolutiontabulatedthermo": "binary-solution-tabulated", "idealsolidsolution": "ideal-condensed", + "fixedchempot": "fixed-chemical-potential", } _kinetics_model_mapping = { "gaskinetics": "gas", @@ -189,6 +190,8 @@ def __init__(self, phase): phase_attribs["tabulated-species"] = node.get("name") elif node.tag == "tabulatedThermo": phase_attribs["tabulated-thermo"] = self.get_tabulated_thermo(node) + elif node.tag == "chemicalPotential": + phase_attribs["chemical-potential"] = get_float_or_units(node) elements = phase.find("elementArray").text if elements is not None: @@ -492,7 +495,8 @@ def __init__(self, species, **kwargs): species_attribs["note"] = species.findtext("note") thermo = species.find("thermo") - species_attribs["thermo"] = SpeciesThermo(thermo) + if thermo is not None: + species_attribs["thermo"] = SpeciesThermo(thermo) activity_parameters = kwargs.get("activity_parameters", False) if activity_parameters: @@ -1005,8 +1009,10 @@ def convert(inpfile, outfile): # Reactions reaction_data = [] - for reaction in ctml_tree.find("reactionData").iterfind("reaction"): - reaction_data.append(Reaction(reaction)) + reactionData_node = ctml_tree.find("reactionData") + if reactionData_node is not None: + for reaction in reactionData_node.iterfind("reaction"): + reaction_data.append(Reaction(reaction)) output_reactions = BlockMap() for phase_name, pattern in reaction_filters: diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 036777b1dae..0fe4a70a216 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -986,3 +986,9 @@ def test_fractional_stoich_coeffs(self): ctmlGas, yamlGas = self.checkConversion("frac") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + + def test_fixed_chemical_potential_thermo(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("LiFixed.xml"), + Path(self.test_work_dir).joinpath("LiFixed.yaml")) + ctmlGas, yamlGas = self.checkConversion("LiFixed") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) From ee690e8452c6143079c02bb80722094e0281c9c9 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 5 Oct 2019 20:14:03 -0400 Subject: [PATCH 42/99] [ctml2yaml] Add liquid water phase --- interfaces/cython/cantera/ctml2yaml.py | 2 ++ interfaces/cython/cantera/test/test_convert.py | 14 ++++++++++++++ 2 files changed, 16 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 9f464a6089d..a466c3143eb 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -134,6 +134,7 @@ class Phase: "binarysolutiontabulatedthermo": "binary-solution-tabulated", "idealsolidsolution": "ideal-condensed", "fixedchempot": "fixed-chemical-potential", + "pureliquidwater": "liquid-water-IAPWS95", } _kinetics_model_mapping = { "gaskinetics": "gas", @@ -146,6 +147,7 @@ class Phase: "multi": "multicomponent", "none": None, "ion": "ionized-gas", + "water": "water", } _state_properties_mapping = { diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 0fe4a70a216..491cf38b78e 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -992,3 +992,17 @@ def test_fixed_chemical_potential_thermo(self): Path(self.test_work_dir).joinpath("LiFixed.yaml")) ctmlGas, yamlGas = self.checkConversion("LiFixed") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + + def test_water_IAPWS95_thermo(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("liquid-water.xml"), + Path(self.test_work_dir).joinpath("liquid-water.yaml")) + ctmlWater, yamlWater = self.checkConversion("liquid-water") + self.checkThermo(ctmlWater, yamlWater, [300, 500, 1300, 2000]) + self.assertEqual(ctmlWater.transport_model, yamlWater.transport_model) + dens = ctmlWater.density + for T in [298, 1001, 2400]: + ctmlWater.TD = T, dens + yamlWater.TD = T, dens + self.assertNear(ctmlWater.viscosity, yamlWater.viscosity) + self.assertNear(ctmlWater.thermal_conductivity, + yamlWater.thermal_conductivity) \ No newline at end of file From 449fbd2b94a2b2dc28be000ad1a766e30faaab96 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 6 Oct 2019 17:20:31 -0400 Subject: [PATCH 43/99] [ctml2yaml] Use speciesData id name If a speciesData node has an id other than species_data, use it to label the corresponding section in the YAML output file. --- interfaces/cython/cantera/ctml2yaml.py | 22 +++++++++++++--------- 1 file changed, 13 insertions(+), 9 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index a466c3143eb..d5a5849f880 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -8,7 +8,6 @@ import xml.etree.ElementTree as etree from email.utils import formatdate -from itertools import chain from collections import defaultdict from typing import Any @@ -295,8 +294,10 @@ def get_species_array(self, speciesArray_node): speciesArray_node.text.replace("\n", " ").strip().split() ) datasrc = speciesArray_node.get("datasrc", "") - if datasrc.startswith("#"): + if datasrc == "#species_data": return {"species": species_list} + elif datasrc.startswith("#"): + return {datasrc[1:]: species_list} else: filename, location = datasrc.split("#", 1) name = str(Path(filename).with_suffix(".yaml")) @@ -963,8 +964,9 @@ def convert(inpfile, outfile): # Species species_data = [] - species_data_node = ctml_tree.find("speciesData") - if species_data_node is not None: + output_species = BlockMap() + for species_data_node in ctml_tree.findall("speciesData"): + this_data_node_id = species_data_node.get("id", "") for species_node in species_data_node.iterfind("species"): species_name = species_node.get("name") # Does it make more sense to modify the object after construction @@ -1009,6 +1011,13 @@ def convert(inpfile, outfile): species_data.append(this_species) + if this_data_node_id == "species_data": + output_species["species"] = species_data + output_species.yaml_set_comment_before_after_key("species", before="\n") + else: + output_species[this_data_node_id] = species_data + output_species.yaml_set_comment_before_after_key(this_data_node_id, before="\n") + # Reactions reaction_data = [] reactionData_node = ctml_tree.find("reactionData") @@ -1064,11 +1073,6 @@ def convert(inpfile, outfile): output_phases = BlockMap({"phases": phases}) output_phases.yaml_set_comment_before_after_key("phases", before="\n") - output_species = BlockMap() - if species_data: - output_species["species"] = species_data - output_species.yaml_set_comment_before_after_key("species", before="\n") - emitter = yaml.YAML() for cl in [Phase, Species, SpeciesThermo, SpeciesTransport, Reaction]: emitter.register_class(cl) From be0e17e2b926c687e72b9016394dc9dd1f41b0cd Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 6 Oct 2019 22:49:09 -0400 Subject: [PATCH 44/99] [ctml2yaml] Add Shomate species thermo Fixes error when speciesData has an id attribute other than species_data and the phase is StoichSubstance. --- interfaces/cython/cantera/ctml2yaml.py | 42 +++++++++++++++++-- .../cython/cantera/test/test_convert.py | 8 +++- test/data/NaCl_Solid.xml | 8 ++-- 3 files changed, 49 insertions(+), 9 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d5a5849f880..f19816cd70f 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -395,11 +395,28 @@ class SpeciesThermo: def __init__(self, thermo): thermo_type = thermo[0].tag - if thermo_type not in ["NASA", "NASA9", "const_cp"]: + if thermo_type not in ["NASA", "NASA9", "const_cp", "Shomate"]: raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) func = getattr(self, thermo_type) self.thermo_attribs = func(thermo) + def Shomate(self, thermo): + """Process a Shomate polynomial from XML to a dictionary.""" + thermo_attribs = BlockMap({"model": "Shomate", "data": []}) + temperature_ranges = set() + model_nodes = thermo.findall("Shomate") + for node in model_nodes: + temperature_ranges.add(float(node.get("Tmin"))) + temperature_ranges.add(float(node.get("Tmax"))) + coeffs = node.find("floatArray").text.replace("\n", " ").strip().split(",") + thermo_attribs["data"].append(FlowList(map(float, coeffs))) + if len(temperature_ranges) != len(model_nodes) + 1: + raise ValueError( + "The midpoint temperature is not consistent between Shomate entries" + ) + thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) + return thermo_attribs + def NASA(self, thermo): """Process a NASA 7 thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "NASA7", "data": []}) @@ -955,11 +972,28 @@ def convert(inpfile, outfile): act_cross_params[this_phase.phase_attribs["thermo"]].extend( list(ac_coeff_node.iterfind("crossFluidParameters")) ) + + # The density previously associated with the phase has been moved + # to the species definition in the YAML format. StoichSubstance is + # the only model I know of that uses this node phase_thermo_node = phase_node.find("thermo") if phase_thermo_node.get("model") == "StoichSubstance": - for den_node in phase_thermo_node: - if den_node.tag == "density": - for spec in this_phase.phase_attribs["species"]: + den_node = phase_thermo_node.find("density") + if den_node is None: + den_node = phase_thermo_node.find("molar-density") + if den_node is None: + den_node = phase_thermo_node.find("molar-volume") + if den_node is None: + raise ValueError( + "Phase node '{}' is missing a density node.".format( + this_phase.phase_attribs["name"] + ) + ) + for spec_or_dict in this_phase.phase_attribs["species"]: + if isinstance(spec_or_dict, str): + const_density_specs[spec_or_dict] = den_node + else: + for spec in list(spec_or_dict.values())[0]: const_density_specs[spec] = den_node # Species diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 491cf38b78e..1b99d827ac7 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1005,4 +1005,10 @@ def test_water_IAPWS95_thermo(self): yamlWater.TD = T, dens self.assertNear(ctmlWater.viscosity, yamlWater.viscosity) self.assertNear(ctmlWater.thermal_conductivity, - yamlWater.thermal_conductivity) \ No newline at end of file + yamlWater.thermal_conductivity) + + def test_NaCl_solid_phase(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("NaCl_Solid.xml"), + Path(self.test_work_dir).joinpath("NaCl_Solid.yaml")) + ctmlPhase, yamlPhase = self.checkConversion("NaCl_Solid") + self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000]) diff --git a/test/data/NaCl_Solid.xml b/test/data/NaCl_Solid.xml index d711be8ff0d..3d7259b0fdf 100644 --- a/test/data/NaCl_Solid.xml +++ b/test/data/NaCl_Solid.xml @@ -2,8 +2,8 @@ - - + + O H C Fe Ca N Na Cl @@ -16,7 +16,7 @@ - + @@ -27,7 +27,7 @@ 50.72389, 6.672267, -2.517167, 10.15934, -0.200675, -427.2115, - 130.3973 + 130.3973 From 9e1598c34f3acaf051344d041a31208c852a2720 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 7 Oct 2019 15:15:34 -0400 Subject: [PATCH 45/99] [ctml2yaml] Specialize for Redlich-Kwong EOS The R-K EOS is moving activity coefficient data from the phase node to the species node in YAML. However, other phases are retaining this data in the phase node. This change specializes the previous processing of the activityCoefficient node to be only for the R-K phase thermo model. --- interfaces/cython/cantera/ctml2yaml.py | 34 ++++++++++++++++---------- 1 file changed, 21 insertions(+), 13 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index f19816cd70f..d058263a773 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -550,7 +550,10 @@ def __init__(self, species, **kwargs): self.species_attribs = species_attribs def process_act_coeff(self, species_name, activity_coefficients): - """If a species has activity coefficients, create an equation-of-state mapping.""" + """If a species has activity coefficients, create an equation-of-state mapping. + + This appears to only be necessary for Redlich-Kwong phase thermo model. + """ eq_of_state = BlockMap({"model": activity_coefficients["model"]}) pure_params = activity_coefficients["pure_params"] pure_a = pure_params.findtext("a_coeff") @@ -962,18 +965,21 @@ def convert(inpfile, outfile): ) # Collect all of the activityCoefficients nodes from all of the phase # definitions. This allows us to check that each species has only one - # definition of pure fluid parameters. Note that activityCoefficients are - # only defined for some phase thermo models. - ac_coeff_node = phase_node.find("./thermo/activityCoefficients") - if ac_coeff_node is not None: - act_pure_params[this_phase.phase_attribs["thermo"]].extend( - list(ac_coeff_node.iterfind("pureFluidParameters")) - ) - act_cross_params[this_phase.phase_attribs["thermo"]].extend( - list(ac_coeff_node.iterfind("crossFluidParameters")) - ) + # definition of pure fluid parameters. This check is necessary because + # for Redlich-Kwong, activity coefficient data is moving from the phase to + # the species definition + this_phase_thermo = this_phase.phase_attribs["thermo"] + if this_phase_thermo == "Redlich-Kwong": + ac_coeff_node = phase_node.find("./thermo/activityCoefficients") + if ac_coeff_node is not None: + act_pure_params[this_phase_thermo].extend( + list(ac_coeff_node.iterfind("pureFluidParameters")) + ) + act_cross_params[this_phase_thermo].extend( + list(ac_coeff_node.iterfind("crossFluidParameters")) + ) - # The density previously associated with the phase has been moved + # The density associated with the phase in XML has been moved # to the species definition in the YAML format. StoichSubstance is # the only model I know of that uses this node phase_thermo_node = phase_node.find("thermo") @@ -1005,7 +1011,9 @@ def convert(inpfile, outfile): species_name = species_node.get("name") # Does it make more sense to modify the object after construction # with these equation-of-state type parameters? Right now, all of this - # is done during construction. + # is done during construction. The trouble is that they come from the + # phase node, which isn't passed to Species, since any species can be + # present in multiple phases. activity_params = {} for phase_thermo, params_list in act_pure_params.items(): for params in params_list: From 3ed782bf9945d0f6cc4612c5a833f9d3191afa27 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 7 Oct 2019 15:26:12 -0400 Subject: [PATCH 46/99] [ctml2yaml] Phase model lookups are case-sensitive --- interfaces/cython/cantera/ctml2yaml.py | 47 +++++++++++++------------- 1 file changed, 24 insertions(+), 23 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d058263a773..207ec4ff101 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -121,32 +121,33 @@ class Phase: """ _thermo_model_mapping = { - "idealgas": "ideal-gas", - "surface": "ideal-surface", - "metal": "electron-cloud", - "lattice": "lattice", - "edge": "edge", - "purefluid": "pure-fluid", - "redlichkwongmftp": "Redlich-Kwong", - "stoichsubstance": "fixed-stoichiometry", - "surface": "ideal-surface", - "binarysolutiontabulatedthermo": "binary-solution-tabulated", - "idealsolidsolution": "ideal-condensed", - "fixedchempot": "fixed-chemical-potential", - "pureliquidwater": "liquid-water-IAPWS95", + "IdealGas": "ideal-gas", + "Surface": "ideal-surface", + "Metal": "electron-cloud", + "Lattice": "lattice", + "Edge": "edge", + "PureFluid": "pure-fluid", + "RedlichKwongMFTP": "Redlich-Kwong", + "StoichSubstance": "fixed-stoichiometry", + "BinarySolutionTabulatedThermo": "binary-solution-tabulated", + "IdealSolidSolution": "ideal-condensed", + "FixedChemPot": "fixed-chemical-potential", + "PureLiquidWater": "liquid-water-IAPWS95", } _kinetics_model_mapping = { - "gaskinetics": "gas", - "interface": "surface", + "GasKinetics": "gas", + "Interface": "surface", "none": None, - "edge": "edge", + "Edge": "edge", + "None": None, } _transport_model_mapping = { - "mix": "mixture-averaged", - "multi": "multicomponent", + "Mix": "mixture-averaged", + "Multi": "multicomponent", + "None": None, + "Ion": "ionized-gas", + "Water": "water", "none": None, - "ion": "ionized-gas", - "water": "water", } _state_properties_mapping = { @@ -173,7 +174,7 @@ def __init__(self, phase): phase_attribs = BlockMap({"name": phase_name}) phase_thermo = phase.find("thermo") phase_attribs["thermo"] = self._thermo_model_mapping[ - phase_thermo.get("model").lower() + phase_thermo.get("model") ] # Convert pure fluid type integer into the name if phase_thermo.get("model") == "PureFluid": @@ -216,7 +217,7 @@ def __init__(self, phase): transport_node = phase.find("transport") if transport_node is not None: transport_model = self._transport_model_mapping[ - transport_node.get("model").lower() + transport_node.get("model") ] if transport_model is not None: phase_attribs["transport"] = transport_model @@ -227,7 +228,7 @@ def __init__(self, phase): has_reactionArray = phase.find("reactionArray") is not None if kinetics_node is not None and has_reactionArray: kinetics_model = self._kinetics_model_mapping[ - kinetics_node.get("model", "").lower() + kinetics_node.get("model", "") ] reactionArray_nodes = phase.findall("reactionArray") reactions = [] From 47d33fb20c5d3b4d968538a11d2d365f081692c1 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 6 Oct 2019 17:38:55 -0400 Subject: [PATCH 47/99] [ctml2yaml] HMW-electrolyte and piecewise-Gibbs Use ThermoPhase to load the input files rather than Solution. This avoids a bug in TransportFactory that would be hit by using Solution. --- interfaces/cython/cantera/ctml2yaml.py | 116 +++++++++++++++--- .../cython/cantera/test/test_convert.py | 15 +++ 2 files changed, 116 insertions(+), 15 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 207ec4ff101..c891189feb2 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -133,6 +133,7 @@ class Phase: "IdealSolidSolution": "ideal-condensed", "FixedChemPot": "fixed-chemical-potential", "PureLiquidWater": "liquid-water-IAPWS95", + "HMW": "HMW-electrolyte", } _kinetics_model_mapping = { "GasKinetics": "gas", @@ -156,6 +157,7 @@ class Phase: "temperature": "T", "pressure": "P", "coverages": "coverages", + "soluteMolalities": "molalities", } _pure_fluid_mapping = { @@ -173,14 +175,15 @@ def __init__(self, phase): phase_name = phase.get("id") phase_attribs = BlockMap({"name": phase_name}) phase_thermo = phase.find("thermo") - phase_attribs["thermo"] = self._thermo_model_mapping[ - phase_thermo.get("model") - ] + phase_attribs["thermo"] = self._thermo_model_mapping[phase_thermo.get("model")] # Convert pure fluid type integer into the name if phase_thermo.get("model") == "PureFluid": phase_attribs["pure-fluid-name"] = self._pure_fluid_mapping[ phase_thermo.get("fluid_type") ] + elif phase_thermo.get("model") == "HMW": + activity_coefficients = phase_thermo.find("activityCoefficients") + phase_attribs["activity-data"] = self.hmw_electrolyte(activity_coefficients) for node in phase_thermo: if node.tag == "site_density": @@ -216,9 +219,7 @@ def __init__(self, phase): transport_node = phase.find("transport") if transport_node is not None: - transport_model = self._transport_model_mapping[ - transport_node.get("model") - ] + transport_model = self._transport_model_mapping[transport_node.get("model")] if transport_model is not None: phase_attribs["transport"] = transport_model @@ -272,10 +273,15 @@ def __init__(self, phase): state_node = phase.find("state") if state_node is not None: - phase_state = FlowMap({}) + phase_state = FlowMap() for prop in state_node: property_name = self._state_properties_mapping[prop.tag] - if prop.tag in ["moleFractions", "massFractions", "coverages"]: + if prop.tag in [ + "moleFractions", + "massFractions", + "coverages", + "soluteMolalities", + ]: value = split_species_value_string(prop.text) else: value = get_float_or_units(prop) @@ -386,6 +392,43 @@ def get_tabulated_thermo(self, tab_thermo_node): return tab_thermo + def hmw_electrolyte(self, activity_node): + """Process the activity coefficients for HMW-electrolyte.""" + activity_data = BlockMap({"temperature-model": activity_node.get("TempModel")}) + A_Debye_node = activity_node.find("A_Debye") + if A_Debye_node.get("model") == "water": + activity_data["A_Debye"] = "variable" + else: + # Assume the units are kg^0.5/gmol^0.5. Apparently, + # this is not handled in the same way as other units? + activity_data["A_Debye"] = A_Debye_node.text.strip() + " kg^0.5/gmol^0.5" + + interactions = [] + for inter_node in activity_node: + if inter_node.tag not in [ + "binarySaltParameters", + "thetaAnion", + "psiCommonCation", + "thetaCation", + "psiCommonAnion", + "lambdaNeutral", + "zetaCation", + ]: + continue + this_interaction = {"species": FlowList([i[1] for i in inter_node.items()])} + for param_node in inter_node: + data = param_node.text.replace("\n", "").strip().split(",") + param_name = param_node.tag.lower() + if param_name == "cphi": + param_name = "Cphi" + if len(data) == 1: + this_interaction[param_name] = float(data[0]) + else: + this_interaction[param_name] = FlowList(map(float, data)) + interactions.append(this_interaction) + activity_data["interactions"] = interactions + return activity_data + @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.phase_attribs) @@ -396,7 +439,7 @@ class SpeciesThermo: def __init__(self, thermo): thermo_type = thermo[0].tag - if thermo_type not in ["NASA", "NASA9", "const_cp", "Shomate"]: + if thermo_type not in ["NASA", "NASA9", "const_cp", "Shomate", "Mu0"]: raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) func = getattr(self, thermo_type) self.thermo_attribs = func(thermo) @@ -422,12 +465,13 @@ def NASA(self, thermo): """Process a NASA 7 thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "NASA7", "data": []}) temperature_ranges = set() - for model in thermo.iterfind("NASA"): + model_nodes = thermo.findall("NASA") + for model in model_nodes: temperature_ranges.add(float(model.get("Tmin"))) temperature_ranges.add(float(model.get("Tmax"))) coeffs = model.find("floatArray").text.replace("\n", " ").strip().split(",") thermo_attribs["data"].append(FlowList(map(float, coeffs))) - if len(temperature_ranges) != 3: + if len(temperature_ranges) != len(model_nodes) + 1: raise ValueError( "The midpoint temperature is not consistent between NASA7 entries" ) @@ -460,6 +504,32 @@ def const_cp(self, thermo): return thermo_attribs + def Mu0(self, thermo): + """Process a piecewise Gibbs thermo entry from XML to a dictionary.""" + thermo_attribs = BlockMap({"model": "piecewise-Gibbs"}) + Mu0_node = thermo.find("Mu0") + thermo_attribs["reference-pressure"] = float(Mu0_node.get("Pref")) + thermo_attribs["h0"] = get_float_or_units(Mu0_node.find("H298")) + for float_node in Mu0_node.iterfind("floatArray"): + title = float_node.get("title") + if title == "Mu0Values": + dimensions = float_node.get("units") + if dimensions == "Dimensionless": + thermo_attribs["dimensionless"] = True + dimensions = "" + values = float_node.text.replace("\n", "").strip().split(",") + if dimensions: + values = [v.strip() + " " + dimensions for v in values] + else: + values = map(float, values) + elif title == "Mu0Temperatures": + temperatures = map( + float, float_node.text.replace("\n", "").strip().split(",") + ) + thermo_attribs["data"] = dict(zip(temperatures, values)) + + return thermo_attribs + @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.thermo_attribs) @@ -502,6 +572,11 @@ def to_yaml(cls, representer, data): class Species: """Represents a species.""" + _standard_state_model_mapping = { + "constant_incompressible": "constant-volume", + "waterIAPWS": "liquid-water-IAPWS95", + } + def __init__(self, species, **kwargs): species_attribs = BlockMap() species_name = species.get("name") @@ -543,10 +618,19 @@ def __init__(self, species, **kwargs): std_state = species.find("standardState") if std_state is not None: - species_attribs["equation-of-state"] = { - "model": "constant-volume", - "molar-volume": get_float_or_units(std_state.find("molarVolume")), + if const_dens is not None: + raise ValueError( + "The standard state of the species '{}' was specified " + "along with stuff from the phase.".format(species_name) + ) + eqn_of_state = { + "model": self._standard_state_model_mapping[std_state.get("model")] } + if eqn_of_state["model"] == "constant-volume": + eqn_of_state["molar-volume"] = get_float_or_units( + std_state.find("molarVolume") + ) + species_attribs["equation-of-state"] = eqn_of_state self.species_attribs = species_attribs @@ -1059,7 +1143,9 @@ def convert(inpfile, outfile): output_species.yaml_set_comment_before_after_key("species", before="\n") else: output_species[this_data_node_id] = species_data - output_species.yaml_set_comment_before_after_key(this_data_node_id, before="\n") + output_species.yaml_set_comment_before_after_key( + this_data_node_id, before="\n" + ) # Reactions reaction_data = [] diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 1b99d827ac7..aa68149de8e 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1007,6 +1007,21 @@ def test_water_IAPWS95_thermo(self): self.assertNear(ctmlWater.thermal_conductivity, yamlWater.thermal_conductivity) + def test_hmw_nacl_phase(self): + basename = "HMW_NaCl_sp1977_alt" + xml_file = Path(self.test_data_dir).joinpath(basename).with_suffix(".xml") + yaml_file = Path(self.test_data_dir).joinpath(basename).with_suffix(".yaml") + ctml2yaml.convert(xml_file, yaml_file) + + # Can only be loaded by ThermoPhase due to a bug in TransportFactory + # ThermoPhase does not have reactions (neither does the input file) + # so we can't use checkConversion + ctmlPhase = ct.ThermoPhase(str(xml_file)) + yamlPhase = ct.ThermoPhase(str(yaml_file)) + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + def test_NaCl_solid_phase(self): ctml2yaml.convert(Path(self.test_data_dir).joinpath("NaCl_Solid.xml"), Path(self.test_work_dir).joinpath("NaCl_Solid.yaml")) From c435b845c9754bdc66b683c77a40e5e2f801c3fd Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Thu, 17 Oct 2019 17:00:10 -0400 Subject: [PATCH 48/99] [Thermo] Fix typos in error messages --- src/thermo/DebyeHuckel.cpp | 4 ++-- src/thermo/IdealMolalSoln.cpp | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/src/thermo/DebyeHuckel.cpp b/src/thermo/DebyeHuckel.cpp index f1ad7168e67..240dcad1d45 100644 --- a/src/thermo/DebyeHuckel.cpp +++ b/src/thermo/DebyeHuckel.cpp @@ -119,7 +119,7 @@ void DebyeHuckel::setDensity(doublereal rho) { double dens = density(); if (rho != dens) { - throw CanteraError("Idea;MolalSoln::setDensity", + throw CanteraError("DebyeHuckel::setDensity", "Density is not an independent variable"); } } @@ -128,7 +128,7 @@ void DebyeHuckel::setMolarDensity(const doublereal conc) { double concI = molarDensity(); if (conc != concI) { - throw CanteraError("Idea;MolalSoln::setMolarDensity", + throw CanteraError("DebyeHuckel::setMolarDensity", "molarDensity/density is not an independent variable"); } } diff --git a/src/thermo/IdealMolalSoln.cpp b/src/thermo/IdealMolalSoln.cpp index 4dd134791e8..47a8f74146b 100644 --- a/src/thermo/IdealMolalSoln.cpp +++ b/src/thermo/IdealMolalSoln.cpp @@ -142,7 +142,7 @@ doublereal IdealMolalSoln::thermalExpansionCoeff() const void IdealMolalSoln::setDensity(const doublereal rho) { if (rho != density()) { - throw CanteraError("Idea;MolalSoln::setDensity", + throw CanteraError("IdealMolalSoln::setDensity", "Density is not an independent variable"); } } From 49bf4b50c7c46e3cffbc26c9f26e406ff2b2cccd Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 19 Oct 2019 09:58:28 -0400 Subject: [PATCH 49/99] [ctml2yaml] Add Debye-Huckel phase-thermo type The implementation is not quite complete, some refactoring needs to be done to make it easier to move data from the phase node to the species nodes. --- interfaces/cython/cantera/ctml2yaml.py | 77 +++ .../cython/cantera/test/test_convert.py | 19 +- test/data/debye-huckel-all.xml | 460 ++++++++++++++++++ 3 files changed, 555 insertions(+), 1 deletion(-) create mode 100644 test/data/debye-huckel-all.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c891189feb2..9abc2b3594c 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -134,6 +134,7 @@ class Phase: "FixedChemPot": "fixed-chemical-potential", "PureLiquidWater": "liquid-water-IAPWS95", "HMW": "HMW-electrolyte", + "DebyeHuckel": "Debye-Huckel", } _kinetics_model_mapping = { "GasKinetics": "gas", @@ -184,6 +185,9 @@ def __init__(self, phase): elif phase_thermo.get("model") == "HMW": activity_coefficients = phase_thermo.find("activityCoefficients") phase_attribs["activity-data"] = self.hmw_electrolyte(activity_coefficients) + elif phase_thermo.get("model") == "DebyeHuckel": + activity_coefficients = phase_thermo.find("activityCoefficients") + phase_attribs["activity-data"] = self.debye_huckel(activity_coefficients) for node in phase_thermo: if node.tag == "site_density": @@ -429,6 +433,63 @@ def hmw_electrolyte(self, activity_node): activity_data["interactions"] = interactions return activity_data + def debye_huckel(self, activity_node): + """Process the activity coefficiences data for the Debye Huckel model.""" + model_map = { + "dilute_limit": "dilute-limit", + "bdot_with_variable_a": "B-dot-with-variable-a", + "bdot_with_common_a": "B-dot-with-common-a", + "pitzer_with_beta_ij": "Pitzer-with-beta_ij", + "beta_ij": "beta_ij", + } + activity_data = BlockMap( + {"model": model_map[activity_node.get("model").lower()]} + ) + A_Debye = activity_node.findtext("A_Debye") + if A_Debye is not None: + # Assume the units are kg^0.5/gmol^0.5. Apparently, + # this is not handled in the same way as other units? + activity_data["A_Debye"] = A_Debye.strip() + " kg^0.5/gmol^0.5" + + B_Debye = activity_node.findtext("B_Debye") + if B_Debye is not None: + # Assume the units are kg^0.5/gmol^0.5/m. Apparently, + # this is not handled in the same way as other units? + activity_data["B_Debye"] = B_Debye.strip() + " kg^0.5/gmol^0.5/m" + + max_ionic_strength = activity_node.findtext("maxIonicStrength") + if max_ionic_strength is not None: + activity_data["max-ionic-strength"] = float(max_ionic_strength) + + if activity_node.find("UseHelgesonFixedForm") is not None: + activity_data["use-Helgeson-fixed-form"] = True + + B_dot_node = activity_node.find("B_dot") + if B_dot_node is not None: + activity_data["B-dot"] = get_float_or_units(B_dot_node) + + ionic_radius_node = activity_node.find("ionicRadius") + if ionic_radius_node is not None: + default_radius = ionic_radius_node.get("default") + radius_units = ionic_radius_node.get("units") + if default_radius is not None: + if radius_units is not None: + if radius_units == "Angstroms": + radius_units = "angstrom" + default_radius += " {}".format(radius_units) + activity_data["default-ionic-radius"] = default_radius + radii = ionic_radius_node.text.strip().replace("\n", " ").split() + if radii: + activity_data["ionic-radius"] = [] + for r in radii: + species, radius = r.strip().rsplit(":", 1) + radius += " {}".format(radius_units) + activity_data["ionic-radius"].append( + BlockMap({"species": species, "radius": radius}) + ) + + return activity_data + @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.phase_attribs) @@ -576,6 +637,13 @@ class Species: "constant_incompressible": "constant-volume", "waterIAPWS": "liquid-water-IAPWS95", } + _electrolyte_species_type_mapping = { + "weakAcidAssociated": "weak-acid-associated", + "chargedSpecies": "charged-species", + "strongAcidAssociated": "strong-acid-associated", + "polarNetural": "polar-neutral", + "nonpolarNeutral": "nonpolar-neutral", + } def __init__(self, species, **kwargs): species_attribs = BlockMap() @@ -632,6 +700,15 @@ def __init__(self, species, **kwargs): ) species_attribs["equation-of-state"] = eqn_of_state + electrolyte = species.findtext("electrolyteSpeciesType") + if electrolyte is not None: + electrolyte = self._electrolyte_species_type_mapping[electrolyte.strip()] + species_attribs["electrolyte-species-type"] = electrolyte + + weak_acid_charge = species.find("stoichIsMods") + if weak_acid_charge is not None: + species_attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) + self.species_attribs = species_attribs def process_act_coeff(self, species_name, activity_coefficients): diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index aa68149de8e..407eb0828d3 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -769,7 +769,7 @@ def checkThermo(self, ctmlPhase, yamlPhase, temperatures, tol=1e-7): s_yaml = yamlPhase.partial_molar_entropies self.assertNear(ctmlPhase.density, yamlPhase.density) for i in range(ctmlPhase.n_species): - message = ' for species {0} at T = {1}'.format(i, T) + message = ' for species {0} at T = {1}'.format(ctmlPhase.species_names[i], T) self.assertNear(cp_ctml[i], cp_yaml[i], tol, msg='cp'+message) self.assertNear(h_ctml[i], h_yaml[i], tol, msg='h'+message) self.assertNear(s_ctml[i], s_yaml[i], tol, msg='s'+message) @@ -1027,3 +1027,20 @@ def test_NaCl_solid_phase(self): Path(self.test_work_dir).joinpath("NaCl_Solid.yaml")) ctmlPhase, yamlPhase = self.checkConversion("NaCl_Solid") self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000]) + + def test_DH_NaCl_phase(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("debye-huckel-all.xml"), + Path(self.test_work_dir).joinpath("debye-huckel-all.yaml")) + for phaseid in [ + "debye-huckel-dilute", + "debye-huckel-B-dot-ak", + "debye-huckel-B-dot-a", + "debye-huckel-pitzer-beta_ij", + "debye-huckel-beta_ij", + ]: + try: + ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", phaseid=phaseid) + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + except: + print(phaseid) + raise diff --git a/test/data/debye-huckel-all.xml b/test/data/debye-huckel-all.xml new file mode 100644 index 00000000000..4c8f50658b9 --- /dev/null +++ b/test/data/debye-huckel-all.xml @@ -0,0 +1,460 @@ + + + + + 300 + 101325.0 + + Na+:9.3549 + Cl-:9.3549 + H+:1.0499E-8 + OH-:1.3765E-6 + NaCl(aq):0.98492 + NaOH(aq):3.8836E-6 + NaH3SiO4(aq):6.8798E-5 + SiO2(aq):3.0179E-5 + H3SiO4-:1.0231E-6 + + + + + + + + 1.172576 + + 3.28640E9 + 3.0 + + + Na+:4.0 + Cl-:3.0 + H+:9.0 + OH-:3.5 + + + H2O(L) + + O H C E Fe Si N Na Cl + + H2O(L) Na+ Cl- H+ OH- NaCl(aq) NaOH(aq) SiO2(aq) + NaH3SiO4(aq) H3SiO4- + + + + + + + 300 + 101325.0 + + Na+:9.3549 + Cl-:9.3549 + H+:1.0499E-8 + OH-:1.3765E-6 + NaCl(aq):0.98492 + NaOH(aq):3.8836E-6 + NaH3SiO4(aq):6.8798E-5 + SiO2(aq):3.0179E-5 + H3SiO4-:1.0231E-6 + + + + + + + + 1.172576 + + 3.28640E9 + 0.0410 + 50.0 + + Na+:4.0 + Cl-:3.0 + H+:9.0 + OH-:3.5 + + + H2O(L) + + O H C E Fe Si N Na Cl + + H2O(L) Na+ Cl- H+ OH- NaCl(aq) NaOH(aq) SiO2(aq) + NaH3SiO4(aq) H3SiO4- + + + + + + + 300 + 101325.0 + + Na+:9.3549 + Cl-:9.3549 + H+:1.0499E-8 + OH-:1.3765E-6 + NaCl(aq):0.98492 + NaOH(aq):3.8836E-6 + NaH3SiO4(aq):6.8798E-5 + SiO2(aq):3.0179E-5 + H3SiO4-:1.0231E-6 + + + + + + + + 1.172576 + + 3.28640E9 + 0.00 + 50.0 + + Na+:4.0 + Cl-:3.0 + H+:9.0 + OH-:3.5 + + + H2O(L) + + O H C E Fe Si N Na Cl + + H2O(L) Na+ Cl- H+ OH- NaCl(aq) NaOH(aq) SiO2(aq) + NaH3SiO4(aq) H3SiO4- + + + + + + + H2O(L) Na+ Cl- H+ OH- NaCl(aq) NaOH(aq) + NaH3SiO4(aq) SiO2(aq) H3SiO4- + + + 300 + 101325.0 + + Na+:3.0 + Cl-:3.0 + H+:1.0499E-8 + OH-:1.3765E-6 + NaCl(aq):0.98492 + NaOH(aq):3.8836E-6 + NaH3SiO4(aq):6.8798E-5 + SiO2(aq):3.0179E-5 + H3SiO4-:1.0231E-6 + + + + + + + + 1.172576 + + 3.28640E9 + + + + H+:Cl-:0.27 + Na+:Cl-:0.15 + Na+:OH-:0.06 + + + NaCl(aq):-1.0 + + + H+:chargedSpecies + NaCl(aq):weakAcidAssociated + + + H2O(L) + + O H C E Fe Si N Na Cl + + + + + + H2O(L) Na+ Cl- H+ OH- NaCl(aq) NaOH(aq) + NaH3SiO4(aq) SiO2(aq) H3SiO4- + + + 300 + 101325.0 + + Na+:3.0 + Cl-:3.0 + H+:1.0499E-8 + OH-:1.3765E-6 + NaCl(aq):0.98492 + NaOH(aq):3.8836E-6 + NaH3SiO4(aq):6.8798E-5 + SiO2(aq):3.0179E-5 + H3SiO4-:1.0231E-6 + + + + + + + + 1.172576 + + 3.28640E9 + + + + H+:Cl-:0.27 + Na+:Cl-:0.15 + Na+:OH-:0.06 + + + NaCl(aq):-1.0 + + + H+:chargedSpecies + NaCl(aq):weakAcidAssociated + + + H2O(L) + + O H C E Fe Si N Na Cl + + + + + + + + H:2 O:1 + + + + 7.255750050E+01, -6.624454020E-01, + 2.561987460E-03, -4.365919230E-06, + 2.781789810E-09, -4.188671E+04, -2.8827879E+02 + + + + + 0.05555555 + + + + + Na:1 E:-1 + +1 + + + -240.34 + 2 + + -103.98186, -103.98186 + + + 298.15, 333.15 + + + + + 1.3 + + + + + Cl:1 E:1 + -1 + + 1.3 + + + + -167.08 + 2 + + -74.20664, -74.20664 + + + 298.15, 333.15 + + + + + + + H:1 E:-1 + +1 + + 0.0 + + + + 0.0 + 2 + + 0.0, 0.0 + + + 298.15, 333.15 + + + + + + + O:1 H:1 E:1 + -1 + + 1.3 + + + + -230.015 + 2 + + -91.50963 , -85. + + + 298.15, 333.15 + + + + + + + Na:1 Cl:1 + + 1.3 + + -1.0 + weakAcidAssociated + + + -96.03E3 + 2 + + + -174.5057463, -174.5057463 + + + 298.15, 333.15 + + + + + + + Na:1 O:1 H:1 + + 1.3 + + -1.0 + weakAcidAssociated + + + -472.4865 + 2 + + + -195.02569, -195.02569 + + + 298.15, 323.15 + + + + + + + Si:1 O:2 + + 1.3 + + 0.0 + nonpolarNeutral + + + -890. + 2 + + -363.2104, -300. + + + 298.15, 323.15 + + + + + + + Na:1 H:3 Si:1 O:4 + 0 + -1.0 + weakAcidAssociated + + 1.3 + + + + -890. + 2 + + -694.683918 , -300. + + + 298.15, 323.15 + + + + + + + Si:1 O:4 H:3 E:1 + -1 + -1.0 + chargedSpecies + + 1.3 + + + + 0.0 + 2 + + -588.0556 , -450 + + + 298.15, 333.15 + + + + + + + + From 28f4c78d32b368bec78cdb678614adf00246f404 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 19 Oct 2019 10:28:34 -0400 Subject: [PATCH 50/99] [ctml2yaml] Rename Species parameter species_node Just clarify what the input argument is --- interfaces/cython/cantera/ctml2yaml.py | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 9abc2b3594c..48addd80ebf 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -645,20 +645,20 @@ class Species: "nonpolarNeutral": "nonpolar-neutral", } - def __init__(self, species, **kwargs): + def __init__(self, species_node, **kwargs): species_attribs = BlockMap() - species_name = species.get("name") + species_name = species_node.get("name") species_attribs["name"] = species_name - atom_array = species.find("atomArray") + atom_array = species_node.find("atomArray") if atom_array.text is not None: species_attribs["composition"] = split_species_value_string(atom_array.text) else: species_attribs["composition"] = {} - if species.findtext("note") is not None: - species_attribs["note"] = species.findtext("note") + if species_node.findtext("note") is not None: + species_attribs["note"] = species_node.findtext("note") - thermo = species.find("thermo") + thermo = species_node.find("thermo") if thermo is not None: species_attribs["thermo"] = SpeciesThermo(thermo) @@ -680,11 +680,11 @@ def __init__(self, species, **kwargs): const_prop: get_float_or_units(const_dens), } - transport = species.find("transport") + transport = species_node.find("transport") if transport is not None: species_attribs["transport"] = SpeciesTransport(transport) - std_state = species.find("standardState") + std_state = species_node.find("standardState") if std_state is not None: if const_dens is not None: raise ValueError( @@ -700,12 +700,12 @@ def __init__(self, species, **kwargs): ) species_attribs["equation-of-state"] = eqn_of_state - electrolyte = species.findtext("electrolyteSpeciesType") + electrolyte = species_node.findtext("electrolyteSpeciesType") if electrolyte is not None: electrolyte = self._electrolyte_species_type_mapping[electrolyte.strip()] species_attribs["electrolyte-species-type"] = electrolyte - weak_acid_charge = species.find("stoichIsMods") + weak_acid_charge = species_node.find("stoichIsMods") if weak_acid_charge is not None: species_attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) From 8fc20ee8b00dff7c933c918116b84ce0e31d7159 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 23 Oct 2019 15:39:05 -0400 Subject: [PATCH 51/99] [ctml2yaml] Add types and resolve mypy errors --- interfaces/cython/cantera/ctml2yaml.py | 624 +++++++++++++++++-------- 1 file changed, 429 insertions(+), 195 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 48addd80ebf..0085c81aa4e 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -10,7 +10,7 @@ from email.utils import formatdate from collections import defaultdict -from typing import Any +from typing import Any, Dict, Union, Iterable, Optional, List try: import ruamel_yaml as yaml @@ -38,7 +38,7 @@ def FlowList(*args, **kwargs): HAS_FMT_FLT_POS = hasattr(np, "format_float_positional") -def float2string(data): +def float2string(data: float) -> str: if not HAS_FMT_FLT_POS: return repr(data) @@ -50,8 +50,7 @@ def float2string(data): return np.format_float_scientific(data, trim="0") -def represent_float(self, data): - # type: (Any, Any) -> Any +def represent_float(self: Any, data: Any) -> Any: if data != data: value = ".nan" elif data == self.inf_value: @@ -67,10 +66,13 @@ def represent_float(self, data): yaml.RoundTripRepresenter.add_representer(float, represent_float) -def get_float_or_units(node): +def get_float_or_units(node: etree.Element) -> Union[str, float]: + if node.text is None: + raise ValueError("Node '{}' must contain text".format(node)) + value = float(node.text.strip()) - if node.get("units") is not None: - units = node.get("units") + units = node.get("units") + if units is not None: units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", units) units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", units) return "{} {}".format(float2string(value), units) @@ -78,7 +80,7 @@ def get_float_or_units(node): return value -def check_float_neq_zero(value, name): +def check_float_neq_zero(value: float, name: str) -> Dict[str, float]: """Check that the text value associated with a tag is non-zero. If the value is not zero, return a dictionary with the key ``name`` @@ -92,16 +94,16 @@ def check_float_neq_zero(value, name): return {} -def split_species_value_string(text): +def split_species_value_string(node: etree.Element) -> Dict[str, float]: """Split a string of species:value pairs into a dictionary. The keys of the dictionary are species names and the values are the number associated with each species. This is useful for things like elemental composition, mole fraction mappings, coverage mappings, etc. - - The keyword argument sep is used to determine how the pairs are split, - typically either " " or ",". """ + text = node.text + if text is None: + raise ValueError("The text of the node must exist: '{}'".format(node)) pairs = FlowMap({}) for t in text.replace("\n", " ").replace(",", " ").strip().split(): key, value = t.split(":") @@ -113,6 +115,14 @@ def split_species_value_string(text): return pairs +def clean_node_text(node: etree.Element) -> str: + """Clean the text of a node.""" + text = node.text + if text is None: + raise ValueError("The text of the node must exist: '{}'".format(node)) + return text.replace("\n", " ").strip() + + class Phase: """Represents a phase. @@ -150,6 +160,7 @@ class Phase: "Ion": "ionized-gas", "Water": "water", "none": None, + None: None, } _state_properties_mapping = { @@ -172,21 +183,31 @@ class Phase: "8": "heptane", } - def __init__(self, phase): + def __init__(self, phase: etree.Element): phase_name = phase.get("id") phase_attribs = BlockMap({"name": phase_name}) phase_thermo = phase.find("thermo") - phase_attribs["thermo"] = self._thermo_model_mapping[phase_thermo.get("model")] + if phase_thermo is None: + raise ValueError("The phase node requires a thermo node") + phase_thermo_model = phase.get("model") + if phase_thermo_model is None: + raise ValueError("The thermo node requires a model") + phase_attribs["thermo"] = self._thermo_model_mapping[phase_thermo_model] # Convert pure fluid type integer into the name - if phase_thermo.get("model") == "PureFluid": - phase_attribs["pure-fluid-name"] = self._pure_fluid_mapping[ - phase_thermo.get("fluid_type") - ] - elif phase_thermo.get("model") == "HMW": + if phase_thermo_model == "PureFluid": + pure_fluid_type = phase_thermo.get("fluid_type") + if pure_fluid_type is None: + raise ValueError("PureFluid model requires the fluid_type") + phase_attribs["pure-fluid-name"] = self._pure_fluid_mapping[pure_fluid_type] + elif phase_thermo_model == "HMW": activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is None: + raise ValueError("HMW thermo model requires activity coefficients") phase_attribs["activity-data"] = self.hmw_electrolyte(activity_coefficients) - elif phase_thermo.get("model") == "DebyeHuckel": + elif phase_thermo_model == "DebyeHuckel": activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is None: + raise ValueError("Debye Huckel thermo model requires activity") phase_attribs["activity-data"] = self.debye_huckel(activity_coefficients) for node in phase_thermo: @@ -202,7 +223,7 @@ def __init__(self, phase): elif node.tag == "chemicalPotential": phase_attribs["chemical-potential"] = get_float_or_units(node) - elements = phase.find("elementArray").text + elements = phase.findtext("elementArray") if elements is not None: phase_attribs["elements"] = FlowList(elements.strip().split()) @@ -240,7 +261,7 @@ def __init__(self, phase): for rA_node in reactionArray_nodes: filter = rA_node.find("include") if filter is not None: - if filter.get("min").lower() == "none": + if filter.get("min", "none").lower() == "none": continue else: has_filter = True @@ -286,10 +307,12 @@ def __init__(self, phase): "coverages", "soluteMolalities", ]: - value = split_species_value_string(prop.text) + composition = split_species_value_string(prop) + phase_state[property_name] = composition else: value = get_float_or_units(prop) - phase_state[property_name] = value + phase_state[property_name] = value + if phase_state: phase_attribs["state"] = phase_state @@ -299,10 +322,16 @@ def __init__(self, phase): self.phase_attribs = phase_attribs - def get_species_array(self, speciesArray_node): + def get_species_array( + self, speciesArray_node: etree.Element + ) -> Dict[str, Iterable[str]]: """Process a list of species from a speciesArray node.""" + if speciesArray_node.text is None: + raise ValueError( + "The speciesArray node must have text: '{}'".format(speciesArray_node) + ) species_list = FlowList( - speciesArray_node.text.replace("\n", " ").strip().split() + clean_node_text(speciesArray_node).split() ) datasrc = speciesArray_node.get("datasrc", "") if datasrc == "#species_data": @@ -317,7 +346,7 @@ def get_species_array(self, speciesArray_node): datasrc = "{}/{}".format(name, location) return {datasrc: species_list} - def get_reaction_array(self, reactionArray_node): + def get_reaction_array(self, reactionArray_node: etree.Element) -> Dict[str, str]: """Process reactions from a reactionArray node in a phase definition.""" datasrc = reactionArray_node.get("datasrc", "") has_filter = reactionArray_node.find("include") is not None @@ -360,35 +389,54 @@ def get_reaction_array(self, reactionArray_node): return {datasrc: reaction_option} - def get_tabulated_thermo(self, tab_thermo_node): + def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str]: tab_thermo = BlockMap() enthalpy_node = tab_thermo_node.find("enthalpy") - enthalpy_units = enthalpy_node.get("units").split("/") + if enthalpy_node is None or enthalpy_node.text is None: + raise LookupError( + "Tabulated thermo must have an enthalpy node " + "with text: '{}'".format(tab_thermo_node) + ) + enthalpy_units = enthalpy_node.get("units", "").split("/") + if not enthalpy_units: + raise ValueError("The units of tabulated enthalpy must be specified") entropy_node = tab_thermo_node.find("entropy") - entropy_units = entropy_node.get("units").split("/") + if entropy_node is None or entropy_node.text is None: + raise LookupError( + "Tabulated thermo must have an entropy node " + "with text: '{}'".format(tab_thermo_node) + ) + entropy_units = entropy_node.get("units", "").split("/") + if not entropy_units: + raise ValueError("The units of tabulated entropy must be specified") if enthalpy_units[:2] != entropy_units[:2]: raise ValueError("Tabulated thermo must have the same units.") tab_thermo["units"] = FlowMap( {"energy": entropy_units[0], "quantity": entropy_units[1]} ) - enthalpy = enthalpy_node.text.replace("\n", " ").split(",") - if len(enthalpy) != int(enthalpy_node.get("size")): + enthalpy = clean_node_text(enthalpy_node).split(",") + if len(enthalpy) != int(enthalpy_node.get("size", 0)): raise ValueError( "The number of entries in the enthalpy list is different from the " "indicated size." ) tab_thermo["enthalpy"] = FlowList(map(float, enthalpy)) - entropy = entropy_node.text.replace("\n", " ").split(",") + entropy = clean_node_text(entropy_node).split(",") tab_thermo["entropy"] = FlowList(map(float, entropy)) - if len(entropy) != int(entropy_node.get("size")): + if len(entropy) != int(entropy_node.get("size", 0)): raise ValueError( "The number of entries in the entropy list is different from the " "indicated size." ) mole_fraction_node = tab_thermo_node.find("moleFraction") - mole_fraction = mole_fraction_node.text.replace("\n", " ").split(",") + if mole_fraction_node is None or mole_fraction_node.text is None: + raise LookupError( + "Tabulated thermo must have a mole fraction node " + "with text: '{}'".format(tab_thermo_node) + ) + mole_fraction = clean_node_text(mole_fraction_node).split(",") tab_thermo["mole-fractions"] = FlowList(map(float, mole_fraction)) - if len(mole_fraction) != int(mole_fraction_node.get("size")): + if len(mole_fraction) != int(mole_fraction_node.get("size", 0)): raise ValueError( "The number of entries in the mole_fraction list is different from the " "indicated size." @@ -396,15 +444,22 @@ def get_tabulated_thermo(self, tab_thermo_node): return tab_thermo - def hmw_electrolyte(self, activity_node): + def hmw_electrolyte(self, activity_node: etree.Element): """Process the activity coefficients for HMW-electrolyte.""" activity_data = BlockMap({"temperature-model": activity_node.get("TempModel")}) A_Debye_node = activity_node.find("A_Debye") + if A_Debye_node is None: + raise LookupError( + "Activity coefficients for HMW must have " + "A_Debye: '{}'".format(activity_node) + ) if A_Debye_node.get("model") == "water": activity_data["A_Debye"] = "variable" else: # Assume the units are kg^0.5/gmol^0.5. Apparently, # this is not handled in the same way as other units? + if A_Debye_node.text is None: + raise ValueError("The A_Debye node must have a text value") activity_data["A_Debye"] = A_Debye_node.text.strip() + " kg^0.5/gmol^0.5" interactions = [] @@ -421,7 +476,9 @@ def hmw_electrolyte(self, activity_node): continue this_interaction = {"species": FlowList([i[1] for i in inter_node.items()])} for param_node in inter_node: - data = param_node.text.replace("\n", "").strip().split(",") + if param_node.text is None: + raise ValueError("The interaction nodes must have text values.") + data = clean_node_text(param_node).split(",") param_name = param_node.tag.lower() if param_name == "cphi": param_name = "Cphi" @@ -433,7 +490,7 @@ def hmw_electrolyte(self, activity_node): activity_data["interactions"] = interactions return activity_data - def debye_huckel(self, activity_node): + def debye_huckel(self, activity_node: etree.Element): """Process the activity coefficiences data for the Debye Huckel model.""" model_map = { "dilute_limit": "dilute-limit", @@ -441,10 +498,12 @@ def debye_huckel(self, activity_node): "bdot_with_common_a": "B-dot-with-common-a", "pitzer_with_beta_ij": "Pitzer-with-beta_ij", "beta_ij": "beta_ij", + "": "dilute-limit", } - activity_data = BlockMap( - {"model": model_map[activity_node.get("model").lower()]} - ) + activity_model = activity_node.get("model") + if activity_model is None: + raise ValueError("The Debye Huckel model must be specified") + activity_data = BlockMap({"model": model_map[activity_model.lower()]}) A_Debye = activity_node.findtext("A_Debye") if A_Debye is not None: # Assume the units are kg^0.5/gmol^0.5. Apparently, @@ -478,15 +537,16 @@ def debye_huckel(self, activity_node): radius_units = "angstrom" default_radius += " {}".format(radius_units) activity_data["default-ionic-radius"] = default_radius - radii = ionic_radius_node.text.strip().replace("\n", " ").split() - if radii: - activity_data["ionic-radius"] = [] - for r in radii: - species, radius = r.strip().rsplit(":", 1) - radius += " {}".format(radius_units) - activity_data["ionic-radius"].append( - BlockMap({"species": species, "radius": radius}) - ) + if ionic_radius_node.text is not None: + radii = clean_node_text(ionic_radius_node).split() + if radii: + activity_data["ionic-radius"] = [] + for r in radii: + species, radius = r.strip().rsplit(":", 1) + radius += " {}".format(radius_units) + activity_data["ionic-radius"].append( + BlockMap({"species": species, "radius": radius}) + ) return activity_data @@ -498,22 +558,32 @@ def to_yaml(cls, representer, data): class SpeciesThermo: """Represents a species thermodynamic model.""" - def __init__(self, thermo): + def __init__(self, thermo: etree.Element) -> None: thermo_type = thermo[0].tag if thermo_type not in ["NASA", "NASA9", "const_cp", "Shomate", "Mu0"]: raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) func = getattr(self, thermo_type) self.thermo_attribs = func(thermo) - def Shomate(self, thermo): + def Shomate(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: """Process a Shomate polynomial from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "Shomate", "data": []}) temperature_ranges = set() model_nodes = thermo.findall("Shomate") for node in model_nodes: - temperature_ranges.add(float(node.get("Tmin"))) - temperature_ranges.add(float(node.get("Tmax"))) - coeffs = node.find("floatArray").text.replace("\n", " ").strip().split(",") + Tmin = float(node.get("Tmin", 0)) + Tmax = float(node.get("Tmax", 0)) + if not Tmin or Tmax: + raise ValueError("Tmin and Tmax must both be specified") + temperature_ranges.add(float(Tmin)) + temperature_ranges.add(float(Tmax)) + float_array = node.findtext("floatArray") + if float_array is None: + raise ValueError( + "Shomate entry missing floatArray node with text: " + "'{}'".format(node) + ) + coeffs = float_array.replace("\n", " ").strip().split(",") thermo_attribs["data"].append(FlowList(map(float, coeffs))) if len(temperature_ranges) != len(model_nodes) + 1: raise ValueError( @@ -522,15 +592,25 @@ def Shomate(self, thermo): thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) return thermo_attribs - def NASA(self, thermo): + def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: """Process a NASA 7 thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "NASA7", "data": []}) temperature_ranges = set() model_nodes = thermo.findall("NASA") - for model in model_nodes: - temperature_ranges.add(float(model.get("Tmin"))) - temperature_ranges.add(float(model.get("Tmax"))) - coeffs = model.find("floatArray").text.replace("\n", " ").strip().split(",") + for node in model_nodes: + Tmin = float(node.get("Tmin", 0)) + Tmax = float(node.get("Tmax", 0)) + if not Tmin or Tmax: + raise ValueError("Tmin and Tmax must both be specified") + temperature_ranges.add(float(Tmin)) + temperature_ranges.add(float(Tmax)) + float_array = node.findtext("floatArray") + if float_array is None: + raise ValueError( + "Shomate entry missing floatArray node with text: " + "'{}'".format(node) + ) + coeffs = float_array.replace("\n", " ").strip().split(",") thermo_attribs["data"].append(FlowList(map(float, coeffs))) if len(temperature_ranges) != len(model_nodes) + 1: raise ValueError( @@ -539,15 +619,25 @@ def NASA(self, thermo): thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) return thermo_attribs - def NASA9(self, thermo): + def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: """Process a NASA 9 thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "NASA9", "data": []}) temperature_ranges = set() model_nodes = thermo.findall("NASA9") for node in model_nodes: - temperature_ranges.add(float(node.get("Tmin"))) - temperature_ranges.add(float(node.get("Tmax"))) - coeffs = node.find("floatArray").text.replace("\n", " ").strip().split(",") + Tmin = float(node.get("Tmin", 0)) + Tmax = float(node.get("Tmax", 0)) + if not Tmin or Tmax: + raise ValueError("Tmin and Tmax must both be specified") + temperature_ranges.add(float(Tmin)) + temperature_ranges.add(float(Tmax)) + float_array = node.findtext("floatArray") + if float_array is None: + raise ValueError( + "Shomate entry missing floatArray node with text: " + "'{}'".format(node) + ) + coeffs = float_array.replace("\n", " ").strip().split(",") thermo_attribs["data"].append(FlowList(map(float, coeffs))) if len(temperature_ranges) != len(model_nodes) + 1: raise ValueError( @@ -556,21 +646,39 @@ def NASA9(self, thermo): thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) return thermo_attribs - def const_cp(self, thermo): + def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: """Process a constant c_p thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "constant-cp"}) - for node in thermo.find("const_cp"): - value = get_float_or_units(node) - thermo_attribs[node.tag] = value + const_cp_node = thermo.find("const_cp") + if const_cp_node is None: + raise LookupError( + "The thermo node must constain a const_cp node: '{}'".format(thermo) + ) + for node in const_cp_node: + tag = node.tag + if tag == "t0": + tag = "T0" + thermo_attribs[tag] = get_float_or_units(node) return thermo_attribs - def Mu0(self, thermo): + def Mu0( + self, thermo: etree.Element + ) -> Dict[str, Union[str, Dict[float, Iterable]]]: """Process a piecewise Gibbs thermo entry from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "piecewise-Gibbs"}) Mu0_node = thermo.find("Mu0") + if Mu0_node is None: + raise LookupError( + "The thermo entry must contain a Mu0 node: '{}'".format(thermo) + ) thermo_attribs["reference-pressure"] = float(Mu0_node.get("Pref")) - thermo_attribs["h0"] = get_float_or_units(Mu0_node.find("H298")) + H298_node = Mu0_node.find("H298") + if H298_node is None: + raise LookupError( + "The Mu0 entry must contain an H298 node: '{}'".format(Mu0_node) + ) + thermo_attribs["h0"] = get_float_or_units(H298_node) for float_node in Mu0_node.iterfind("floatArray"): title = float_node.get("title") if title == "Mu0Values": @@ -578,15 +686,15 @@ def Mu0(self, thermo): if dimensions == "Dimensionless": thermo_attribs["dimensionless"] = True dimensions = "" - values = float_node.text.replace("\n", "").strip().split(",") + # I don't like doing this, but if we want to continue supporting + # Python 3.5, it is the cleanest way to add the type hint + values = [] # type: Union[Iterable[float], Iterable[str]] + values = map(float, clean_node_text(float_node).split(",")) if dimensions: - values = [v.strip() + " " + dimensions for v in values] - else: - values = map(float, values) + values = [float2string(v) + " " + dimensions for v in values] elif title == "Mu0Temperatures": - temperatures = map( - float, float_node.text.replace("\n", "").strip().split(",") - ) + temperatures = map(float, clean_node_text(float_node).split(",")) + thermo_attribs["data"] = dict(zip(temperatures, values)) return thermo_attribs @@ -610,7 +718,7 @@ class SpeciesTransport: "quadrupole_polarizability": "quadrupole-polarizability", } - def __init__(self, transport): + def __init__(self, transport: etree.Element): transport_attribs = BlockMap({}) transport_model = transport.get("model") if transport_model not in self._species_transport_mapping: @@ -645,13 +753,17 @@ class Species: "nonpolarNeutral": "nonpolar-neutral", } - def __init__(self, species_node, **kwargs): + def __init__(self, species_node: etree.Element, **kwargs): species_attribs = BlockMap() species_name = species_node.get("name") + if species_name is None: + raise LookupError( + "The species name must be specified: '{}'".format(species_node) + ) species_attribs["name"] = species_name atom_array = species_node.find("atomArray") - if atom_array.text is not None: - species_attribs["composition"] = split_species_value_string(atom_array.text) + if atom_array is not None: + species_attribs["composition"] = split_species_value_string(atom_array) else: species_attribs["composition"] = {} @@ -691,13 +803,22 @@ def __init__(self, species_node, **kwargs): "The standard state of the species '{}' was specified " "along with stuff from the phase.".format(species_name) ) - eqn_of_state = { - "model": self._standard_state_model_mapping[std_state.get("model")] - } - if eqn_of_state["model"] == "constant-volume": - eqn_of_state["molar-volume"] = get_float_or_units( - std_state.find("molarVolume") + std_state_model = std_state.get("model") + if std_state_model not in self._standard_state_model_mapping: + raise ValueError( + "Unknown standard state model: '{}'".format(std_state_model) ) + eqn_of_state = { + "model": self._standard_state_model_mapping[std_state_model] + } # type: Dict[str, Union[str, float]] + if std_state_model == "constant_incompressible": + molar_volume_node = std_state.find("molarVolume") + if molar_volume_node is None: + raise LookupError( + "If the standard state model is constant_incompressible, it " + "must include a molarVolume node" + ) + eqn_of_state["molar-volume"] = get_float_or_units(molar_volume_node) species_attribs["equation-of-state"] = eqn_of_state electrolyte = species_node.findtext("electrolyteSpeciesType") @@ -711,31 +832,48 @@ def __init__(self, species_node, **kwargs): self.species_attribs = species_attribs - def process_act_coeff(self, species_name, activity_coefficients): + def process_act_coeff( + self, species_name: str, activity_coefficients: Dict[str, Any] + ): """If a species has activity coefficients, create an equation-of-state mapping. This appears to only be necessary for Redlich-Kwong phase thermo model. """ eq_of_state = BlockMap({"model": activity_coefficients["model"]}) - pure_params = activity_coefficients["pure_params"] - pure_a = pure_params.findtext("a_coeff") - pure_a = list(map(float, pure_a.replace("\n", " ").strip().split(","))) - pure_a_units = pure_params.find("a_coeff").get("units") + pure_params = activity_coefficients["pure_params"] # type: etree.Element + pure_a_node = pure_params.find("a_coeff") + if pure_a_node is None: + raise LookupError("The pure fluid coefficients requires the a_coeff node.") + + pure_a_units = pure_a_node.get("units") if pure_a_units is not None: pure_a_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_a_units) pure_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_a_units) - pure_a[0] = "{} {}".format(float2string(pure_a[0]), pure_a_units + "*K^0.5") - pure_a[1] = "{} {}".format(float2string(pure_a[1]), pure_a_units + "/K^0.5") - pure_b = float(pure_params.findtext("b_coeff").strip()) - pure_b_units = pure_params.find("b_coeff").get("units") - if pure_b_units is not None: - pure_b_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_b_units) - pure_b_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_b_units) - pure_b = "{} {}".format(float2string(pure_b), pure_b_units) + + eq_of_state["a"] = FlowList() + pure_a = [float(a) for a in clean_node_text(pure_a_node).split(",")] + eq_of_state["a"].append( + "{} {}".format(float2string(pure_a[0]), pure_a_units + "*K^0.5") + ) + eq_of_state["a"].append( + "{} {}".format(float2string(pure_a[1]), pure_a_units + "/K^0.5") + ) + else: + eq_of_state["a"] = FlowList( + float(a) for a in clean_node_text(pure_a_node).split(",") + ) + + pure_b_node = pure_params.find("b_coeff") + if pure_b_node is None: + raise LookupError("The pure fluid coefficients requires the b_coeff node.") + pure_b = get_float_or_units(pure_b_node) + eq_of_state["a"] = FlowList(pure_a) eq_of_state["b"] = pure_b - cross_params = activity_coefficients.get("cross_params") + cross_params = activity_coefficients.get( + "cross_params" + ) # type: Optional[etree.Element] if cross_params is not None: related_species = [ cross_params.get("species1"), @@ -745,21 +883,33 @@ def process_act_coeff(self, species_name, activity_coefficients): other_species = related_species[1] else: other_species = related_species[0] - cross_a = cross_params.findtext("a_coeff") - cross_a = list(map(float, cross_a.replace("\n", " ").strip().split(","))) - cross_a_units = cross_params.find("a_coeff").get("units") - if cross_a_units is not None: - cross_a_units = re.sub( - r"([A-Za-z])-([A-Za-z])", r"\1*\2", cross_a_units + + cross_a_node = cross_params.find("a_coeff") + if cross_a_node is None: + raise LookupError( + "The cross-fluid coefficients requires the a_coeff node" ) - cross_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", cross_a_units) - cross_a[0] = "{} {}".format( - float2string(cross_a[0]), cross_a_units + "*K^0.5" + + cross_a_unit = cross_a_node.get("units") + if cross_a_unit is not None: + cross_a_unit = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", cross_a_unit) + cross_a_unit = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", cross_a_unit) + + cross_a = [float(a) for a in clean_node_text(cross_a_node).split(",")] + eq_params = [] + eq_params.append( + "{} {}".format(float2string(cross_a[0]), cross_a_unit + "*K^0.5") ) - cross_a[1] = "{} {}".format( - float2string(cross_a[1]), cross_a_units + "/K^0.5" + eq_params.append( + "{} {}".format(float2string(cross_a[1]), cross_a_unit + "/K^0.5") ) - eq_of_state["binary-a"] = {other_species: FlowList(cross_a)} + eq_of_state["binary-a"] = {other_species: FlowList(eq_params)} + else: + eq_of_state["binary-a"] = { + other_species: FlowList( + float(a) for a in clean_node_text(cross_a_node).split(",") + ) + } return eq_of_state @@ -775,9 +925,9 @@ class Reaction: An ETree Element node with the reaction information """ - def __init__(self, reaction): + def __init__(self, reaction: etree.Element): reaction_attribs = BlockMap({}) - reaction_id = reaction.get("id", False) + reaction_id = reaction.get("id", False) # type: Union[str, int, bool] if reaction_id: # If the reaction_id can be converted to an integer, it was likely # added automatically, so there's no need to include it in the @@ -788,10 +938,12 @@ def __init__(self, reaction): except ValueError: reaction_attribs["id"] = reaction_id - reaction_type = reaction.get("type") + reaction_type = reaction.get("type", "arrhenius") rate_coeff = reaction.find("rateCoeff") + if rate_coeff is None: + raise LookupError("The reaction must have a rateCoeff node.") if reaction_type not in [ - None, + "arrhenius", "threeBody", "plog", "chebyshev", @@ -805,11 +957,11 @@ def __init__(self, reaction): reaction_type, reaction.get("id") ) ) - if reaction_type is None: - # The default type is an Arrhenius reaction - reaction_type = "arrhenius" - elif reaction_type in ["falloff"]: - falloff_type = rate_coeff.find("falloff").get("type") + elif reaction_type == "falloff": + falloff_node = rate_coeff.find("falloff") + if falloff_node is None: + raise LookupError("Falloff reaction types must have a falloff node.") + falloff_type = falloff_node.get("type") if falloff_type not in ["Lindemann", "Troe"]: raise TypeError( "Unknown falloff type '{}' for reaction id {}".format( @@ -819,7 +971,10 @@ def __init__(self, reaction): else: reaction_type = falloff_type elif reaction_type == "chemAct": - falloff_type = rate_coeff.find("falloff").get("type") + falloff_node = rate_coeff.find("falloff") + if falloff_node is None: + raise LookupError("chemAct reaction types must have a falloff node.") + falloff_type = falloff_node.get("type") if falloff_type != "Troe": raise TypeError( "Unknown activation type '{}' for reaction id {}".format( @@ -830,17 +985,24 @@ def __init__(self, reaction): func = getattr(self, reaction_type.lower()) reaction_attribs.update(func(rate_coeff)) - reaction_attribs["equation"] = ( - # This has to replace the reaction direction symbols separately because - # species names can have [ or ] in them - reaction.find("equation") - .text.replace("[=]", "<=>") - .replace("=]", "=>") + reaction_equation = reaction.findtext("equation") + if reaction_equation is None: + raise LookupError( + "The reaction '{}' must have an equation".format(reaction) + ) + + # This has to replace the reaction direction symbols separately because + # species names can have [ or ] in them + reaction_attribs["equation"] = reaction_equation.replace("[=]", "<=>").replace( + "=]", "=>" ) if reaction.get("negative_A", "").lower() == "yes": reaction_attribs["negative-A"] = True - reactants = split_species_value_string(reaction.findtext("reactants")) + reactants_node = reaction.find("reactants") + if reactants_node is None: + raise LookupError("The reactants must be present in the reaction") + reactants = split_species_value_string(reactants_node) # products = { # a.split(":")[0]: float(a.split(":")[1]) # for a in reaction.findtext("products").replace("\n", " ").strip().split() @@ -849,6 +1011,10 @@ def __init__(self, reaction): # Need to make this more general, for non-reactant orders for order_node in reaction.iterfind("order"): species = order_node.get("species") + if species is None: + raise LookupError("A reaction order node must have a species") + if order_node.text is None: + raise ValueError("A reaction order node must have a text value") order = float(order_node.text) if not np.isclose(reactants[species], order): orders[species] = order @@ -867,7 +1033,9 @@ def __init__(self, reaction): def to_yaml(cls, representer, data): return representer.represent_dict(data.reaction_attribs) - def threebody(self, rate_coeff): + def threebody( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: """Process a three-body reaction. Returns a dictionary with the appropriate fields set that is @@ -883,7 +1051,9 @@ def threebody(self, rate_coeff): return reaction_attribs - def lindemann(self, rate_coeff): + def lindemann( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: """Process a Lindemann falloff reaction. Returns a dictionary with the appropriate fields set that is @@ -907,7 +1077,9 @@ def lindemann(self, rate_coeff): return reaction_attribs - def troe(self, rate_coeff): + def troe( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: """Process a Troe falloff reaction. Returns a dictionary with the appropriate fields set that is @@ -916,18 +1088,23 @@ def troe(self, rate_coeff): # This gets the low-p and high-p rate constants and the efficiencies reaction_attribs = self.lindemann(rate_coeff) - troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() + troe_node = rate_coeff.find("falloff") + if troe_node is None: + raise LookupError("Troe reaction types must include a falloff node") + troe_params = clean_node_text(troe_node).split() troe_names = ["A", "T3", "T1", "T2"] reaction_attribs["Troe"] = FlowMap() # zip stops when the shortest iterable is exhausted. If T2 is not present # in the Troe parameters (i.e., troe_params is three elements long), it # will be omitted here as well. for name, param in zip(troe_names, troe_params): - reaction_attribs["Troe"].update({name: float(param)}) + reaction_attribs["Troe"].update({name: float(param)}) # type: ignore return reaction_attribs - def chemact(self, rate_coeff): + def chemact( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: """Process a chemically activated falloff reaction. Returns a dictionary with the appropriate fields set that is @@ -949,7 +1126,10 @@ def chemact(self, rate_coeff): if eff_node is not None: reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) - troe_params = rate_coeff.find("falloff").text.replace("\n", " ").strip().split() + troe_node = rate_coeff.find("falloff") + if troe_node is None: + raise LookupError("Troe reaction types must include a falloff node") + troe_params = clean_node_text(troe_node).split() troe_names = ["A", "T3", "T1", "T2"] reaction_attribs["Troe"] = FlowMap() # zip stops when the shortest iterable is exhausted. If T2 is not present @@ -960,7 +1140,9 @@ def chemact(self, rate_coeff): return reaction_attribs - def plog(self, rate_coeff): + def plog( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Dict[str, Union[str, float]]]]: """Process a PLOG reaction. Returns a dictionary with the appropriate fields set that is @@ -970,13 +1152,18 @@ def plog(self, rate_coeff): rate_constants = [] for arr_coeff in rate_coeff.iterfind("Arrhenius"): rate_constant = self.process_arrhenius_parameters(arr_coeff) - rate_constant["P"] = get_float_or_units(arr_coeff.find("P")) + P_node = arr_coeff.find("P") + if P_node is None: + raise LookupError("The pressure for a plog reaction must be specified") + rate_constant["P"] = get_float_or_units(P_node) rate_constants.append(rate_constant) reaction_attributes["rate-constants"] = rate_constants return reaction_attributes - def chebyshev(self, rate_coeff): + def chebyshev( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable[float]]]: """Process a Chebyshev reaction. Returns a dictionary with the appropriate fields set that is @@ -985,46 +1172,61 @@ def chebyshev(self, rate_coeff): reaction_attributes = FlowMap( { "type": "Chebyshev", - "temperature-range": FlowList( - [ - get_float_or_units(rate_coeff.find("Tmin")), - get_float_or_units(rate_coeff.find("Tmax")), - ] - ), - "pressure-range": FlowList( - [ - get_float_or_units(rate_coeff.find("Pmin")), - get_float_or_units(rate_coeff.find("Pmax")), - ] - ), + "temperature-range": FlowList(), + "pressure-range": FlowList(), } ) + for range_tag in ["Tmin", "Tmax", "Pmin", "Pmax"]: + range_node = rate_coeff.find(range_tag) + if range_node is None: + raise LookupError( + "A Chebyshev reaction must include a {} node".format(range_tag) + ) + if range_tag.startswith("T"): + reaction_attributes["temperature-range"].append( + get_float_or_units(range_node) + ) + elif range_tag.startswith("P"): + reaction_attributes["pressure-range"].append( + get_float_or_units(range_node) + ) data_node = rate_coeff.find("floatArray") - n_p_values = int(data_node.get("degreeP")) - n_T_values = int(data_node.get("degreeT")) - data_text = list( - map(float, data_node.text.replace("\n", " ").strip().split(",")) - ) + if data_node is None: + raise LookupError("Chebyshev reaction must include a floatArray node.") + n_p_values = int(data_node.get("degreeP", 0)) + n_T_values = int(data_node.get("degreeT", 0)) + if not n_p_values or not n_T_values: + raise ValueError( + "The polynomial degree in pressure and temperature must be specified" + ) + raw_data = [float(a) for a in clean_node_text(data_node).split(",")] data = [] - for i in range(0, len(data_text), n_p_values): - data.append(FlowList(data_text[i : i + n_p_values])) + for i in range(0, len(raw_data), n_p_values): + data.append(FlowList(raw_data[i : i + n_p_values])) # NOQA: E203 if len(data) != n_T_values: raise ValueError( - "The number of rows of the data do not match the specified temperature degree." + "The number of rows of the data do not match the specified " + "temperature degree." ) reaction_attributes["data"] = data return reaction_attributes - def surface(self, rate_coeff): + def surface( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: """Process a surface reaction. Returns a dictionary with the appropriate fields set that is used to update the parent reaction entry dictionary. """ arr_node = rate_coeff.find("Arrhenius") - sticking = arr_node.get("type", "") == "stick" + if arr_node is None: + raise LookupError( + "Surface reaction requires Arrhenius node: '{}'".format(rate_coeff) + ) + sticking = arr_node.get("type") == "stick" if sticking: reaction_attributes = FlowMap( {"sticking-coefficient": self.process_arrhenius_parameters(arr_node)} @@ -1036,33 +1238,55 @@ def surface(self, rate_coeff): cov_node = arr_node.find("coverage") if cov_node is not None: cov_species = cov_node.get("species") - cov_a = get_float_or_units(cov_node.find("a")) - cov_m = get_float_or_units(cov_node.find("m")) - cov_e = get_float_or_units(cov_node.find("e")) + cov_a = cov_node.find("a") + if cov_a is None: + raise LookupError("Coverage requires a: '{}'".format(cov_node)) + cov_m = cov_node.find("m") + if cov_m is None: + raise LookupError("Coverage requires m: '{}'".format(cov_node)) + cov_e = cov_node.find("e") + if cov_e is None: + raise LookupError("Coverage requires e: '{}'".format(cov_node)) reaction_attributes["coverage-dependencies"] = { - cov_species: {"a": cov_a, "m": cov_m, "E": cov_e} + cov_species: { + "a": get_float_or_units(cov_a), + "m": get_float_or_units(cov_m), + "E": get_float_or_units(cov_e), + } } return reaction_attributes - def edge(self, rate_coeff): + def edge( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: """Process an edge reaction. Returns a dictionary with the appropriate fields set that is used to update the parent reaction entry dictionary. """ arr_node = rate_coeff.find("Arrhenius") + echem_node = rate_coeff.find("electrochem") + if echem_node is None: + raise LookupError( + "Edge reaction missing electrochem node: '{}'".format(rate_coeff) + ) + beta = echem_node.get("beta") + if beta is None: + raise LookupError( + "Beta must be specified for edge reaction: '{}'".format(echem_node) + ) reaction_attributes = BlockMap( { "rate-constant": self.process_arrhenius_parameters(arr_node), - "beta": float(rate_coeff.find("electrochem").get("beta")), + "beta": float(beta), } ) - if rate_coeff.get("type", "") == "exchangecurrentdensity": + if rate_coeff.get("type") == "exchangecurrentdensity": reaction_attributes["exchange-current-density-formulation"] = True return reaction_attributes - def arrhenius(self, rate_coeff): + def arrhenius(self, rate_coeff: etree.Element) -> Dict[str, Dict[str, float]]: """Process a standard Arrhenius-type reaction. Returns a dictionary with the appropriate fields set that is @@ -1076,29 +1300,34 @@ def arrhenius(self, rate_coeff): } ) - def process_arrhenius_parameters(self, arr_node): + def process_arrhenius_parameters( + self, arr_node: Optional[etree.Element] + ) -> Dict[str, Union[float, str]]: """Process the parameters from an Arrhenius child of a rateCoeff node.""" + if arr_node is None: + raise TypeError("The Arrhenius node must be present.") + A_node = arr_node.find("A") + b_node = arr_node.find("b") + E_node = arr_node.find("E") + if A_node is None or b_node is None or E_node is None: + raise LookupError( + "All of A, b, and E must be specified for the Arrhenius parameters." + ) return FlowMap( { - "A": get_float_or_units(arr_node.find("A")), - "b": get_float_or_units(arr_node.find("b")), - "Ea": get_float_or_units(arr_node.find("E")), + "A": get_float_or_units(A_node), + "b": get_float_or_units(b_node), + "Ea": get_float_or_units(E_node), } ) - def process_efficiencies(self, eff_node): + def process_efficiencies(self, eff_node: etree.Element) -> Dict[str, float]: """Process the efficiency information about a reaction.""" - efficiencies = FlowMap({}) - effs = eff_node.text.replace("\n", " ").strip().split() - # Is there any way to do this with a comprehension? - for eff in effs: - s, e = eff.split(":") - efficiencies[s] = float(e) - - return efficiencies + efficiencies = [eff.rsplit(":", 1) for eff in clean_node_text(eff_node).split()] + return FlowMap({s: float(e) for s, e in efficiencies}) -def convert(inpfile, outfile): +def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) ctml_tree = etree.parse(str(inpfile)).getroot() @@ -1106,8 +1335,8 @@ def convert(inpfile, outfile): # Phases phases = [] reaction_filters = [] - act_pure_params = defaultdict(list) - act_cross_params = defaultdict(list) + act_pure_params = defaultdict(list) # type: Dict[str, List[etree.Element]] + act_cross_params = defaultdict(list) # type: Dict[str, List[etree.Element]] const_density_specs = {} for phase_node in ctml_tree.iterfind("phase"): this_phase = Phase(phase_node) @@ -1117,8 +1346,8 @@ def convert(inpfile, outfile): if reaction_filter is not None: if reaction_filter.get("min") != reaction_filter.get("max"): raise ValueError("Can't handle differing reaction filter criteria") - filter_value = reaction_filter.get("min") - if filter_value != "None": + filter_value = reaction_filter.get("min", "none") + if filter_value.lower() != "none": reaction_filters.append( ( "{}-reactions".format(this_phase.phase_attribs["name"]), @@ -1145,7 +1374,10 @@ def convert(inpfile, outfile): # to the species definition in the YAML format. StoichSubstance is # the only model I know of that uses this node phase_thermo_node = phase_node.find("thermo") - if phase_thermo_node.get("model") == "StoichSubstance": + if ( + phase_thermo_node is not None + and phase_thermo_node.get("model") == "StoichSubstance" + ): den_node = phase_thermo_node.find("density") if den_node is None: den_node = phase_thermo_node.find("molar-density") @@ -1171,12 +1403,14 @@ def convert(inpfile, outfile): this_data_node_id = species_data_node.get("id", "") for species_node in species_data_node.iterfind("species"): species_name = species_node.get("name") + if species_name is None: + raise LookupError("Species '{}' must have a name.".format(species_node)) # Does it make more sense to modify the object after construction # with these equation-of-state type parameters? Right now, all of this # is done during construction. The trouble is that they come from the # phase node, which isn't passed to Species, since any species can be # present in multiple phases. - activity_params = {} + activity_params = {} # type: Dict[str, Union[str, etree.Element]] for phase_thermo, params_list in act_pure_params.items(): for params in params_list: if params.get("species") != species_name: @@ -1228,16 +1462,16 @@ def convert(inpfile, outfile): reaction_data = [] reactionData_node = ctml_tree.find("reactionData") if reactionData_node is not None: - for reaction in reactionData_node.iterfind("reaction"): - reaction_data.append(Reaction(reaction)) + for reaction_node in reactionData_node.iterfind("reaction"): + reaction_data.append(Reaction(reaction_node)) output_reactions = BlockMap() for phase_name, pattern in reaction_filters: - pattern = re.compile(pattern.replace("*", ".*")) + re_pattern = re.compile(pattern.replace("*", ".*")) hits = [] misses = [] for reaction in reaction_data: - if pattern.match(reaction.reaction_attribs.get("id", "")): + if re_pattern.match(reaction.reaction_attribs.get("id", "")): hits.append(reaction) else: misses.append(reaction) From c76891d1565b9fa58cb69ee74d4932cf822a159e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 29 Oct 2019 12:13:01 -0400 Subject: [PATCH 52/99] [ctml2yaml] Refactor species and reaction creation Create Species and Reaction objects before Phase objects. This allows the Phase constructor to modify the appropriate objects based on reaction filtering or various equation of state models where information is moving from the phase node to the Species. --- interfaces/cython/cantera/ctml2yaml.py | 887 +++++++++++++------------ 1 file changed, 471 insertions(+), 416 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 0085c81aa4e..976c5abc2fb 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -8,12 +8,11 @@ import xml.etree.ElementTree as etree from email.utils import formatdate -from collections import defaultdict from typing import Any, Dict, Union, Iterable, Optional, List try: - import ruamel_yaml as yaml + import ruamel_yaml as yaml # type: ignore except ImportError: from ruamel import yaml @@ -34,6 +33,48 @@ def FlowList(*args, **kwargs): return lst +class MissingXMLNode(LookupError): + """Error raised when a required node is missing in the XML tree.""" + + def __init__(self, message: str = "", node: Optional[etree.Element] = None): + if node is not None: + node_str = etree.tostring(node).decode("utf-8") + if message: + message += ": '" + node_str + "'" + else: + message = node_str + + super().__init__(message) + + +class MissingXMLAttribute(LookupError): + """Error raised when a required attribute is missing in the XML node.""" + + def __init__(self, message: str = "", node: Optional[etree.Element] = None): + if node is not None: + node_str = etree.tostring(node).decode("utf-8") + if message: + message += ": '" + node_str + "'" + else: + message = node_str + + super().__init__(message) + + +class MissingNodeText(LookupError): + """Error raised when the text of an XML node is missing.""" + + def __init__(self, message: str = "", node: Optional[etree.Element] = None): + if node is not None: + node_str = etree.tostring(node).decode("utf-8") + if message: + message += ": '" + node_str + "'" + else: + message = node_str + + super().__init__(message) + + # Improved float formatting requires Numpy >= 1.14 HAS_FMT_FLT_POS = hasattr(np, "format_float_positional") @@ -68,7 +109,7 @@ def represent_float(self: Any, data: Any) -> Any: def get_float_or_units(node: etree.Element) -> Union[str, float]: if node.text is None: - raise ValueError("Node '{}' must contain text".format(node)) + raise MissingNodeText("Node '{}' must contain text".format(node)) value = float(node.text.strip()) units = node.get("units") @@ -103,7 +144,11 @@ def split_species_value_string(node: etree.Element) -> Dict[str, float]: """ text = node.text if text is None: - raise ValueError("The text of the node must exist: '{}'".format(node)) + raise MissingNodeText( + "The text of the node must exist: '{}'".format( + etree.tostring(node).decode("utf-8") + ) + ) pairs = FlowMap({}) for t in text.replace("\n", " ").replace(",", " ").strip().split(): key, value = t.split(":") @@ -119,7 +164,7 @@ def clean_node_text(node: etree.Element) -> str: """Clean the text of a node.""" text = node.text if text is None: - raise ValueError("The text of the node must exist: '{}'".format(node)) + raise MissingNodeText("The text of the node must exist", node) return text.replace("\n", " ").strip() @@ -183,70 +228,101 @@ class Phase: "8": "heptane", } - def __init__(self, phase: etree.Element): + def __init__( + self, + phase: etree.Element, + species_data: Dict[str, List["Species"]], + reaction_data: Dict[str, List["Reaction"]], + ): phase_name = phase.get("id") - phase_attribs = BlockMap({"name": phase_name}) + if phase_name is None: + raise MissingXMLAttribute( + "The phase node requires an id attribute: '{}'".format(phase) + ) + self.attribs = BlockMap({"name": phase_name}) + + elem_text = phase.findtext("elementArray") + if elem_text is not None: + elements = elem_text.replace("\n", "").strip().split() + # This second check is necessary because self-closed tags + # have an empty text when checked with 'findtext' but + # have 'None' when 'find().text' is used + if elements: + self.attribs["elements"] = FlowList(elements) + + species = [] + speciesArray_nodes = phase.findall("speciesArray") + for sA_node in speciesArray_nodes: + species.append(self.get_species_array(sA_node)) + + species_skip = sA_node.find("skip") + if species_skip is not None: + element_skip = species_skip.get("element", "") + if element_skip == "undeclared": + self.attribs["skip-undeclared-elements"] = True + if species: + if len(species) == 1 and "species" in species[0]: + self.attribs.update(species[0]) + else: + self.attribs["species"] = species + phase_thermo = phase.find("thermo") if phase_thermo is None: - raise ValueError("The phase node requires a thermo node") - phase_thermo_model = phase.get("model") + raise MissingXMLNode("The phase node requires a thermo node", phase) + phase_thermo_model = phase_thermo.get("model") if phase_thermo_model is None: - raise ValueError("The thermo node requires a model") - phase_attribs["thermo"] = self._thermo_model_mapping[phase_thermo_model] - # Convert pure fluid type integer into the name + raise MissingXMLAttribute("The thermo node requires a model") + self.attribs["thermo"] = self._thermo_model_mapping[phase_thermo_model] + if phase_thermo_model == "PureFluid": pure_fluid_type = phase_thermo.get("fluid_type") if pure_fluid_type is None: - raise ValueError("PureFluid model requires the fluid_type") - phase_attribs["pure-fluid-name"] = self._pure_fluid_mapping[pure_fluid_type] + raise MissingXMLAttribute( + "PureFluid model requires the fluid_type", phase_thermo + ) + self.attribs["pure-fluid-name"] = self._pure_fluid_mapping[pure_fluid_type] elif phase_thermo_model == "HMW": activity_coefficients = phase_thermo.find("activityCoefficients") if activity_coefficients is None: - raise ValueError("HMW thermo model requires activity coefficients") - phase_attribs["activity-data"] = self.hmw_electrolyte(activity_coefficients) + raise MissingXMLNode( + "HMW thermo model requires activity coefficients", phase_thermo + ) + self.attribs["activity-data"] = self.hmw_electrolyte(activity_coefficients) elif phase_thermo_model == "DebyeHuckel": activity_coefficients = phase_thermo.find("activityCoefficients") if activity_coefficients is None: - raise ValueError("Debye Huckel thermo model requires activity") - phase_attribs["activity-data"] = self.debye_huckel(activity_coefficients) + raise MissingXMLNode( + "Debye Huckel thermo model requires activity", phase_thermo + ) + self.attribs["activity-data"] = self.debye_huckel(activity_coefficients) + elif phase_thermo_model == "StoichSubstance": + self.move_density_to_species(species, phase_thermo, species_data) + elif phase_thermo_model == "RedlichKwongMFTP": + activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is None: + raise MissingXMLNode( + "Redlich-Kwong thermo model requires activity", phase_thermo + ) + self.move_RK_coeffs_to_species(species, activity_coefficients, species_data) for node in phase_thermo: if node.tag == "site_density": - phase_attribs["site-density"] = get_float_or_units(node) + self.attribs["site-density"] = get_float_or_units(node) elif node.tag == "density": - if phase_attribs["thermo"] == "electron-cloud": - phase_attribs["density"] = get_float_or_units(node) + if self.attribs["thermo"] == "electron-cloud": + self.attribs["density"] = get_float_or_units(node) elif node.tag == "tabulatedSpecies": - phase_attribs["tabulated-species"] = node.get("name") + self.attribs["tabulated-species"] = node.get("name") elif node.tag == "tabulatedThermo": - phase_attribs["tabulated-thermo"] = self.get_tabulated_thermo(node) + self.attribs["tabulated-thermo"] = self.get_tabulated_thermo(node) elif node.tag == "chemicalPotential": - phase_attribs["chemical-potential"] = get_float_or_units(node) - - elements = phase.findtext("elementArray") - if elements is not None: - phase_attribs["elements"] = FlowList(elements.strip().split()) - - species = [] - for sA_node in phase.findall("speciesArray"): - species.append(self.get_species_array(sA_node)) - - species_skip = sA_node.find("skip") - if species_skip is not None: - element_skip = species_skip.get("element", "") - if element_skip == "undeclared": - phase_attribs["skip-undeclared-elements"] = True - if species: - if len(species) == 1 and "species" in species[0]: - phase_attribs.update(species[0]) - else: - phase_attribs["species"] = species + self.attribs["chemical-potential"] = get_float_or_units(node) transport_node = phase.find("transport") if transport_node is not None: transport_model = self._transport_model_mapping[transport_node.get("model")] if transport_model is not None: - phase_attribs["transport"] = transport_model + self.attribs["transport"] = transport_model # The phase requires both a kinetics model and a set of # reactions to include the kinetics @@ -256,45 +332,26 @@ def __init__(self, phase: etree.Element): kinetics_model = self._kinetics_model_mapping[ kinetics_node.get("model", "") ] - reactionArray_nodes = phase.findall("reactionArray") reactions = [] - for rA_node in reactionArray_nodes: - filter = rA_node.find("include") - if filter is not None: - if filter.get("min", "none").lower() == "none": - continue - else: - has_filter = True - else: - has_filter = False - skip_node = rA_node.find("skip") - if skip_node is not None and skip_node.get("third_bodies") is not None: - if skip_node.get("third_bodies") == "undeclared": - phase_attribs["skip-undeclared-third-bodies"] = True - else: - raise ValueError( - "Undefined value '{}' for third_bodies skip " - "parameter".format(skip_node.get("third_bodies")) - ) - this_reactions = self.get_reaction_array(rA_node) - if has_filter: - section_name = "{}-reactions".format(phase_name) - reactions.append({section_name: this_reactions["reactions"]}) - else: - reactions.append(this_reactions) - # The reactions list may be empty, don't include it if it is - if reactions: - phase_attribs["kinetics"] = kinetics_model - internal_source = "reactions" in reactions[0] - # If there is one reactionArray node, no reaction filter - # has been specified, and the reactions are all from - # within this file, the output should be reactions: all, + for rA_node in phase.iterfind("reactionArray"): + # If the reaction list associated with the datasrc for this + # reactionArray is empty, don't do anything. + datasrc = rA_node.get("datasrc", "") + if datasrc.startswith("#") and not reaction_data[datasrc[1:]]: + continue + reactions.append(self.get_reaction_array(rA_node, reaction_data)) + # The reactions list may be empty, don't include any kinetics stuff + # if it is + if reactions and kinetics_model is not None: + self.attribs["kinetics"] = kinetics_model + # If there is one reactionArray and the datasrc was reaction_data + # (munged to just reactions) the output should be 'reactions: all', # so we use update. Otherwise, there needs to be a list # of mappings. - if len(reactions) == 1 and not has_filter and internal_source: - phase_attribs.update(reactions[0]) + if len(reactions) == 1 and "reactions" in reactions[0]: + self.attribs.update(reactions[0]) else: - phase_attribs["reactions"] = reactions + self.attribs["reactions"] = reactions state_node = phase.find("state") if state_node is not None: @@ -314,65 +371,215 @@ def __init__(self, phase: etree.Element): phase_state[property_name] = value if phase_state: - phase_attribs["state"] = phase_state + self.attribs["state"] = phase_state std_conc_node = phase.find("standardConc") if std_conc_node is not None: - phase_attribs["standard-concentration-basis"] = std_conc_node.get("model") + self.attribs["standard-concentration-basis"] = std_conc_node.get("model") + + def move_RK_coeffs_to_species( + self, + this_phase_species: List[Dict[str, Iterable[str]]], + activity_coeffs: etree.Element, + species_data: Dict[str, List["Species"]], + ) -> None: + """Move the Redlich-Kwong activity coefficient data from phase to species. + + This modifies the species objects in-place in the species_data object. + """ + all_pure_params = activity_coeffs.findall("pureFluidParameters") + all_species_eos = {} + for pure_param in all_pure_params: + eq_of_state = BlockMap({"model": "Redlich-Kwong"}) + pure_species = pure_param.get("species") + pure_a_node = pure_param.find("a_coeff") + if pure_a_node is None: + raise MissingXMLNode( + "The pure fluid coefficients requires the a_coeff node.", pure_param + ) + + pure_a_units = pure_a_node.get("units") + if pure_a_units is not None: + pure_a_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_a_units) + pure_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_a_units) + + eq_of_state["a"] = FlowList() + pure_a = [float(a) for a in clean_node_text(pure_a_node).split(",")] + eq_of_state["a"].append( + "{} {}".format(float2string(pure_a[0]), pure_a_units + "*K^0.5") + ) + eq_of_state["a"].append( + "{} {}".format(float2string(pure_a[1]), pure_a_units + "/K^0.5") + ) + else: + eq_of_state["a"] = FlowList( + float(a) for a in clean_node_text(pure_a_node).split(",") + ) + + pure_b_node = pure_param.find("b_coeff") + if pure_b_node is None: + raise MissingXMLNode( + "The pure fluid coefficients requires the b_coeff node.", pure_param + ) + eq_of_state["b"] = get_float_or_units(pure_b_node) + all_species_eos[pure_species] = eq_of_state + + all_cross_params = activity_coeffs.findall("crossFluidParameters") + for cross_param in all_cross_params: + species_1_name = cross_param.get("species1") + species_2_name = cross_param.get("species2") + if species_1_name is None or species_2_name is None: + raise MissingXMLAttribute( + "The cross-fluid coefficients requires 2 species names", cross_param + ) + species_1 = all_species_eos[species_1_name] + species_2 = all_species_eos[species_2_name] + cross_a_node = cross_param.find("a_coeff") + if cross_a_node is None: + raise MissingXMLNode( + "The cross-fluid coefficients requires the a_coeff node", + cross_param, + ) - self.phase_attribs = phase_attribs + cross_a_unit = cross_a_node.get("units") + if cross_a_unit is not None: + cross_a_unit = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", cross_a_unit) + cross_a_unit = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", cross_a_unit) + + cross_a = [float(a) for a in clean_node_text(cross_a_node).split(",")] + eq_params = [] + eq_params.append( + "{} {}".format(float2string(cross_a[0]), cross_a_unit + "*K^0.5") + ) + eq_params.append( + "{} {}".format(float2string(cross_a[1]), cross_a_unit + "/K^0.5") + ) + species_1["binary-a"] = {species_2_name: FlowList(eq_params)} + species_2["binary-a"] = {species_1_name: FlowList(eq_params)} + else: + species_1["binary-a"] = { + species_2_name: FlowList( + float(a) for a in clean_node_text(cross_a_node).split(",") + ) + } + species_2["binary-a"] = { + species_1_name: FlowList( + float(a) for a in clean_node_text(cross_a_node).split(",") + ) + } + + for node in this_phase_species: + for datasrc, species_names in node.items(): + if datasrc == "species": + datasrc = "species_data" + species = species_data.get(datasrc) + if species is None: + continue + for spec in species: + if spec.species_attribs["name"] in species_names: + spec.species_attribs["equation-of-state"] = all_species_eos[ + spec.species_attribs["name"] + ] + + def move_density_to_species( + self, + this_phase_species: List[Dict[str, Iterable[str]]], + phase_thermo: etree.Element, + species_data: Dict[str, List["Species"]], + ) -> None: + """Move the phase density information into each species definition. + + This modifies the species objects in-place in the species_data object. + """ + den_node = phase_thermo.find("density") + const_prop = "density" + if den_node is None: + den_node = phase_thermo.find("molarDensity") + const_prop = "molar-density" + if den_node is None: + den_node = phase_thermo.find("molarVolume") + const_prop = "molar-volume" + if den_node is None: + raise MissingXMLNode("Thermo node is missing a density node.", phase_thermo) + const_prop = { + "density": "density", + "molarDensity": "molar-density", + "molarVolume": "molar-volume", + }[den_node.tag] + equation_of_state = { + "model": "constant-volume", + const_prop: get_float_or_units(den_node), + } + for node in this_phase_species: + for datasrc, species_names in node.items(): + if datasrc == "species": + datasrc = "species_data" + species = species_data.get(datasrc) + if species is None: + continue + for spec in species: + if spec.species_attribs["name"] in species_names: + spec.species_attribs["equation-of-state"] = equation_of_state def get_species_array( self, speciesArray_node: etree.Element ) -> Dict[str, Iterable[str]]: """Process a list of species from a speciesArray node.""" - if speciesArray_node.text is None: - raise ValueError( - "The speciesArray node must have text: '{}'".format(speciesArray_node) - ) - species_list = FlowList( - clean_node_text(speciesArray_node).split() - ) + species_list = FlowList(clean_node_text(speciesArray_node).split()) datasrc = speciesArray_node.get("datasrc", "") if datasrc == "#species_data": - return {"species": species_list} + new_datasrc = "species" elif datasrc.startswith("#"): - return {datasrc[1:]: species_list} + new_datasrc = datasrc[1:] else: filename, location = datasrc.split("#", 1) name = str(Path(filename).with_suffix(".yaml")) if location == "species_data": location = "species" - datasrc = "{}/{}".format(name, location) - return {datasrc: species_list} + new_datasrc = "{}/{}".format(name, location) + + return {new_datasrc: species_list} - def get_reaction_array(self, reactionArray_node: etree.Element) -> Dict[str, str]: + def get_reaction_array( + self, + reactionArray_node: etree.Element, + reaction_data: Dict[str, List["Reaction"]], + ) -> Dict[str, str]: """Process reactions from a reactionArray node in a phase definition.""" datasrc = reactionArray_node.get("datasrc", "") - has_filter = reactionArray_node.find("include") is not None + if not datasrc: + raise MissingXMLAttribute("The reactionArray must include a datasrc") + + filter_node = reactionArray_node.find("include") + if filter_node is not None: + filter_text = filter_node.get("min", "none") + if filter_text != filter_node.get("max"): + raise ValueError("Cannot handle differing reaction filter criteria") + else: + filter_text = "none" + skip_node = reactionArray_node.find("skip") if skip_node is not None: - species_skip = skip_node.get("species") - if species_skip is not None and species_skip == "undeclared": + # "undeclared" is the only allowed option for third_bodies and species + # here, so ignore other options + if skip_node.get("third_bodies") == "undeclared": + self.attribs["skip-undeclared-third-bodies"] = True + if skip_node.get("species") == "undeclared": reaction_option = "declared-species" else: - raise ValueError( - "Unknown value in species skip parameter: " - "'{}'".format(species_skip) - ) + reaction_option = "all" else: reaction_option = "all" if not datasrc.startswith("#"): - if has_filter: + if filter_text.lower() != "none": raise ValueError( - "Filtering reaction lists is not possible with external data " - "sources" + "Filtering reactions is not allowed with an external datasrc" ) if skip_node is None: - raise ValueError( - "Must include skip node for external data sources: " - "'{}'".format(datasrc) + raise MissingXMLNode( + "Must include skip node for external data sources", + reactionArray_node, ) # This code does not handle the # character in a filename filename, location = datasrc.split("#", 1) @@ -381,7 +588,10 @@ def get_reaction_array(self, reactionArray_node: etree.Element) -> Dict[str, str location = "reactions" datasrc = "{}/{}".format(name, location) elif datasrc == "#reaction_data": - datasrc = "reactions" + if filter_text.lower() == "none": + datasrc = "reactions" + else: + datasrc = self.filter_reaction_list(datasrc, filter_text, reaction_data) else: raise ValueError( "Unable to parse the reaction data source: '{}'".format(datasrc) @@ -389,26 +599,65 @@ def get_reaction_array(self, reactionArray_node: etree.Element) -> Dict[str, str return {datasrc: reaction_option} + def filter_reaction_list( + self, datasrc: str, filter_text: str, reaction_data: Dict[str, List["Reaction"]] + ) -> str: + """Filter the reaction_data list to only include specified reactions. + + Returns a string that should be used as the data source in the YAML file. + """ + all_reactions = reaction_data[datasrc.lstrip("#")] + hits = [] + misses = [] + re_pattern = re.compile(filter_text.replace("*", ".*")) + for reaction in all_reactions: + reaction_id = reaction.reaction_attribs.get("id") + if re_pattern.match(reaction_id): + hits.append(reaction) + else: + misses.append(reaction) + + if not hits: + raise ValueError( + "The filter text '{}' resulted in an empty set of " + "reactions".format(filter_text) + ) + else: + new_datasrc = self.attribs["name"] + "-reactions" + reaction_data[new_datasrc] = hits + # If misses is not empty, replace the old list of reactions with + # a new list where filtered out reactions are removed. If there + # are no remaining reactions, remove the entry for this datasrc + # from the dictionary + if misses: + reaction_data[datasrc] = misses + else: + del reaction_data[datasrc] + + return new_datasrc + def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str]: tab_thermo = BlockMap() enthalpy_node = tab_thermo_node.find("enthalpy") if enthalpy_node is None or enthalpy_node.text is None: - raise LookupError( - "Tabulated thermo must have an enthalpy node " - "with text: '{}'".format(tab_thermo_node) + raise MissingXMLNode( + "Tabulated thermo must have an enthalpy node with text", tab_thermo_node ) enthalpy_units = enthalpy_node.get("units", "").split("/") if not enthalpy_units: - raise ValueError("The units of tabulated enthalpy must be specified") + raise MissingXMLAttribute( + "The units of tabulated enthalpy must be specified", enthalpy_node + ) entropy_node = tab_thermo_node.find("entropy") if entropy_node is None or entropy_node.text is None: - raise LookupError( - "Tabulated thermo must have an entropy node " - "with text: '{}'".format(tab_thermo_node) + raise MissingXMLNode( + "Tabulated thermo must have an entropy node with text", tab_thermo_node ) entropy_units = entropy_node.get("units", "").split("/") if not entropy_units: - raise ValueError("The units of tabulated entropy must be specified") + raise MissingXMLAttribute( + "The units of tabulated entropy must be specified", entropy_node + ) if enthalpy_units[:2] != entropy_units[:2]: raise ValueError("Tabulated thermo must have the same units.") tab_thermo["units"] = FlowMap( @@ -430,9 +679,9 @@ def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str] ) mole_fraction_node = tab_thermo_node.find("moleFraction") if mole_fraction_node is None or mole_fraction_node.text is None: - raise LookupError( - "Tabulated thermo must have a mole fraction node " - "with text: '{}'".format(tab_thermo_node) + raise MissingXMLNode( + "Tabulated thermo must have a mole fraction node with text", + tab_thermo_node, ) mole_fraction = clean_node_text(mole_fraction_node).split(",") tab_thermo["mole-fractions"] = FlowList(map(float, mole_fraction)) @@ -449,9 +698,8 @@ def hmw_electrolyte(self, activity_node: etree.Element): activity_data = BlockMap({"temperature-model": activity_node.get("TempModel")}) A_Debye_node = activity_node.find("A_Debye") if A_Debye_node is None: - raise LookupError( - "Activity coefficients for HMW must have " - "A_Debye: '{}'".format(activity_node) + raise MissingXMLNode( + "Activity coefficients for HMW must have A_Debye", activity_node ) if A_Debye_node.get("model") == "water": activity_data["A_Debye"] = "variable" @@ -459,7 +707,7 @@ def hmw_electrolyte(self, activity_node: etree.Element): # Assume the units are kg^0.5/gmol^0.5. Apparently, # this is not handled in the same way as other units? if A_Debye_node.text is None: - raise ValueError("The A_Debye node must have a text value") + raise MissingNodeText("The A_Debye node must have a text value") activity_data["A_Debye"] = A_Debye_node.text.strip() + " kg^0.5/gmol^0.5" interactions = [] @@ -477,7 +725,9 @@ def hmw_electrolyte(self, activity_node: etree.Element): this_interaction = {"species": FlowList([i[1] for i in inter_node.items()])} for param_node in inter_node: if param_node.text is None: - raise ValueError("The interaction nodes must have text values.") + raise MissingNodeText( + "The interaction nodes must have text values.", param_node + ) data = clean_node_text(param_node).split(",") param_name = param_node.tag.lower() if param_name == "cphi": @@ -502,7 +752,9 @@ def debye_huckel(self, activity_node: etree.Element): } activity_model = activity_node.get("model") if activity_model is None: - raise ValueError("The Debye Huckel model must be specified") + raise MissingXMLAttribute( + "The Debye Huckel model must be specified", activity_node + ) activity_data = BlockMap({"model": model_map[activity_model.lower()]}) A_Debye = activity_node.findtext("A_Debye") if A_Debye is not None: @@ -552,7 +804,7 @@ def debye_huckel(self, activity_node: etree.Element): @classmethod def to_yaml(cls, representer, data): - return representer.represent_dict(data.phase_attribs) + return representer.represent_dict(data.attribs) class SpeciesThermo: @@ -573,7 +825,7 @@ def Shomate(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: for node in model_nodes: Tmin = float(node.get("Tmin", 0)) Tmax = float(node.get("Tmax", 0)) - if not Tmin or Tmax: + if not Tmin or not Tmax: raise ValueError("Tmin and Tmax must both be specified") temperature_ranges.add(float(Tmin)) temperature_ranges.add(float(Tmax)) @@ -600,7 +852,7 @@ def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: for node in model_nodes: Tmin = float(node.get("Tmin", 0)) Tmax = float(node.get("Tmax", 0)) - if not Tmin or Tmax: + if not Tmin or not Tmax: raise ValueError("Tmin and Tmax must both be specified") temperature_ranges.add(float(Tmin)) temperature_ranges.add(float(Tmax)) @@ -627,7 +879,7 @@ def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: for node in model_nodes: Tmin = float(node.get("Tmin", 0)) Tmax = float(node.get("Tmax", 0)) - if not Tmin or Tmax: + if not Tmin or not Tmax: raise ValueError("Tmin and Tmax must both be specified") temperature_ranges.add(float(Tmin)) temperature_ranges.add(float(Tmax)) @@ -753,7 +1005,7 @@ class Species: "nonpolarNeutral": "nonpolar-neutral", } - def __init__(self, species_node: etree.Element, **kwargs): + def __init__(self, species_node: etree.Element): species_attribs = BlockMap() species_name = species_node.get("name") if species_name is None: @@ -762,7 +1014,7 @@ def __init__(self, species_node: etree.Element, **kwargs): ) species_attribs["name"] = species_name atom_array = species_node.find("atomArray") - if atom_array is not None: + if atom_array is not None and atom_array.text is not None: species_attribs["composition"] = split_species_value_string(atom_array) else: species_attribs["composition"] = {} @@ -774,49 +1026,25 @@ def __init__(self, species_node: etree.Element, **kwargs): if thermo is not None: species_attribs["thermo"] = SpeciesThermo(thermo) - activity_parameters = kwargs.get("activity_parameters", False) - if activity_parameters: - species_attribs["equation-of-state"] = self.process_act_coeff( - species_name, activity_parameters - ) - - const_dens = kwargs.get("const_dens") - if const_dens is not None: - const_prop = { - "density": "density", - "molarDensity": "molar-density", - "molarVolume": "molar-volume", - }[const_dens.tag] - species_attribs["equation-of-state"] = { - "model": "constant-volume", - const_prop: get_float_or_units(const_dens), - } - transport = species_node.find("transport") if transport is not None: species_attribs["transport"] = SpeciesTransport(transport) std_state = species_node.find("standardState") if std_state is not None: - if const_dens is not None: - raise ValueError( - "The standard state of the species '{}' was specified " - "along with stuff from the phase.".format(species_name) - ) std_state_model = std_state.get("model") - if std_state_model not in self._standard_state_model_mapping: - raise ValueError( - "Unknown standard state model: '{}'".format(std_state_model) - ) + if std_state_model is None: + raise MissingXMLAttribute("Unknown standard state model", std_state) eqn_of_state = { "model": self._standard_state_model_mapping[std_state_model] } # type: Dict[str, Union[str, float]] if std_state_model == "constant_incompressible": molar_volume_node = std_state.find("molarVolume") if molar_volume_node is None: - raise LookupError( + raise MissingXMLNode( "If the standard state model is constant_incompressible, it " - "must include a molarVolume node" + "must include a molarVolume node", + std_state, ) eqn_of_state["molar-volume"] = get_float_or_units(molar_volume_node) species_attribs["equation-of-state"] = eqn_of_state @@ -832,87 +1060,6 @@ def __init__(self, species_node: etree.Element, **kwargs): self.species_attribs = species_attribs - def process_act_coeff( - self, species_name: str, activity_coefficients: Dict[str, Any] - ): - """If a species has activity coefficients, create an equation-of-state mapping. - - This appears to only be necessary for Redlich-Kwong phase thermo model. - """ - eq_of_state = BlockMap({"model": activity_coefficients["model"]}) - pure_params = activity_coefficients["pure_params"] # type: etree.Element - pure_a_node = pure_params.find("a_coeff") - if pure_a_node is None: - raise LookupError("The pure fluid coefficients requires the a_coeff node.") - - pure_a_units = pure_a_node.get("units") - if pure_a_units is not None: - pure_a_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_a_units) - pure_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_a_units) - - eq_of_state["a"] = FlowList() - pure_a = [float(a) for a in clean_node_text(pure_a_node).split(",")] - eq_of_state["a"].append( - "{} {}".format(float2string(pure_a[0]), pure_a_units + "*K^0.5") - ) - eq_of_state["a"].append( - "{} {}".format(float2string(pure_a[1]), pure_a_units + "/K^0.5") - ) - else: - eq_of_state["a"] = FlowList( - float(a) for a in clean_node_text(pure_a_node).split(",") - ) - - pure_b_node = pure_params.find("b_coeff") - if pure_b_node is None: - raise LookupError("The pure fluid coefficients requires the b_coeff node.") - pure_b = get_float_or_units(pure_b_node) - - eq_of_state["a"] = FlowList(pure_a) - eq_of_state["b"] = pure_b - - cross_params = activity_coefficients.get( - "cross_params" - ) # type: Optional[etree.Element] - if cross_params is not None: - related_species = [ - cross_params.get("species1"), - cross_params.get("species2"), - ] - if species_name == related_species[0]: - other_species = related_species[1] - else: - other_species = related_species[0] - - cross_a_node = cross_params.find("a_coeff") - if cross_a_node is None: - raise LookupError( - "The cross-fluid coefficients requires the a_coeff node" - ) - - cross_a_unit = cross_a_node.get("units") - if cross_a_unit is not None: - cross_a_unit = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", cross_a_unit) - cross_a_unit = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", cross_a_unit) - - cross_a = [float(a) for a in clean_node_text(cross_a_node).split(",")] - eq_params = [] - eq_params.append( - "{} {}".format(float2string(cross_a[0]), cross_a_unit + "*K^0.5") - ) - eq_params.append( - "{} {}".format(float2string(cross_a[1]), cross_a_unit + "/K^0.5") - ) - eq_of_state["binary-a"] = {other_species: FlowList(eq_params)} - else: - eq_of_state["binary-a"] = { - other_species: FlowList( - float(a) for a in clean_node_text(cross_a_node).split(",") - ) - } - - return eq_of_state - @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.species_attribs) @@ -1012,9 +1159,13 @@ def __init__(self, reaction: etree.Element): for order_node in reaction.iterfind("order"): species = order_node.get("species") if species is None: - raise LookupError("A reaction order node must have a species") + raise MissingXMLAttribute( + "A reaction order node must have a species", order_node + ) if order_node.text is None: - raise ValueError("A reaction order node must have a text value") + raise MissingNodeText( + "A reaction order node must have a text value", order_node + ) order = float(order_node.text) if not np.isclose(reactants[species], order): orders[species] = order @@ -1327,188 +1478,92 @@ def process_efficiencies(self, eff_node: etree.Element) -> Dict[str, float]: return FlowMap({s: float(e) for s, e in efficiencies}) -def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): - """Convert an input CTML file to a YAML file.""" - inpfile = Path(inpfile) - ctml_tree = etree.parse(str(inpfile)).getroot() - - # Phases - phases = [] - reaction_filters = [] - act_pure_params = defaultdict(list) # type: Dict[str, List[etree.Element]] - act_cross_params = defaultdict(list) # type: Dict[str, List[etree.Element]] - const_density_specs = {} - for phase_node in ctml_tree.iterfind("phase"): - this_phase = Phase(phase_node) - phases.append(this_phase) - - reaction_filter = phase_node.find("./reactionArray/include") - if reaction_filter is not None: - if reaction_filter.get("min") != reaction_filter.get("max"): - raise ValueError("Can't handle differing reaction filter criteria") - filter_value = reaction_filter.get("min", "none") - if filter_value.lower() != "none": - reaction_filters.append( - ( - "{}-reactions".format(this_phase.phase_attribs["name"]), - filter_value, - ) - ) - # Collect all of the activityCoefficients nodes from all of the phase - # definitions. This allows us to check that each species has only one - # definition of pure fluid parameters. This check is necessary because - # for Redlich-Kwong, activity coefficient data is moving from the phase to - # the species definition - this_phase_thermo = this_phase.phase_attribs["thermo"] - if this_phase_thermo == "Redlich-Kwong": - ac_coeff_node = phase_node.find("./thermo/activityCoefficients") - if ac_coeff_node is not None: - act_pure_params[this_phase_thermo].extend( - list(ac_coeff_node.iterfind("pureFluidParameters")) - ) - act_cross_params[this_phase_thermo].extend( - list(ac_coeff_node.iterfind("crossFluidParameters")) - ) - - # The density associated with the phase in XML has been moved - # to the species definition in the YAML format. StoichSubstance is - # the only model I know of that uses this node - phase_thermo_node = phase_node.find("thermo") - if ( - phase_thermo_node is not None - and phase_thermo_node.get("model") == "StoichSubstance" - ): - den_node = phase_thermo_node.find("density") - if den_node is None: - den_node = phase_thermo_node.find("molar-density") - if den_node is None: - den_node = phase_thermo_node.find("molar-volume") - if den_node is None: +def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Species]]: + """Take a speciesData node and return a dictionary of Species objects.""" + species = {} # type: Dict[str, List[Species]] + for species_data_node in ctml_tree.iterfind("speciesData"): + this_data_node_id = species_data_node.get("id", "") + for key in species.keys(): + if key.startswith(this_data_node_id): raise ValueError( - "Phase node '{}' is missing a density node.".format( - this_phase.phase_attribs["name"] - ) + "Duplicate speciesData id found: '{}'".format(this_data_node_id) ) - for spec_or_dict in this_phase.phase_attribs["species"]: - if isinstance(spec_or_dict, str): - const_density_specs[spec_or_dict] = den_node - else: - for spec in list(spec_or_dict.values())[0]: - const_density_specs[spec] = den_node - - # Species - species_data = [] - output_species = BlockMap() - for species_data_node in ctml_tree.findall("speciesData"): - this_data_node_id = species_data_node.get("id", "") + this_node_species = [] # type: List[Species] for species_node in species_data_node.iterfind("species"): - species_name = species_node.get("name") - if species_name is None: - raise LookupError("Species '{}' must have a name.".format(species_node)) - # Does it make more sense to modify the object after construction - # with these equation-of-state type parameters? Right now, all of this - # is done during construction. The trouble is that they come from the - # phase node, which isn't passed to Species, since any species can be - # present in multiple phases. - activity_params = {} # type: Dict[str, Union[str, etree.Element]] - for phase_thermo, params_list in act_pure_params.items(): - for params in params_list: - if params.get("species") != species_name: - continue - if activity_params: - raise ValueError( - "Multiple sets of pureFluidParameters found for species " - "'{}'".format(species_name) + this_species = Species(species_node) + for s in this_node_species: + if this_species.species_attribs["name"] == s.species_attribs["name"]: + raise ValueError( + "Duplicate specification of species '{}' in node '{}'".format( + this_species.species_attribs["name"], this_data_node_id ) - activity_params["model"] = phase_thermo - activity_params["pure_params"] = params - - for phase_thermo, params_list in act_cross_params.items(): - for params in params_list: - related_species = [params.get("species1"), params.get("species2")] - if species_name in related_species: - if phase_thermo != activity_params["model"]: - raise ValueError( - "crossFluidParameters found for phase thermo '{}' with " - "pureFluidParameters found for phase thermo '{}' " - "for species '{}'".format( - phase_thermo, activity_params["model"], species_name - ) - ) - activity_params["cross_params"] = params - - const_dens_params = const_density_specs.get(species_name) - if activity_params: - this_species = Species( - species_node, activity_parameters=activity_params - ) - elif const_dens_params is not None: - this_species = Species(species_node, const_dens=const_dens_params) - else: - this_species = Species(species_node) + ) + this_node_species.append(this_species) + species[this_data_node_id] = this_node_species - species_data.append(this_species) + return species - if this_data_node_id == "species_data": - output_species["species"] = species_data - output_species.yaml_set_comment_before_after_key("species", before="\n") - else: - output_species[this_data_node_id] = species_data - output_species.yaml_set_comment_before_after_key( - this_data_node_id, before="\n" - ) - # Reactions - reaction_data = [] - reactionData_node = ctml_tree.find("reactionData") - if reactionData_node is not None: +def create_reactions_from_data_node( + ctml_tree: etree.Element +) -> Dict[str, List[Reaction]]: + """Take a reactionData node and return a dictionary of Reaction objects.""" + reactions = {} # type: Dict[str, List[Reaction]] + for reactionData_node in ctml_tree.iterfind("reactionData"): + this_data_node_id = reactionData_node.get("id", "") + for key in reactions.keys(): + if key.startswith(this_data_node_id): + raise ValueError( + "Duplicate reactionData id found: '{}'".format(this_data_node_id) + ) + this_node_reactions = [] # type: List[Reaction] for reaction_node in reactionData_node.iterfind("reaction"): - reaction_data.append(Reaction(reaction_node)) + this_node_reactions.append(Reaction(reaction_node)) + reactions[this_data_node_id] = this_node_reactions - output_reactions = BlockMap() - for phase_name, pattern in reaction_filters: - re_pattern = re.compile(pattern.replace("*", ".*")) - hits = [] - misses = [] - for reaction in reaction_data: - if re_pattern.match(reaction.reaction_attribs.get("id", "")): - hits.append(reaction) - else: - misses.append(reaction) - reaction_data = misses - if hits: - output_reactions[phase_name] = hits - output_reactions.yaml_set_comment_before_after_key(phase_name, before="\n") - if reaction_data: - output_reactions["reactions"] = reaction_data - output_reactions.yaml_set_comment_before_after_key("reactions", before="\n") - - # If there are no reactions to put into the local file, then we need to delete - # the sections of the phase entry that specify reactions are present in the - # local file. - if not output_reactions: - for this_phase in phases: - phase_reactions = this_phase.phase_attribs.get("reactions", "") - if phase_reactions == "all": - del this_phase.phase_attribs["reactions"] - del this_phase.phase_attribs["kinetics"] - elif isinstance(phase_reactions, list): - sources_to_remove = [] - for i, reac_source in enumerate(phase_reactions): - # reac_source is a dictionary. If reactions is the - # key in that dictionary, the source is from the local - # file and should be removed because there are no - # reactions listed in the local file. - if "reactions" in reac_source: - sources_to_remove.append(i) - for i in sources_to_remove: - del phase_reactions[i] - # If there are no more reaction sources in the list, - # delete the reactions and kinetics entries from the - # phase object - if len(phase_reactions) == 0: - del this_phase.phase_attribs["reactions"] - del this_phase.phase_attribs["kinetics"] + return reactions + + +def create_phases_from_data_node( + ctml_tree: etree.Element, + species_data: Dict[str, List[Species]], + reaction_data: Dict[str, List[Reaction]], +) -> List[Phase]: + """Take a phase node and return a phase object.""" + return [ + Phase(node, species_data, reaction_data) for node in ctml_tree.iterfind("phase") + ] + + +def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): + """Convert an input CTML file to a YAML file.""" + inpfile = Path(inpfile) + ctml_tree = etree.parse(str(inpfile)).getroot() + + species_data = create_species_from_data_node(ctml_tree) + reaction_data = create_reactions_from_data_node(ctml_tree) + phases = create_phases_from_data_node(ctml_tree, species_data, reaction_data) + + # This should be done after phase processing + output_species = BlockMap({}) + for species_node_id, species_list in species_data.items(): + if not species_list: + continue + if species_node_id == "species_data": + species_node_id = "species" + output_species[species_node_id] = species_list + output_species.yaml_set_comment_before_after_key(species_node_id, before="\n") + + output_reactions = BlockMap({}) + for reaction_node_id, reaction_list in reaction_data.items(): + if not reaction_list: + continue + if reaction_node_id == "reaction_data": + reaction_node_id = "reactions" + output_reactions[reaction_node_id] = reaction_list + output_reactions.yaml_set_comment_before_after_key( + reaction_node_id, before="\n" + ) output_phases = BlockMap({"phases": phases}) output_phases.yaml_set_comment_before_after_key("phases", before="\n") From a3884026eb8932802ca3e59a2c096e1ee04f133e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 29 Oct 2019 20:53:39 -0400 Subject: [PATCH 53/99] [ctml2yaml] Use the fancy new error classes Use error classes that better describe what is missing and print the actual node instead of its memory address --- interfaces/cython/cantera/ctml2yaml.py | 95 +++++++++++++++----------- 1 file changed, 55 insertions(+), 40 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 976c5abc2fb..6eecd8e56be 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -903,8 +903,8 @@ def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: thermo_attribs = BlockMap({"model": "constant-cp"}) const_cp_node = thermo.find("const_cp") if const_cp_node is None: - raise LookupError( - "The thermo node must constain a const_cp node: '{}'".format(thermo) + raise MissingXMLNode( + "The thermo node must constain a const_cp node", thermo ) for node in const_cp_node: tag = node.tag @@ -921,15 +921,16 @@ def Mu0( thermo_attribs = BlockMap({"model": "piecewise-Gibbs"}) Mu0_node = thermo.find("Mu0") if Mu0_node is None: - raise LookupError( - "The thermo entry must contain a Mu0 node: '{}'".format(thermo) + raise MissingXMLNode("The thermo entry must contain a Mu0 node", thermo) + ref_pressure = Mu0_node.get("Pref") + if ref_pressure is None: + raise MissingXMLAttribute( + "Reference pressure for piecewise Gibbs species thermo", Mu0_node ) - thermo_attribs["reference-pressure"] = float(Mu0_node.get("Pref")) + thermo_attribs["reference-pressure"] = float(ref_pressure) H298_node = Mu0_node.find("H298") if H298_node is None: - raise LookupError( - "The Mu0 entry must contain an H298 node: '{}'".format(Mu0_node) - ) + raise MissingXMLNode("The Mu0 entry must contain an H298 node", Mu0_node) thermo_attribs["h0"] = get_float_or_units(H298_node) for float_node in Mu0_node.iterfind("floatArray"): title = float_node.get("title") @@ -938,8 +939,6 @@ def Mu0( if dimensions == "Dimensionless": thermo_attribs["dimensionless"] = True dimensions = "" - # I don't like doing this, but if we want to continue supporting - # Python 3.5, it is the cleanest way to add the type hint values = [] # type: Union[Iterable[float], Iterable[str]] values = map(float, clean_node_text(float_node).split(",")) if dimensions: @@ -1009,8 +1008,8 @@ def __init__(self, species_node: etree.Element): species_attribs = BlockMap() species_name = species_node.get("name") if species_name is None: - raise LookupError( - "The species name must be specified: '{}'".format(species_node) + raise MissingXMLAttribute( + "The species name must be specified", species_node ) species_attribs["name"] = species_name atom_array = species_node.find("atomArray") @@ -1088,7 +1087,7 @@ def __init__(self, reaction: etree.Element): reaction_type = reaction.get("type", "arrhenius") rate_coeff = reaction.find("rateCoeff") if rate_coeff is None: - raise LookupError("The reaction must have a rateCoeff node.") + raise MissingXMLNode("The reaction must have a rateCoeff node.", reaction) if reaction_type not in [ "arrhenius", "threeBody", @@ -1107,7 +1106,9 @@ def __init__(self, reaction: etree.Element): elif reaction_type == "falloff": falloff_node = rate_coeff.find("falloff") if falloff_node is None: - raise LookupError("Falloff reaction types must have a falloff node.") + raise MissingXMLNode( + "Falloff reaction types must have a falloff node.", rate_coeff + ) falloff_type = falloff_node.get("type") if falloff_type not in ["Lindemann", "Troe"]: raise TypeError( @@ -1120,7 +1121,9 @@ def __init__(self, reaction: etree.Element): elif reaction_type == "chemAct": falloff_node = rate_coeff.find("falloff") if falloff_node is None: - raise LookupError("chemAct reaction types must have a falloff node.") + raise MissingXMLNode( + "chemAct reaction types must have a falloff node.", rate_coeff + ) falloff_type = falloff_node.get("type") if falloff_type != "Troe": raise TypeError( @@ -1134,9 +1137,7 @@ def __init__(self, reaction: etree.Element): reaction_equation = reaction.findtext("equation") if reaction_equation is None: - raise LookupError( - "The reaction '{}' must have an equation".format(reaction) - ) + raise MissingNodeText("The reaction must have an equation", reaction) # This has to replace the reaction direction symbols separately because # species names can have [ or ] in them @@ -1148,7 +1149,9 @@ def __init__(self, reaction: etree.Element): reactants_node = reaction.find("reactants") if reactants_node is None: - raise LookupError("The reactants must be present in the reaction") + raise MissingXMLNode( + "The reactants must be present in the reaction", reaction + ) reactants = split_species_value_string(reactants_node) # products = { # a.split(":")[0]: float(a.split(":")[1]) @@ -1241,7 +1244,9 @@ def troe( troe_node = rate_coeff.find("falloff") if troe_node is None: - raise LookupError("Troe reaction types must include a falloff node") + raise MissingXMLNode( + "Troe reaction types must include a falloff node", rate_coeff + ) troe_params = clean_node_text(troe_node).split() troe_names = ["A", "T3", "T1", "T2"] reaction_attribs["Troe"] = FlowMap() @@ -1279,7 +1284,9 @@ def chemact( troe_node = rate_coeff.find("falloff") if troe_node is None: - raise LookupError("Troe reaction types must include a falloff node") + raise MissingXMLNode( + "Troe reaction types must include a falloff node", rate_coeff + ) troe_params = clean_node_text(troe_node).split() troe_names = ["A", "T3", "T1", "T2"] reaction_attribs["Troe"] = FlowMap() @@ -1305,7 +1312,9 @@ def plog( rate_constant = self.process_arrhenius_parameters(arr_coeff) P_node = arr_coeff.find("P") if P_node is None: - raise LookupError("The pressure for a plog reaction must be specified") + raise MissingXMLNode( + "The pressure for a plog reaction must be specified", arr_coeff + ) rate_constant["P"] = get_float_or_units(P_node) rate_constants.append(rate_constant) reaction_attributes["rate-constants"] = rate_constants @@ -1330,8 +1339,9 @@ def chebyshev( for range_tag in ["Tmin", "Tmax", "Pmin", "Pmax"]: range_node = rate_coeff.find(range_tag) if range_node is None: - raise LookupError( - "A Chebyshev reaction must include a {} node".format(range_tag) + raise MissingXMLNode( + "A Chebyshev reaction must include a {} node".format(range_tag), + rate_coeff, ) if range_tag.startswith("T"): reaction_attributes["temperature-range"].append( @@ -1343,12 +1353,16 @@ def chebyshev( ) data_node = rate_coeff.find("floatArray") if data_node is None: - raise LookupError("Chebyshev reaction must include a floatArray node.") + raise MissingXMLNode( + "Chebyshev reaction must include a floatArray node.", rate_coeff + ) n_p_values = int(data_node.get("degreeP", 0)) n_T_values = int(data_node.get("degreeT", 0)) if not n_p_values or not n_T_values: - raise ValueError( - "The polynomial degree in pressure and temperature must be specified" + raise MissingXMLAttribute( + "The Chebyshev polynomial degree in pressure and temperature must be " + "specified", + data_node, ) raw_data = [float(a) for a in clean_node_text(data_node).split(",")] data = [] @@ -1357,7 +1371,7 @@ def chebyshev( if len(data) != n_T_values: raise ValueError( - "The number of rows of the data do not match the specified " + "The number of rows of the Chebyshev data do not match the specified " "temperature degree." ) reaction_attributes["data"] = data @@ -1374,8 +1388,8 @@ def surface( """ arr_node = rate_coeff.find("Arrhenius") if arr_node is None: - raise LookupError( - "Surface reaction requires Arrhenius node: '{}'".format(rate_coeff) + raise MissingXMLNode( + "Surface reaction requires Arrhenius node", rate_coeff ) sticking = arr_node.get("type") == "stick" if sticking: @@ -1391,13 +1405,13 @@ def surface( cov_species = cov_node.get("species") cov_a = cov_node.find("a") if cov_a is None: - raise LookupError("Coverage requires a: '{}'".format(cov_node)) + raise MissingXMLNode("Coverage requires a", cov_node) cov_m = cov_node.find("m") if cov_m is None: - raise LookupError("Coverage requires m: '{}'".format(cov_node)) + raise MissingXMLNode("Coverage requires m", cov_node) cov_e = cov_node.find("e") if cov_e is None: - raise LookupError("Coverage requires e: '{}'".format(cov_node)) + raise MissingXMLNode("Coverage requires e", cov_node) reaction_attributes["coverage-dependencies"] = { cov_species: { "a": get_float_or_units(cov_a), @@ -1419,13 +1433,13 @@ def edge( arr_node = rate_coeff.find("Arrhenius") echem_node = rate_coeff.find("electrochem") if echem_node is None: - raise LookupError( - "Edge reaction missing electrochem node: '{}'".format(rate_coeff) + raise MissingXMLNode( + "Edge reaction missing electrochem node", rate_coeff ) beta = echem_node.get("beta") if beta is None: - raise LookupError( - "Beta must be specified for edge reaction: '{}'".format(echem_node) + raise MissingXMLAttribute( + "Beta must be specified for edge reaction", echem_node ) reaction_attributes = BlockMap( { @@ -1456,13 +1470,14 @@ def process_arrhenius_parameters( ) -> Dict[str, Union[float, str]]: """Process the parameters from an Arrhenius child of a rateCoeff node.""" if arr_node is None: - raise TypeError("The Arrhenius node must be present.") + raise MissingXMLNode("The Arrhenius node must be present.") A_node = arr_node.find("A") b_node = arr_node.find("b") E_node = arr_node.find("E") if A_node is None or b_node is None or E_node is None: - raise LookupError( - "All of A, b, and E must be specified for the Arrhenius parameters." + raise MissingXMLNode( + "All of A, b, and E must be specified for the Arrhenius parameters.", + arr_node, ) return FlowMap( { From 1418d1a777b882b7f233190668ea76b6fbc163ea Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 30 Oct 2019 11:38:20 -0400 Subject: [PATCH 54/99] [ctml2yaml] Fix RK phase generation after refactor --- interfaces/cython/cantera/ctml2yaml.py | 60 ++++++++++++++++---------- 1 file changed, 38 insertions(+), 22 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 6eecd8e56be..c6966b3cbf4 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -10,6 +10,7 @@ from email.utils import formatdate from typing import Any, Dict, Union, Iterable, Optional, List +from typing import TYPE_CHECKING try: import ruamel_yaml as yaml # type: ignore @@ -18,6 +19,20 @@ import numpy as np +if TYPE_CHECKING: + # This is available in the built-in typing module in Python 3.8 + from typing_extensions import TypedDict + + RK_EOS_DICT = TypedDict( + "RK_EOS_DICT", + { + "a": List[Union[str, float]], + "b": Union[str, float], + "binary-a": Dict[str, List[Union[str, float]]], + }, + total=False, + ) + BlockMap = yaml.comments.CommentedMap @@ -387,11 +402,14 @@ def move_RK_coeffs_to_species( This modifies the species objects in-place in the species_data object. """ - all_pure_params = activity_coeffs.findall("pureFluidParameters") - all_species_eos = {} - for pure_param in all_pure_params: + all_species_eos = {} # type: Dict[str, RK_EOS_DICT] + for pure_param in activity_coeffs.iterfind("pureFluidParameters"): eq_of_state = BlockMap({"model": "Redlich-Kwong"}) pure_species = pure_param.get("species") + if pure_species is None: + raise MissingXMLAttribute( + "The pure fluid coefficients requires a species name", pure_param + ) pure_a_node = pure_param.find("a_coeff") if pure_a_node is None: raise MissingXMLNode( @@ -399,12 +417,12 @@ def move_RK_coeffs_to_species( ) pure_a_units = pure_a_node.get("units") + pure_a = [float(a) for a in clean_node_text(pure_a_node).split(",")] if pure_a_units is not None: pure_a_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", pure_a_units) pure_a_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", pure_a_units) eq_of_state["a"] = FlowList() - pure_a = [float(a) for a in clean_node_text(pure_a_node).split(",")] eq_of_state["a"].append( "{} {}".format(float2string(pure_a[0]), pure_a_units + "*K^0.5") ) @@ -412,9 +430,7 @@ def move_RK_coeffs_to_species( "{} {}".format(float2string(pure_a[1]), pure_a_units + "/K^0.5") ) else: - eq_of_state["a"] = FlowList( - float(a) for a in clean_node_text(pure_a_node).split(",") - ) + eq_of_state["a"] = FlowList(pure_a) pure_b_node = pure_param.find("b_coeff") if pure_b_node is None: @@ -433,7 +449,11 @@ def move_RK_coeffs_to_species( "The cross-fluid coefficients requires 2 species names", cross_param ) species_1 = all_species_eos[species_1_name] + if "binary-a" not in species_1: + species_1["binary-a"] = {} species_2 = all_species_eos[species_2_name] + if "binary-a" not in species_2: + species_2["binary-a"] = {} cross_a_node = cross_param.find("a_coeff") if cross_a_node is None: raise MissingXMLNode( @@ -442,31 +462,27 @@ def move_RK_coeffs_to_species( ) cross_a_unit = cross_a_node.get("units") + cross_a = [float(a) for a in clean_node_text(cross_a_node).split(",")] if cross_a_unit is not None: cross_a_unit = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", cross_a_unit) cross_a_unit = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", cross_a_unit) - cross_a = [float(a) for a in clean_node_text(cross_a_node).split(",")] - eq_params = [] - eq_params.append( + cross_a_w_units = [] + cross_a_w_units.append( "{} {}".format(float2string(cross_a[0]), cross_a_unit + "*K^0.5") ) - eq_params.append( + cross_a_w_units.append( "{} {}".format(float2string(cross_a[1]), cross_a_unit + "/K^0.5") ) - species_1["binary-a"] = {species_2_name: FlowList(eq_params)} - species_2["binary-a"] = {species_1_name: FlowList(eq_params)} - else: - species_1["binary-a"] = { - species_2_name: FlowList( - float(a) for a in clean_node_text(cross_a_node).split(",") + species_1["binary-a"].update( + {species_2_name: FlowList(cross_a_w_units)} ) - } - species_2["binary-a"] = { - species_1_name: FlowList( - float(a) for a in clean_node_text(cross_a_node).split(",") + species_2["binary-a"].update( + {species_1_name: FlowList(cross_a_w_units)} ) - } + else: + species_1["binary-a"].update({species_2_name: FlowList(cross_a)}) + species_2["binary-a"].update({species_1_name: FlowList(cross_a)}) for node in this_phase_species: for datasrc, species_names in node.items(): From e61f4b8002f5eb830a8fb08ed180c2db833ac21d Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 30 Oct 2019 11:39:43 -0400 Subject: [PATCH 55/99] [ctml2yaml] Change XXXX_attribs to just attribs For Species and Reaction classes. This makes it easier to reference these attributes and the extra specifier is not needed because it is implied by the type of class already. --- interfaces/cython/cantera/ctml2yaml.py | 84 ++++++++++++-------------- 1 file changed, 37 insertions(+), 47 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c6966b3cbf4..4ce1a70df7f 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -492,9 +492,9 @@ def move_RK_coeffs_to_species( if species is None: continue for spec in species: - if spec.species_attribs["name"] in species_names: - spec.species_attribs["equation-of-state"] = all_species_eos[ - spec.species_attribs["name"] + if spec.attribs["name"] in species_names: + spec.attribs["equation-of-state"] = all_species_eos[ + spec.attribs["name"] ] def move_density_to_species( @@ -534,8 +534,8 @@ def move_density_to_species( if species is None: continue for spec in species: - if spec.species_attribs["name"] in species_names: - spec.species_attribs["equation-of-state"] = equation_of_state + if spec.attribs["name"] in species_names: + spec.attribs["equation-of-state"] = equation_of_state def get_species_array( self, speciesArray_node: etree.Element @@ -627,7 +627,7 @@ def filter_reaction_list( misses = [] re_pattern = re.compile(filter_text.replace("*", ".*")) for reaction in all_reactions: - reaction_id = reaction.reaction_attribs.get("id") + reaction_id = reaction.attribs.get("id") if re_pattern.match(reaction_id): hits.append(reaction) else: @@ -831,7 +831,7 @@ def __init__(self, thermo: etree.Element) -> None: if thermo_type not in ["NASA", "NASA9", "const_cp", "Shomate", "Mu0"]: raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) func = getattr(self, thermo_type) - self.thermo_attribs = func(thermo) + self.attribs = func(thermo) def Shomate(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: """Process a Shomate polynomial from XML to a dictionary.""" @@ -968,7 +968,7 @@ def Mu0( @classmethod def to_yaml(cls, representer, data): - return representer.represent_dict(data.thermo_attribs) + return representer.represent_dict(data.attribs) class SpeciesTransport: @@ -986,23 +986,21 @@ class SpeciesTransport: } def __init__(self, transport: etree.Element): - transport_attribs = BlockMap({}) + self.attribs = BlockMap({}) transport_model = transport.get("model") if transport_model not in self._species_transport_mapping: raise TypeError( "Unknown transport model type: '{}'".format(transport.get("model")) ) - transport_attribs["model"] = self._species_transport_mapping[transport_model] - transport_attribs["geometry"] = transport.findtext("string[@title='geometry']") + self.attribs["model"] = self._species_transport_mapping[transport_model] + self.attribs["geometry"] = transport.findtext("string[@title='geometry']") for tag, name in self._transport_properties_mapping.items(): value = float(transport.findtext(tag, default=0.0)) - transport_attribs.update(check_float_neq_zero(value, name)) - - self.transport_attribs = transport_attribs + self.attribs.update(check_float_neq_zero(value, name)) @classmethod def to_yaml(cls, representer, data): - return representer.represent_dict(data.transport_attribs) + return representer.represent_dict(data.attribs) class Species: @@ -1021,29 +1019,29 @@ class Species: } def __init__(self, species_node: etree.Element): - species_attribs = BlockMap() + self.attribs = BlockMap() species_name = species_node.get("name") if species_name is None: raise MissingXMLAttribute( "The species name must be specified", species_node ) - species_attribs["name"] = species_name + self.attribs["name"] = species_name atom_array = species_node.find("atomArray") if atom_array is not None and atom_array.text is not None: - species_attribs["composition"] = split_species_value_string(atom_array) + self.attribs["composition"] = split_species_value_string(atom_array) else: - species_attribs["composition"] = {} + self.attribs["composition"] = {} if species_node.findtext("note") is not None: - species_attribs["note"] = species_node.findtext("note") + self.attribs["note"] = species_node.findtext("note") thermo = species_node.find("thermo") if thermo is not None: - species_attribs["thermo"] = SpeciesThermo(thermo) + self.attribs["thermo"] = SpeciesThermo(thermo) transport = species_node.find("transport") if transport is not None: - species_attribs["transport"] = SpeciesTransport(transport) + self.attribs["transport"] = SpeciesTransport(transport) std_state = species_node.find("standardState") if std_state is not None: @@ -1062,22 +1060,20 @@ def __init__(self, species_node: etree.Element): std_state, ) eqn_of_state["molar-volume"] = get_float_or_units(molar_volume_node) - species_attribs["equation-of-state"] = eqn_of_state + self.attribs["equation-of-state"] = eqn_of_state electrolyte = species_node.findtext("electrolyteSpeciesType") if electrolyte is not None: electrolyte = self._electrolyte_species_type_mapping[electrolyte.strip()] - species_attribs["electrolyte-species-type"] = electrolyte + self.attribs["electrolyte-species-type"] = electrolyte weak_acid_charge = species_node.find("stoichIsMods") if weak_acid_charge is not None: - species_attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) - - self.species_attribs = species_attribs + self.attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) @classmethod def to_yaml(cls, representer, data): - return representer.represent_dict(data.species_attribs) + return representer.represent_dict(data.attribs) class Reaction: @@ -1088,7 +1084,7 @@ class Reaction: """ def __init__(self, reaction: etree.Element): - reaction_attribs = BlockMap({}) + self.attribs = BlockMap({}) reaction_id = reaction.get("id", False) # type: Union[str, int, bool] if reaction_id: # If the reaction_id can be converted to an integer, it was likely @@ -1098,7 +1094,7 @@ def __init__(self, reaction: etree.Element): try: reaction_id = int(reaction_id) except ValueError: - reaction_attribs["id"] = reaction_id + self.attribs["id"] = reaction_id reaction_type = reaction.get("type", "arrhenius") rate_coeff = reaction.find("rateCoeff") @@ -1149,7 +1145,7 @@ def __init__(self, reaction: etree.Element): ) func = getattr(self, reaction_type.lower()) - reaction_attribs.update(func(rate_coeff)) + self.attribs.update(func(rate_coeff)) reaction_equation = reaction.findtext("equation") if reaction_equation is None: @@ -1157,11 +1153,11 @@ def __init__(self, reaction: etree.Element): # This has to replace the reaction direction symbols separately because # species names can have [ or ] in them - reaction_attribs["equation"] = reaction_equation.replace("[=]", "<=>").replace( + self.attribs["equation"] = reaction_equation.replace("[=]", "<=>").replace( "=]", "=>" ) if reaction.get("negative_A", "").lower() == "yes": - reaction_attribs["negative-A"] = True + self.attribs["negative-A"] = True reactants_node = reaction.find("reactants") if reactants_node is None: @@ -1189,19 +1185,17 @@ def __init__(self, reaction: etree.Element): if not np.isclose(reactants[species], order): orders[species] = order if orders: - reaction_attribs["orders"] = orders + self.attribs["orders"] = orders if reaction.get("negative_orders") == "yes": - reaction_attribs["negative-orders"] = True + self.attribs["negative-orders"] = True if reaction.get("duplicate", "") == "yes": - reaction_attribs["duplicate"] = True - - self.reaction_attribs = reaction_attribs + self.attribs["duplicate"] = True @classmethod def to_yaml(cls, representer, data): - return representer.represent_dict(data.reaction_attribs) + return representer.represent_dict(data.attribs) def threebody( self, rate_coeff: etree.Element @@ -1404,9 +1398,7 @@ def surface( """ arr_node = rate_coeff.find("Arrhenius") if arr_node is None: - raise MissingXMLNode( - "Surface reaction requires Arrhenius node", rate_coeff - ) + raise MissingXMLNode("Surface reaction requires Arrhenius node", rate_coeff) sticking = arr_node.get("type") == "stick" if sticking: reaction_attributes = FlowMap( @@ -1449,9 +1441,7 @@ def edge( arr_node = rate_coeff.find("Arrhenius") echem_node = rate_coeff.find("electrochem") if echem_node is None: - raise MissingXMLNode( - "Edge reaction missing electrochem node", rate_coeff - ) + raise MissingXMLNode("Edge reaction missing electrochem node", rate_coeff) beta = echem_node.get("beta") if beta is None: raise MissingXMLAttribute( @@ -1523,10 +1513,10 @@ def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Sp for species_node in species_data_node.iterfind("species"): this_species = Species(species_node) for s in this_node_species: - if this_species.species_attribs["name"] == s.species_attribs["name"]: + if this_species.attribs["name"] == s.attribs["name"]: raise ValueError( "Duplicate specification of species '{}' in node '{}'".format( - this_species.species_attribs["name"], this_data_node_id + this_species.attribs["name"], this_data_node_id ) ) this_node_species.append(this_species) From c7181a30b203ee235a286e3f9bb4fae7cffd1b3a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 30 Oct 2019 12:22:16 -0400 Subject: [PATCH 56/99] [ctml2yaml] Finish Debye-Huckel phase-thermo type --- interfaces/cython/cantera/ctml2yaml.py | 112 ++++++++++++++++-- .../cython/cantera/test/test_convert.py | 9 +- 2 files changed, 103 insertions(+), 18 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 4ce1a70df7f..c9d1cb379bd 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -8,6 +8,7 @@ import xml.etree.ElementTree as etree from email.utils import formatdate +import warnings from typing import Any, Dict, Union, Iterable, Optional, List from typing import TYPE_CHECKING @@ -32,6 +33,9 @@ }, total=False, ) + DH_BETA_MATRIX = TypedDict( + "DH_BETA_MATRIX", {"species": List[str], "beta": Union[str, float]}, total=False + ) BlockMap = yaml.comments.CommentedMap @@ -309,7 +313,9 @@ def __init__( raise MissingXMLNode( "Debye Huckel thermo model requires activity", phase_thermo ) - self.attribs["activity-data"] = self.debye_huckel(activity_coefficients) + self.attribs["activity-data"] = self.debye_huckel( + species, activity_coefficients, species_data + ) elif phase_thermo_model == "StoichSubstance": self.move_density_to_species(species, phase_thermo, species_data) elif phase_thermo_model == "RedlichKwongMFTP": @@ -476,10 +482,10 @@ def move_RK_coeffs_to_species( ) species_1["binary-a"].update( {species_2_name: FlowList(cross_a_w_units)} - ) + ) species_2["binary-a"].update( {species_1_name: FlowList(cross_a_w_units)} - ) + ) else: species_1["binary-a"].update({species_2_name: FlowList(cross_a)}) species_2["binary-a"].update({species_1_name: FlowList(cross_a)}) @@ -756,7 +762,12 @@ def hmw_electrolyte(self, activity_node: etree.Element): activity_data["interactions"] = interactions return activity_data - def debye_huckel(self, activity_node: etree.Element): + def debye_huckel( + self, + this_phase_species: List[Dict[str, Iterable[str]]], + activity_node: etree.Element, + species_data: Dict[str, List["Species"]], + ) -> Dict[str, Union[str, float, bool]]: """Process the activity coefficiences data for the Debye Huckel model.""" model_map = { "dilute_limit": "dilute-limit", @@ -796,6 +807,7 @@ def debye_huckel(self, activity_node: etree.Element): activity_data["B-dot"] = get_float_or_units(B_dot_node) ionic_radius_node = activity_node.find("ionicRadius") + species_ionic_radii = {} # type: Dict[str, Union[float, str]] if ionic_radius_node is not None: default_radius = ionic_radius_node.get("default") radius_units = ionic_radius_node.get("units") @@ -804,17 +816,93 @@ def debye_huckel(self, activity_node: etree.Element): if radius_units == "Angstroms": radius_units = "angstrom" default_radius += " {}".format(radius_units) - activity_data["default-ionic-radius"] = default_radius + activity_data["default-ionic-radius"] = default_radius + else: + activity_data["default-ionic-radius"] = float(default_radius) if ionic_radius_node.text is not None: radii = clean_node_text(ionic_radius_node).split() - if radii: - activity_data["ionic-radius"] = [] - for r in radii: - species, radius = r.strip().rsplit(":", 1) + for r in radii: + species_name, radius = r.strip().rsplit(":", 1) + if radius_units is not None: radius += " {}".format(radius_units) - activity_data["ionic-radius"].append( - BlockMap({"species": species, "radius": radius}) - ) + species_ionic_radii[species_name] = radius + else: + species_ionic_radii[species_name] = float(radius) + + beta_matrix_node = activity_node.find("DHBetaMatrix") + if beta_matrix_node is not None: + beta_matrix = [] + beta_units = beta_matrix_node.get("units") + for beta_text in clean_node_text(beta_matrix_node).split(): + # The C++ code to process this matrix from XML assumes that the species + # names in this matrix do not contain colons, so we retain that + # behavior here. + species_1, species_2, beta_value = beta_text.split(":") + beta_dict = { + "species": FlowList([species_1, species_2]) + } # type: DH_BETA_MATRIX + if beta_units is not None: + beta_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", beta_units) + beta_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", beta_units) + beta_dict["beta"] = beta_value + " " + beta_units + else: + beta_dict["beta"] = float(beta_value) + beta_matrix.append(beta_dict) + + if beta_matrix: + activity_data["beta"] = beta_matrix + + ionic_strength_mods_node = activity_node.find("stoichIsMods") + is_mods = {} + if ionic_strength_mods_node is not None: + mods = clean_node_text(ionic_strength_mods_node).split() + for m in mods: + species_name, mod = m.strip().rsplit(":", 1) + is_mods[species_name] = float(mod) + + electrolyte_species_type_node = activity_node.find("electrolyteSpeciesType") + etype_mods = {} + if electrolyte_species_type_node is not None: + mods = clean_node_text(electrolyte_species_type_node).split() + for m in mods: + species_name, mod = m.strip().rsplit(":", 1) + etype_mods[species_name] = mod + + flat_species = {k: v for d in this_phase_species for k, v in d.items()} + for datasrc, species_names in flat_species.items(): + if datasrc == "species": + datasrc = "species_data" + species = species_data.get(datasrc) + if species is None: + continue + for spec in species: + name = spec.attribs["name"] + if name not in species_names: + continue + if name in species_ionic_radii: + spec.attribs["ionic-radius"] = species_ionic_radii[name] + if name in is_mods: + if "weak-acid-charge" not in spec.attribs: + spec.attribs["weak-acid-charge"] = is_mods[name] + else: + if is_mods[name] != spec.attribs["weak-acid-charge"]: + warnings.warn( + "The stoichIsMods node was specified at the phase and " + "species level for species '{}'. The value specified " + "in the species node will be used".format(name) + ) + if name in etype_mods: + etype = spec._electrolyte_species_type_mapping[etype_mods[name]] + if "electrolyte-species-type" not in spec.attribs: + spec.attribs["electrolyte-species-type"] = etype + else: + if spec.attribs["electrolyte-species-type"] != etype: + warnings.warn( + "The electrolyteSpeciesType node was specified at the " + "phase and species level for species '{}'. The value " + "specified in the species node will be " + "used".format(name) + ) return activity_data diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 407eb0828d3..057bd3a0980 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -2,6 +2,7 @@ from os.path import join as pjoin import itertools from pathlib import Path +import warnings from . import utilities import cantera as ct @@ -1038,9 +1039,5 @@ def test_DH_NaCl_phase(self): "debye-huckel-pitzer-beta_ij", "debye-huckel-beta_ij", ]: - try: - ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", phaseid=phaseid) - self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) - except: - print(phaseid) - raise + ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", phaseid=phaseid) + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) From 66ce20b15f32cfecc356cfdff1244a6d88e523ca Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 30 Oct 2019 13:21:13 -0400 Subject: [PATCH 57/99] [ctml2yaml] Add MaskellSolidSolnPhase --- interfaces/cython/cantera/ctml2yaml.py | 12 ++++++++++++ interfaces/cython/cantera/test/test_convert.py | 11 +++++++++++ test/data/MaskellSolidSolnPhase_valid.xml | 1 - 3 files changed, 23 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c9d1cb379bd..11c765b8836 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -209,6 +209,7 @@ class Phase: "PureLiquidWater": "liquid-water-IAPWS95", "HMW": "HMW-electrolyte", "DebyeHuckel": "Debye-Huckel", + "MaskellSolidSolnPhase": "Maskell-solid-solution", } _kinetics_model_mapping = { "GasKinetics": "gas", @@ -325,6 +326,17 @@ def __init__( "Redlich-Kwong thermo model requires activity", phase_thermo ) self.move_RK_coeffs_to_species(species, activity_coefficients, species_data) + elif phase_thermo_model == "MaskellSolidSolnPhase": + try: + self.move_density_to_species(species, phase_thermo, species_data) + except MissingXMLNode: + pass + excess_h_node = phase_thermo.find("h_mix") + if excess_h_node is not None: + self.attribs["excess-enthalpy"] = get_float_or_units(excess_h_node) + product_spec_node = phase_thermo.find("product_species") + if product_spec_node is not None: + self.attribs["product-species"] = clean_node_text(product_spec_node) for node in phase_thermo: if node.tag == "site_density": diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 057bd3a0980..a09e91f6a4c 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1041,3 +1041,14 @@ def test_DH_NaCl_phase(self): ]: ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", phaseid=phaseid) self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_Maskell_solid_soln(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("MaskellSolidSolnPhase_valid.xml"), + Path(self.test_work_dir).joinpath("MaskellSolidSolnPhase_valid.yaml")) + + ctmlPhase, yamlPhase = self.checkConversion("MaskellSolidSolnPhase_valid") + # Maskell phase doesn't support partial molar properties, so just check density + for T in [300, 500, 1300, 2000]: + ctmlPhase.TP = T, ct.one_atm + yamlPhase.TP = T, ct.one_atm + self.assertNear(ctmlPhase.density, yamlPhase.density) diff --git a/test/data/MaskellSolidSolnPhase_valid.xml b/test/data/MaskellSolidSolnPhase_valid.xml index 4b07a596fe2..04c3d6b8f85 100644 --- a/test/data/MaskellSolidSolnPhase_valid.xml +++ b/test/data/MaskellSolidSolnPhase_valid.xml @@ -1,4 +1,3 @@ - From e2af32ad64426a4e3a045e0a51b46ea90f94e453 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 13:48:57 -0400 Subject: [PATCH 58/99] [Thermo] Make sure the neutral phase is initialized In the IonsFromNeutral phase, make sure the neutralMoleculePhase is initialized before we try to use it. This prevents a segfault if the input YAML file does not have the neutral-phase key. --- src/thermo/IonsFromNeutralVPSSTP.cpp | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/src/thermo/IonsFromNeutralVPSSTP.cpp b/src/thermo/IonsFromNeutralVPSSTP.cpp index 1bd988c347e..12c4aa80df1 100644 --- a/src/thermo/IonsFromNeutralVPSSTP.cpp +++ b/src/thermo/IonsFromNeutralVPSSTP.cpp @@ -489,6 +489,14 @@ void IonsFromNeutralVPSSTP::initThermo() setNeutralMoleculePhase(newPhase(phaseNode, infile)); } + if (!neutralMoleculePhase_) { + throw CanteraError( + "IonsFromNeutralVPSSTP::initThermo", + "The neutral phase has not been initialized. Are you missing the " + "'neutral-phase' key?" + ); + } + size_t nElementsN = neutralMoleculePhase_->nElements(); const std::vector& elnamesVN = neutralMoleculePhase_->elementNames(); vector_fp elemVectorN(nElementsN); From 1de5d66f8932d6ca9ebb7bb520e73a5f27034a6a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 13:50:52 -0400 Subject: [PATCH 59/99] [Thermo] Check that special species was specified In the IonsFromNeutral phase, if a special-species is not included the index is never set after initialization and remains at npos. The check here ensures that a special-species is specified. Also move building the elemVectorI outside of the jNeut loop. It doesn't appear to need to be done for every neutral species, so this would save some effort --- src/thermo/IonsFromNeutralVPSSTP.cpp | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/thermo/IonsFromNeutralVPSSTP.cpp b/src/thermo/IonsFromNeutralVPSSTP.cpp index 12c4aa80df1..111212b4ac7 100644 --- a/src/thermo/IonsFromNeutralVPSSTP.cpp +++ b/src/thermo/IonsFromNeutralVPSSTP.cpp @@ -505,14 +505,21 @@ void IonsFromNeutralVPSSTP::initThermo() const std::vector& elnamesVI = elementNames(); vector_fp elemVectorI(nElementsI); + if (indexSpecialSpecies_ == npos) { + throw CanteraError( + "IonsFromNeutralVPSSTP::initThermo", + "No special-species were specified in the phase." + ); + } + for (size_t m = 0; m < nElementsI; m++) { + elemVectorI[m] = nAtoms(indexSpecialSpecies_, m); + } + for (size_t jNeut = 0; jNeut < numNeutralMoleculeSpecies_; jNeut++) { for (size_t m = 0; m < nElementsN; m++) { elemVectorN[m] = neutralMoleculePhase_->nAtoms(jNeut, m); } - for (size_t m = 0; m < nElementsI; m++) { - elemVectorI[m] = nAtoms(indexSpecialSpecies_, m); - } double fac = factorOverlap(elnamesVN, elemVectorN, nElementsN, elnamesVI ,elemVectorI, nElementsI); if (fac > 0.0) { From 3b5bccab9f3bc61a398826beafa67edeb7aaff0a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 13:53:01 -0400 Subject: [PATCH 60/99] [Thermo] IonsFromNeutral can specify the neutral phase in another file When specified in YAML input, the neutral-phase can now be specified as being in another file by using a / to delimit the file from the phase name. This is similar to specifying species or elements in another file. This brings back parity with the XML specification of these phases. --- src/thermo/IonsFromNeutralVPSSTP.cpp | 19 +++++++++++++++---- 1 file changed, 15 insertions(+), 4 deletions(-) diff --git a/src/thermo/IonsFromNeutralVPSSTP.cpp b/src/thermo/IonsFromNeutralVPSSTP.cpp index 111212b4ac7..71f88118d5d 100644 --- a/src/thermo/IonsFromNeutralVPSSTP.cpp +++ b/src/thermo/IonsFromNeutralVPSSTP.cpp @@ -482,11 +482,22 @@ static double factorOverlap(const std::vector& elnamesVN , void IonsFromNeutralVPSSTP::initThermo() { - if (m_input.hasKey("neutral-phase") && m_input.hasKey("__file__")) { + if (m_input.hasKey("neutral-phase")) { string neutralName = m_input["neutral-phase"].asString(); - AnyMap infile = AnyMap::fromYamlFile(m_input["__file__"].asString()); - AnyMap& phaseNode = infile["phases"].getMapWhere("name", neutralName); - setNeutralMoleculePhase(newPhase(phaseNode, infile)); + const auto& slash = boost::ifind_last(neutralName, "/"); + AnyMap infile; + if (slash) { + string fileName(neutralName.begin(), slash.begin()); + string node(slash.end(), neutralName.end()); + infile = AnyMap::fromYamlFile(fileName, + m_input.getString("__file__", "")); + AnyMap& phaseNode = infile["phases"].getMapWhere("name", node); + setNeutralMoleculePhase(newPhase(phaseNode, infile)); + } else { + infile = AnyMap::fromYamlFile(m_input["__file__"].asString()); + AnyMap& phaseNode = infile["phases"].getMapWhere("name", neutralName); + setNeutralMoleculePhase(newPhase(phaseNode, infile)); + } } if (!neutralMoleculePhase_) { From bc58b9cffa7d0c074d185aefa3c173d2d16b6bc4 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 14:01:16 -0400 Subject: [PATCH 61/99] [ctml2yaml] Margules and ions-from-neutral phases Add Margules and ions-from-neutral-molecule phases. Margules is not complete, it is missing the interaction parameters. --- interfaces/cython/cantera/ctml2yaml.py | 119 ++++++++++++++++-- .../cython/cantera/test/test_convert.py | 19 +++ test/data/mock_ion.xml | 15 ++- 3 files changed, 134 insertions(+), 19 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 11c765b8836..f6d5263ced5 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -210,6 +210,8 @@ class Phase: "HMW": "HMW-electrolyte", "DebyeHuckel": "Debye-Huckel", "MaskellSolidSolnPhase": "Maskell-solid-solution", + "IonsFromNeutralMolecule": "ions-from-neutral-molecule", + "Margules": "Margules", } _kinetics_model_mapping = { "GasKinetics": "gas", @@ -337,6 +339,23 @@ def __init__( product_spec_node = phase_thermo.find("product_species") if product_spec_node is not None: self.attribs["product-species"] = clean_node_text(product_spec_node) + elif phase_thermo_model == "IonsFromNeutralMolecule": + neutral_phase_node = phase_thermo.find("neutralMoleculePhase") + if neutral_phase_node is None: + raise MissingXMLNode( + "The IonsFromNeutralMolecule phase requires the " + "neutralMoleculePhase node", + phase_thermo, + ) + neutral_phase_src = neutral_phase_node.get("datasrc") + if neutral_phase_src is None: + raise MissingXMLAttribute( + "The neutralMoleculePhase requires the datasrc attribute", + neutral_phase_node, + ) + filename, location = neutral_phase_src.split("#") + filename = str(Path(filename).with_suffix(".yaml")) + self.attribs["neutral-phase"] = "{}/{}".format(filename, location) for node in phase_thermo: if node.tag == "site_density": @@ -410,6 +429,37 @@ def __init__( if std_conc_node is not None: self.attribs["standard-concentration-basis"] = std_conc_node.get("model") + self.check_elements(species, species_data) + + def check_elements( + self, + this_phase_species: List[Dict[str, Iterable[str]]], + species_data: Dict[str, List["Species"]], + ) -> None: + """Check the species elements for inclusion in the phase level specification. + + Some species include a charge node that adds an electron to the species + composition, so we need to update the phase-level elements to include + the electron. + """ + phase_elements = self.attribs.get("elements") + if phase_elements is None: + return + flat_species = {k: v for d in this_phase_species for k, v in d.items()} + for datasrc, species_names in flat_species.items(): + if datasrc == "species": + datasrc = "species_data" + species = species_data.get(datasrc) + if species is None: + continue + for spec in species: + species_elements = spec.attribs.get("composition", {}) + if spec.attribs["name"] not in species_names: + continue + for species_element, amount in species_elements.items(): + if species_element not in phase_elements and amount > 0.0: + phase_elements.append(species_element) + def move_RK_coeffs_to_species( self, this_phase_species: List[Dict[str, Iterable[str]]], @@ -1114,7 +1164,7 @@ class Species: "weakAcidAssociated": "weak-acid-associated", "chargedSpecies": "charged-species", "strongAcidAssociated": "strong-acid-associated", - "polarNetural": "polar-neutral", + "polarNeutral": "polar-neutral", "nonpolarNeutral": "nonpolar-neutral", } @@ -1132,22 +1182,78 @@ def __init__(self, species_node: etree.Element): else: self.attribs["composition"] = {} + charge_node = species_node.find("charge") + if charge_node is not None: + charge = float(clean_node_text(charge_node)) + if charge != 0.0: + self.attribs["composition"]["E"] = -1 * charge + if species_node.findtext("note") is not None: self.attribs["note"] = species_node.findtext("note") thermo = species_node.find("thermo") if thermo is not None: - self.attribs["thermo"] = SpeciesThermo(thermo) + thermo_model = thermo.get("model", "") + # HKFT and IonFromNeutral species thermo nodes are not used from the XML + # and are not present in the YAML specification. However, there is some + # information in the thermo node for IonFromNeutral that needs to end up + # in the equation-of-state YAML node + if thermo_model.lower() == "ionfromneutral": + neutral_spec_mult_node = thermo.find("neutralSpeciesMultipliers") + if neutral_spec_mult_node is None: + raise MissingXMLNode( + "IonFromNeutral requires a neutralSpeciesMultipliers node", + thermo, + ) + species_multipliers = FlowMap({}) + neutral_spec_mult = clean_node_text(neutral_spec_mult_node).split() + for spec_mult in neutral_spec_mult: + species, multiplier = spec_mult.rsplit(":", 1) + species_multipliers[species] = float(multiplier) + if species_multipliers: + self.attribs["equation-of-state"] = { + "model": "ions-from-neutral-molecule", + "multipliers": species_multipliers, + } + if thermo.find("specialSpecies") is not None: + self.attribs["equation-of-state"]["special-species"] = True + elif thermo_model.lower() == "hkft": + pass + else: + self.attribs["thermo"] = SpeciesThermo(thermo) transport = species_node.find("transport") if transport is not None: self.attribs["transport"] = SpeciesTransport(transport) + self.process_standard_state_node(species_node) + + electrolyte = species_node.findtext("electrolyteSpeciesType") + if electrolyte is not None: + electrolyte = self._electrolyte_species_type_mapping[electrolyte.strip()] + self.attribs["electrolyte-species-type"] = electrolyte + + weak_acid_charge = species_node.find("stoichIsMods") + if weak_acid_charge is not None: + self.attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) + + def process_standard_state_node(self, species_node: etree.Element) -> None: + """Process the standardState node in a species definition. + + If the model is IonFromNeutral, this function doesn't do anything to + the species object. Otherwise, the model data is put into the YAML + equation-of-state node. + """ std_state = species_node.find("standardState") if std_state is not None: std_state_model = std_state.get("model") if std_state_model is None: raise MissingXMLAttribute("Unknown standard state model", std_state) + elif std_state_model.lower() == "ionfromneutral": + # If the standard state model is IonFromNeutral, we don't need to do + # anything with it + return + eqn_of_state = { "model": self._standard_state_model_mapping[std_state_model] } # type: Dict[str, Union[str, float]] @@ -1162,15 +1268,6 @@ def __init__(self, species_node: etree.Element): eqn_of_state["molar-volume"] = get_float_or_units(molar_volume_node) self.attribs["equation-of-state"] = eqn_of_state - electrolyte = species_node.findtext("electrolyteSpeciesType") - if electrolyte is not None: - electrolyte = self._electrolyte_species_type_mapping[electrolyte.strip()] - self.attribs["electrolyte-species-type"] = electrolyte - - weak_acid_charge = species_node.find("stoichIsMods") - if weak_acid_charge is not None: - self.attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) - @classmethod def to_yaml(cls, representer, data): return representer.represent_dict(data.attribs) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index a09e91f6a4c..e80aef21a95 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1052,3 +1052,22 @@ def test_Maskell_solid_soln(self): ctmlPhase.TP = T, ct.one_atm yamlPhase.TP = T, ct.one_atm self.assertNear(ctmlPhase.density, yamlPhase.density) + + def test_mock_ion(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("mock_ion.xml"), + Path(self.test_work_dir).joinpath("mock_ion.yaml")) + ctmlPhase = ct.ThermoPhase("mock_ion.xml") + yamlPhase = ct.ThermoPhase("mock_ion.yaml") + # Due to changes in how the species elements are specified, the composition + # of the species differs from XML to YAML (electrons are used to specify charge + # in YAML while the charge node is used in XML). Therefore, checkConversion + # won't work and we have to check a few things manually. There are also no + # reactions specified for these phases so don't need to do any checks for that. + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + # ions-from-neutral-molecule phase doesn't support partial molar properties, + # so just check density + for T in [300, 500, 1300, 2000]: + ctmlPhase.TP = T, ct.one_atm + yamlPhase.TP = T, ct.one_atm + self.assertNear(ctmlPhase.density, yamlPhase.density) \ No newline at end of file diff --git a/test/data/mock_ion.xml b/test/data/mock_ion.xml index e375ef2e069..cb16dfd4913 100644 --- a/test/data/mock_ion.xml +++ b/test/data/mock_ion.xml @@ -1,4 +1,3 @@ - @@ -11,9 +10,9 @@ K+ Cl- - - - + + + @@ -27,15 +26,15 @@ KCl(L) - + - + - + K:1 @@ -63,7 +62,7 @@ K:1 Cl:1 - + 73.59698, 0.0, 0.0, 0.0, 0.0, -443.7341, From 70f062af9dd5cda3ee971f8c1993501c8b392f53 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 18:22:14 -0400 Subject: [PATCH 62/99] [ctml2yaml] Add Motz-Wise correction option If the Motz-Wise option is specified at the global reactionData node level, this is used to set the option for all the reactions in that node that don't otherwise specify the option. This method is easier than setting the equivalent phase-level option. --- interfaces/cython/cantera/ctml2yaml.py | 18 +- .../cython/cantera/test/test_convert.py | 13 +- test/data/ptcombust-motzwise.xml | 404 ++++++++++++++++++ 3 files changed, 432 insertions(+), 3 deletions(-) create mode 100644 test/data/ptcombust-motzwise.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index f6d5263ced5..8c84986daa5 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1280,7 +1280,7 @@ class Reaction: An ETree Element node with the reaction information """ - def __init__(self, reaction: etree.Element): + def __init__(self, reaction: etree.Element, node_motz_wise: bool): self.attribs = BlockMap({}) reaction_id = reaction.get("id", False) # type: Union[str, int, bool] if reaction_id: @@ -1344,6 +1344,9 @@ def __init__(self, reaction: etree.Element): func = getattr(self, reaction_type.lower()) self.attribs.update(func(rate_coeff)) + if node_motz_wise and self.attribs.get("Motz-Wise") is None: + self.attribs["Motz-Wise"] = True + reaction_equation = reaction.findtext("equation") if reaction_equation is None: raise MissingNodeText("The reaction must have an equation", reaction) @@ -1601,6 +1604,14 @@ def surface( reaction_attributes = FlowMap( {"sticking-coefficient": self.process_arrhenius_parameters(arr_node)} ) + species = arr_node.get("species", "") + if species: + reaction_attributes["sticking-species"] = species + motz_wise = arr_node.get("motz_wise", "").lower() + if motz_wise == "true": + reaction_attributes["Motz-Wise"] = True + elif motz_wise == "false": + reaction_attributes["Motz-Wise"] = False else: reaction_attributes = FlowMap( {"rate-constant": self.process_arrhenius_parameters(arr_node)} @@ -1734,9 +1745,12 @@ def create_reactions_from_data_node( raise ValueError( "Duplicate reactionData id found: '{}'".format(this_data_node_id) ) + node_motz_wise = False + if reactionData_node.get("motz_wise", "").lower() == "true": + node_motz_wise = True this_node_reactions = [] # type: List[Reaction] for reaction_node in reactionData_node.iterfind("reaction"): - this_node_reactions.append(Reaction(reaction_node)) + this_node_reactions.append(Reaction(reaction_node, node_motz_wise)) reactions[this_data_node_id] = this_node_reactions return reactions diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index e80aef21a95..e2bd96ab002 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -833,6 +833,17 @@ def test_ptcombust(self): self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600]) self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5]) + def test_ptcombust_motzwise(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath('ptcombust-motzwise.xml'), + Path(self.test_work_dir).joinpath('ptcombust-motzwise.yaml')) + ctmlGas, yamlGas = self.checkConversion('ptcombust-motzwise') + ctmlSurf, yamlSurf = self.checkConversion('ptcombust-motzwise', ct.Interface, + phaseid='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) + + self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5]) + self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600]) + self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5]) + def test_sofc(self): ctml2yaml.convert(Path(self.cantera_data).joinpath('sofc.xml'), Path(self.test_work_dir).joinpath('sofc.yaml')) @@ -1070,4 +1081,4 @@ def test_mock_ion(self): for T in [300, 500, 1300, 2000]: ctmlPhase.TP = T, ct.one_atm yamlPhase.TP = T, ct.one_atm - self.assertNear(ctmlPhase.density, yamlPhase.density) \ No newline at end of file + self.assertNear(ctmlPhase.density, yamlPhase.density) diff --git a/test/data/ptcombust-motzwise.xml b/test/data/ptcombust-motzwise.xml new file mode 100644 index 00000000000..2d58dca6a3c --- /dev/null +++ b/test/data/ptcombust-motzwise.xml @@ -0,0 +1,404 @@ + + + + + + + O H C N Ar + + H2 H O O2 OH + H2O HO2 H2O2 + C CH CH2 CH2(S) CH3 CH4 CO CO2 + HCO CH2O CH2OH CH3O CH3OH C2H C2H2 C2H3 + C2H4 C2H5 C2H6 HCCO CH2CO HCCOH AR N2 + + + + + + + 300.0 + 101325.0 + CH4:0.095, O2:0.21, AR:0.79 + + + + + + + + + Pt H O C + + PT(S) H(S) H2O(S) OH(S) CO(S) + CO2(S) CH3(S) CH2(S)s CH(S) C(S) O(S) + + + 900.0 + O(S):0.0, PT(S):0.5, H(S):0.5 + + + 2.7063e-09 + + + + gas + + + + + + + + + H2 + 2 PT(S) =] 2 H(S) + 1.0 + 1.0 + + + 4.457900E+07 + 0.5 + 0.000000 + + + H2:1.0 PT(S):2.0 + H(S):2.0 + + + + + 2 H(S) =] H2 + 2 PT(S) + + + 3.700000E+20 + 0 + 67400.000000 + + 0.000000 + 0.0 + -6000.000000 + + + + H(S):2.0 + H2:1.0 PT(S):2.0 + + + + + H + PT(S) =] H(S) + + + 1.000000E+00 + 0 + 0.000000 + + + H:1.0 PT(S):1 + H(S):1.0 + + + + + O2 + 2 PT(S) =] 2 O(S) + + + 1.800000E+17 + -0.5 + 0.000000 + + + O2:1.0 PT(S):2.0 + O(S):2.0 + + + + + O2 + 2 PT(S) =] 2 O(S) + + + 2.300000E-02 + 0 + 0.000000 + + + O2:1.0 PT(S):2.0 + O(S):2.0 + + + + + 2 O(S) =] O2 + 2 PT(S) + + + 3.700000E+20 + 0 + 213200.000000 + + 0.000000 + 0.0 + -60000.000000 + + + + O(S):2.0 + O2:1.0 PT(S):2.0 + + + + + O + PT(S) =] O(S) + + + 1.000000E+00 + 0 + 0.000000 + + + O:1.0 PT(S):1 + O(S):1.0 + + + + + H2O + PT(S) =] H2O(S) + + + 7.500000E-01 + 0 + 0.000000 + + + H2O:1.0 PT(S):1 + H2O(S):1.0 + + + + + H2O(S) =] H2O + PT(S) + + + 1.000000E+13 + 0 + 40300.000000 + + + H2O(S):1.0 + H2O:1.0 PT(S):1 + + + + + OH + PT(S) =] OH(S) + + + 1.000000E+00 + 0 + 0.000000 + + + OH:1.0 PT(S):1 + OH(S):1.0 + + + + + OH(S) =] OH + PT(S) + + + 1.000000E+13 + 0 + 192800.000000 + + + OH(S):1.0 + OH:1.0 PT(S):1 + + + + + H(S) + O(S) [=] OH(S) + PT(S) + + + 3.700000E+20 + 0 + 11500.000000 + + + H(S):1.0 O(S):1 + OH(S):1.0 PT(S):1 + + + + + H(S) + OH(S) [=] H2O(S) + PT(S) + + + 3.700000E+20 + 0 + 17400.000000 + + + H(S):1.0 OH(S):1 + H2O(S):1.0 PT(S):1 + + + + + OH(S) + OH(S) [=] H2O(S) + O(S) + + + 3.700000E+20 + 0 + 48200.000000 + + + OH(S):2.0 + H2O(S):1.0 O(S):1 + + + + + CO + PT(S) =] CO(S) + 1.0 + 2.0 + + + 1.618000E+16 + 0.5 + 0.000000 + + + CO:1.0 PT(S):1 + CO(S):1.0 + + + + + CO(S) =] CO + PT(S) + + + 1.000000E+13 + 0 + 125500.000000 + + + CO(S):1.0 + CO:1.0 PT(S):1 + + + + + CO2(S) =] CO2 + PT(S) + + + 1.000000E+13 + 0 + 20500.000000 + + + CO2(S):1.0 + CO2:1.0 PT(S):1 + + + + + CO(S) + O(S) =] CO2(S) + PT(S) + + + 3.700000E+20 + 0 + 105000.000000 + + + CO(S):1.0 O(S):1 + CO2(S):1.0 PT(S):1 + + + + + CH4 + 2 PT(S) =] CH3(S) + H(S) + 1.0 + 2.3 + + + 2.322201E+16 + 0.5 + 0.000000 + + + CH4:1.0 PT(S):2.0 + CH3(S):1.0 H(S):1 + + + + + CH3(S) + PT(S) =] CH2(S)s + H(S) + + + 3.700000E+20 + 0 + 20000.000000 + + + CH3(S):1.0 PT(S):1 + CH2(S)s:1.0 H(S):1 + + + + + CH2(S)s + PT(S) =] CH(S) + H(S) + + + 3.700000E+20 + 0 + 20000.000000 + + + CH2(S)s:1.0 PT(S):1 + CH(S):1.0 H(S):1 + + + + + CH(S) + PT(S) =] C(S) + H(S) + + + 3.700000E+20 + 0 + 20000.000000 + + + CH(S):1.0 PT(S):1 + C(S):1.0 H(S):1 + + + + + C(S) + O(S) =] CO(S) + PT(S) + + + 3.700000E+20 + 0 + 62800.000000 + + + C(S):1.0 O(S):1 + CO(S):1.0 PT(S):1 + + + + + CO(S) + PT(S) =] C(S) + O(S) + + + 1.000000E+17 + 0 + 184000.000000 + + + CO(S):1.0 PT(S):1 + C(S):1.0 O(S):1 + + + From 09975ee7c7055caff19e690bd015fa3c345d9c08 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 18:22:38 -0400 Subject: [PATCH 63/99] [ctml2yaml] Simplify duplicate id checks --- interfaces/cython/cantera/ctml2yaml.py | 54 +++++++++++++------------- 1 file changed, 26 insertions(+), 28 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 8c84986daa5..27bb88ff105 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1616,25 +1616,25 @@ def surface( reaction_attributes = FlowMap( {"rate-constant": self.process_arrhenius_parameters(arr_node)} ) - cov_node = arr_node.find("coverage") - if cov_node is not None: - cov_species = cov_node.get("species") - cov_a = cov_node.find("a") - if cov_a is None: - raise MissingXMLNode("Coverage requires a", cov_node) - cov_m = cov_node.find("m") - if cov_m is None: - raise MissingXMLNode("Coverage requires m", cov_node) - cov_e = cov_node.find("e") - if cov_e is None: - raise MissingXMLNode("Coverage requires e", cov_node) - reaction_attributes["coverage-dependencies"] = { - cov_species: { - "a": get_float_or_units(cov_a), - "m": get_float_or_units(cov_m), - "E": get_float_or_units(cov_e), - } + cov_node = arr_node.find("coverage") + if cov_node is not None: + cov_species = cov_node.get("species") + cov_a = cov_node.find("a") + if cov_a is None: + raise MissingXMLNode("Coverage requires a", cov_node) + cov_m = cov_node.find("m") + if cov_m is None: + raise MissingXMLNode("Coverage requires m", cov_node) + cov_e = cov_node.find("e") + if cov_e is None: + raise MissingXMLNode("Coverage requires e", cov_node) + reaction_attributes["coverage-dependencies"] = { + cov_species: { + "a": get_float_or_units(cov_a), + "m": get_float_or_units(cov_m), + "E": get_float_or_units(cov_e), } + } return reaction_attributes @@ -1712,11 +1712,10 @@ def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Sp species = {} # type: Dict[str, List[Species]] for species_data_node in ctml_tree.iterfind("speciesData"): this_data_node_id = species_data_node.get("id", "") - for key in species.keys(): - if key.startswith(this_data_node_id): - raise ValueError( - "Duplicate speciesData id found: '{}'".format(this_data_node_id) - ) + if this_data_node_id in species: + raise ValueError( + "Duplicate speciesData id found: '{}'".format(this_data_node_id) + ) this_node_species = [] # type: List[Species] for species_node in species_data_node.iterfind("species"): this_species = Species(species_node) @@ -1740,11 +1739,10 @@ def create_reactions_from_data_node( reactions = {} # type: Dict[str, List[Reaction]] for reactionData_node in ctml_tree.iterfind("reactionData"): this_data_node_id = reactionData_node.get("id", "") - for key in reactions.keys(): - if key.startswith(this_data_node_id): - raise ValueError( - "Duplicate reactionData id found: '{}'".format(this_data_node_id) - ) + if this_data_node_id in reactions: + raise ValueError( + "Duplicate reactionData id found: '{}'".format(this_data_node_id) + ) node_motz_wise = False if reactionData_node.get("motz_wise", "").lower() == "true": node_motz_wise = True From b2f87f46ee29eb2eaa6e390aceec5bee31136caf Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Fri, 1 Nov 2019 22:57:24 -0400 Subject: [PATCH 64/99] [ctml2yaml] Add Redlich-Kister thermo phase type --- interfaces/cython/cantera/ctml2yaml.py | 62 ++++++++++++++++++- .../cython/cantera/test/test_convert.py | 7 +++ 2 files changed, 68 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 27bb88ff105..e65ba9bb518 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -184,7 +184,7 @@ def clean_node_text(node: etree.Element) -> str: text = node.text if text is None: raise MissingNodeText("The text of the node must exist", node) - return text.replace("\n", " ").strip() + return text.replace("\n", " ").replace("\t", " ").strip() class Phase: @@ -212,6 +212,7 @@ class Phase: "MaskellSolidSolnPhase": "Maskell-solid-solution", "IonsFromNeutralMolecule": "ions-from-neutral-molecule", "Margules": "Margules", + "Redlich-Kister": "Redlich-Kister", } _kinetics_model_mapping = { "GasKinetics": "gas", @@ -356,6 +357,13 @@ def __init__( filename, location = neutral_phase_src.split("#") filename = str(Path(filename).with_suffix(".yaml")) self.attribs["neutral-phase"] = "{}/{}".format(filename, location) + elif phase_thermo_model == "Redlich-Kister": + activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is None: + raise MissingXMLNode( + "Redlich-Kister thermo model requires activity", phase_thermo + ) + self.attribs["interactions"] = self.redlich_kister(activity_coefficients) for node in phase_thermo: if node.tag == "site_density": @@ -431,6 +439,58 @@ def __init__( self.check_elements(species, species_data) + def redlich_kister( + self, activity_coeffs: etree.Element + ) -> List[Dict[str, List[Union[str, float]]]]: + """Process activity coefficents for a Redlich-Kister phase. + + Returns a list of interaction data values. + """ + all_binary_params = activity_coeffs.findall("binaryNeutralSpeciesParameters") + if not all_binary_params: + raise MissingXMLNode( + "Redlich-Kister activity coefficients requires a " + "binaryNeutralSpeciesParameters node", + activity_coeffs, + ) + interactions = [] + for binary_params in all_binary_params: + species_A = binary_params.get("speciesA") + species_B = binary_params.get("speciesB") + if species_A is None or species_B is None: + raise MissingXMLAttribute( + "binaryNeutralSpeciesParameters node requires speciesA and " + "speciesB attributes", + binary_params, + ) + this_node = { + "species": FlowList([species_A, species_B]) + } # type: Dict[str, List[Union[str, float]]] + excess_enthalpy_node = binary_params.find("excessEnthalpy") + if excess_enthalpy_node is not None: + excess_enthalpy = clean_node_text(excess_enthalpy_node).split(",") + enthalpy_units = excess_enthalpy_node.get("units", "") + if not enthalpy_units: + this_node["excess-enthalpy"] = FlowList(map(float, excess_enthalpy)) + else: + this_node["excess-enthalpy"] = FlowList( + [" ".join([e.strip(), enthalpy_units]) for e in excess_enthalpy] + ) + excess_entropy_node = binary_params.find("excessEntropy") + if excess_entropy_node is not None: + excess_entropy = clean_node_text(excess_entropy_node).split(",") + entropy_units = excess_entropy_node.get("units", "") + if not entropy_units: + this_node["excess-entropy"] = FlowList(map(float, excess_entropy)) + else: + this_node["excess-entropy"] = FlowList( + [" ".join([e.strip(), entropy_units]) for e in excess_entropy] + ) + + interactions.append(this_node) + + return interactions + def check_elements( self, this_phase_species: List[Dict[str, Iterable[str]]], diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index e2bd96ab002..5fa03e0edd0 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1082,3 +1082,10 @@ def test_mock_ion(self): ctmlPhase.TP = T, ct.one_atm yamlPhase.TP = T, ct.one_atm self.assertNear(ctmlPhase.density, yamlPhase.density) + + def test_Redlich_Kister(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("RedlichKisterVPSSTP_valid.xml"), + Path(self.test_work_dir).joinpath("RedlichKisterVPSSTP_valid.yaml")) + + ctmlPhase, yamlPhase = self.checkConversion("RedlichKisterVPSSTP_valid") + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) \ No newline at end of file From 7b2a2f9b53ab589e7eb0b858825f0b4a25c514d9 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 2 Nov 2019 23:53:11 -0400 Subject: [PATCH 65/99] [ctml2yaml] Parse pathological species names These changes allow parsing composition map strings where colons are allowed as characters in keys in a colon delimited map, as well as handling comma or space separated pairs where commas are also valid characters in the keys. --- interfaces/cython/cantera/ctml2yaml.py | 53 ++- .../cython/cantera/test/test_convert.py | 8 +- test/data/species-names.xml | 370 ++++++++++++++++++ 3 files changed, 419 insertions(+), 12 deletions(-) create mode 100644 test/data/species-names.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index e65ba9bb518..86166295c89 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -160,21 +160,52 @@ def split_species_value_string(node: etree.Element) -> Dict[str, float]: The keys of the dictionary are species names and the values are the number associated with each species. This is useful for things like elemental composition, mole fraction mappings, coverage mappings, etc. + + The algorithm is reimplemented from ``compositionMap::parseCompString`` in + ``base/stringUtils.cpp``. """ - text = node.text - if text is None: - raise MissingNodeText( - "The text of the node must exist: '{}'".format( - etree.tostring(node).decode("utf-8") - ) - ) + text = clean_node_text(node) pairs = FlowMap({}) - for t in text.replace("\n", " ").replace(",", " ").strip().split(): - key, value = t.split(":") + start, stop, left = 0, 0, 0 + non_whitespace = re.compile(r"\S") + stop_re = re.compile(r"[,;\s]") + while stop < len(text): try: - pairs[key] = int(value) + colon = text.index(":", left) except ValueError: - pairs[key] = float(value) + break + # \S matches the first non-whitespace character + # colon + 1 here excludes the colon itself from the search + valstart_match = non_whitespace.search(text, colon + 1) + if valstart_match is None: + # is this the right thing to do? + print("valstart") + break + valstart = valstart_match.start() + stop_match = stop_re.search(text, valstart) + if stop_match is None: + stop = len(text) + else: + stop = stop_match.start() + name = text[start:colon] + try: + value = float(text[valstart:stop]) + except ValueError: + testname = text[start : stop - start] + if re.search(r"\s", testname) is not None: + raise + elif text[valstart:stop].find(":") != -1: + left = colon + 1 + stop = 0 + continue + else: + raise + pairs[name] = value + start_match = re.search(r"[^,;\s]", text[stop + 1 :]) + if start_match is None: + continue + start = start_match.start() + stop + 1 + left = start return pairs diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 5fa03e0edd0..c654eb35d9c 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1088,4 +1088,10 @@ def test_Redlich_Kister(self): Path(self.test_work_dir).joinpath("RedlichKisterVPSSTP_valid.yaml")) ctmlPhase, yamlPhase = self.checkConversion("RedlichKisterVPSSTP_valid") - self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) \ No newline at end of file + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_species_names(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath('species-names.xml'), + Path(self.test_work_dir).joinpath('species-names.yaml')) + ctmlGas, yamlGas = self.checkConversion('species-names') + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) diff --git a/test/data/species-names.xml b/test/data/species-names.xml new file mode 100644 index 00000000000..731d28905cf --- /dev/null +++ b/test/data/species-names.xml @@ -0,0 +1,370 @@ + + + + + + + H C + + (Parens) @#$%^-2 co:lons: [xy2]*{.} + plus+ eq=uals plus trans_butene + co + + + 300.0 + 101325.0 + + + + + + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + nottheend + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + C:1 H:4 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + + + (Parens) + @#$%^-2 [=] 2 [xy2]*{.} + + + 1.234500E+09 + 1.0 + 200.000000 + + + (Parens):1.0 @#$%^-2:1 + [xy2]*{.}:2.0 + + + + + 2 (Parens) + [xy2]*{.} [=] 3 @#$%^-2 + + + 5.432100E+04 + 1.0 + 500.000000 + + + (Parens):2.0 [xy2]*{.}:1 + @#$%^-2:3.0 + + + + + plus+ + (Parens) [=] 2 plus+ + + + 9.999000E+06 + 9.9 + 999.900000 + + + plus+:1.0 (Parens):1 + plus+:2.0 + + + + + 2 plus+ + eq=uals [=] 3 (Parens) + + + 9.999000E+03 + 9.9 + 999.900000 + + + plus+:2.0 eq=uals:1 + (Parens):3.0 + + + + + plus + plus+ [=] 2 (Parens) + + + 9.999000E+06 + 9.9 + 999.900000 + + + plus:1.0 plus+:1 + (Parens):2.0 + + + + + plus + eq=uals [=] plus+ + (Parens) + + + 9.999000E+06 + 9.9 + 999.900000 + + + plus:1.0 eq=uals:1 + plus+:1.0 (Parens):1 + + + + + co:lons: + eq=uals [=] 2 (Parens) + + + 9.999000E+06 + 9.9 + 999.900000 + + + co:lons::1.0 eq=uals:1 + (Parens):2.0 + + + + + plus+ (+ plus) [=] eq=uals (+ plus) + + + 9.999000E+09 + 9.9 + 999.900000 + + + 8.888000E+05 + 8.8 + 888.800000 + + plus:1.0 + + + plus+:1.0 + eq=uals:1.0 + + + + + plus+ (+ (Parens)) [=] eq=uals (+ (Parens)) + + + 9.999000E+09 + 9.9 + 999.900000 + + + 8.888000E+05 + 8.8 + 888.800000 + + (Parens):1.0 + + + plus+:1.0 + eq=uals:1.0 + + + + + plus+ (+ plus+) [=] eq=uals (+ plus+) + + + 9.999000E+09 + 9.9 + 999.900000 + + + 8.888000E+05 + 8.8 + 888.800000 + + plus+:1.0 + + + plus+:1.0 + eq=uals:1.0 + + + + + trans_butene + co:lons: [=] 2 plus+ + + + 9.999000E+06 + 9.9 + 999.900000 + + + trans_butene:1.0 co:lons::1 + plus+:2.0 + + + + + co + co:lons: [=] 2 plus+ + + + 9.999000E+06 + 9.9 + 999.900000 + + + co:1.0 co:lons::1 + plus+:2.0 + + + From 51ae226642de363b23532dedfd62a7bf35f55709 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 12:24:36 -0500 Subject: [PATCH 66/99] [ctml2yaml] Add SRI falloff reaction type --- interfaces/cython/cantera/ctml2yaml.py | 22 ++++++++++++++++++- .../cython/cantera/test/test_convert.py | 7 ++++++ 2 files changed, 28 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 86166295c89..f8169ae4fd3 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1410,7 +1410,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): "Falloff reaction types must have a falloff node.", rate_coeff ) falloff_type = falloff_node.get("type") - if falloff_type not in ["Lindemann", "Troe"]: + if falloff_type not in ["Lindemann", "Troe", "SRI"]: raise TypeError( "Unknown falloff type '{}' for reaction id {}".format( falloff_type, reaction.get("id") @@ -1488,6 +1488,26 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): def to_yaml(cls, representer, data): return representer.represent_dict(data.attribs) + def sri( + self, rate_coeff: etree.Element + ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + """Process an SRI reaction. + + Returns a dictionary with the appropriate fields set that is + used to update the parent reaction entry dictionary. + """ + reaction_attribs = self.lindemann((rate_coeff)) + falloff_node = rate_coeff.find("falloff") + if falloff_node is None: + raise MissingXMLNode("SRI reaction requires falloff node", rate_coeff) + SRI_names = list("ABCDE") + SRI_data = FlowMap({}) + for name, param in zip(SRI_names, clean_node_text(falloff_node).split()): + SRI_data[name] = float(param) + + reaction_attribs["SRI"] = SRI_data + return reaction_attribs + def threebody( self, rate_coeff: etree.Element ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index c654eb35d9c..b148d6e73da 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1095,3 +1095,10 @@ def test_species_names(self): Path(self.test_work_dir).joinpath('species-names.yaml')) ctmlGas, yamlGas = self.checkConversion('species-names') self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + + def test_sri_falloff_reaction(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("sri-falloff.xml"), + Path(self.test_work_dir).joinpath("sri-falloff.yaml")) + ctmlGas, yamlGas = self.checkConversion("sri-falloff") + self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) + self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) From 80816a3c8f4f10dc75d8eefd41b3114d4dcbb26e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 3 Nov 2019 11:53:16 -0500 Subject: [PATCH 67/99] [ctml2yaml] Move reaction equation to top of node --- interfaces/cython/cantera/ctml2yaml.py | 19 ++++++++++--------- 1 file changed, 10 insertions(+), 9 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index f8169ae4fd3..efaaa575f44 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1384,6 +1384,16 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): except ValueError: self.attribs["id"] = reaction_id + reaction_equation = reaction.findtext("equation") + if reaction_equation is None: + raise MissingNodeText("The reaction must have an equation", reaction) + + # This has to replace the reaction direction symbols separately because + # species names can have [ or ] in them + self.attribs["equation"] = reaction_equation.replace("[=]", "<=>").replace( + "=]", "=>" + ) + reaction_type = reaction.get("type", "arrhenius") rate_coeff = reaction.find("rateCoeff") if rate_coeff is None: @@ -1438,15 +1448,6 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): if node_motz_wise and self.attribs.get("Motz-Wise") is None: self.attribs["Motz-Wise"] = True - reaction_equation = reaction.findtext("equation") - if reaction_equation is None: - raise MissingNodeText("The reaction must have an equation", reaction) - - # This has to replace the reaction direction symbols separately because - # species names can have [ or ] in them - self.attribs["equation"] = reaction_equation.replace("[=]", "<=>").replace( - "=]", "=>" - ) if reaction.get("negative_A", "").lower() == "yes": self.attribs["negative-A"] = True From df6bcc844466ae32860e8b34954864110eb24fca Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 3 Nov 2019 15:49:45 -0500 Subject: [PATCH 68/99] [ctml2yaml] Simplify reaction type hints --- interfaces/cython/cantera/ctml2yaml.py | 71 ++++++++++++++------------ 1 file changed, 37 insertions(+), 34 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index efaaa575f44..0cd76d6c24f 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -36,6 +36,26 @@ DH_BETA_MATRIX = TypedDict( "DH_BETA_MATRIX", {"species": List[str], "beta": Union[str, float]}, total=False ) + ARRHENIUS_PARAMS = Dict[str, Union[float, str]] + EFFICIENCY_PARAMS = Dict[str, float] + LINDEMANN_PARAMS = Union[str, ARRHENIUS_PARAMS, EFFICIENCY_PARAMS] + TROE_PARAMS = Dict[str, float] + SRI_PARAMS = Dict[str, float] + COVERAGE_PARAMS = Dict[str, ARRHENIUS_PARAMS] + + ARRHENIUS_TYPE = Dict[str, ARRHENIUS_PARAMS] + EDGE_TYPE = Dict[str, Union[ARRHENIUS_PARAMS, float, bool]] + SURFACE_TYPE = Dict[str, Union[ARRHENIUS_PARAMS, bool, str, COVERAGE_PARAMS]] + NESTED_LIST_OF_FLOATS = List[List[float]] + CHEBYSHEV_TYPE = Dict[str, Union[List[float], NESTED_LIST_OF_FLOATS, str]] + PLOG_TYPE = Dict[str, Union[str, List[ARRHENIUS_PARAMS]]] + CHEMACT_TYPE = Dict[ + str, Union[str, ARRHENIUS_PARAMS, EFFICIENCY_PARAMS, TROE_PARAMS] + ] + LINDEMANN_TYPE = Dict[str, LINDEMANN_PARAMS] + TROE_TYPE = Dict[str, Union[LINDEMANN_PARAMS, TROE_PARAMS]] + THREEBODY_TYPE = Dict[str, Union[ARRHENIUS_PARAMS, EFFICIENCY_PARAMS]] + SRI_TYPE = Dict[str, Union[LINDEMANN_PARAMS, SRI_PARAMS]] BlockMap = yaml.comments.CommentedMap @@ -1489,9 +1509,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): def to_yaml(cls, representer, data): return representer.represent_dict(data.attribs) - def sri( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def sri(self, rate_coeff: etree.Element) -> "SRI_TYPE": """Process an SRI reaction. Returns a dictionary with the appropriate fields set that is @@ -1509,9 +1527,7 @@ def sri( reaction_attribs["SRI"] = SRI_data return reaction_attribs - def threebody( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def threebody(self, rate_coeff: etree.Element) -> "THREEBODY_TYPE": """Process a three-body reaction. Returns a dictionary with the appropriate fields set that is @@ -1527,9 +1543,7 @@ def threebody( return reaction_attribs - def lindemann( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def lindemann(self, rate_coeff: etree.Element) -> "LINDEMANN_TYPE": """Process a Lindemann falloff reaction. Returns a dictionary with the appropriate fields set that is @@ -1537,7 +1551,7 @@ def lindemann( """ reaction_attribs = FlowMap({"type": "falloff"}) for arr_coeff in rate_coeff.iterfind("Arrhenius"): - if arr_coeff.get("name") is not None and arr_coeff.get("name") == "k0": + if arr_coeff.get("name") == "k0": reaction_attribs[ "low-P-rate-constant" ] = self.process_arrhenius_parameters(arr_coeff) @@ -1553,9 +1567,7 @@ def lindemann( return reaction_attribs - def troe( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def troe(self, rate_coeff: etree.Element) -> "TROE_TYPE": """Process a Troe falloff reaction. Returns a dictionary with the appropriate fields set that is @@ -1580,9 +1592,7 @@ def troe( return reaction_attribs - def chemact( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def chemact(self, rate_coeff: etree.Element) -> "CHEMACT_TYPE": """Process a chemically activated falloff reaction. Returns a dictionary with the appropriate fields set that is @@ -1590,7 +1600,7 @@ def chemact( """ reaction_attribs = FlowMap({"type": "chemically-activated"}) for arr_coeff in rate_coeff.iterfind("Arrhenius"): - if arr_coeff.get("name") is not None and arr_coeff.get("name") == "kHigh": + if arr_coeff.get("name") == "kHigh": reaction_attribs[ "high-P-rate-constant" ] = self.process_arrhenius_parameters(arr_coeff) @@ -1607,7 +1617,8 @@ def chemact( troe_node = rate_coeff.find("falloff") if troe_node is None: raise MissingXMLNode( - "Troe reaction types must include a falloff node", rate_coeff + "Chemically activated reaction types must include a falloff node", + rate_coeff, ) troe_params = clean_node_text(troe_node).split() troe_names = ["A", "T3", "T1", "T2"] @@ -1620,9 +1631,7 @@ def chemact( return reaction_attribs - def plog( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Dict[str, Union[str, float]]]]: + def plog(self, rate_coeff: etree.Element) -> "PLOG_TYPE": """Process a PLOG reaction. Returns a dictionary with the appropriate fields set that is @@ -1643,9 +1652,7 @@ def plog( return reaction_attributes - def chebyshev( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable[float]]]: + def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": """Process a Chebyshev reaction. Returns a dictionary with the appropriate fields set that is @@ -1700,9 +1707,7 @@ def chebyshev( return reaction_attributes - def surface( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def surface(self, rate_coeff: etree.Element) -> "SURFACE_TYPE": """Process a surface reaction. Returns a dictionary with the appropriate fields set that is @@ -1750,9 +1755,7 @@ def surface( return reaction_attributes - def edge( - self, rate_coeff: etree.Element - ) -> Dict[str, Union[str, Iterable, Dict[str, float]]]: + def edge(self, rate_coeff: etree.Element) -> "EDGE_TYPE": """Process an edge reaction. Returns a dictionary with the appropriate fields set that is @@ -1772,12 +1775,12 @@ def edge( "rate-constant": self.process_arrhenius_parameters(arr_node), "beta": float(beta), } - ) + ) # type: EDGE_TYPE if rate_coeff.get("type") == "exchangecurrentdensity": reaction_attributes["exchange-current-density-formulation"] = True return reaction_attributes - def arrhenius(self, rate_coeff: etree.Element) -> Dict[str, Dict[str, float]]: + def arrhenius(self, rate_coeff: etree.Element) -> "ARRHENIUS_TYPE": """Process a standard Arrhenius-type reaction. Returns a dictionary with the appropriate fields set that is @@ -1793,7 +1796,7 @@ def arrhenius(self, rate_coeff: etree.Element) -> Dict[str, Dict[str, float]]: def process_arrhenius_parameters( self, arr_node: Optional[etree.Element] - ) -> Dict[str, Union[float, str]]: + ) -> "ARRHENIUS_PARAMS": """Process the parameters from an Arrhenius child of a rateCoeff node.""" if arr_node is None: raise MissingXMLNode("The Arrhenius node must be present.") @@ -1813,7 +1816,7 @@ def process_arrhenius_parameters( } ) - def process_efficiencies(self, eff_node: etree.Element) -> Dict[str, float]: + def process_efficiencies(self, eff_node: etree.Element) -> "EFFICIENCY_PARAMS": """Process the efficiency information about a reaction.""" efficiencies = [eff.rsplit(":", 1) for eff in clean_node_text(eff_node).split()] return FlowMap({s: float(e) for s, e in efficiencies}) From b10d1d8a492934379af9684e48019ed4d38ff910 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 3 Nov 2019 16:42:30 -0500 Subject: [PATCH 69/99] [Test] Replace phaseid and phases args to converters --- .../cython/cantera/test/test_convert.py | 42 +++++++++---------- 1 file changed, 21 insertions(+), 21 deletions(-) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index b148d6e73da..12b8e95c7b1 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -736,8 +736,8 @@ class ctml2yamlTest(utilities.CanteraTest): def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(), yamlphases=(), **kwargs): - ctmlPhase = cls(basename + '.xml', phases=ctmlphases, **kwargs) - yamlPhase = cls(basename + '.yaml', phases=yamlphases, **kwargs) + ctmlPhase = cls(basename + '.xml', adjacent=ctmlphases, **kwargs) + yamlPhase = cls(basename + '.yaml', adjacent=yamlphases, **kwargs) self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) @@ -827,7 +827,7 @@ def test_ptcombust(self): Path(self.test_work_dir).joinpath('ptcombust.yaml')) ctmlGas, yamlGas = self.checkConversion('ptcombust') ctmlSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface, - phaseid='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) + name='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5]) self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600]) @@ -838,7 +838,7 @@ def test_ptcombust_motzwise(self): Path(self.test_work_dir).joinpath('ptcombust-motzwise.yaml')) ctmlGas, yamlGas = self.checkConversion('ptcombust-motzwise') ctmlSurf, yamlSurf = self.checkConversion('ptcombust-motzwise', ct.Interface, - phaseid='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) + name='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) self.checkKinetics(ctmlGas, yamlGas, [500, 1200], [1e4, 3e5]) self.checkThermo(ctmlSurf, yamlSurf, [400, 800, 1600]) @@ -848,16 +848,16 @@ def test_sofc(self): ctml2yaml.convert(Path(self.cantera_data).joinpath('sofc.xml'), Path(self.test_work_dir).joinpath('sofc.yaml')) ctmlGas, yamlGas = self.checkConversion('sofc') - ctmlMetal, yamlMetal = self.checkConversion('sofc', phaseid='metal') - ctmlOxide, yamlOxide = self.checkConversion('sofc', phaseid='oxide_bulk') + ctmlMetal, yamlMetal = self.checkConversion('sofc', name='metal') + ctmlOxide, yamlOxide = self.checkConversion('sofc', name='oxide_bulk') ctmlMSurf, yamlMSurf = self.checkConversion('sofc', ct.Interface, - phaseid='metal_surface', ctmlphases=[ctmlGas, ctmlMetal], + name='metal_surface', ctmlphases=[ctmlGas, ctmlMetal], yamlphases=[yamlGas, yamlMetal]) ctmlOSurf, yamlOSurf = self.checkConversion('sofc', ct.Interface, - phaseid='oxide_surface', ctmlphases=[ctmlGas, ctmlOxide], + name='oxide_surface', ctmlphases=[ctmlGas, ctmlOxide], yamlphases=[yamlGas, yamlOxide]) ctml_tpb, yaml_tpb = self.checkConversion('sofc', ct.Interface, - phaseid='tpb', ctmlphases=[ctmlMetal, ctmlMSurf, ctmlOSurf], + name='tpb', ctmlphases=[ctmlMetal, ctmlMSurf, ctmlOSurf], yamlphases=[yamlMetal, yamlMSurf, yamlOSurf]) self.checkThermo(ctmlMSurf, yamlMSurf, [900, 1000, 1100]) @@ -872,7 +872,7 @@ def test_liquidvapor(self): Path(self.test_work_dir).joinpath('liquidvapor.yaml')) for name in ['water', 'nitrogen', 'methane', 'hydrogen', 'oxygen', 'hfc134a', 'carbondioxide', 'heptane']: - ctmlPhase, yamlPhase = self.checkConversion('liquidvapor', phaseid=name) + ctmlPhase, yamlPhase = self.checkConversion('liquidvapor', name=name) self.checkThermo(ctmlPhase, yamlPhase, [1.3 * ctmlPhase.min_temp, 0.7 * ctmlPhase.max_temp]) @@ -897,10 +897,10 @@ def test_Redlich_Kwong_ndodecane(self): def test_diamond(self): ctml2yaml.convert(Path(self.cantera_data).joinpath('diamond.xml'), Path(self.test_work_dir).joinpath('diamond.yaml')) - ctmlGas, yamlGas = self.checkConversion('diamond', phaseid='gas') - ctmlSolid, yamlSolid = self.checkConversion('diamond', phaseid='diamond') + ctmlGas, yamlGas = self.checkConversion('diamond', name='gas') + ctmlSolid, yamlSolid = self.checkConversion('diamond', name='diamond') ctmlSurf, yamlSurf = self.checkConversion('diamond', - ct.Interface, phaseid='diamond_100', ctmlphases=[ctmlGas, ctmlSolid], + ct.Interface, name='diamond_100', ctmlphases=[ctmlGas, ctmlSolid], yamlphases=[yamlGas, yamlSolid]) self.checkThermo(ctmlSolid, yamlSolid, [300, 500]) self.checkThermo(ctmlSurf, yamlSurf, [330, 490]) @@ -910,16 +910,16 @@ def test_lithium_ion_battery(self): name = 'lithium_ion_battery' ctml2yaml.convert(Path(self.cantera_data).joinpath(name + ".xml"), Path(self.test_work_dir).joinpath(name + ".yaml")) - ctmlAnode, yamlAnode = self.checkConversion(name, phaseid='anode') - ctmlCathode, yamlCathode = self.checkConversion(name, phaseid='cathode') - ctmlMetal, yamlMetal = self.checkConversion(name, phaseid='electron') - ctmlElyt, yamlElyt = self.checkConversion(name, phaseid='electrolyte') + ctmlAnode, yamlAnode = self.checkConversion(name, name='anode') + ctmlCathode, yamlCathode = self.checkConversion(name, name='cathode') + ctmlMetal, yamlMetal = self.checkConversion(name, name='electron') + ctmlElyt, yamlElyt = self.checkConversion(name, name='electrolyte') ctmlAnodeInt, yamlAnodeInt = self.checkConversion(name, - phaseid='edge_anode_electrolyte', + name='edge_anode_electrolyte', ctmlphases=[ctmlAnode, ctmlMetal, ctmlElyt], yamlphases=[yamlAnode, yamlMetal, yamlElyt]) ctmlCathodeInt, yamlCathodeInt = self.checkConversion(name, - phaseid='edge_cathode_electrolyte', + name='edge_cathode_electrolyte', ctmlphases=[ctmlCathode, ctmlMetal, ctmlElyt], yamlphases=[yamlCathode, yamlMetal, yamlElyt]) @@ -1043,14 +1043,14 @@ def test_NaCl_solid_phase(self): def test_DH_NaCl_phase(self): ctml2yaml.convert(Path(self.test_data_dir).joinpath("debye-huckel-all.xml"), Path(self.test_work_dir).joinpath("debye-huckel-all.yaml")) - for phaseid in [ + for name in [ "debye-huckel-dilute", "debye-huckel-B-dot-ak", "debye-huckel-B-dot-a", "debye-huckel-pitzer-beta_ij", "debye-huckel-beta_ij", ]: - ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", phaseid=phaseid) + ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", name=name) self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_Maskell_solid_soln(self): From 794905c23c5213013ae3e298309cc55e434e2d0f Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 3 Nov 2019 19:02:00 -0500 Subject: [PATCH 70/99] [ctml2yaml] Refactor SpeciesThermo for polynomials Extract a method to process the polynomials. Ensure that sorting of the temperature ranges correlates with the sorting of the data. --- interfaces/cython/cantera/ctml2yaml.py | 107 ++++++++++--------------- 1 file changed, 43 insertions(+), 64 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 0cd76d6c24f..a171415d3fc 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -10,7 +10,7 @@ from email.utils import formatdate import warnings -from typing import Any, Dict, Union, Iterable, Optional, List +from typing import Any, Dict, Union, Iterable, Optional, List, Tuple from typing import TYPE_CHECKING try: @@ -57,6 +57,8 @@ THREEBODY_TYPE = Dict[str, Union[ARRHENIUS_PARAMS, EFFICIENCY_PARAMS]] SRI_TYPE = Dict[str, Union[LINDEMANN_PARAMS, SRI_PARAMS]] + THERMO_POLY_TYPE = Union[List[List[float]], List[float]] + BlockMap = yaml.comments.CommentedMap @@ -1094,85 +1096,62 @@ def __init__(self, thermo: etree.Element) -> None: func = getattr(self, thermo_type) self.attribs = func(thermo) - def Shomate(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: - """Process a Shomate polynomial from XML to a dictionary.""" - thermo_attribs = BlockMap({"model": "Shomate", "data": []}) + def process_polynomial( + self, thermo: etree.Element, poly_type: str + ) -> Tuple[List[List[float]], List[float]]: + """Process data from one of the polynomial type species thermo types.""" temperature_ranges = set() - model_nodes = thermo.findall("Shomate") + model_nodes = thermo.findall(poly_type) + unsorted_data = {} for node in model_nodes: Tmin = float(node.get("Tmin", 0)) Tmax = float(node.get("Tmax", 0)) if not Tmin or not Tmax: - raise ValueError("Tmin and Tmax must both be specified") - temperature_ranges.add(float(Tmin)) - temperature_ranges.add(float(Tmax)) - float_array = node.findtext("floatArray") + raise MissingXMLAttribute("Tmin and Tmax must both be specified", node) + temperature_ranges.add(Tmin) + temperature_ranges.add(Tmax) + float_array = node.find("floatArray") if float_array is None: - raise ValueError( - "Shomate entry missing floatArray node with text: " - "'{}'".format(node) + raise MissingXMLNode( + "{} entry missing floatArray node".format(poly_type), node ) - coeffs = float_array.replace("\n", " ").strip().split(",") - thermo_attribs["data"].append(FlowList(map(float, coeffs))) + unsorted_data[Tmin] = FlowList( + map(float, clean_node_text(float_array).split(",")) + ) + if len(temperature_ranges) != len(model_nodes) + 1: raise ValueError( - "The midpoint temperature is not consistent between Shomate entries" + "The midpoint temperature is not consistent between {} " + "entries".format(poly_type) ) - thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) + data = [] + for key in sorted(unsorted_data.keys()): + data.append(unsorted_data[key]) + + return data, FlowList(sorted(temperature_ranges)) + + def Shomate(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: + """Process a Shomate polynomial from XML to a dictionary.""" + thermo_attribs = BlockMap({"model": "Shomate"}) + data, temperature_ranges = self.process_polynomial(thermo, "Shomate") + thermo_attribs["temperature-ranges"] = temperature_ranges + thermo_attribs["data"] = data return thermo_attribs - def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: + def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: """Process a NASA 7 thermo entry from XML to a dictionary.""" - thermo_attribs = BlockMap({"model": "NASA7", "data": []}) - temperature_ranges = set() - model_nodes = thermo.findall("NASA") - for node in model_nodes: - Tmin = float(node.get("Tmin", 0)) - Tmax = float(node.get("Tmax", 0)) - if not Tmin or not Tmax: - raise ValueError("Tmin and Tmax must both be specified") - temperature_ranges.add(float(Tmin)) - temperature_ranges.add(float(Tmax)) - float_array = node.findtext("floatArray") - if float_array is None: - raise ValueError( - "Shomate entry missing floatArray node with text: " - "'{}'".format(node) - ) - coeffs = float_array.replace("\n", " ").strip().split(",") - thermo_attribs["data"].append(FlowList(map(float, coeffs))) - if len(temperature_ranges) != len(model_nodes) + 1: - raise ValueError( - "The midpoint temperature is not consistent between NASA7 entries" - ) - thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) + thermo_attribs = BlockMap({"model": "NASA7"}) + data, temperature_ranges = self.process_polynomial(thermo, "NASA") + thermo_attribs["temperature-ranges"] = temperature_ranges + thermo_attribs["data"] = data return thermo_attribs - def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, Iterable]]: + def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: """Process a NASA 9 thermo entry from XML to a dictionary.""" - thermo_attribs = BlockMap({"model": "NASA9", "data": []}) - temperature_ranges = set() - model_nodes = thermo.findall("NASA9") - for node in model_nodes: - Tmin = float(node.get("Tmin", 0)) - Tmax = float(node.get("Tmax", 0)) - if not Tmin or not Tmax: - raise ValueError("Tmin and Tmax must both be specified") - temperature_ranges.add(float(Tmin)) - temperature_ranges.add(float(Tmax)) - float_array = node.findtext("floatArray") - if float_array is None: - raise ValueError( - "Shomate entry missing floatArray node with text: " - "'{}'".format(node) - ) - coeffs = float_array.replace("\n", " ").strip().split(",") - thermo_attribs["data"].append(FlowList(map(float, coeffs))) - if len(temperature_ranges) != len(model_nodes) + 1: - raise ValueError( - "The midpoint temperature is not consistent between NASA9 entries" - ) - thermo_attribs["temperature-ranges"] = FlowList(sorted(temperature_ranges)) + thermo_attribs = BlockMap({"model": "NASA9"}) + data, temperature_ranges = self.process_polynomial(thermo, "NASA9") + thermo_attribs["temperature-ranges"] = temperature_ranges + thermo_attribs["data"] = data return thermo_attribs def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: From 89fd38ba6744bb80e84b94f205e99337c977738d Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 4 Nov 2019 14:08:47 -0500 Subject: [PATCH 71/99] [ctml2yaml] Add Ideal VPSS and PDSS_HKFT types --- interfaces/cython/cantera/ctml2yaml.py | 78 +++++++++-- .../cython/cantera/test/test_convert.py | 17 +++ test/data/pdss_hkft.xml | 132 ++++++++++++++++++ 3 files changed, 215 insertions(+), 12 deletions(-) create mode 100644 test/data/pdss_hkft.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index a171415d3fc..58090fc9cfd 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -58,6 +58,7 @@ SRI_TYPE = Dict[str, Union[LINDEMANN_PARAMS, SRI_PARAMS]] THERMO_POLY_TYPE = Union[List[List[float]], List[float]] + HKFT_THERMO_TYPE = Union[str, float, List[Union[str, float]]] BlockMap = yaml.comments.CommentedMap @@ -262,7 +263,8 @@ class Phase: "PureLiquidWater": "liquid-water-IAPWS95", "HMW": "HMW-electrolyte", "DebyeHuckel": "Debye-Huckel", - "MaskellSolidSolnPhase": "Maskell-solid-solution", + "IdealGasVPSS": "ideal-gas-VPSS", + "IdealSolnVPSS": "ideal-solution-VPSS", "IonsFromNeutralMolecule": "ions-from-neutral-molecule", "Margules": "Margules", "Redlich-Kister": "Redlich-Kister", @@ -1130,7 +1132,9 @@ def process_polynomial( return data, FlowList(sorted(temperature_ranges)) - def Shomate(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: + def Shomate( + self, thermo: etree.Element + ) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: """Process a Shomate polynomial from XML to a dictionary.""" thermo_attribs = BlockMap({"model": "Shomate"}) data, temperature_ranges = self.process_polynomial(thermo, "Shomate") @@ -1284,10 +1288,10 @@ def __init__(self, species_node: etree.Element): thermo = species_node.find("thermo") if thermo is not None: thermo_model = thermo.get("model", "") - # HKFT and IonFromNeutral species thermo nodes are not used from the XML - # and are not present in the YAML specification. However, there is some - # information in the thermo node for IonFromNeutral that needs to end up - # in the equation-of-state YAML node + # The IonFromNeutral species thermo node does not correspond to a + # SpeciesThermo type and the IonFromNeutral model doesn't have a thermo + # node in the YAML format. Instead, the data from the XML thermo node are + # moved to the equation-of-state node in YAML if thermo_model.lower() == "ionfromneutral": neutral_spec_mult_node = thermo.find("neutralSpeciesMultipliers") if neutral_spec_mult_node is None: @@ -1308,7 +1312,7 @@ def __init__(self, species_node: etree.Element): if thermo.find("specialSpecies") is not None: self.attribs["equation-of-state"]["special-species"] = True elif thermo_model.lower() == "hkft": - pass + self.attribs["equation-of-state"] = self.hkft(species_node) else: self.attribs["thermo"] = SpeciesThermo(thermo) @@ -1327,10 +1331,59 @@ def __init__(self, species_node: etree.Element): if weak_acid_charge is not None: self.attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) + def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: + """Process a species with HKFT thermo type. + + Done in a method because it requires synthesizing data from the thermo node + and the standardState node. + """ + thermo_node = species_node.find("./thermo/HKFT") + std_state_node = species_node.find("standardState") + if thermo_node is None or std_state_node is None: + raise MissingXMLNode( + "An HKFT species requires both the thermo and standardState nodes", + species_node, + ) + eqn_of_state = BlockMap({"model": "HKFT"}) + for t_node in thermo_node: + if t_node.tag == "DH0_f_Pr_Tr": + eqn_of_state["h0"] = get_float_or_units(t_node) + elif t_node.tag == "DG0_f_Pr_Tr": + eqn_of_state["g0"] = get_float_or_units(t_node) + elif t_node.tag == "S0_Pr_Tr": + eqn_of_state["s0"] = get_float_or_units(t_node) + + a = FlowList([]) + c = FlowList([]) + for tag in ["a1", "a2", "a3", "a4", "c1", "c2"]: + node = std_state_node.find(tag) + if node is None: + raise MissingXMLNode( + "The HKFT standardState node requires a child node with " + "tag '{}'".format(tag), + std_state_node, + ) + if tag.startswith("a"): + a.append(get_float_or_units(node)) + elif tag.startswith("c"): + c.append(get_float_or_units(node)) + eqn_of_state["a"] = a + eqn_of_state["c"] = c + omega_node = std_state_node.find("omega_Pr_Tr") + if omega_node is None: + raise MissingXMLNode( + "The HKFT standardState node requires a child node with " + "tag 'omega_Pr_Tr'", + std_state_node + ) + eqn_of_state["omega"] = get_float_or_units(omega_node) + + return eqn_of_state + def process_standard_state_node(self, species_node: etree.Element) -> None: """Process the standardState node in a species definition. - If the model is IonFromNeutral, this function doesn't do anything to + If the model is IonFromNeutral or HKFT, this function doesn't do anything to the species object. Otherwise, the model data is put into the YAML equation-of-state node. """ @@ -1338,10 +1391,11 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: if std_state is not None: std_state_model = std_state.get("model") if std_state_model is None: - raise MissingXMLAttribute("Unknown standard state model", std_state) - elif std_state_model.lower() == "ionfromneutral": - # If the standard state model is IonFromNeutral, we don't need to do - # anything with it + std_state_model = "ideal-gas" + elif std_state_model.lower() in ["ionfromneutral", "hkft"]: + # If the standard state model is IonFromNeutral or HKFT, we don't + # need to do anything with it because it is processed above in the + # species __init__ function return eqn_of_state = { diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 12b8e95c7b1..2a92d60e4e6 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1102,3 +1102,20 @@ def test_sri_falloff_reaction(self): ctmlGas, yamlGas = self.checkConversion("sri-falloff") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) + + def test_vpss_and_hkft(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("pdss_hkft.xml"), + Path(self.test_work_dir).joinpath("pdss_hkft.yaml")) + + for name in ["vpss_gas_pdss_hkft_phase", "vpss_soln_pdss_hkft_phase"]: + ctmlPhase = ct.ThermoPhase("pdss_hkft.xml", name=name) + yamlPhase = ct.ThermoPhase("pdss_hkft.yaml", name=name) + # Due to changes in how the species elements are specified, the + # composition of the species differs from XML to YAML (electrons are used + # to specify charge in YAML while the charge node is used in XML). + # Therefore, checkConversion won't work and we have to check a few things + # manually. There are also no reactions specified for these phases so don't + # need to do any checks for that. + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) diff --git a/test/data/pdss_hkft.xml b/test/data/pdss_hkft.xml new file mode 100644 index 00000000000..39b9e644403 --- /dev/null +++ b/test/data/pdss_hkft.xml @@ -0,0 +1,132 @@ + + + + + + + Na Cl H O + + + H2O(L) Na+ Cl- H+ OH- + + + + + + + + + Na Cl H O + + + H2O(L) Na+ Cl- H+ OH- + + + + + + + + + + + H:2 O:1 + + + + 7.255750050E+01, -6.624454020E-01, 2.561987460E-03, -4.365919230E-06, + 2.781789810E-09, -4.188654990E+04, -2.882801370E+02 + + + + + + + Na:1 + 1 + + 0.1839 + -228.5 + 3.256 + -27260. + 18.18 + -29810. + 33060 + + + + -57433. + 13.96 + + + + + + Cl:1 + -1 + + 0.4032 + 480.1 + 5.563 + -28470. + -4.4 + -57140. + 145600. + + + + -31379 + 13.56 + + + + + + H:1 + 1 + + 0 + 0 + 0 + 0 + 0 + 0 + 0 + + + + 0 + 0 + + + + + + O:1 H:1 + -1 + + 0.12527 + 7.38 + 1.8423 + -27821 + 4.15 + -103460. + 172460. + + + + -37595. + -54977. + -2.56 + + + + + From 7c800db0466a9b0006a3793014a13b7fbef2154d Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 4 Nov 2019 15:31:11 -0500 Subject: [PATCH 72/99] [ctml2yaml] Add last species standard state models --- interfaces/cython/cantera/ctml2yaml.py | 3 +++ 1 file changed, 3 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 58090fc9cfd..657c07f82e8 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1251,7 +1251,10 @@ class Species: """Represents a species.""" _standard_state_model_mapping = { + "ideal-gas": "ideal-gas", "constant_incompressible": "constant-volume", + "constant-incompressible": "constant-volume", + "waterPDSS": "liquid-water-IAPWS95", "waterIAPWS": "liquid-water-IAPWS95", } _electrolyte_species_type_mapping = { From f1474d7644e4eaf418fe95a675c5f6e57e4c2a24 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 4 Nov 2019 16:05:14 -0500 Subject: [PATCH 73/99] [ctml2yaml] Add LatticeSolid phase --- interfaces/cython/cantera/ctml2yaml.py | 61 +++++-- .../cython/cantera/test/test_convert.py | 13 ++ test/data/Li7Si3_ls.xml | 153 ++++++++++++++++++ 3 files changed, 216 insertions(+), 11 deletions(-) create mode 100644 test/data/Li7Si3_ls.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 657c07f82e8..1c1b36eb003 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -9,6 +9,7 @@ import xml.etree.ElementTree as etree from email.utils import formatdate import warnings +import copy from typing import Any, Dict, Union, Iterable, Optional, List, Tuple from typing import TYPE_CHECKING @@ -250,24 +251,28 @@ class Phase: _thermo_model_mapping = { "IdealGas": "ideal-gas", + "Incompressible": "constant-density", # added, don't need test, deprecated "Surface": "ideal-surface", - "Metal": "electron-cloud", - "Lattice": "lattice", "Edge": "edge", - "PureFluid": "pure-fluid", - "RedlichKwongMFTP": "Redlich-Kwong", + "Metal": "electron-cloud", "StoichSubstance": "fixed-stoichiometry", - "BinarySolutionTabulatedThermo": "binary-solution-tabulated", - "IdealSolidSolution": "ideal-condensed", - "FixedChemPot": "fixed-chemical-potential", - "PureLiquidWater": "liquid-water-IAPWS95", + "PureFluid": "pure-fluid", + "LatticeSolid": "compound-lattice", # added + "Lattice": "lattice", "HMW": "HMW-electrolyte", + "IdealSolidSolution": "ideal-condensed", # added "DebyeHuckel": "Debye-Huckel", + "IdealMolalSolution": "ideal-molal-solution", # added "IdealGasVPSS": "ideal-gas-VPSS", "IdealSolnVPSS": "ideal-solution-VPSS", + "Margules": "Margules", # needs update "IonsFromNeutralMolecule": "ions-from-neutral-molecule", - "Margules": "Margules", + "FixedChemPot": "fixed-chemical-potential", "Redlich-Kister": "Redlich-Kister", + "RedlichKwongMFTP": "Redlich-Kwong", + "MaskellSolidSolnPhase": "Maskell-solid-solution", + "PureLiquidWater": "liquid-water-IAPWS95", + "BinarySolutionTabulatedThermo": "binary-solution-tabulated", } _kinetics_model_mapping = { "GasKinetics": "gas", @@ -284,6 +289,10 @@ class Phase: "Water": "water", "none": None, None: None, + "UnityLewis": "unity-Lewis-number", # added + "CK_Mix": "mixture-averaged-CK", # added + "CK_Multi": "multicomponent-CK", # added + "HighP": "high-pressure", # added } _state_properties_mapping = { @@ -419,6 +428,28 @@ def __init__( "Redlich-Kister thermo model requires activity", phase_thermo ) self.attribs["interactions"] = self.redlich_kister(activity_coefficients) + elif phase_thermo_model == "LatticeSolid": + lattice_array_node = phase_thermo.find("LatticeArray") + if lattice_array_node is None: + raise MissingXMLNode( + "LatticeSolid phase thermo requires a LatticeArray node", + phase_thermo, + ) + self.lattice_nodes = [] # type: List[Phase] + for lattice_phase_node in lattice_array_node.findall("phase"): + self.lattice_nodes.append( + Phase(lattice_phase_node, species_data, reaction_data) + ) + lattice_stoich_node = phase_thermo.find("LatticeStoichiometry") + if lattice_stoich_node is None: + raise MissingXMLNode( + "LatticeSolid phase thermo requires a LatticeStoichiometry node", + phase_thermo, + ) + self.attribs["composition"] = {} + for phase_ratio in clean_node_text(lattice_stoich_node).split(): + p_name, ratio = phase_ratio.rsplit(":", 1) + self.attribs["composition"][p_name.strip()] = float(ratio) for node in phase_thermo: if node.tag == "site_density": @@ -1377,7 +1408,7 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: raise MissingXMLNode( "The HKFT standardState node requires a child node with " "tag 'omega_Pr_Tr'", - std_state_node + std_state_node, ) eqn_of_state["omega"] = get_float_or_units(omega_node) @@ -1911,9 +1942,17 @@ def create_phases_from_data_node( reaction_data: Dict[str, List[Reaction]], ) -> List[Phase]: """Take a phase node and return a phase object.""" - return [ + phases = [ Phase(node, species_data, reaction_data) for node in ctml_tree.iterfind("phase") ] + l_nodes = [] + for p in phases: + if hasattr(p, "lattice_nodes"): + l_nodes.extend(copy.deepcopy(p.lattice_nodes)) + del p.lattice_nodes + if l_nodes: + phases.extend(l_nodes) + return phases def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 2a92d60e4e6..087ed079cbe 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1119,3 +1119,16 @@ def test_vpss_and_hkft(self): self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_lattice_solid(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("Li7Si3_ls.xml"), + Path(self.test_work_dir).joinpath("Li7Si3_ls.yaml")) + # Use ThermoPhase to avoid constructing a default Transport object which + # throws an error for the LatticeSolidPhase + basename = "Li7Si3_ls" + name = "Li7Si3_and_Interstitials(S)" + ctmlPhase = ct.ThermoPhase(basename + ".xml", name=name) + yamlPhase = ct.ThermoPhase(basename + ".yaml", name=name) + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) diff --git a/test/data/Li7Si3_ls.xml b/test/data/Li7Si3_ls.xml new file mode 100644 index 00000000000..6a08cfde95b --- /dev/null +++ b/test/data/Li7Si3_ls.xml @@ -0,0 +1,153 @@ + + + + + + Li Si + + + + + + + + + + + Li Si + + Li7Si3(S) + + 1.39 + + + + + + + + + + Li Si + + Li(i) V(i) + + 1.046344E-2 + + + + + 725.0 + 1.0 + Li(i):0.01 V(i):0.99 + + + + + + + Li7Si3(S):1.0 + Li7Si3_Interstitial:1.0 + + + + + + + + + + + + + Li:7 Si:3 + + + + 295.73961 , + -6.753295 , + -44.538551 , + 29.738846 , + -1.022387 , + -348.88919 , + 554.35647 + + + + + 250.51429 , + 51.125155 , + -28.341244 , + 6.242135 , + 1.346861 , + -328.46578 , + 498.84106 + + + + 1.39 + + + + + + Li:2.3333333333333 Si:1.0 + + + + 98.57987 , + -2.2510983 , + -14.846184 , + 9.9129487 , + -0.34079567, + -116.2964 , + 184.78549 + + + + + 83.504763 , + 17.041718 , + -9.4470813 , + 2.0807117 , + 0.44895367 , + -109.48859 , + 166.28035 + + + + 1.39 + + + + + + Li:1 + + + 298.14999999999998 + 0.0E5 + 20.0 + 20.0 + + + + + + + + + + 298.14999999999998 + 89.8 + 0.0 + 0.0 + + + + + + + + + From b9522a4ffbf5e5a3f02dfd52d2ce29d36fd45d1e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 4 Nov 2019 16:31:36 -0500 Subject: [PATCH 74/99] [ctml2yaml] Finish adding Margules phase thermo --- interfaces/cython/cantera/ctml2yaml.py | 95 ++++++++++++- .../cython/cantera/test/test_convert.py | 6 + test/data/LiKCl_liquid.xml | 132 ++++++++++++++++++ 3 files changed, 230 insertions(+), 3 deletions(-) create mode 100644 test/data/LiKCl_liquid.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 1c1b36eb003..be60529e3c4 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -257,7 +257,7 @@ class Phase: "Metal": "electron-cloud", "StoichSubstance": "fixed-stoichiometry", "PureFluid": "pure-fluid", - "LatticeSolid": "compound-lattice", # added + "LatticeSolid": "compound-lattice", "Lattice": "lattice", "HMW": "HMW-electrolyte", "IdealSolidSolution": "ideal-condensed", # added @@ -265,7 +265,7 @@ class Phase: "IdealMolalSolution": "ideal-molal-solution", # added "IdealGasVPSS": "ideal-gas-VPSS", "IdealSolnVPSS": "ideal-solution-VPSS", - "Margules": "Margules", # needs update + "Margules": "Margules", "IonsFromNeutralMolecule": "ions-from-neutral-molecule", "FixedChemPot": "fixed-chemical-potential", "Redlich-Kister": "Redlich-Kister", @@ -450,6 +450,12 @@ def __init__( for phase_ratio in clean_node_text(lattice_stoich_node).split(): p_name, ratio = phase_ratio.rsplit(":", 1) self.attribs["composition"][p_name.strip()] = float(ratio) + elif phase_thermo_model == "Margules": + activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is not None: + margules_interactions = self.margules(activity_coefficients) + if margules_interactions: + self.attribs["interactions"] = margules_interactions for node in phase_thermo: if node.tag == "site_density": @@ -525,6 +531,84 @@ def __init__( self.check_elements(species, species_data) + def margules( + self, activity_coeffs: etree.Element + ) -> List[Dict[str, List[Union[str, float]]]]: + """Process activity coefficients for a Margules phase. + + Returns a list of interaction data values. Margules does not require the + binaryNeutralSpeciesParameters. + """ + all_binary_params = activity_coeffs.findall("binaryNeutralSpeciesParameters") + interactions = [] + for binary_params in all_binary_params: + species_A = binary_params.get("speciesA") + species_B = binary_params.get("speciesB") + if species_A is None or species_B is None: + raise MissingXMLAttribute( + "binaryNeutralSpeciesParameters node requires speciesA and " + "speciesB attributes", + binary_params, + ) + this_node = { + "species": FlowList([species_A, species_B]) + } # type: Dict[str, List[Union[str, float]]] + excess_enthalpy_node = binary_params.find("excessEnthalpy") + if excess_enthalpy_node is not None: + excess_enthalpy = clean_node_text(excess_enthalpy_node).split(",") + enthalpy_units = excess_enthalpy_node.get("units", "") + if not enthalpy_units: + this_node["excess-enthalpy"] = FlowList(map(float, excess_enthalpy)) + else: + this_node["excess-enthalpy"] = FlowList( + [" ".join([e.strip(), enthalpy_units]) for e in excess_enthalpy] + ) + excess_entropy_node = binary_params.find("excessEntropy") + if excess_entropy_node is not None: + excess_entropy = clean_node_text(excess_entropy_node).split(",") + entropy_units = excess_entropy_node.get("units", "") + if not entropy_units: + this_node["excess-entropy"] = FlowList(map(float, excess_entropy)) + else: + this_node["excess-entropy"] = FlowList( + [" ".join([e.strip(), entropy_units]) for e in excess_entropy] + ) + + excessvol_enth_node = binary_params.find("excessVolume_Enthalpy") + if excessvol_enth_node is not None: + excess_vol_enthalpy = clean_node_text(excessvol_enth_node).split(",") + enthalpy_units = excessvol_enth_node.get("units", "") + if not enthalpy_units: + this_node["excess-volume-enthalpy"] = FlowList( + map(float, excess_vol_enthalpy) + ) + else: + this_node["excess-volume-enthalpy"] = FlowList( + [ + " ".join([e.strip(), enthalpy_units]) + for e in excess_vol_enthalpy + ] + ) + excessvol_entr_node = binary_params.find("excessVolume_Entropy") + if excessvol_entr_node is not None: + excess_vol_entropy = clean_node_text(excessvol_entr_node).split(",") + entropy_units = excessvol_entr_node.get("units", "") + if not entropy_units: + this_node["excess-volume-entropy"] = FlowList( + map(float, excess_vol_entropy) + ) + else: + this_node["excess-volume-entropy"] = FlowList( + [ + " ".join([e.strip(), entropy_units]) + for e in excess_vol_entropy + ] + ) + + interactions.append(this_node) + + return interactions + def redlich_kister( self, activity_coeffs: etree.Element ) -> List[Dict[str, List[Union[str, float]]]]: @@ -1322,6 +1406,10 @@ def __init__(self, species_node: etree.Element): thermo = species_node.find("thermo") if thermo is not None: thermo_model = thermo.get("model", "") + # This node is not used anywhere + pseudo_species = thermo.find("pseudoSpecies") + if pseudo_species is not None: + thermo.remove(pseudo_species) # The IonFromNeutral species thermo node does not correspond to a # SpeciesThermo type and the IonFromNeutral model doesn't have a thermo # node in the YAML format. Instead, the data from the XML thermo node are @@ -1348,7 +1436,8 @@ def __init__(self, species_node: etree.Element): elif thermo_model.lower() == "hkft": self.attribs["equation-of-state"] = self.hkft(species_node) else: - self.attribs["thermo"] = SpeciesThermo(thermo) + if len(thermo) > 0: + self.attribs["thermo"] = SpeciesThermo(thermo) transport = species_node.find("transport") if transport is not None: diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 087ed079cbe..fbad2caf0a9 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1132,3 +1132,9 @@ def test_lattice_solid(self): self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_margules(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("LiKCl_liquid.xml"), + Path(self.test_work_dir).joinpath("LiKCl_liquid.yaml")) + ctmlPhase, yamlPhase = self.checkConversion("LiKCl_liquid") + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) \ No newline at end of file diff --git a/test/data/LiKCl_liquid.xml b/test/data/LiKCl_liquid.xml new file mode 100644 index 00000000000..3d0ff8a7358 --- /dev/null +++ b/test/data/LiKCl_liquid.xml @@ -0,0 +1,132 @@ + + + + + + + + + Li K Cl + + + LiCl(L) KCl(L) + + + + + + + + + -17570., -377 + + + -7.627, 4.958 + + + + + + + + + + + + + + + Li:1 + 1 + + + + LiCl(L) + + + + 0.0, 0.0, 0.0, + 0.0, 0.0, 0.0, + 0.0 + + + + 0.0 + + + + + + K:1 + 1 + + + KCl(L) + + + 0.0 + + + + + + Cl- + -1 + + + LiCl(L) + + + 0.0 + + + + + + K:1 Cl:1 + + + + 73.59698, 0.0, 0.0, + 0.0, 0.0, -443.7341, + 175.7209 + + + + + 37.57 + + + + + + Li:1 Cl:1 + + + + 73.18025, -9.047232, -0.316390, + 0.079587, 0.013594, -417.1314, + 157.6711 + + + + + 20.304 + + + + + + + + + From 1745c4a0bebf6bb7af0d9a1304ad8bdc625f78e5 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 4 Nov 2019 18:18:21 -0500 Subject: [PATCH 75/99] [Test] Use ThermoPhase to load Debye-Huckel --- interfaces/cython/cantera/test/test_convert.py | 8 +++++++- 1 file changed, 7 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index fbad2caf0a9..581e4e34c54 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1050,7 +1050,13 @@ def test_DH_NaCl_phase(self): "debye-huckel-pitzer-beta_ij", "debye-huckel-beta_ij", ]: - ctmlPhase, yamlPhase = self.checkConversion("debye-huckel-all", name=name) + # Can only be loaded by ThermoPhase due to a bug in TransportFactory + # ThermoPhase does not have reactions (neither does the input file) + # so we can't use checkConversion + ctmlPhase = ct.ThermoPhase("debye-huckel-all.xml", name=name) + yamlPhase = ct.ThermoPhase("debye-huckel-all.yaml", name=name) + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_Maskell_solid_soln(self): From eb5dbc69e656e9c8920e23a82673d148404e1186 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 11 Nov 2019 15:56:54 -0500 Subject: [PATCH 76/99] [ctml2yaml] Add IdealSolidSolnPhase conversion --- interfaces/cython/cantera/ctml2yaml.py | 20 +-- .../cython/cantera/test/test_convert.py | 17 ++- test/data/IdealSolidSolnPhaseExample.xml | 121 ++++++++++++++++++ 3 files changed, 148 insertions(+), 10 deletions(-) create mode 100644 test/data/IdealSolidSolnPhaseExample.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index be60529e3c4..d99021be26d 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -260,7 +260,7 @@ class Phase: "LatticeSolid": "compound-lattice", "Lattice": "lattice", "HMW": "HMW-electrolyte", - "IdealSolidSolution": "ideal-condensed", # added + "IdealSolidSolution": "ideal-condensed", "DebyeHuckel": "Debye-Huckel", "IdealMolalSolution": "ideal-molal-solution", # added "IdealGasVPSS": "ideal-gas-VPSS", @@ -280,6 +280,7 @@ class Phase: "none": None, "Edge": "edge", "None": None, + "SolidKinetics": None, } _transport_model_mapping = { "Mix": "mixture-averaged", @@ -484,6 +485,11 @@ def __init__( kinetics_model = self._kinetics_model_mapping[ kinetics_node.get("model", "") ] + if kinetics_node.get("model", "").lower() == "solidkinetics": + warnings.warn( + "The SolidKinetics type is not implemented and will not be " + "included in the YAML output." + ) reactions = [] for rA_node in phase.iterfind("reactionArray"): # If the reaction list associated with the datasrc for this @@ -901,15 +907,13 @@ def get_reaction_array( if location == "reaction_data": location = "reactions" datasrc = "{}/{}".format(name, location) - elif datasrc == "#reaction_data": - if filter_text.lower() == "none": + else: + if filter_text.lower() != "none": + datasrc = self.filter_reaction_list(datasrc, filter_text, reaction_data) + elif datasrc == "#reaction_data": datasrc = "reactions" else: - datasrc = self.filter_reaction_list(datasrc, filter_text, reaction_data) - else: - raise ValueError( - "Unable to parse the reaction data source: '{}'".format(datasrc) - ) + datasrc = datasrc.lstrip("#") return {datasrc: reaction_option} diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 581e4e34c54..022607c7cad 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -2,7 +2,6 @@ from os.path import join as pjoin import itertools from pathlib import Path -import warnings from . import utilities import cantera as ct @@ -1143,4 +1142,18 @@ def test_margules(self): ctml2yaml.convert(Path(self.test_data_dir).joinpath("LiKCl_liquid.xml"), Path(self.test_work_dir).joinpath("LiKCl_liquid.yaml")) ctmlPhase, yamlPhase = self.checkConversion("LiKCl_liquid") - self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) \ No newline at end of file + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_idealsolidsoln(self): + with self.assertWarnsRegex(UserWarning, "SolidKinetics type is not implemented"): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("IdealSolidSolnPhaseExample.xml"), + Path(self.test_work_dir).joinpath("IdealSolidSolnPhaseExample.yaml")) + + # SolidKinetics is not implemented, so can't create a Kinetics class instance. + basename = "IdealSolidSolnPhaseExample" + ctmlPhase = ct.ThermoPhase(basename + ".xml") + yamlPhase = ct.ThermoPhase(basename + ".yaml") + + self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) diff --git a/test/data/IdealSolidSolnPhaseExample.xml b/test/data/IdealSolidSolnPhaseExample.xml new file mode 100644 index 00000000000..7940f47f658 --- /dev/null +++ b/test/data/IdealSolidSolnPhaseExample.xml @@ -0,0 +1,121 @@ + + + + + + 500 + + + + H C + + C2H2-graph C-graph H2-solute + + + + + + + + + + + + + This corresponds to new soot + C:2 H:2 + + + + 2.344331120E+000, 7.980520750E-003, -1.947815100E-005, + 2.015720940E-008, -7.376117610E-012, -9.179351730E+002, + 6.830102380E-001 + + + + + 3.337279200E+000, -4.940247310E-005, 4.994567780E-007, + -1.795663940E-010, 2.002553760E-014, -9.501589220E+002, + -3.205023310E+000 + + + + + 1.5 + + + + + This corresponds to old soot + C:1 + + + + 2.344331120E+000, 7.980520750E-003, -1.947815100E-005, + 2.015720940E-008, -7.376117610E-012, -9.179351730E+002, + 6.830102380E-001 + + + + + 3.337279200E+000, -4.940247310E-005, 4.994567780E-007, + -1.795663940E-010, 2.002553760E-014, -9.501589220E+002, + -3.205023310E+000 + + + + + 1.3 + + + + + This species diffuses back into the gas phase + H:2 + + + + 2.344331120E+000, 7.980520750E-003, -1.947815100E-005, + 2.015720940E-008, -7.376117610E-012, -9.179351730E+002, + 6.830102380E-001 + + + + + 3.337279200E+000, -4.940247310E-005, 4.994567780E-007, + -1.795663940E-010, 2.002553760E-014, -9.501589220E+002, + -3.205023310E+000 + + + + + 0.1 + + + + + + + + + + + C2H2-graph [=] H2-solute + 2 C-graph + C2H2-graph:1 + H2-solute:1 C-graph:2 + + + 1.0E10 + 0.0 + 10000. + + + + + + From 0534b1170ad83349a9d16b0bb0306e6431b5c3d0 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 07:30:11 -0700 Subject: [PATCH 77/99] [ctml2yaml] Rework reaction type factory The changes now match all of the options in newReaction, including synonyms. Butler-Volmer parameters are deprecated so the user is warned. Simplify processing interface type reactions into one method, since the electrochem node is optional. --- interfaces/cython/cantera/ctml2yaml.py | 102 +++++++++++++------------ 1 file changed, 55 insertions(+), 47 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d99021be26d..3f7897debe0 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -45,8 +45,9 @@ COVERAGE_PARAMS = Dict[str, ARRHENIUS_PARAMS] ARRHENIUS_TYPE = Dict[str, ARRHENIUS_PARAMS] - EDGE_TYPE = Dict[str, Union[ARRHENIUS_PARAMS, float, bool]] - SURFACE_TYPE = Dict[str, Union[ARRHENIUS_PARAMS, bool, str, COVERAGE_PARAMS]] + INTERFACE_TYPE = Dict[ + str, Union[ARRHENIUS_PARAMS, bool, str, COVERAGE_PARAMS, float] + ] NESTED_LIST_OF_FLOATS = List[List[float]] CHEBYSHEV_TYPE = Dict[str, Union[List[float], NESTED_LIST_OF_FLOATS, str]] PLOG_TYPE = Dict[str, Union[str, List[ARRHENIUS_PARAMS]]] @@ -1574,25 +1575,14 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): "=]", "=>" ) - reaction_type = reaction.get("type", "arrhenius") + reaction_type = reaction.get("type", "arrhenius").lower() rate_coeff = reaction.find("rateCoeff") if rate_coeff is None: raise MissingXMLNode("The reaction must have a rateCoeff node.", reaction) - if reaction_type not in [ - "arrhenius", - "threeBody", - "plog", - "chebyshev", - "surface", - "edge", - "falloff", - "chemAct", - ]: - raise TypeError( - "Unknown reaction type '{}' for reaction id {}".format( - reaction_type, reaction.get("id") - ) - ) + if reaction_type in ["arrhenius", "elementary"]: + reaction_type = "arrhenius" + elif reaction_type in ["threebody", "three_body"]: + reaction_type = "threebody" elif reaction_type == "falloff": falloff_node = rate_coeff.find("falloff") if falloff_node is None: @@ -1608,7 +1598,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): ) else: reaction_type = falloff_type - elif reaction_type == "chemAct": + elif reaction_type in ["chemact", "chemically_activated"]: falloff_node = rate_coeff.find("falloff") if falloff_node is None: raise MissingXMLNode( @@ -1621,7 +1611,40 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): falloff_type, reaction.get("id") ) ) - + elif reaction_type in ["plog", "pdep_arrhenius"]: + reaction_type = "plog" + elif reaction_type == "chebyshev": + # There's only one way to spell Chebyshev, so no need to change anything + # However, we need to catch this case so it doesn't raise the TypeError + # in the else clause + pass + elif reaction_type in [ + "interface", + "edge", + "surface", + "global", + "electrochemical", + ]: + reaction_type = "interface" + elif reaction_type in [ + "butlervolmer_noactivitycoeffs", + "butlervolmer", + "surfaceaffinity", + ]: + warnings.warn( + "Butler-Volmer parameters are not supported in the YAML " + "format. If this is an important feature to you, please see the " + "following issue and pull request on GitHub:\n" + "https://github.com/Cantera/cantera/issues/749\n" + "https://github.com/Cantera/cantera/pulls/750" + ) + reaction_type = "interface" + else: + raise TypeError( + "Unknown reaction type '{}' for reaction id {}".format( + reaction_type, reaction.get("id") + ) + ) func = getattr(self, reaction_type.lower()) self.attribs.update(func(rate_coeff)) @@ -1867,17 +1890,19 @@ def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": return reaction_attributes - def surface(self, rate_coeff: etree.Element) -> "SURFACE_TYPE": - """Process a surface reaction. + def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": + """Process an interface reaction. + This represents both interface and electrochemical reactions. Returns a dictionary with the appropriate fields set that is used to update the parent reaction entry dictionary. """ arr_node = rate_coeff.find("Arrhenius") if arr_node is None: - raise MissingXMLNode("Surface reaction requires Arrhenius node", rate_coeff) - sticking = arr_node.get("type") == "stick" - if sticking: + raise MissingXMLNode( + "Interface reaction requires Arrhenius node", rate_coeff + ) + if arr_node.get("type", "").lower() == "stick": reaction_attributes = FlowMap( {"sticking-coefficient": self.process_arrhenius_parameters(arr_node)} ) @@ -1913,31 +1938,14 @@ def surface(self, rate_coeff: etree.Element) -> "SURFACE_TYPE": } } - return reaction_attributes - - def edge(self, rate_coeff: etree.Element) -> "EDGE_TYPE": - """Process an edge reaction. - - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. - """ - arr_node = rate_coeff.find("Arrhenius") echem_node = rate_coeff.find("electrochem") - if echem_node is None: - raise MissingXMLNode("Edge reaction missing electrochem node", rate_coeff) - beta = echem_node.get("beta") - if beta is None: - raise MissingXMLAttribute( - "Beta must be specified for edge reaction", echem_node - ) - reaction_attributes = BlockMap( - { - "rate-constant": self.process_arrhenius_parameters(arr_node), - "beta": float(beta), - } - ) # type: EDGE_TYPE + if echem_node is not None: + beta = echem_node.get("beta") + if beta is not None: + reaction_attributes["beta"] = float(beta) if rate_coeff.get("type") == "exchangecurrentdensity": reaction_attributes["exchange-current-density-formulation"] = True + return reaction_attributes def arrhenius(self, rate_coeff: etree.Element) -> "ARRHENIUS_TYPE": From c5c0902da23001f7fe085f7251107d60c11f033a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 07:54:53 -0700 Subject: [PATCH 78/99] [ctml2yaml] Add IdealMolalSoln thermo type --- interfaces/cython/cantera/ctml2yaml.py | 28 ++- .../cython/cantera/test/test_convert.py | 14 ++ test/data/IdealMolalSolnPhaseExample.xml | 178 ++++++++++++++++++ 3 files changed, 218 insertions(+), 2 deletions(-) create mode 100644 test/data/IdealMolalSolnPhaseExample.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 3f7897debe0..327c2f6fc9d 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -252,7 +252,7 @@ class Phase: _thermo_model_mapping = { "IdealGas": "ideal-gas", - "Incompressible": "constant-density", # added, don't need test, deprecated + "Incompressible": "constant-density", "Surface": "ideal-surface", "Edge": "edge", "Metal": "electron-cloud", @@ -263,7 +263,7 @@ class Phase: "HMW": "HMW-electrolyte", "IdealSolidSolution": "ideal-condensed", "DebyeHuckel": "Debye-Huckel", - "IdealMolalSolution": "ideal-molal-solution", # added + "IdealMolalSolution": "ideal-molal-solution", "IdealGasVPSS": "ideal-gas-VPSS", "IdealSolnVPSS": "ideal-solution-VPSS", "Margules": "Margules", @@ -458,6 +458,12 @@ def __init__( margules_interactions = self.margules(activity_coefficients) if margules_interactions: self.attribs["interactions"] = margules_interactions + elif phase_thermo_model == "IdealMolalSolution": + activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is not None: + ideal_molal_cutoff = self.ideal_molal_solution(activity_coefficients) + if ideal_molal_cutoff: + self.attribs["cutoff"] = ideal_molal_cutoff for node in phase_thermo: if node.tag == "site_density": @@ -538,6 +544,24 @@ def __init__( self.check_elements(species, species_data) + def ideal_molal_solution( + self, activity_coeffs: etree.Element + ) -> Dict[str, Union[str, float]]: + """Process the cutoff data in an IdealMolalSolution type. + + Returns a (possibly empty) dictionary to update the phase attributes. + """ + cutoff = {} # type: Dict[str, Union[str, float]] + cutoff_node = activity_coeffs.find("idealMolalSolnCutoff") + if cutoff_node is not None: + cutoff_model = cutoff_node.get("model") + if cutoff_model is not None: + cutoff["model"] = cutoff_model + for limit_node in cutoff_node: + tag = limit_node.tag.rsplit("_", 1)[0] + cutoff[tag] = get_float_or_units(limit_node) + return cutoff + def margules( self, activity_coeffs: etree.Element ) -> List[Dict[str, List[Union[str, float]]]]: diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 022607c7cad..d4ae73e950c 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1157,3 +1157,17 @@ def test_idealsolidsoln(self): self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_idealmolalsoln(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("IdealMolalSolnPhaseExample.xml"), + Path(self.test_work_dir).joinpath("IdealMolalSolnPhaseExample.yaml")) + + # # SolidKinetics is not implemented, so can't create a Kinetics class instance. + # basename = "IdealMolalSolnPhaseExample" + # ctmlPhase = ct.ThermoPhase(basename + ".xml") + # yamlPhase = ct.ThermoPhase(basename + ".yaml") + + # self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) + # self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) + ctmlPhase, yamlPhase = self.checkConversion("IdealMolalSolnPhaseExample") + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) diff --git a/test/data/IdealMolalSolnPhaseExample.xml b/test/data/IdealMolalSolnPhaseExample.xml new file mode 100644 index 00000000000..848a6cdaa33 --- /dev/null +++ b/test/data/IdealMolalSolnPhaseExample.xml @@ -0,0 +1,178 @@ + + + + + + H2O(L) Cl- H+ Na+ OH- + + + 298.15 + 101325.0 + + Na+:6.0954 + Cl-:6.0954 + H+:2.1628E-9 + OH-:1.3977E-6 + + + + + + + + 1.0 + 1.0 + 1.0 + 1.0 + 1.0 + 1.0 + + + + O H C E Fe Si N Na Cl + + + + + + + + + H:2 O:1 + + + + 7.255750050E+01, -6.624454020E-01, 2.561987460E-03, -4.365919230E-06, + 2.781789810E-09, -4.188654990E+04, -2.882801370E+02 + + + + + + + + + + + Na:1 E:-1 + +1 + + + + -57993.47558 , 305112.6040 , -592222.1591 , + 401977.9827 , 804.4195980 , 10625.24901 , + -133796.2298 + + + + + + + 0.00834 + + + + + + Cl:1 E:1 + -1 + + + + 0.00834 + + + + + 56696.2042 , -297835.978 , 581426.549 , + -401759.991 , -804.301136 , -10873.8257 , + 130650.697 + + + + + + + + H:1 E:-1 + +1 + + 0.0 + + + + 0.0 + 3 + + 0.0 , 0.0, 0.0 + + + 273.15, 298.15 , 623.15 + + + + + + + + O:1 H:1 E:1 + -1 + + + 0.00834 + + + + + 44674.99961 , -234943.0414 , 460522.8260 , + -320695.1836 , -638.5044716 , -8683.955813 , + 102874.2667 + + + + + + + + From 645a86a7ca993190b131c66ffc66c408f719ef4f Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 08:05:27 -0700 Subject: [PATCH 79/99] [ctml2yaml] Add conversion for more transport models --- interfaces/cython/cantera/ctml2yaml.py | 8 +-- .../cython/cantera/test/test_convert.py | 7 ++ test/data/transport_models_test.xml | 65 +++++++++++++++++++ 3 files changed, 76 insertions(+), 4 deletions(-) create mode 100644 test/data/transport_models_test.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 327c2f6fc9d..49d3b8c369f 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -291,10 +291,10 @@ class Phase: "Water": "water", "none": None, None: None, - "UnityLewis": "unity-Lewis-number", # added - "CK_Mix": "mixture-averaged-CK", # added - "CK_Multi": "multicomponent-CK", # added - "HighP": "high-pressure", # added + "UnityLewis": "unity-Lewis-number", + "CK_Mix": "mixture-averaged-CK", + "CK_Multi": "multicomponent-CK", + "HighP": "high-pressure", } _state_properties_mapping = { diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index d4ae73e950c..af0f2be0bc7 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1171,3 +1171,10 @@ def test_idealmolalsoln(self): # self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) ctmlPhase, yamlPhase = self.checkConversion("IdealMolalSolnPhaseExample") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_transport_models(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("transport_models_test.xml"), + Path(self.test_work_dir).joinpath("transport_models_test.yaml")) + for name in ["UnityLewis", "CK_Mix", "CK_Multi", "HighP"]: + ctmlPhase, yamlPhase = self.checkConversion("transport_models_test", name=name) + self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2500]) diff --git a/test/data/transport_models_test.xml b/test/data/transport_models_test.xml new file mode 100644 index 00000000000..6306739b600 --- /dev/null +++ b/test/data/transport_models_test.xml @@ -0,0 +1,65 @@ + + + + + + H C + H H2 CH3 CH4 + + + + + 1200.0 + 2666.4473684210525 + H:0.002, H2:0.988, CH3:0.0002, CH4:0.01 + + + + + + + H C + H H2 CH3 CH4 + + + + + 1200.0 + 2666.4473684210525 + H:0.002, H2:0.988, CH3:0.0002, CH4:0.01 + + + + + + + H C + H H2 CH3 CH4 + + + + + 1200.0 + 2666.4473684210525 + H:0.002, H2:0.988, CH3:0.0002, CH4:0.01 + + + + + + + H C + H H2 CH3 CH4 + + + + + 1200.0 + 2666.4473684210525 + H:0.002, H2:0.988, CH3:0.0002, CH4:0.01 + + + + + + From 3a74c979a8604d78f3e787eefa900e16d79d321a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 08:19:23 -0700 Subject: [PATCH 80/99] [ctml2yaml] Add non-reactant orders --- interfaces/cython/cantera/ctml2yaml.py | 32 ++++------ .../cython/cantera/test/test_convert.py | 15 ++--- test/data/reaction-orders.xml | 59 +++++++++++++++++++ 3 files changed, 80 insertions(+), 26 deletions(-) create mode 100644 test/data/reaction-orders.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 49d3b8c369f..d3a17206f1b 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -907,9 +907,9 @@ def get_reaction_array( if skip_node is not None: # "undeclared" is the only allowed option for third_bodies and species # here, so ignore other options - if skip_node.get("third_bodies") == "undeclared": + if skip_node.get("third_bodies", "").lower() == "undeclared": self.attribs["skip-undeclared-third-bodies"] = True - if skip_node.get("species") == "undeclared": + if skip_node.get("species", "").lower() == "undeclared": reaction_option = "declared-species" else: reaction_option = "all" @@ -1044,7 +1044,7 @@ def hmw_electrolyte(self, activity_node: etree.Element): raise MissingXMLNode( "Activity coefficients for HMW must have A_Debye", activity_node ) - if A_Debye_node.get("model") == "water": + if A_Debye_node.get("model", "").lower() == "water": activity_data["A_Debye"] = "variable" else: # Assume the units are kg^0.5/gmol^0.5. Apparently, @@ -1684,32 +1684,26 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): "The reactants must be present in the reaction", reaction ) reactants = split_species_value_string(reactants_node) - # products = { - # a.split(":")[0]: float(a.split(":")[1]) - # for a in reaction.findtext("products").replace("\n", " ").strip().split() - # } orders = {} - # Need to make this more general, for non-reactant orders for order_node in reaction.iterfind("order"): - species = order_node.get("species") - if species is None: + species = order_node.get("species", "") + if not species: raise MissingXMLAttribute( "A reaction order node must have a species", order_node ) - if order_node.text is None: - raise MissingNodeText( - "A reaction order node must have a text value", order_node - ) - order = float(order_node.text) - if not np.isclose(reactants[species], order): + order = get_float_or_units(order_node) + if species not in reactants or not np.isclose(reactants[species], order): orders[species] = order if orders: self.attribs["orders"] = orders - if reaction.get("negative_orders") == "yes": + if reaction.get("negative_orders", "").lower() == "yes": self.attribs["negative-orders"] = True - if reaction.get("duplicate", "") == "yes": + if reaction.get("nonreactant_orders", "").lower() == "yes": + self.attribs["nonreactant-orders"] = True + + if reaction.get("duplicate", "").lower() == "yes": self.attribs["duplicate"] = True @classmethod @@ -1967,7 +1961,7 @@ def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": beta = echem_node.get("beta") if beta is not None: reaction_attributes["beta"] = float(beta) - if rate_coeff.get("type") == "exchangecurrentdensity": + if rate_coeff.get("type", "").lower() == "exchangecurrentdensity": reaction_attributes["exchange-current-density-formulation"] = True return reaction_attributes diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index af0f2be0bc7..73dddaf5935 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1162,13 +1162,6 @@ def test_idealmolalsoln(self): ctml2yaml.convert(Path(self.test_data_dir).joinpath("IdealMolalSolnPhaseExample.xml"), Path(self.test_work_dir).joinpath("IdealMolalSolnPhaseExample.yaml")) - # # SolidKinetics is not implemented, so can't create a Kinetics class instance. - # basename = "IdealMolalSolnPhaseExample" - # ctmlPhase = ct.ThermoPhase(basename + ".xml") - # yamlPhase = ct.ThermoPhase(basename + ".yaml") - - # self.assertEqual(ctmlPhase.element_names, yamlPhase.element_names) - # self.assertEqual(ctmlPhase.species_names, yamlPhase.species_names) ctmlPhase, yamlPhase = self.checkConversion("IdealMolalSolnPhaseExample") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) @@ -1178,3 +1171,11 @@ def test_transport_models(self): for name in ["UnityLewis", "CK_Mix", "CK_Multi", "HighP"]: ctmlPhase, yamlPhase = self.checkConversion("transport_models_test", name=name) self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2500]) + + def test_nonreactant_orders(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("reaction-orders.xml"), + Path(self.test_work_dir).joinpath("reaction-orders.yaml")) + + ctmlPhase, yamlPhase = self.checkConversion("reaction-orders") + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + self.checkKinetics(ctmlPhase, yamlPhase, [300, 1001, 2500], [1e5, 10e5]) diff --git a/test/data/reaction-orders.xml b/test/data/reaction-orders.xml new file mode 100644 index 00000000000..2a8e22ac1f8 --- /dev/null +++ b/test/data/reaction-orders.xml @@ -0,0 +1,59 @@ + + + + + + + O H + H2 O2 H2O OH + + + + + 300.0 + 101325.0 + + + + + + + + + + + + + H2 + O2 =] 2 OH + 1.0 + 1 + -0.25 + + + 1.000000E+10 + 0.0 + 0.000000 + + + H2:1.0 O2:1 + OH:2.0 + + + + + 2 H2 + O2 =] 2 H2O + -0.25 + 1 + 0.15 + + + 5.623413E+13 + 0.0 + 0.000000 + + + H2:2.0 O2:1 + H2O:2.0 + + + From 76ab074bb0ddc6ce43bfaa01e248505c4c0bc088 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 11:18:16 -0600 Subject: [PATCH 81/99] [ctml2yaml] Fix trailing space if units is empty Add a docstring for get_float_or_units. Use the clean_node_text function. --- interfaces/cython/cantera/ctml2yaml.py | 24 +++++++++++++++++++----- 1 file changed, 19 insertions(+), 5 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d3a17206f1b..0dc3fc4d24d 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -152,12 +152,26 @@ def represent_float(self: Any, data: Any) -> Any: def get_float_or_units(node: etree.Element) -> Union[str, float]: - if node.text is None: - raise MissingNodeText("Node '{}' must contain text".format(node)) + """Process an XML node into a float value or a value with units. - value = float(node.text.strip()) - units = node.get("units") - if units is not None: + :param node: + The XML node with a value in the text and optionally a units attribute. + + Given an XML nodes like:: + + 1000.0 + 1000.0 + + this function returns, respectively:: + + 1000.0 cal/mol + 1000.0 + + where the first value is a string and the second is a float. + """ + value = float(clean_node_text(node)) + units = node.get("units", "") + if units: units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", units) units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", units) return "{} {}".format(float2string(value), units) From 844f4a02badc1320b656a47501783533c08dd6df Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 11:22:37 -0600 Subject: [PATCH 82/99] [ctml2yaml] Rename class attributes to non-private --- interfaces/cython/cantera/ctml2yaml.py | 40 +++++++++++++------------- 1 file changed, 20 insertions(+), 20 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 0dc3fc4d24d..c43ea5d1771 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -264,7 +264,7 @@ class Phase: ElementTree Element node with a phase definition. """ - _thermo_model_mapping = { + thermo_model_mapping = { "IdealGas": "ideal-gas", "Incompressible": "constant-density", "Surface": "ideal-surface", @@ -289,7 +289,7 @@ class Phase: "PureLiquidWater": "liquid-water-IAPWS95", "BinarySolutionTabulatedThermo": "binary-solution-tabulated", } - _kinetics_model_mapping = { + kinetics_model_mapping = { "GasKinetics": "gas", "Interface": "surface", "none": None, @@ -297,7 +297,7 @@ class Phase: "None": None, "SolidKinetics": None, } - _transport_model_mapping = { + transport_model_mapping = { "Mix": "mixture-averaged", "Multi": "multicomponent", "None": None, @@ -311,7 +311,7 @@ class Phase: "HighP": "high-pressure", } - _state_properties_mapping = { + state_properties_mapping = { "moleFractions": "X", "massFractions": "Y", "temperature": "T", @@ -320,7 +320,7 @@ class Phase: "soluteMolalities": "molalities", } - _pure_fluid_mapping = { + pure_fluid_mapping = { "0": "water", "1": "nitrogen", "2": "methane", @@ -375,7 +375,7 @@ def __init__( phase_thermo_model = phase_thermo.get("model") if phase_thermo_model is None: raise MissingXMLAttribute("The thermo node requires a model") - self.attribs["thermo"] = self._thermo_model_mapping[phase_thermo_model] + self.attribs["thermo"] = self.thermo_model_mapping[phase_thermo_model] if phase_thermo_model == "PureFluid": pure_fluid_type = phase_thermo.get("fluid_type") @@ -383,7 +383,7 @@ def __init__( raise MissingXMLAttribute( "PureFluid model requires the fluid_type", phase_thermo ) - self.attribs["pure-fluid-name"] = self._pure_fluid_mapping[pure_fluid_type] + self.attribs["pure-fluid-name"] = self.pure_fluid_mapping[pure_fluid_type] elif phase_thermo_model == "HMW": activity_coefficients = phase_thermo.find("activityCoefficients") if activity_coefficients is None: @@ -494,7 +494,7 @@ def __init__( transport_node = phase.find("transport") if transport_node is not None: - transport_model = self._transport_model_mapping[transport_node.get("model")] + transport_model = self.transport_model_mapping[transport_node.get("model")] if transport_model is not None: self.attribs["transport"] = transport_model @@ -503,7 +503,7 @@ def __init__( kinetics_node = phase.find("kinetics") has_reactionArray = phase.find("reactionArray") is not None if kinetics_node is not None and has_reactionArray: - kinetics_model = self._kinetics_model_mapping[ + kinetics_model = self.kinetics_model_mapping[ kinetics_node.get("model", "") ] if kinetics_node.get("model", "").lower() == "solidkinetics": @@ -536,7 +536,7 @@ def __init__( if state_node is not None: phase_state = FlowMap() for prop in state_node: - property_name = self._state_properties_mapping[prop.tag] + property_name = self.state_properties_mapping[prop.tag] if prop.tag in [ "moleFractions", "massFractions", @@ -1227,7 +1227,7 @@ def debye_huckel( "in the species node will be used".format(name) ) if name in etype_mods: - etype = spec._electrolyte_species_type_mapping[etype_mods[name]] + etype = spec.electrolyte_species_type_mapping[etype_mods[name]] if "electrolyte-species-type" not in spec.attribs: spec.attribs["electrolyte-species-type"] = etype else: @@ -1376,8 +1376,8 @@ def to_yaml(cls, representer, data): class SpeciesTransport: """Represents the transport properties of a species.""" - _species_transport_mapping = {"gas_transport": "gas"} - _transport_properties_mapping = { + species_transport_mapping = {"gas_transport": "gas"} + transport_properties_mapping = { "LJ_welldepth": "well-depth", "LJ_diameter": "diameter", "polarizability": "polarizability", @@ -1390,13 +1390,13 @@ class SpeciesTransport: def __init__(self, transport: etree.Element): self.attribs = BlockMap({}) transport_model = transport.get("model") - if transport_model not in self._species_transport_mapping: + if transport_model not in self.species_transport_mapping: raise TypeError( "Unknown transport model type: '{}'".format(transport.get("model")) ) - self.attribs["model"] = self._species_transport_mapping[transport_model] + self.attribs["model"] = self.species_transport_mapping[transport_model] self.attribs["geometry"] = transport.findtext("string[@title='geometry']") - for tag, name in self._transport_properties_mapping.items(): + for tag, name in self.transport_properties_mapping.items(): value = float(transport.findtext(tag, default=0.0)) self.attribs.update(check_float_neq_zero(value, name)) @@ -1408,14 +1408,14 @@ def to_yaml(cls, representer, data): class Species: """Represents a species.""" - _standard_state_model_mapping = { + standard_state_model_mapping = { "ideal-gas": "ideal-gas", "constant_incompressible": "constant-volume", "constant-incompressible": "constant-volume", "waterPDSS": "liquid-water-IAPWS95", "waterIAPWS": "liquid-water-IAPWS95", } - _electrolyte_species_type_mapping = { + electrolyte_species_type_mapping = { "weakAcidAssociated": "weak-acid-associated", "chargedSpecies": "charged-species", "strongAcidAssociated": "strong-acid-associated", @@ -1490,7 +1490,7 @@ def __init__(self, species_node: etree.Element): electrolyte = species_node.findtext("electrolyteSpeciesType") if electrolyte is not None: - electrolyte = self._electrolyte_species_type_mapping[electrolyte.strip()] + electrolyte = self.electrolyte_species_type_mapping[electrolyte.strip()] self.attribs["electrolyte-species-type"] = electrolyte weak_acid_charge = species_node.find("stoichIsMods") @@ -1565,7 +1565,7 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: return eqn_of_state = { - "model": self._standard_state_model_mapping[std_state_model] + "model": self.standard_state_model_mapping[std_state_model] } # type: Dict[str, Union[str, float]] if std_state_model == "constant_incompressible": molar_volume_node = std_state.find("molarVolume") From 0ec1c9d99becb143aa5a9b779cb33d3647131709 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 15:17:43 -0500 Subject: [PATCH 83/99] [ctml2yaml] Retain zero values in the YAML output As demonstrated in #683, sometimes the zero values are intentional and meaningful. Rework the SpeciesTransport class to not use this function. --- interfaces/cython/cantera/ctml2yaml.py | 29 +++++++++++--------------- 1 file changed, 12 insertions(+), 17 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c43ea5d1771..22ffe8ff987 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -179,20 +179,6 @@ def get_float_or_units(node: etree.Element) -> Union[str, float]: return value -def check_float_neq_zero(value: float, name: str) -> Dict[str, float]: - """Check that the text value associated with a tag is non-zero. - - If the value is not zero, return a dictionary with the key ``name`` - and the value. If the value is zero, return an empty dictionary. - Calling functions can use this function to ``update`` a dictionary of - attributes without adding keys whose values are zero. - """ - if not np.isclose(value, 0.0): - return {name: value} - else: - return {} - - def split_species_value_string(node: etree.Element) -> Dict[str, float]: """Split a string of species:value pairs into a dictionary. @@ -1396,9 +1382,18 @@ def __init__(self, transport: etree.Element): ) self.attribs["model"] = self.species_transport_mapping[transport_model] self.attribs["geometry"] = transport.findtext("string[@title='geometry']") - for tag, name in self.transport_properties_mapping.items(): - value = float(transport.findtext(tag, default=0.0)) - self.attribs.update(check_float_neq_zero(value, name)) + for prop_node in transport: + if prop_node.tag == "string": + continue + # Don't use get_float_or_units because the units of the gas_transport + # parameters are assumed to be customary units in YAML. + value = float(clean_node_text(prop_node)) + name = self.transport_properties_mapping.get(prop_node.tag) + if name is None: + raise TypeError( + "Unknown transport property node: '{}'".format(prop_node.tag) + ) + self.attribs[name] = value @classmethod def to_yaml(cls, representer, data): From 66c14331014492765ea2c4f8f92a219f95d0ead9 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Wed, 13 Nov 2019 14:48:48 -0600 Subject: [PATCH 84/99] [ctml2yaml] Add temperature_polynomial conversion --- interfaces/cython/cantera/ctml2yaml.py | 66 ++++++++++++---- .../cython/cantera/test/test_convert.py | 7 ++ test/data/Li_Liquid.xml | 76 +++++++++++++++++++ 3 files changed, 134 insertions(+), 15 deletions(-) create mode 100644 test/data/Li_Liquid.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 22ffe8ff987..9a80812558e 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -489,9 +489,7 @@ def __init__( kinetics_node = phase.find("kinetics") has_reactionArray = phase.find("reactionArray") is not None if kinetics_node is not None and has_reactionArray: - kinetics_model = self.kinetics_model_mapping[ - kinetics_node.get("model", "") - ] + kinetics_model = self.kinetics_model_mapping[kinetics_node.get("model", "")] if kinetics_node.get("model", "").lower() == "solidkinetics": warnings.warn( "The SolidKinetics type is not implemented and will not be " @@ -855,16 +853,19 @@ def move_density_to_species( "model": "constant-volume", const_prop: get_float_or_units(den_node), } - for node in this_phase_species: - for datasrc, species_names in node.items(): - if datasrc == "species": - datasrc = "species_data" - species = species_data.get(datasrc) - if species is None: - continue - for spec in species: - if spec.attribs["name"] in species_names: - spec.attribs["equation-of-state"] = equation_of_state + flat_species = {k: v for d in this_phase_species for k, v in d.items()} + for datasrc, species_names in flat_species.items(): + if datasrc == "species": + datasrc = "species_data" + species = species_data.get(datasrc) + if species is None: + continue + for spec in species: + if ( + spec.attribs["name"] in species_names + and "equation-of-state" not in spec.attribs + ): + spec.attribs["equation-of-state"] = equation_of_state def get_species_array( self, speciesArray_node: etree.Element @@ -1409,6 +1410,8 @@ class Species: "constant-incompressible": "constant-volume", "waterPDSS": "liquid-water-IAPWS95", "waterIAPWS": "liquid-water-IAPWS95", + "temperature_polynomial": "molar-volume-temperature-polynomial", + "density_temperature_polynomial": "density-temperature-polynomial", } electrolyte_species_type_mapping = { "weakAcidAssociated": "weak-acid-associated", @@ -1444,7 +1447,8 @@ def __init__(self, species_node: etree.Element): thermo = species_node.find("thermo") if thermo is not None: thermo_model = thermo.get("model", "") - # This node is not used anywhere + # This node is not used anywhere, but we don't want it to be processed by + # the SpeciesThermo constructor or the hkft method pseudo_species = thermo.find("pseudoSpecies") if pseudo_species is not None: thermo.remove(pseudo_species) @@ -1561,7 +1565,7 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: eqn_of_state = { "model": self.standard_state_model_mapping[std_state_model] - } # type: Dict[str, Union[str, float]] + } # type: Dict[str, Union[str, float, List[Union[str, float]]]] if std_state_model == "constant_incompressible": molar_volume_node = std_state.find("molarVolume") if molar_volume_node is None: @@ -1571,6 +1575,38 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: std_state, ) eqn_of_state["molar-volume"] = get_float_or_units(molar_volume_node) + elif "temperature_polynomial" in std_state_model: + poly_node = std_state.find("volumeTemperaturePolynomial") + if poly_node is None: + raise MissingXMLNode( + "{} standard state model requires a " + "volumeTemperaturePolynomial node".format(std_state_model), + std_state + ) + poly_values_node = poly_node.find("floatArray") + if poly_values_node is None: + raise MissingXMLNode( + "The floatArray node must be specified", std_state + ) + values = clean_node_text(poly_values_node).split(",") + + poly_units = poly_values_node.get("units", "") + if not poly_units: + eqn_of_state["data"] = FlowList(map(float, values)) + else: + poly_units = re.sub(r"([A-Za-z])-([A-Za-z])", r"\1*\2", poly_units) + poly_units = re.sub(r"([A-Za-z])([-\d])", r"\1^\2", poly_units) + + # Need to put units on each term in the polynomial because we can't + # reliably parse the units attribute string into a mass and a length + # (for example, if the units are g/L) and there's no way to specify + # YAML node-level units of volume. + data = [] + for v, suffix in zip(values, ("", "/K", "/K^2", "/K^3")): + data.append("{} {}{}".format(v.strip(), poly_units, suffix)) + + eqn_of_state["data"] = FlowList(data) + self.attribs["equation-of-state"] = eqn_of_state @classmethod diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 73dddaf5935..04d15a86a8f 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1179,3 +1179,10 @@ def test_nonreactant_orders(self): ctmlPhase, yamlPhase = self.checkConversion("reaction-orders") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) self.checkKinetics(ctmlPhase, yamlPhase, [300, 1001, 2500], [1e5, 10e5]) + + def test_species_ss_temperature_polynomials(self): + ctml2yaml.convert(Path(self.test_data_dir).joinpath("Li_Liquid.xml"), + Path(self.test_work_dir).joinpath("Li_Liquid.yaml")) + + ctmlPhase, yamlPhase = self.checkConversion("Li_Liquid") + self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) diff --git a/test/data/Li_Liquid.xml b/test/data/Li_Liquid.xml new file mode 100644 index 00000000000..6320373bf74 --- /dev/null +++ b/test/data/Li_Liquid.xml @@ -0,0 +1,76 @@ + + + + + + + Li + + Li(L) Li(L)2 + + + 300.0 + 101325.0 + Li(L):0.5 Li(L)2:0.5 + + + + + + + + + + + + Li:1 + + + + + 0.536504, -1.04279e-4, 3.84825e-9, -5.2853e-12 + + + + + + + 26.3072, 30.4657, -69.1692, 44.1951, 0.0776, -6.0337, 59.8106 + + + + + 22.6832, 10.476, -6.5428, 1.3255, 0.8783, -2.0426, 62.8859 + + + + + + + + + Li:1 + + + + 0.536504, -1.04279e-4, 3.84825e-9, -5.2853e-12 + + + + + + + 26.3072, 30.4657, -69.1692, 44.1951, 0.0776, -6.0337, 59.8106 + + + + + 22.6832, 10.476, -6.5428, 1.3255, 0.8783, -2.0426, 62.8859 + + + + + + + + From dfd4e6d4bf645001cbce385bd5af08f81f7cfccd Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 17 Nov 2019 21:17:20 -0500 Subject: [PATCH 85/99] [ctml2yaml] Fix several XML parsing errors The Python Expat parser requires that the tag occur as the first characters in the file, even before any blank space, so lstrip is used to remove any whitespace. In addition, raw & characters are replaced with their escaped version. --- interfaces/cython/cantera/ctml2yaml.py | 15 +++++++++++- test/data/species-names.xml | 34 +++++++++++++++++++++++++- 2 files changed, 47 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 9a80812558e..7060a4a5799 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -2122,7 +2122,20 @@ def create_phases_from_data_node( def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): """Convert an input CTML file to a YAML file.""" inpfile = Path(inpfile) - ctml_tree = etree.parse(str(inpfile)).getroot() + ctml_text = inpfile.read_text().lstrip() + # Replace any raw ampersands in the text with an escaped ampersand. This + # substitution is necessary because ctml_writer outputs literal & characters + # from text data into the XML output. Although this doesn't cause a problem + # with the custom XML parser in Cantera, standards-compliant XML parsers + # like the Expat one included in Python can't handle the raw & character. I + # could not figure out a way to override the parsing logic such that & could + # be escaped in the data during parsing, so it has to be done manually here. + # According to https://stackoverflow.com/a/1091953 there are 5 escaped + # characters in XML: " ("), ' ('), & (&), < (<), and > + # (>). This code only replaces & not followed by one of the escaped + # character codes. + ctml_text = re.sub("&(?!amp;|quot;|apos;|lt;|gt;)", "&", ctml_text) + ctml_tree = etree.fromstring(ctml_text) species_data = create_species_from_data_node(ctml_tree) reaction_data = create_reactions_from_data_node(ctml_tree) diff --git a/test/data/species-names.xml b/test/data/species-names.xml index 731d28905cf..9b394b14a57 100644 --- a/test/data/species-names.xml +++ b/test/data/species-names.xml @@ -8,7 +8,7 @@ (Parens) @#$%^-2 co:lons: [xy2]*{.} plus+ eq=uals plus trans_butene - co + co amp&ersand 300.0 @@ -175,6 +175,24 @@ + + + + C:1 H:4 + Contains a raw & character + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + @@ -366,5 +384,19 @@ co:1.0 co:lons::1 plus+:2.0 + + + + amp&ersand [=] plus+ + + + 9.999000E+06 + 9.9 + 999.900000 + + + amp&ersand:1.0 + plus+:1.0 + From e31ad91a159359d5f10fe3b2e5fac3a2c7751a32 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 19 Nov 2019 11:37:04 -0500 Subject: [PATCH 86/99] [ctml2yaml] Combine duplicate section ids If a reactionData or speciesData node has a duplicated id attribute, combine the duplicate sections together. If duplicate reaction id attributes or species names are found, either warn or raise an error. --- interfaces/cython/cantera/ctml2yaml.py | 40 ++++----- .../cython/cantera/test/test_convert.py | 12 +++ test/data/duplicate-reactionData-ids.xml | 89 +++++++++++++++++++ test/data/duplicate-speciesData-ids.xml | 57 ++++++++++++ test/data/frac.xml | 27 +++--- 5 files changed, 189 insertions(+), 36 deletions(-) create mode 100644 test/data/duplicate-reactionData-ids.xml create mode 100644 test/data/duplicate-speciesData-ids.xml diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 7060a4a5799..c6806c7fef9 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -2059,21 +2059,14 @@ def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Sp for species_data_node in ctml_tree.iterfind("speciesData"): this_data_node_id = species_data_node.get("id", "") if this_data_node_id in species: - raise ValueError( - "Duplicate speciesData id found: '{}'".format(this_data_node_id) + warnings.warn( + "Duplicate 'speciesData' id found: '{}'. Only the first section will " + "be included in the output file.".format(this_data_node_id) ) - this_node_species = [] # type: List[Species] - for species_node in species_data_node.iterfind("species"): - this_species = Species(species_node) - for s in this_node_species: - if this_species.attribs["name"] == s.attribs["name"]: - raise ValueError( - "Duplicate specification of species '{}' in node '{}'".format( - this_species.attribs["name"], this_data_node_id - ) - ) - this_node_species.append(this_species) - species[this_data_node_id] = this_node_species + continue + species[this_data_node_id] = [ + Species(s) for s in species_data_node.iterfind("species") + ] return species @@ -2084,18 +2077,19 @@ def create_reactions_from_data_node( """Take a reactionData node and return a dictionary of Reaction objects.""" reactions = {} # type: Dict[str, List[Reaction]] for reactionData_node in ctml_tree.iterfind("reactionData"): - this_data_node_id = reactionData_node.get("id", "") - if this_data_node_id in reactions: - raise ValueError( - "Duplicate reactionData id found: '{}'".format(this_data_node_id) - ) node_motz_wise = False if reactionData_node.get("motz_wise", "").lower() == "true": node_motz_wise = True - this_node_reactions = [] # type: List[Reaction] - for reaction_node in reactionData_node.iterfind("reaction"): - this_node_reactions.append(Reaction(reaction_node, node_motz_wise)) - reactions[this_data_node_id] = this_node_reactions + this_data_node_id = reactionData_node.get("id", "") + if this_data_node_id in reactions: + warnings.warn( + "Duplicate 'reactionData' id found: '{}'. Only the first section will " + "be included in the output file.".format(this_data_node_id) + ) + continue + reactions[this_data_node_id] = [ + Reaction(r, node_motz_wise) for r in reactionData_node.iterfind("reaction") + ] return reactions diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 04d15a86a8f..63102ec091e 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1186,3 +1186,15 @@ def test_species_ss_temperature_polynomials(self): ctmlPhase, yamlPhase = self.checkConversion("Li_Liquid") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) + + def test_duplicate_section_ids(self): + with self.assertWarnsRegex(UserWarning, "Duplicate 'speciesData' id"): + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("duplicate-speciesData-ids.xml"), + Path(self.test_work_dir).joinpath("duplicate-speciesData-ids.yaml") + ) + with self.assertWarnsRegex(UserWarning, "Duplicate 'reactionData' id"): + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("duplicate-reactionData-ids.xml"), + Path(self.test_work_dir).joinpath("duplicate-reactionData-ids.yaml") + ) \ No newline at end of file diff --git a/test/data/duplicate-reactionData-ids.xml b/test/data/duplicate-reactionData-ids.xml new file mode 100644 index 00000000000..d5966eeb32d --- /dev/null +++ b/test/data/duplicate-reactionData-ids.xml @@ -0,0 +1,89 @@ + + + + + + + H C + R1A R1B + + + 300.0 + 101325.0 + + + + + + + + + + + H:4 C:1 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + H:4 C:1 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + + + + + + + R1A [=] R1B + + + 1.000000E+09 + 0.0 + 20000.000000 + + + R1A:1.0 + R1B:1.0 + + + + + + + R1B [=] R1A + + + 1.000000E+09 + 0.0 + 20000.000000 + + + R1B:1.0 + R1A:1.0 + + + + diff --git a/test/data/duplicate-speciesData-ids.xml b/test/data/duplicate-speciesData-ids.xml new file mode 100644 index 00000000000..d50571576a7 --- /dev/null +++ b/test/data/duplicate-speciesData-ids.xml @@ -0,0 +1,57 @@ + + + + + + + H + H H + + 300.0 + 101325.0 + + + + + + + + + + + + H:1 + + + + 2.500000000E+00, 7.053328190E-13, -1.995919640E-15, 2.300816320E-18, + -9.277323320E-22, 2.547365990E+04, -4.466828530E-01 + + + + 2.500000010E+00, -2.308429730E-11, 1.615619480E-14, -4.735152350E-18, + 4.981973570E-22, 2.547365990E+04, -4.466829140E-01 + + + + + + + + + H:1 + + + + 2.500000000E+00, 7.053328190E-13, -1.995919640E-15, 2.300816320E-18, + -9.277323320E-22, 2.547365990E+04, -4.466828530E-01 + + + + 2.500000010E+00, -2.308429730E-11, 1.615619480E-14, -4.735152350E-18, + 4.981973570E-22, 2.547365990E+04, -4.466829140E-01 + + + + + diff --git a/test/data/frac.xml b/test/data/frac.xml index 53a0b40b530..d4c7fd2e628 100644 --- a/test/data/frac.xml +++ b/test/data/frac.xml @@ -25,12 +25,12 @@ - 2.344331120E+00, 7.980520750E-03, -1.947815100E-05, 2.015720940E-08, + 2.344331120E+00, 7.980520750E-03, -1.947815100E-05, 2.015720940E-08, -7.376117610E-12, -9.179351730E+02, 6.830102380E-01 - 3.337279200E+00, -4.940247310E-05, 4.994567780E-07, -1.795663940E-10, + 3.337279200E+00, -4.940247310E-05, 4.994567780E-07, -1.795663940E-10, 2.002553760E-14, -9.501589220E+02, -3.205023310E+00 @@ -42,12 +42,12 @@ - 2.500000000E+00, 7.053328190E-13, -1.995919640E-15, 2.300816320E-18, + 2.500000000E+00, 7.053328190E-13, -1.995919640E-15, 2.300816320E-18, -9.277323320E-22, 2.547365990E+04, -4.466828530E-01 - 2.500000010E+00, -2.308429730E-11, 1.615619480E-14, -4.735152350E-18, + 2.500000010E+00, -2.308429730E-11, 1.615619480E-14, -4.735152350E-18, 4.981973570E-22, 2.547365990E+04, -4.466829140E-01 @@ -59,12 +59,12 @@ - 3.168267100E+00, -3.279318840E-03, 6.643063960E-06, -6.128066240E-09, + 3.168267100E+00, -3.279318840E-03, 6.643063960E-06, -6.128066240E-09, 2.112659710E-12, 2.912225920E+04, 2.051933460E+00 - 2.569420780E+00, -8.597411370E-05, 4.194845890E-08, -1.001777990E-11, + 2.569420780E+00, -8.597411370E-05, 4.194845890E-08, -1.001777990E-11, 1.228336910E-15, 2.921757910E+04, 4.784338640E+00 @@ -76,12 +76,12 @@ - 3.782456360E+00, -2.996734160E-03, 9.847302010E-06, -9.681295090E-09, + 3.782456360E+00, -2.996734160E-03, 9.847302010E-06, -9.681295090E-09, 3.243728370E-12, -1.063943560E+03, 3.657675730E+00 - 3.282537840E+00, 1.483087540E-03, -7.579666690E-07, 2.094705550E-10, + 3.282537840E+00, 1.483087540E-03, -7.579666690E-07, 2.094705550E-10, -2.167177940E-14, -1.088457720E+03, 5.453231290E+00 @@ -93,12 +93,12 @@ - 3.992015430E+00, -2.401317520E-03, 4.617938410E-06, -3.881133330E-09, + 3.992015430E+00, -2.401317520E-03, 4.617938410E-06, -3.881133330E-09, 1.364114700E-12, 3.615080560E+03, -1.039254580E-01 - 3.092887670E+00, 5.484297160E-04, 1.265052280E-07, -8.794615560E-11, + 3.092887670E+00, 5.484297160E-04, 1.265052280E-07, -8.794615560E-11, 1.174123760E-14, 3.858657000E+03, 4.476696100E+00 @@ -110,12 +110,12 @@ - 4.198640560E+00, -2.036434100E-03, 6.520402110E-06, -5.487970620E-09, + 4.198640560E+00, -2.036434100E-03, 6.520402110E-06, -5.487970620E-09, 1.771978170E-12, -3.029372670E+04, -8.490322080E-01 - 3.033992490E+00, 2.176918040E-03, -1.640725180E-07, -9.704198700E-11, + 3.033992490E+00, 2.176918040E-03, -1.640725180E-07, -9.704198700E-11, 1.682009920E-14, -3.000429710E+04, 4.966770100E+00 @@ -154,7 +154,8 @@ H2O:1.0 - + + H2 + 0.5 O2 =] H2O 1.0 -0.25 From 3376fcbcb0ad2df62f6573ddec60e59df859bc09 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 24 Nov 2019 13:23:06 -0500 Subject: [PATCH 87/99] [ctml2yaml/Doc] Add docstrings --- interfaces/cython/cantera/ctml2yaml.py | 524 +++++++++++++++++++++---- 1 file changed, 437 insertions(+), 87 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index c6806c7fef9..446910a05ff 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1,6 +1,4 @@ -""" -This file will convert CTML format files to YAML. -""" +"""Convert legacy CTML input to YAML format.""" from pathlib import Path import sys @@ -66,21 +64,28 @@ def FlowMap(*args, **kwargs): + """A YAML mapping that flows onto one line.""" m = yaml.comments.CommentedMap(*args, **kwargs) m.fa.set_flow_style() return m def FlowList(*args, **kwargs): + """A YAML sequence that flows onto one line.""" lst = yaml.comments.CommentedSeq(*args, **kwargs) lst.fa.set_flow_style() return lst class MissingXMLNode(LookupError): - """Error raised when a required node is missing in the XML tree.""" - def __init__(self, message: str = "", node: Optional[etree.Element] = None): + """Error raised when a required node is missing in the XML tree. + + :param message: + The error message to be displayed to the user. + :param node: + The XML node from which the requested node is missing. + """ if node is not None: node_str = etree.tostring(node).decode("utf-8") if message: @@ -92,9 +97,14 @@ def __init__(self, message: str = "", node: Optional[etree.Element] = None): class MissingXMLAttribute(LookupError): - """Error raised when a required attribute is missing in the XML node.""" - def __init__(self, message: str = "", node: Optional[etree.Element] = None): + """Error raised when a required attribute is missing in the XML node. + + :param message: + The error message to be displayed to the user. + :param node: + The XML node from which the requested attribute is missing. + """ if node is not None: node_str = etree.tostring(node).decode("utf-8") if message: @@ -106,9 +116,14 @@ def __init__(self, message: str = "", node: Optional[etree.Element] = None): class MissingNodeText(LookupError): - """Error raised when the text of an XML node is missing.""" - def __init__(self, message: str = "", node: Optional[etree.Element] = None): + """Error raised when the text of an XML node is missing. + + :param message: + The error message to be displayed to the user. + :param node: + The XML node from which the text is missing. + """ if node is not None: node_str = etree.tostring(node).decode("utf-8") if message: @@ -124,6 +139,16 @@ def __init__(self, message: str = "", node: Optional[etree.Element] = None): def float2string(data: float) -> str: + """Format a float into a string. + + :param data: The floating point data to be formatted. + + Uses NumPy's ``format_float_positional()`` and ``format_float_scientific()`` if they + are is available, requires NumPy >= 1.14. In that case, values with magnitude + between 0.01 and 10000 are formatted using ``format_float_positional ()`` and other + values are formatted using ``format_float_scientific()``. If those NumPy functions + are not available, returns the ``repr`` of the input. + """ if not HAS_FMT_FLT_POS: return repr(data) @@ -136,6 +161,14 @@ def float2string(data: float) -> str: def represent_float(self: Any, data: Any) -> Any: + """Format floating point numbers for ruamel YAML. + + :param data: + The floating point data to be formatted. + + Uses `float2string` to format the floating point input to a string, then inserts + the resulting string into the YAML tree as a scalar. + """ if data != data: value = ".nan" elif data == self.inf_value: @@ -157,7 +190,9 @@ def get_float_or_units(node: etree.Element) -> Union[str, float]: :param node: The XML node with a value in the text and optionally a units attribute. - Given an XML nodes like:: + Given XML nodes like: + + .. code:: XML 1000.0 1000.0 @@ -182,8 +217,11 @@ def get_float_or_units(node: etree.Element) -> Union[str, float]: def split_species_value_string(node: etree.Element) -> Dict[str, float]: """Split a string of species:value pairs into a dictionary. - The keys of the dictionary are species names and the values are the - number associated with each species. This is useful for things like + :param node: + An XML node whose text contains the species: value pairs + + Returns a dictionary where the keys of the dictionary are species names and the + values are the number associated with each species. This is useful for things like elemental composition, mole fraction mappings, coverage mappings, etc. The algorithm is reimplemented from ``compositionMap::parseCompString`` in @@ -192,6 +230,7 @@ def split_species_value_string(node: etree.Element) -> Dict[str, float]: text = clean_node_text(node) pairs = FlowMap({}) start, stop, left = 0, 0, 0 + # \S matches the first non-whitespace character non_whitespace = re.compile(r"\S") stop_re = re.compile(r"[,;\s]") while stop < len(text): @@ -199,13 +238,12 @@ def split_species_value_string(node: etree.Element) -> Dict[str, float]: colon = text.index(":", left) except ValueError: break - # \S matches the first non-whitespace character + # colon + 1 here excludes the colon itself from the search valstart_match = non_whitespace.search(text, colon + 1) if valstart_match is None: - # is this the right thing to do? - print("valstart") break + valstart = valstart_match.start() stop_match = stop_re.search(text, valstart) if stop_match is None: @@ -236,7 +274,15 @@ def split_species_value_string(node: etree.Element) -> Dict[str, float]: def clean_node_text(node: etree.Element) -> str: - """Clean the text of a node.""" + """Clean the text of a node. + + :param node: + An XML node with a text value. + + Raises `MissingNodeText` if the node text is not present. Otherwise, replaces + newlines and tab characters with spaces, then strips the resulting string. This + turns multi-line text values into a single line that can be split on whitespace. + """ text = node.text if text is None: raise MissingNodeText("The text of the node must exist", node) @@ -244,12 +290,6 @@ def clean_node_text(node: etree.Element) -> str: class Phase: - """Represents a phase. - - :param phase: - ElementTree Element node with a phase definition. - """ - thermo_model_mapping = { "IdealGas": "ideal-gas", "Incompressible": "constant-density", @@ -323,6 +363,20 @@ def __init__( species_data: Dict[str, List["Species"]], reaction_data: Dict[str, List["Reaction"]], ): + """Represent an XML ``phase`` node. + + :param phase: + XML node containing a phase definition. + :param species_data: + Mapping of species data sources to lists of `Species` instances. + :param reaction_data: + Mapping of reaction data sources to lists of `Reaction` instances. + + This class processes the XML node of a phase definition and generates a mapping + for the YAML output. The mapping is stored in the ``attribs`` instance + attribute and automatically formatted to YAML by the `~Phase.to_yaml` class + method. + """ phase_name = phase.get("id") if phase_name is None: raise MissingXMLAttribute( @@ -545,9 +599,16 @@ def __init__( def ideal_molal_solution( self, activity_coeffs: etree.Element ) -> Dict[str, Union[str, float]]: - """Process the cutoff data in an IdealMolalSolution type. + """Process the cutoff data in an ``IdealMolalSolution`` phase-thermo type. + + :param activity_coeffs: + XML ``activityCoefficients`` node. For the ``IdealMolalSolution`` thermo + type, this node contains information about cutoff limits for the + thermodynamic properties. - Returns a (possibly empty) dictionary to update the phase attributes. + Returns a (possibly empty) dictionary to update the `Phase` attributes. The + dictionary will be empty when there are no cutoff nodes in the + ``activityCoefficients`` node. """ cutoff = {} # type: Dict[str, Union[str, float]] cutoff_node = activity_coeffs.find("idealMolalSolnCutoff") @@ -556,6 +617,7 @@ def ideal_molal_solution( if cutoff_model is not None: cutoff["model"] = cutoff_model for limit_node in cutoff_node: + # Remove _limit or _cutoff from the right side of the node tag tag = limit_node.tag.rsplit("_", 1)[0] cutoff[tag] = get_float_or_units(limit_node) return cutoff @@ -563,10 +625,17 @@ def ideal_molal_solution( def margules( self, activity_coeffs: etree.Element ) -> List[Dict[str, List[Union[str, float]]]]: - """Process activity coefficients for a Margules phase. + """Process activity coefficients for a ``Margules`` phase-thermo type. + + :param activity_coeffs: + XML ``activityCoefficients`` node. For the ``Margules`` phase-thermo type + these are interaction parameters between the species in the phase. Returns a list of interaction data values. Margules does not require the - binaryNeutralSpeciesParameters. + ``binaryNeutralSpeciesParameters`` node to be present. Almost a superset of the + Redlich-Kister parameters, but since the ``binaryNeutralSpeciesParameters`` are + processed in a loop, there's no advantage to re-use Redlich-Kister processing + because this function would have to go through the same nodes again. """ all_binary_params = activity_coeffs.findall("binaryNeutralSpeciesParameters") interactions = [] @@ -641,9 +710,14 @@ def margules( def redlich_kister( self, activity_coeffs: etree.Element ) -> List[Dict[str, List[Union[str, float]]]]: - """Process activity coefficents for a Redlich-Kister phase. + """Process activity coefficents for a Redlich-Kister phase-thermo type. + + :param activity_coeffs: + XML ``activityCoefficients`` node. For the ``RedlichKister`` phase-thermo + type these are interaction parameters between the species in the phase. - Returns a list of interaction data values. + Returns a list of interaction data values. The ``activityCoefficients`` node + must have a ``binaryNeutralSpeciesParameters`` child node. """ all_binary_params = activity_coeffs.findall("binaryNeutralSpeciesParameters") if not all_binary_params: @@ -695,11 +769,21 @@ def check_elements( this_phase_species: List[Dict[str, Iterable[str]]], species_data: Dict[str, List["Species"]], ) -> None: - """Check the species elements for inclusion in the phase level specification. + """Check the species elements for inclusion in the `Phase`-level specification. + + :param this_phase_species: + A list of mappings of species data sources to the species names in that data + source. Passed as an argument instead of using the ``species`` key in the + instance ``attribs`` dictionary because the attribute could be a mapping or + a list of mappings, whereas this argument is always a list of mappings. + :param species_data: + Mapping of species data sources (i.e., ``id`` attributes on ``speciesData`` + nodes) to lists of `Species` instances. Some species include a charge node that adds an electron to the species - composition, so we need to update the phase-level elements to include - the electron. + composition. The `Phase`s that include these species don't necessarily include + the electron in the `Phase`-level elements list, so we need to update that to + include it if necessary. """ phase_elements = self.attribs.get("elements") if phase_elements is None: @@ -727,7 +811,20 @@ def move_RK_coeffs_to_species( ) -> None: """Move the Redlich-Kwong activity coefficient data from phase to species. - This modifies the species objects in-place in the species_data object. + :param this_phase_species: + A list of mappings of species data sources to the species names in that + data source. Passed as an argument instead of using the ``species`` key + in the instance ``attribs`` because the attribute could be a mapping or + a list of mappings, whereas this argument is always a list of mappings. + :param activity_coeffs: + XML ``activityCoefficients`` node. + :param species_data: + Mapping of species data sources (i.e., ``id`` attributes on ``speciesData`` + nodes) to lists of `Species` instances. + + The YAML format moves the specification of Redlich-Kwong binary interaction + parameters from the `Phase` node into the `Species` nodes. This modifies the + `Species` objects in-place in the ``species_data`` list. """ all_species_eos = {} # type: Dict[str, RK_EOS_DICT] for pure_param in activity_coeffs.iterfind("pureFluidParameters"): @@ -832,7 +929,20 @@ def move_density_to_species( ) -> None: """Move the phase density information into each species definition. - This modifies the species objects in-place in the species_data object. + :param this_phase_species: + A list of mappings of species data sources to the species names in that + data source. Passed as an argument instead of using the ``species`` key + in the instance ``attribs`` because the attribute could be a mapping or + a list of mappings, whereas this argument is always a list of mappings. + :param phase_thermo: + XML ``thermo`` node. + :param species_data: + Mapping of species data sources (i.e., ``id`` attributes on ``speciesData`` + nodes) to lists of `Species` instances. + + The YAML format moves the specification of density for ``StoichSubstance`` + phase-thermo types from the `Phase` node into the `Species` nodes. This modifies + the `Species` objects in-place in the ``species_data`` list. """ den_node = phase_thermo.find("density") const_prop = "density" @@ -870,7 +980,16 @@ def move_density_to_species( def get_species_array( self, speciesArray_node: etree.Element ) -> Dict[str, Iterable[str]]: - """Process a list of species from a speciesArray node.""" + """Process a list of species from a ``speciesArray`` node. + + :param speciesArray_node: + An XML ``speciesArray`` node. + + The ``speciesArray`` node has the data source plus a list of species to derive + from that data source. If the data source specifies an XML file, convert the + extension to ``.yaml``. If the data source ``id`` is ``species_data``, reformat + to just ``species`` for the YAML file. Otherwise, retain the ``id`` as-is. + """ species_list = FlowList(clean_node_text(speciesArray_node).split()) datasrc = speciesArray_node.get("datasrc", "") if datasrc == "#species_data": @@ -891,7 +1010,16 @@ def get_reaction_array( reactionArray_node: etree.Element, reaction_data: Dict[str, List["Reaction"]], ) -> Dict[str, str]: - """Process reactions from a reactionArray node in a phase definition.""" + """Process reactions from a ``reactionArray`` node in a phase definition. + + :param reactionArray_node: + An XML ``reactionArray`` node. + + The ``reactionArray`` node has the data source plus a list of reaction to derive + from that data source. If the data source specifies an XML file, convert the + extension to ``.yaml``. If the data source ``id`` is ``reaction_data``, reformat + to just ``reaction`` for the YAML file. Otherwise, retain the ``id`` as-is. + """ datasrc = reactionArray_node.get("datasrc", "") if not datasrc: raise MissingXMLAttribute("The reactionArray must include a datasrc") @@ -948,7 +1076,20 @@ def filter_reaction_list( ) -> str: """Filter the reaction_data list to only include specified reactions. - Returns a string that should be used as the data source in the YAML file. + :param datasrc: + The XML source of the reaction data that is being filtered. + :param filter_text: + The text specified in the ``filter`` node telling which reactions are being + filtered. + :param reaction_data: + Mapping of reaction data sources (i.e., ``id`` attributes on + ``reactionData`` nodes) to lists of `Reaction` instances. + + The YAML format does not support filtering reactions by setting options in the + `Phase` node, like the XML format does. Instead, when filters are used in XML, + the reactions should be split into separate top-level nodes in the YAML file, + which then become the data source in the YAML reactions specification. Returns + a string that should be used as the data source in the YAML file. """ all_reactions = reaction_data[datasrc.lstrip("#")] hits = [] @@ -981,6 +1122,11 @@ def filter_reaction_list( return new_datasrc def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str]: + """Process data from the ``tabulatedThermo`` node. + + :param tab_thermo_node: + The XML node with the tabulated thermodynamics data. + """ tab_thermo = BlockMap() enthalpy_node = tab_thermo_node.find("enthalpy") if enthalpy_node is None or enthalpy_node.text is None: @@ -1038,7 +1184,14 @@ def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str] return tab_thermo def hmw_electrolyte(self, activity_node: etree.Element): - """Process the activity coefficients for HMW-electrolyte.""" + """Process the activity coefficients for an ``HMW`` phase-thermo type. + + :param activity_coeffs: + XML ``activityCoefficients`` node. + + The ``activityCoefficients`` must include the ``A_debye`` node, as well as + any interaction parameters between species. + """ activity_data = BlockMap({"temperature-model": activity_node.get("TempModel")}) A_Debye_node = activity_node.find("A_Debye") if A_Debye_node is None: @@ -1090,7 +1243,19 @@ def debye_huckel( activity_node: etree.Element, species_data: Dict[str, List["Species"]], ) -> Dict[str, Union[str, float, bool]]: - """Process the activity coefficiences data for the Debye Huckel model.""" + """Process the activity coefficients for the ``DebyeHuckel`` phase-thermo type. + + :param this_phase_species: + A list of mappings of species data sources to the species names in that + data source. Passed as an argument instead of using the ``species`` key + in the instance ``attribs`` because the attribute could be a mapping or + a list of mappings, whereas this argument is always a list of mappings. + :param activity_node: + XML ``activityCoefficients`` node. + :param species_data: + Mapping of species data sources (i.e., ``id`` attributes on ``speciesData`` + nodes) to lists of `Species` instances. + """ model_map = { "dilute_limit": "dilute-limit", "bdot_with_variable_a": "B-dot-with-variable-a", @@ -1230,13 +1395,32 @@ def debye_huckel( @classmethod def to_yaml(cls, representer, data): + """Serialize the class instance to YAML format suitable for ruamel.yaml. + + :param representer: + An instance of a ruamel.yaml representer type. + :param data: + An instance of this class that will be serialized. + + The class instance should have an instance attribute called ``attribs`` which + is a dictionary representing the information about the instance. The dictionary + is serialized using the ``represent_dict`` method of the ``representer``. + """ return representer.represent_dict(data.attribs) class SpeciesThermo: - """Represents a species thermodynamic model.""" - def __init__(self, thermo: etree.Element) -> None: + """Represent the polynomial-type thermodynamic data for a `Species`. + + :param thermo: + A ``species/thermo`` XML node. Must have one or more child nodes with tag + ``NASA``, ``NASA9``, ``const_cp``, ``Shomate``, or ``Mu0``. + + This class will process the `Species`-level thermodynamic information for the + polynomial thermo types. The pressure-dependent standard state types are + processed directly in the `Species` instance. + """ thermo_type = thermo[0].tag if thermo_type not in ["NASA", "NASA9", "const_cp", "Shomate", "Mu0"]: raise TypeError("Unknown thermo model type: '{}'".format(thermo[0].tag)) @@ -1246,7 +1430,18 @@ def __init__(self, thermo: etree.Element) -> None: def process_polynomial( self, thermo: etree.Element, poly_type: str ) -> Tuple[List[List[float]], List[float]]: - """Process data from one of the polynomial type species thermo types.""" + """Process the `Species` thermodynamic polynomial for several types. + + :param thermo: + A ``species/thermo`` XML node. Must have one or more child nodes with tag + ``NASA``, ``NASA9``, or ``Shomate``. + :param poly_type: + A string determining the type of polynomial. One of ``NASA``, ``NASA9``, + or ``Shomate``. + + This method converts the polynomial data for the ``NASA``, ``NASA9``, and + ``Shomate`` thermodynamic types into the appropriate YAML structure. + """ temperature_ranges = set() model_nodes = thermo.findall(poly_type) unsorted_data = {} @@ -1280,7 +1475,12 @@ def process_polynomial( def Shomate( self, thermo: etree.Element ) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: - """Process a Shomate polynomial from XML to a dictionary.""" + """Process a Shomate `Species` thermodynamic polynomial. + + :param thermo: + A ``species/thermo`` XML node. There must be one or more child nodes with + the tag ``Shomate``. + """ thermo_attribs = BlockMap({"model": "Shomate"}) data, temperature_ranges = self.process_polynomial(thermo, "Shomate") thermo_attribs["temperature-ranges"] = temperature_ranges @@ -1288,7 +1488,12 @@ def Shomate( return thermo_attribs def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: - """Process a NASA 7 thermo entry from XML to a dictionary.""" + """Process a NASA 7-coefficient thermodynamic polynomial. + + :param thermo: + A ``species/thermo`` XML node. There must be one or more child nodes with + the tag ``NASA``. + """ thermo_attribs = BlockMap({"model": "NASA7"}) data, temperature_ranges = self.process_polynomial(thermo, "NASA") thermo_attribs["temperature-ranges"] = temperature_ranges @@ -1296,7 +1501,12 @@ def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE" return thermo_attribs def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: - """Process a NASA 9 thermo entry from XML to a dictionary.""" + """Process a NASA 9-coefficient thermodynamic polynomial. + + :param thermo: + A ``species/thermo`` XML node. There must be one or more child nodes with + the tag ``NASA9``. + """ thermo_attribs = BlockMap({"model": "NASA9"}) data, temperature_ranges = self.process_polynomial(thermo, "NASA9") thermo_attribs["temperature-ranges"] = temperature_ranges @@ -1304,7 +1514,12 @@ def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE return thermo_attribs def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: - """Process a constant c_p thermo entry from XML to a dictionary.""" + """Process a `Species` thermodynamic type with constant specific heat. + + :param thermo: + A ``species/thermo`` XML node. There must be one child node with the tag + ``const_cp``. + """ thermo_attribs = BlockMap({"model": "constant-cp"}) const_cp_node = thermo.find("const_cp") if const_cp_node is None: @@ -1322,7 +1537,12 @@ def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: def Mu0( self, thermo: etree.Element ) -> Dict[str, Union[str, Dict[float, Iterable]]]: - """Process a piecewise Gibbs thermo entry from XML to a dictionary.""" + """Process a piecewise Gibbs Free Energy thermodynamic polynomial. + + :param thermo: + A ``species/thermo`` XML node. There must be one child node with the tag + ``Mu0``. + """ thermo_attribs = BlockMap({"model": "piecewise-Gibbs"}) Mu0_node = thermo.find("Mu0") if Mu0_node is None: @@ -1357,12 +1577,21 @@ def Mu0( @classmethod def to_yaml(cls, representer, data): + """Serialize the class instance to YAML format suitable for ruamel.yaml. + + :param representer: + An instance of a ruamel.yaml representer type. + :param data: + An instance of this class that will be serialized. + + The class instance should have an instance attribute called ``attribs`` which + is a dictionary representing the information about the instance. The dictionary + is serialized using the ``represent_dict`` method of the ``representer``. + """ return representer.represent_dict(data.attribs) class SpeciesTransport: - """Represents the transport properties of a species.""" - species_transport_mapping = {"gas_transport": "gas"} transport_properties_mapping = { "LJ_welldepth": "well-depth", @@ -1375,6 +1604,13 @@ class SpeciesTransport: } def __init__(self, transport: etree.Element): + """Represent the Lennard-Jones transport properties of a species. + + :param transport: + A ``species/transport`` XML node. + + This class only supports one type of transport model, ``gas_transport``. + """ self.attribs = BlockMap({}) transport_model = transport.get("model") if transport_model not in self.species_transport_mapping: @@ -1398,12 +1634,21 @@ def __init__(self, transport: etree.Element): @classmethod def to_yaml(cls, representer, data): + """Serialize the class instance to YAML format suitable for ruamel.yaml. + + :param representer: + An instance of a ruamel.yaml representer type. + :param data: + An instance of this class that will be serialized. + + The class instance should have an instance attribute called ``attribs`` which + is a dictionary representing the information about the instance. The dictionary + is serialized using the ``represent_dict`` method of the ``representer``. + """ return representer.represent_dict(data.attribs) class Species: - """Represents a species.""" - standard_state_model_mapping = { "ideal-gas": "ideal-gas", "constant_incompressible": "constant-volume", @@ -1422,6 +1667,16 @@ class Species: } def __init__(self, species_node: etree.Element): + """Represent an XML ``species`` node. + + :param species_node: + The XML node with the species information. + + This class processes the XML node of a species definition and generates a + mapping for the YAML output. The mapping is stored in the ``attribs`` instance + attribute and automatically formatted to YAML by the `~Species.to_yaml` class + method. + """ self.attribs = BlockMap() species_name = species_node.get("name") if species_name is None: @@ -1499,8 +1754,11 @@ def __init__(self, species_node: etree.Element): def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: """Process a species with HKFT thermo type. - Done in a method because it requires synthesizing data from the thermo node - and the standardState node. + :param species_node: + The XML node with the species information. + + Requires synthesizing data from the ``thermo`` node and the ``standardState`` + node. """ thermo_node = species_node.find("./thermo/HKFT") std_state_node = species_node.find("standardState") @@ -1546,11 +1804,14 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: return eqn_of_state def process_standard_state_node(self, species_node: etree.Element) -> None: - """Process the standardState node in a species definition. + """Process the ``standardState`` node in a species definition. + + :param species_node: + The XML node with the species information. - If the model is IonFromNeutral or HKFT, this function doesn't do anything to - the species object. Otherwise, the model data is put into the YAML - equation-of-state node. + If the model is ``IonFromNeutral`` or ``HKFT``, this function doesn't do + anything to the `Species` object. Otherwise, the model data is put into the YAML + ``equation-of-state`` node. """ std_state = species_node.find("standardState") if std_state is not None: @@ -1581,7 +1842,7 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: raise MissingXMLNode( "{} standard state model requires a " "volumeTemperaturePolynomial node".format(std_state_model), - std_state + std_state, ) poly_values_node = poly_node.find("floatArray") if poly_values_node is None: @@ -1611,17 +1872,33 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: @classmethod def to_yaml(cls, representer, data): - return representer.represent_dict(data.attribs) + """Serialize the class instance to YAML format suitable for ruamel.yaml. + :param representer: + An instance of a ruamel.yaml representer type. + :param data: + An instance of this class that will be serialized. -class Reaction: - """Represents a reaction. + The class instance should have an instance attribute called ``attribs`` which + is a dictionary representing the information about the instance. The dictionary + is serialized using the ``represent_dict`` method of the ``representer``. + """ + return representer.represent_dict(data.attribs) - :param reaction: - An ETree Element node with the reaction information - """ +class Reaction: def __init__(self, reaction: etree.Element, node_motz_wise: bool): + """Represent an XML ``reaction`` node. + + :param reaction: + The XML node with the reaction information. + :param node_motz_wise: + ``True`` if the ``reactionData`` node that contains this ``reaction`` node + has the ``motz_wise`` attribute set to ``True``. Otherwise, ``False``. This + argument is used to adjust each reaction instead of setting the + `Phase`-level option because the reactions are processed before the phases, + so it isn't known at this point what phase these reactions will apply to. + """ self.attribs = BlockMap({}) reaction_id = reaction.get("id", False) # type: Union[str, int, bool] if reaction_id: @@ -1753,13 +2030,24 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): @classmethod def to_yaml(cls, representer, data): + """Serialize the class instance to YAML format suitable for ruamel.yaml. + + :param representer: + An instance of a ruamel.yaml representer type. + :param data: + An instance of this class that will be serialized. + + The class instance should have an instance attribute called ``attribs`` which + is a dictionary representing the information about the instance. The dictionary + is serialized using the ``represent_dict`` method of the ``representer``. + """ return representer.represent_dict(data.attribs) def sri(self, rate_coeff: etree.Element) -> "SRI_TYPE": """Process an SRI reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ reaction_attribs = self.lindemann((rate_coeff)) falloff_node = rate_coeff.find("falloff") @@ -1776,8 +2064,8 @@ def sri(self, rate_coeff: etree.Element) -> "SRI_TYPE": def threebody(self, rate_coeff: etree.Element) -> "THREEBODY_TYPE": """Process a three-body reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ reaction_attribs = FlowMap({"type": "three-body"}) reaction_attribs["rate-constant"] = self.process_arrhenius_parameters( @@ -1792,8 +2080,8 @@ def threebody(self, rate_coeff: etree.Element) -> "THREEBODY_TYPE": def lindemann(self, rate_coeff: etree.Element) -> "LINDEMANN_TYPE": """Process a Lindemann falloff reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ reaction_attribs = FlowMap({"type": "falloff"}) for arr_coeff in rate_coeff.iterfind("Arrhenius"): @@ -1816,8 +2104,8 @@ def lindemann(self, rate_coeff: etree.Element) -> "LINDEMANN_TYPE": def troe(self, rate_coeff: etree.Element) -> "TROE_TYPE": """Process a Troe falloff reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ # This gets the low-p and high-p rate constants and the efficiencies reaction_attribs = self.lindemann(rate_coeff) @@ -1841,8 +2129,8 @@ def troe(self, rate_coeff: etree.Element) -> "TROE_TYPE": def chemact(self, rate_coeff: etree.Element) -> "CHEMACT_TYPE": """Process a chemically activated falloff reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ reaction_attribs = FlowMap({"type": "chemically-activated"}) for arr_coeff in rate_coeff.iterfind("Arrhenius"): @@ -1880,8 +2168,8 @@ def chemact(self, rate_coeff: etree.Element) -> "CHEMACT_TYPE": def plog(self, rate_coeff: etree.Element) -> "PLOG_TYPE": """Process a PLOG reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ reaction_attributes = FlowMap({"type": "pressure-dependent-Arrhenius"}) rate_constants = [] @@ -1901,8 +2189,8 @@ def plog(self, rate_coeff: etree.Element) -> "PLOG_TYPE": def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": """Process a Chebyshev reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ reaction_attributes = FlowMap( { @@ -1956,9 +2244,10 @@ def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": """Process an interface reaction. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. + This represents both interface and electrochemical reactions. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. """ arr_node = rate_coeff.find("Arrhenius") if arr_node is None: @@ -2014,8 +2303,8 @@ def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": def arrhenius(self, rate_coeff: etree.Element) -> "ARRHENIUS_TYPE": """Process a standard Arrhenius-type reaction. - Returns a dictionary with the appropriate fields set that is - used to update the parent reaction entry dictionary. + :param rate_coeff: + The XML node with rate coefficient information for this reaction. """ return FlowMap( { @@ -2028,7 +2317,12 @@ def arrhenius(self, rate_coeff: etree.Element) -> "ARRHENIUS_TYPE": def process_arrhenius_parameters( self, arr_node: Optional[etree.Element] ) -> "ARRHENIUS_PARAMS": - """Process the parameters from an Arrhenius child of a rateCoeff node.""" + """Process the parameters from an ``Arrhenius`` child of a ``rateCoeff`` node. + + :param arr_node: + The XML node with the Arrhenius parameters. Must have three child nodes + with tags ``A``, ``b``, and ``E``. + """ if arr_node is None: raise MissingXMLNode("The Arrhenius node must be present.") A_node = arr_node.find("A") @@ -2048,13 +2342,33 @@ def process_arrhenius_parameters( ) def process_efficiencies(self, eff_node: etree.Element) -> "EFFICIENCY_PARAMS": - """Process the efficiency information about a reaction.""" + """Process the efficiency information about a reaction. + + :param eff_node: + The XML efficiency node. The text of the node must be a space-delimited + string of ``species:value`` pairs. + """ efficiencies = [eff.rsplit(":", 1) for eff in clean_node_text(eff_node).split()] return FlowMap({s: float(e) for s, e in efficiencies}) def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Species]]: - """Take a speciesData node and return a dictionary of Species objects.""" + """Generate lists of `Species` instances mapped to the ``speciesData`` id string. + + :param ctml_tree: + The root XML node of the CTML document. + + The CTML document is searched for ``speciesData`` nodes that contain ``species`` + child nodes. Each ``speciesData`` node must have an ``id`` attribute, which is used + as the key of the returned dictionary. The values in the dictionary are lists of + `Species` instances representing the ``species`` nodes in that ``speciesData`` + node. The ``id`` attribute is also used as the top-level key in the YAML document + for that set of species, with the exception that ``species_data`` is changed to + just ``species``. + + If ``speciesData`` nodes with the same ``id`` attribute are found, only the first + section with that ``id`` is put into the YAML output file. + """ species = {} # type: Dict[str, List[Species]] for species_data_node in ctml_tree.iterfind("speciesData"): this_data_node_id = species_data_node.get("id", "") @@ -2074,7 +2388,22 @@ def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Sp def create_reactions_from_data_node( ctml_tree: etree.Element ) -> Dict[str, List[Reaction]]: - """Take a reactionData node and return a dictionary of Reaction objects.""" + """Generate lists of `Reaction` instances mapped to the ``reactionData`` id string. + + :param ctml_tree: + The root XML node of the CTML document. + + The CTML document is searched for ``reactionData`` nodes that contain ``reaction`` + child nodes. Each ``reactionData`` node must have an ``id`` attribute, which is used + as the key of the returned dictionary. The values in the dictionary are lists of + `Reaction` instances representing the ``reaction`` nodes in that ``reactionData`` + node. The ``id`` attribute is also used as the top-level key in the YAML document + for that set of reactions, with the exception that ``reaction_data`` is changed to + just ``reactions``. + + If ``reactionData`` nodes with the same ``id`` attribute are found, only the first + section with that ``id`` is put into the YAML output file. + """ reactions = {} # type: Dict[str, List[Reaction]] for reactionData_node in ctml_tree.iterfind("reactionData"): node_motz_wise = False @@ -2099,7 +2428,19 @@ def create_phases_from_data_node( species_data: Dict[str, List[Species]], reaction_data: Dict[str, List[Reaction]], ) -> List[Phase]: - """Take a phase node and return a phase object.""" + """Generate a list of `Phase` instances from XML ``phase`` nodes. + + :param ctml_tree: + The root XML node of the CTML document. + :param species_data: + Mapping of ``speciesData`` id strings to lists of `Species` instances. + :param reaction_data: + Mapping of ``reactionData`` id strings to lists of `Reaction` instances. + + The CTML document is searched for ``phase`` nodes, which are processed into `Phase` + instances. For any Lattice-type phases, the child ``phase`` nodes are un-nested + from their parent node. + """ phases = [ Phase(node, species_data, reaction_data) for node in ctml_tree.iterfind("phase") ] @@ -2114,7 +2455,16 @@ def create_phases_from_data_node( def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): - """Convert an input CTML file to a YAML file.""" + """Convert an input legacy CTML file to a YAML file. + + :param inpfile: + The input CTML file name. + :param outfile: + The output YAML file name. + + All files are assumed to be relative to the current working directory of the Python + process running this script. + """ inpfile = Path(inpfile) ctml_text = inpfile.read_text().lstrip() # Replace any raw ampersands in the text with an escaped ampersand. This From 1d62ec0439836051284d2c4defec4fc6dab98942 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 24 Nov 2019 13:54:08 -0500 Subject: [PATCH 88/99] [Doc] Add documentation for ctml2yaml script Add intersphinx extension to auto-link to Python docs. Bump KaTeX version. --- doc/sphinx/conf.py | 9 +++++- doc/sphinx/yaml/ctml_conversion.rst | 50 +++++++++++++++++++++++++++++ doc/sphinx/yaml/index.rst | 1 + 3 files changed, 59 insertions(+), 1 deletion(-) create mode 100644 doc/sphinx/yaml/ctml_conversion.rst diff --git a/doc/sphinx/conf.py b/doc/sphinx/conf.py index cf0a3019716..5af05dc193e 100644 --- a/doc/sphinx/conf.py +++ b/doc/sphinx/conf.py @@ -40,9 +40,10 @@ 'sphinx.ext.autosummary', 'sphinxcontrib.doxylink', 'sphinxcontrib.katex', # Use KaTeX because it's faster and the main site uses it + 'sphinx.ext.intersphinx', ] -katex_version = '0.10.0-beta' +katex_version = '0.11.1' autodoc_default_flags = ['members','show-inheritance','undoc-members'] @@ -53,6 +54,12 @@ '../../doxygen/html/') } +intersphinx_mapping = { + 'python': ('https://docs.python.org/3', None), + 'pandas': ('https://pandas.pydata.org/pandas-docs/stable/', None), + 'numpy': ('https://docs.scipy.org/doc/numpy/', None), +} + # Ensure that the primary domain is the Python domain, since we've added the # MATLAB domain with sphinxcontrib.matlab primary_domain = 'py' diff --git a/doc/sphinx/yaml/ctml_conversion.rst b/doc/sphinx/yaml/ctml_conversion.rst new file mode 100644 index 00000000000..d4ea25f8c53 --- /dev/null +++ b/doc/sphinx/yaml/ctml_conversion.rst @@ -0,0 +1,50 @@ +*********************** +CTML to YAML conversion +*********************** + +.. py:module:: cantera.ctml2yaml +.. py:currentmodule:: cantera.ctml2yaml + +The script ``ctml2yaml.py`` will convert files from the legacy CTML format to YAML input +format. The documentation below describes the classes and functions in the script. Each +function/method is annotated with the Python types that the function accepts. + +Most users will access the functionality of this module via the command line with the +``ctml2yaml`` entry-point script. For programmatic access, the `main` and/or `convert` +functions should be used. `main` should be used when command line arguments must be +processed, while `convert` takes an input filename or a string containing the CTML file +to be converted, and optionally the name of the output file. + +Module-level functions +====================== + +.. autofunction:: float2string +.. autofunction:: represent_float +.. autofunction:: get_float_or_units +.. autofunction:: split_species_value_string +.. autofunction:: clean_node_text +.. autofunction:: create_species_from_data_node +.. autofunction:: create_reactions_from_data_node +.. autofunction:: create_phases_from_data_node +.. autofunction:: convert + +Conversion classes +================== + +.. autoclass:: Phase + :no-undoc-members: +.. autoclass:: Species + :no-undoc-members: +.. autoclass:: SpeciesThermo + :no-undoc-members: +.. autoclass:: SpeciesTransport + :no-undoc-members: +.. autoclass:: Reaction + :no-undoc-members: + +Exceptions +========== + +.. autoexception:: MissingXMLNode +.. autoexception:: MissingXMLAttribute +.. autoexception:: MissingNodeText diff --git a/doc/sphinx/yaml/index.rst b/doc/sphinx/yaml/index.rst index ea66b558892..f5f3abf85ff 100644 --- a/doc/sphinx/yaml/index.rst +++ b/doc/sphinx/yaml/index.rst @@ -11,3 +11,4 @@ YAML Input File Reference elements species reactions + ctml_conversion From 6a47942eefd4f7dd51ce6974042d454ad01d90bc Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 24 Nov 2019 19:46:28 -0500 Subject: [PATCH 89/99] [ctml2yaml] Add QUANTITY type definition A quantity is a value that can be either just a number (i.e., a float) or a number plus a unit (i.e., a str). --- doc/sphinx/yaml/ctml_conversion.rst | 2 +- interfaces/cython/cantera/ctml2yaml.py | 108 ++++++++++++------------- 2 files changed, 55 insertions(+), 55 deletions(-) diff --git a/doc/sphinx/yaml/ctml_conversion.rst b/doc/sphinx/yaml/ctml_conversion.rst index d4ea25f8c53..0d1e246fdfc 100644 --- a/doc/sphinx/yaml/ctml_conversion.rst +++ b/doc/sphinx/yaml/ctml_conversion.rst @@ -20,7 +20,7 @@ Module-level functions .. autofunction:: float2string .. autofunction:: represent_float -.. autofunction:: get_float_or_units +.. autofunction:: get_float_or_quantity .. autofunction:: split_species_value_string .. autofunction:: clean_node_text .. autofunction:: create_species_from_data_node diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 446910a05ff..d56ca8ca54c 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -23,19 +23,17 @@ # This is available in the built-in typing module in Python 3.8 from typing_extensions import TypedDict + QUANTITY = Union[float, str] + RK_EOS_DICT = TypedDict( "RK_EOS_DICT", - { - "a": List[Union[str, float]], - "b": Union[str, float], - "binary-a": Dict[str, List[Union[str, float]]], - }, + {"a": List[QUANTITY], "b": QUANTITY, "binary-a": Dict[str, List[QUANTITY]]}, total=False, ) DH_BETA_MATRIX = TypedDict( - "DH_BETA_MATRIX", {"species": List[str], "beta": Union[str, float]}, total=False + "DH_BETA_MATRIX", {"species": List[str], "beta": QUANTITY}, total=False ) - ARRHENIUS_PARAMS = Dict[str, Union[float, str]] + ARRHENIUS_PARAMS = Dict[str, Union[str, QUANTITY]] EFFICIENCY_PARAMS = Dict[str, float] LINDEMANN_PARAMS = Union[str, ARRHENIUS_PARAMS, EFFICIENCY_PARAMS] TROE_PARAMS = Dict[str, float] @@ -58,7 +56,11 @@ SRI_TYPE = Dict[str, Union[LINDEMANN_PARAMS, SRI_PARAMS]] THERMO_POLY_TYPE = Union[List[List[float]], List[float]] - HKFT_THERMO_TYPE = Union[str, float, List[Union[str, float]]] + HKFT_THERMO_TYPE = Union[str, QUANTITY, List[QUANTITY]] + # The last Union[str, float] here is not a QUANTITY + HMW_THERMO_TYPE = Union[ + str, QUANTITY, bool, Dict[str, Union[float, List[Union[str, float]]]] + ] BlockMap = yaml.comments.CommentedMap @@ -184,7 +186,7 @@ def represent_float(self: Any, data: Any) -> Any: yaml.RoundTripRepresenter.add_representer(float, represent_float) -def get_float_or_units(node: etree.Element) -> Union[str, float]: +def get_float_or_quantity(node: etree.Element) -> "QUANTITY": """Process an XML node into a float value or a value with units. :param node: @@ -456,7 +458,7 @@ def __init__( pass excess_h_node = phase_thermo.find("h_mix") if excess_h_node is not None: - self.attribs["excess-enthalpy"] = get_float_or_units(excess_h_node) + self.attribs["excess-enthalpy"] = get_float_or_quantity(excess_h_node) product_spec_node = phase_thermo.find("product_species") if product_spec_node is not None: self.attribs["product-species"] = clean_node_text(product_spec_node) @@ -521,16 +523,16 @@ def __init__( for node in phase_thermo: if node.tag == "site_density": - self.attribs["site-density"] = get_float_or_units(node) + self.attribs["site-density"] = get_float_or_quantity(node) elif node.tag == "density": if self.attribs["thermo"] == "electron-cloud": - self.attribs["density"] = get_float_or_units(node) + self.attribs["density"] = get_float_or_quantity(node) elif node.tag == "tabulatedSpecies": self.attribs["tabulated-species"] = node.get("name") elif node.tag == "tabulatedThermo": self.attribs["tabulated-thermo"] = self.get_tabulated_thermo(node) elif node.tag == "chemicalPotential": - self.attribs["chemical-potential"] = get_float_or_units(node) + self.attribs["chemical-potential"] = get_float_or_quantity(node) transport_node = phase.find("transport") if transport_node is not None: @@ -584,7 +586,7 @@ def __init__( composition = split_species_value_string(prop) phase_state[property_name] = composition else: - value = get_float_or_units(prop) + value = get_float_or_quantity(prop) phase_state[property_name] = value if phase_state: @@ -598,7 +600,7 @@ def __init__( def ideal_molal_solution( self, activity_coeffs: etree.Element - ) -> Dict[str, Union[str, float]]: + ) -> Dict[str, Union[str, "QUANTITY"]]: """Process the cutoff data in an ``IdealMolalSolution`` phase-thermo type. :param activity_coeffs: @@ -610,7 +612,7 @@ def ideal_molal_solution( dictionary will be empty when there are no cutoff nodes in the ``activityCoefficients`` node. """ - cutoff = {} # type: Dict[str, Union[str, float]] + cutoff = {} # type: Dict[str, Union[str, QUANTITY]] cutoff_node = activity_coeffs.find("idealMolalSolnCutoff") if cutoff_node is not None: cutoff_model = cutoff_node.get("model") @@ -619,12 +621,12 @@ def ideal_molal_solution( for limit_node in cutoff_node: # Remove _limit or _cutoff from the right side of the node tag tag = limit_node.tag.rsplit("_", 1)[0] - cutoff[tag] = get_float_or_units(limit_node) + cutoff[tag] = get_float_or_quantity(limit_node) return cutoff def margules( self, activity_coeffs: etree.Element - ) -> List[Dict[str, List[Union[str, float]]]]: + ) -> List[Dict[str, List[Union[str, "QUANTITY"]]]]: """Process activity coefficients for a ``Margules`` phase-thermo type. :param activity_coeffs: @@ -650,7 +652,7 @@ def margules( ) this_node = { "species": FlowList([species_A, species_B]) - } # type: Dict[str, List[Union[str, float]]] + } # type: Dict[str, List[Union[str, QUANTITY]]] excess_enthalpy_node = binary_params.find("excessEnthalpy") if excess_enthalpy_node is not None: excess_enthalpy = clean_node_text(excess_enthalpy_node).split(",") @@ -709,7 +711,7 @@ def margules( def redlich_kister( self, activity_coeffs: etree.Element - ) -> List[Dict[str, List[Union[str, float]]]]: + ) -> List[Dict[str, List[Union[str, "QUANTITY"]]]]: """Process activity coefficents for a Redlich-Kister phase-thermo type. :param activity_coeffs: @@ -738,7 +740,7 @@ def redlich_kister( ) this_node = { "species": FlowList([species_A, species_B]) - } # type: Dict[str, List[Union[str, float]]] + } # type: Dict[str, List[Union[str, QUANTITY]]] excess_enthalpy_node = binary_params.find("excessEnthalpy") if excess_enthalpy_node is not None: excess_enthalpy = clean_node_text(excess_enthalpy_node).split(",") @@ -861,7 +863,7 @@ def move_RK_coeffs_to_species( raise MissingXMLNode( "The pure fluid coefficients requires the b_coeff node.", pure_param ) - eq_of_state["b"] = get_float_or_units(pure_b_node) + eq_of_state["b"] = get_float_or_quantity(pure_b_node) all_species_eos[pure_species] = eq_of_state all_cross_params = activity_coeffs.findall("crossFluidParameters") @@ -961,7 +963,7 @@ def move_density_to_species( }[den_node.tag] equation_of_state = { "model": "constant-volume", - const_prop: get_float_or_units(den_node), + const_prop: get_float_or_quantity(den_node), } flat_species = {k: v for d in this_phase_species for k, v in d.items()} for datasrc, species_names in flat_species.items(): @@ -1183,7 +1185,9 @@ def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str] return tab_thermo - def hmw_electrolyte(self, activity_node: etree.Element): + def hmw_electrolyte( + self, activity_node: etree.Element + ) -> Dict[str, "HMW_THERMO_TYPE"]: """Process the activity coefficients for an ``HMW`` phase-thermo type. :param activity_coeffs: @@ -1221,10 +1225,6 @@ def hmw_electrolyte(self, activity_node: etree.Element): continue this_interaction = {"species": FlowList([i[1] for i in inter_node.items()])} for param_node in inter_node: - if param_node.text is None: - raise MissingNodeText( - "The interaction nodes must have text values.", param_node - ) data = clean_node_text(param_node).split(",") param_name = param_node.tag.lower() if param_name == "cphi": @@ -1242,7 +1242,7 @@ def debye_huckel( this_phase_species: List[Dict[str, Iterable[str]]], activity_node: etree.Element, species_data: Dict[str, List["Species"]], - ) -> Dict[str, Union[str, float, bool]]: + ) -> Dict[str, Union[str, "QUANTITY", bool]]: """Process the activity coefficients for the ``DebyeHuckel`` phase-thermo type. :param this_phase_species: @@ -1291,10 +1291,10 @@ def debye_huckel( B_dot_node = activity_node.find("B_dot") if B_dot_node is not None: - activity_data["B-dot"] = get_float_or_units(B_dot_node) + activity_data["B-dot"] = get_float_or_quantity(B_dot_node) ionic_radius_node = activity_node.find("ionicRadius") - species_ionic_radii = {} # type: Dict[str, Union[float, str]] + species_ionic_radii = {} # type: Dict[str, QUANTITY] if ionic_radius_node is not None: default_radius = ionic_radius_node.get("default") radius_units = ionic_radius_node.get("units") @@ -1513,7 +1513,7 @@ def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE thermo_attribs["data"] = data return thermo_attribs - def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: + def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, "QUANTITY"]]: """Process a `Species` thermodynamic type with constant specific heat. :param thermo: @@ -1530,7 +1530,7 @@ def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, float]]: tag = node.tag if tag == "t0": tag = "T0" - thermo_attribs[tag] = get_float_or_units(node) + thermo_attribs[tag] = get_float_or_quantity(node) return thermo_attribs @@ -1556,7 +1556,7 @@ def Mu0( H298_node = Mu0_node.find("H298") if H298_node is None: raise MissingXMLNode("The Mu0 entry must contain an H298 node", Mu0_node) - thermo_attribs["h0"] = get_float_or_units(H298_node) + thermo_attribs["h0"] = get_float_or_quantity(H298_node) for float_node in Mu0_node.iterfind("floatArray"): title = float_node.get("title") if title == "Mu0Values": @@ -1749,7 +1749,7 @@ def __init__(self, species_node: etree.Element): weak_acid_charge = species_node.find("stoichIsMods") if weak_acid_charge is not None: - self.attribs["weak-acid-charge"] = get_float_or_units(weak_acid_charge) + self.attribs["weak-acid-charge"] = get_float_or_quantity(weak_acid_charge) def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: """Process a species with HKFT thermo type. @@ -1770,11 +1770,11 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: eqn_of_state = BlockMap({"model": "HKFT"}) for t_node in thermo_node: if t_node.tag == "DH0_f_Pr_Tr": - eqn_of_state["h0"] = get_float_or_units(t_node) + eqn_of_state["h0"] = get_float_or_quantity(t_node) elif t_node.tag == "DG0_f_Pr_Tr": - eqn_of_state["g0"] = get_float_or_units(t_node) + eqn_of_state["g0"] = get_float_or_quantity(t_node) elif t_node.tag == "S0_Pr_Tr": - eqn_of_state["s0"] = get_float_or_units(t_node) + eqn_of_state["s0"] = get_float_or_quantity(t_node) a = FlowList([]) c = FlowList([]) @@ -1787,9 +1787,9 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: std_state_node, ) if tag.startswith("a"): - a.append(get_float_or_units(node)) + a.append(get_float_or_quantity(node)) elif tag.startswith("c"): - c.append(get_float_or_units(node)) + c.append(get_float_or_quantity(node)) eqn_of_state["a"] = a eqn_of_state["c"] = c omega_node = std_state_node.find("omega_Pr_Tr") @@ -1799,7 +1799,7 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: "tag 'omega_Pr_Tr'", std_state_node, ) - eqn_of_state["omega"] = get_float_or_units(omega_node) + eqn_of_state["omega"] = get_float_or_quantity(omega_node) return eqn_of_state @@ -1826,7 +1826,7 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: eqn_of_state = { "model": self.standard_state_model_mapping[std_state_model] - } # type: Dict[str, Union[str, float, List[Union[str, float]]]] + } # type: Dict[str, Union[str, QUANTITY, List[QUANTITY]]] if std_state_model == "constant_incompressible": molar_volume_node = std_state.find("molarVolume") if molar_volume_node is None: @@ -1835,7 +1835,7 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: "must include a molarVolume node", std_state, ) - eqn_of_state["molar-volume"] = get_float_or_units(molar_volume_node) + eqn_of_state["molar-volume"] = get_float_or_quantity(molar_volume_node) elif "temperature_polynomial" in std_state_model: poly_node = std_state.find("volumeTemperaturePolynomial") if poly_node is None: @@ -2013,7 +2013,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): raise MissingXMLAttribute( "A reaction order node must have a species", order_node ) - order = get_float_or_units(order_node) + order = get_float_or_quantity(order_node) if species not in reactants or not np.isclose(reactants[species], order): orders[species] = order if orders: @@ -2180,7 +2180,7 @@ def plog(self, rate_coeff: etree.Element) -> "PLOG_TYPE": raise MissingXMLNode( "The pressure for a plog reaction must be specified", arr_coeff ) - rate_constant["P"] = get_float_or_units(P_node) + rate_constant["P"] = get_float_or_quantity(P_node) rate_constants.append(rate_constant) reaction_attributes["rate-constants"] = rate_constants @@ -2208,11 +2208,11 @@ def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": ) if range_tag.startswith("T"): reaction_attributes["temperature-range"].append( - get_float_or_units(range_node) + get_float_or_quantity(range_node) ) elif range_tag.startswith("P"): reaction_attributes["pressure-range"].append( - get_float_or_units(range_node) + get_float_or_quantity(range_node) ) data_node = rate_coeff.find("floatArray") if data_node is None: @@ -2284,9 +2284,9 @@ def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": raise MissingXMLNode("Coverage requires e", cov_node) reaction_attributes["coverage-dependencies"] = { cov_species: { - "a": get_float_or_units(cov_a), - "m": get_float_or_units(cov_m), - "E": get_float_or_units(cov_e), + "a": get_float_or_quantity(cov_a), + "m": get_float_or_quantity(cov_m), + "E": get_float_or_quantity(cov_e), } } @@ -2335,9 +2335,9 @@ def process_arrhenius_parameters( ) return FlowMap( { - "A": get_float_or_units(A_node), - "b": get_float_or_units(b_node), - "Ea": get_float_or_units(E_node), + "A": get_float_or_quantity(A_node), + "b": get_float_or_quantity(b_node), + "Ea": get_float_or_quantity(E_node), } ) @@ -2386,7 +2386,7 @@ def create_species_from_data_node(ctml_tree: etree.Element) -> Dict[str, List[Sp def create_reactions_from_data_node( - ctml_tree: etree.Element + ctml_tree: etree.Element, ) -> Dict[str, List[Reaction]]: """Generate lists of `Reaction` instances mapped to the ``reactionData`` id string. From 06627ed0c3d6b8d84204540f26e8d5892e85ea52 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sun, 24 Nov 2019 21:56:39 -0500 Subject: [PATCH 90/99] [ctml2yaml] Add command line arguments Add parsing of arguments passed when the script is run from the command line. A new function is introduced to parse the arguments and is the default function when the file is executed as a script. Update the documentation to add the new function. Add a new argument to convert that takes a string containing the content of a CTML file. This allows convert to be used from within the Python interpreter more easily. --- doc/sphinx/yaml/ctml_conversion.rst | 1 + interfaces/cython/cantera/ctml2yaml.py | 70 +++++++++++++++++-- .../cython/cantera/test/test_convert.py | 2 +- 3 files changed, 66 insertions(+), 7 deletions(-) diff --git a/doc/sphinx/yaml/ctml_conversion.rst b/doc/sphinx/yaml/ctml_conversion.rst index 0d1e246fdfc..b0b982a7af8 100644 --- a/doc/sphinx/yaml/ctml_conversion.rst +++ b/doc/sphinx/yaml/ctml_conversion.rst @@ -27,6 +27,7 @@ Module-level functions .. autofunction:: create_reactions_from_data_node .. autofunction:: create_phases_from_data_node .. autofunction:: convert +.. autofunction:: main Conversion classes ================== diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index d56ca8ca54c..e92ab7cf270 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1,8 +1,15 @@ -"""Convert legacy CTML input to YAML format.""" +"""Convert legacy CTML input to YAML format. + +There are two main entry points to this script, `main` and `convert`. The former is +used from the command line interface and parses the arguments passed. The latter +accepts either the name of the CTML input file or a string containing the CTML +content. +""" from pathlib import Path import sys import re +import argparse import xml.etree.ElementTree as etree from email.utils import formatdate @@ -2454,19 +2461,39 @@ def create_phases_from_data_node( return phases -def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): +def convert( + inpfile: Union[str, Path] = None, + outfile: Union[str, Path] = None, + text: str = None, +) -> None: """Convert an input legacy CTML file to a YAML file. :param inpfile: - The input CTML file name. + The input CTML file name. Exclusive with ``text``, only one of the two can be + specified. :param outfile: The output YAML file name. + :param text: + Contains a string with the CTML input file content. Exclusive with ``inpfile``, + only one of the two can be specified. All files are assumed to be relative to the current working directory of the Python process running this script. """ - inpfile = Path(inpfile) - ctml_text = inpfile.read_text().lstrip() + if inpfile is not None and text is not None: + raise ValueError("Only one of 'inpfile' or 'text' should be specified.") + elif inpfile is not None: + inpfile = Path(inpfile) + ctml_text = inpfile.read_text().lstrip() + if outfile is None: + outfile = inpfile.with_suffix(".yaml") + elif text is not None: + if outfile is None: + raise ValueError("If 'text' is passed, 'outfile' must also be passed.") + ctml_text = text.lstrip() + else: + raise ValueError("One of 'inpfile' or 'text' must be specified") + # Replace any raw ampersands in the text with an escaped ampersand. This # substitution is necessary because ctml_writer outputs literal & characters # from text data into the XML output. Although this doesn't cause a problem @@ -2531,5 +2558,36 @@ def convert(inpfile: Union[str, Path], outfile: Union[str, Path]): emitter.dump(output_reactions, output_file) +def main(): + """Parse command line arguments and pass them to `convert`.""" + parser = argparse.ArgumentParser( + description="Convert legacy CTML input files to YAML format", + epilog=( + "The 'output' argument is optional. If it is not given, an output " + "file with the same name as the input file is used, with the extension " + "changed to '.yaml'." + ), + ) + parser.add_argument("input", help="The input CTML filename. Must be specified.") + parser.add_argument("output", nargs="?", help="The output YAML filename. Optional.") + if len(sys.argv) not in [2, 3]: + if len(sys.argv) > 3: + print( + "ctml2yaml.py: error: unrecognized arguments:", + ' '.join(sys.argv[3:]), + file=sys.stderr, + ) + parser.print_help(sys.stderr) + sys.exit(1) + args = parser.parse_args() + input_file = Path(args.input) + if args.output is None: + output_file = input_file.with_suffix(".yaml") + else: + output_file = Path(args.output) + + convert(input_file, output_file) + + if __name__ == "__main__": - convert(sys.argv[1], sys.argv[2]) + main() diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 63102ec091e..6066fe1c525 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -1197,4 +1197,4 @@ def test_duplicate_section_ids(self): ctml2yaml.convert( Path(self.test_data_dir).joinpath("duplicate-reactionData-ids.xml"), Path(self.test_work_dir).joinpath("duplicate-reactionData-ids.yaml") - ) \ No newline at end of file + ) From c525c7e1960255bd61d4ba3b10796ffdcbbcdf7c Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Mon, 25 Nov 2019 10:38:27 -0500 Subject: [PATCH 91/99] [ctml2yaml] Improve error messages Node tag names are now quoted for clarity. --- interfaces/cython/cantera/ctml2yaml.py | 217 +++++++++++++++---------- 1 file changed, 128 insertions(+), 89 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index e92ab7cf270..5dc98ca5944 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -389,7 +389,7 @@ def __init__( phase_name = phase.get("id") if phase_name is None: raise MissingXMLAttribute( - "The phase node requires an id attribute: '{}'".format(phase) + "The 'phase' node requires an 'id' attribute.", phase ) self.attribs = BlockMap({"name": phase_name}) @@ -420,31 +420,37 @@ def __init__( phase_thermo = phase.find("thermo") if phase_thermo is None: - raise MissingXMLNode("The phase node requires a thermo node", phase) + raise MissingXMLNode("The 'phase' node requires a 'thermo' node.", phase) phase_thermo_model = phase_thermo.get("model") if phase_thermo_model is None: - raise MissingXMLAttribute("The thermo node requires a model") + raise MissingXMLAttribute( + "The 'thermo' node requires a 'model' attribute.", phase_thermo + ) self.attribs["thermo"] = self.thermo_model_mapping[phase_thermo_model] if phase_thermo_model == "PureFluid": pure_fluid_type = phase_thermo.get("fluid_type") if pure_fluid_type is None: raise MissingXMLAttribute( - "PureFluid model requires the fluid_type", phase_thermo + "The 'PureFluid' model requires the 'fluid_type' attribute.", + phase_thermo, ) self.attribs["pure-fluid-name"] = self.pure_fluid_mapping[pure_fluid_type] elif phase_thermo_model == "HMW": activity_coefficients = phase_thermo.find("activityCoefficients") if activity_coefficients is None: raise MissingXMLNode( - "HMW thermo model requires activity coefficients", phase_thermo + "The 'HMW' thermo model requires the 'activityCoefficients' node.", + phase_thermo, ) self.attribs["activity-data"] = self.hmw_electrolyte(activity_coefficients) elif phase_thermo_model == "DebyeHuckel": activity_coefficients = phase_thermo.find("activityCoefficients") if activity_coefficients is None: raise MissingXMLNode( - "Debye Huckel thermo model requires activity", phase_thermo + "The 'DebyeHuckel' thermo model requires the " + "'activityCoefficients' node.", + phase_thermo, ) self.attribs["activity-data"] = self.debye_huckel( species, activity_coefficients, species_data @@ -473,14 +479,14 @@ def __init__( neutral_phase_node = phase_thermo.find("neutralMoleculePhase") if neutral_phase_node is None: raise MissingXMLNode( - "The IonsFromNeutralMolecule phase requires the " - "neutralMoleculePhase node", + "The 'IonsFromNeutralMolecule' phase requires the " + "'neutralMoleculePhase' node.", phase_thermo, ) neutral_phase_src = neutral_phase_node.get("datasrc") if neutral_phase_src is None: raise MissingXMLAttribute( - "The neutralMoleculePhase requires the datasrc attribute", + "The 'neutralMoleculePhase' requires the 'datasrc' attribute.", neutral_phase_node, ) filename, location = neutral_phase_src.split("#") @@ -490,14 +496,16 @@ def __init__( activity_coefficients = phase_thermo.find("activityCoefficients") if activity_coefficients is None: raise MissingXMLNode( - "Redlich-Kister thermo model requires activity", phase_thermo + "The 'RedlichKister' thermo model requires the " + "'activityCoefficients' node.", + phase_thermo, ) self.attribs["interactions"] = self.redlich_kister(activity_coefficients) elif phase_thermo_model == "LatticeSolid": lattice_array_node = phase_thermo.find("LatticeArray") if lattice_array_node is None: raise MissingXMLNode( - "LatticeSolid phase thermo requires a LatticeArray node", + "The 'LatticeSolid' phase thermo requires a 'LatticeArray' node.", phase_thermo, ) self.lattice_nodes = [] # type: List[Phase] @@ -508,7 +516,8 @@ def __init__( lattice_stoich_node = phase_thermo.find("LatticeStoichiometry") if lattice_stoich_node is None: raise MissingXMLNode( - "LatticeSolid phase thermo requires a LatticeStoichiometry node", + "The 'LatticeSolid' phase thermo requires a " + "'LatticeStoichiometry' node.", phase_thermo, ) self.attribs["composition"] = {} @@ -653,8 +662,8 @@ def margules( species_B = binary_params.get("speciesB") if species_A is None or species_B is None: raise MissingXMLAttribute( - "binaryNeutralSpeciesParameters node requires speciesA and " - "speciesB attributes", + "'binaryNeutralSpeciesParameters' node requires 'speciesA' and " + "'speciesB' attributes", binary_params, ) this_node = { @@ -732,7 +741,7 @@ def redlich_kister( if not all_binary_params: raise MissingXMLNode( "Redlich-Kister activity coefficients requires a " - "binaryNeutralSpeciesParameters node", + "'binaryNeutralSpeciesParameters' node", activity_coeffs, ) interactions = [] @@ -741,8 +750,8 @@ def redlich_kister( species_B = binary_params.get("speciesB") if species_A is None or species_B is None: raise MissingXMLAttribute( - "binaryNeutralSpeciesParameters node requires speciesA and " - "speciesB attributes", + "'binaryNeutralSpeciesParameters' node requires 'speciesA' and " + "'speciesB' attributes", binary_params, ) this_node = { @@ -841,12 +850,14 @@ def move_RK_coeffs_to_species( pure_species = pure_param.get("species") if pure_species is None: raise MissingXMLAttribute( - "The pure fluid coefficients requires a species name", pure_param + "The 'pureFluidParameters' node requires a 'species' attribute", + pure_param, ) pure_a_node = pure_param.find("a_coeff") if pure_a_node is None: raise MissingXMLNode( - "The pure fluid coefficients requires the a_coeff node.", pure_param + "The 'pureFluidParameters' node requires the 'a_coeff' node.", + pure_param, ) pure_a_units = pure_a_node.get("units") @@ -868,7 +879,8 @@ def move_RK_coeffs_to_species( pure_b_node = pure_param.find("b_coeff") if pure_b_node is None: raise MissingXMLNode( - "The pure fluid coefficients requires the b_coeff node.", pure_param + "The 'pureFluidParameters' node requires the 'b_coeff' node.", + pure_param, ) eq_of_state["b"] = get_float_or_quantity(pure_b_node) all_species_eos[pure_species] = eq_of_state @@ -879,7 +891,8 @@ def move_RK_coeffs_to_species( species_2_name = cross_param.get("species2") if species_1_name is None or species_2_name is None: raise MissingXMLAttribute( - "The cross-fluid coefficients requires 2 species names", cross_param + "The 'crossFluidParameters' node requires 2 species names", + cross_param, ) species_1 = all_species_eos[species_1_name] if "binary-a" not in species_1: @@ -890,7 +903,7 @@ def move_RK_coeffs_to_species( cross_a_node = cross_param.find("a_coeff") if cross_a_node is None: raise MissingXMLNode( - "The cross-fluid coefficients requires the a_coeff node", + "The 'crossFluidParameters' node requires the 'a_coeff' node", cross_param, ) @@ -962,12 +975,12 @@ def move_density_to_species( den_node = phase_thermo.find("molarVolume") const_prop = "molar-volume" if den_node is None: - raise MissingXMLNode("Thermo node is missing a density node.", phase_thermo) - const_prop = { - "density": "density", - "molarDensity": "molar-density", - "molarVolume": "molar-volume", - }[den_node.tag] + raise MissingXMLNode( + "Thermo node is missing 'density', 'molarDensity', or 'molarVolume' " + "node.", + phase_thermo, + ) + equation_of_state = { "model": "constant-volume", const_prop: get_float_or_quantity(den_node), @@ -1031,7 +1044,10 @@ def get_reaction_array( """ datasrc = reactionArray_node.get("datasrc", "") if not datasrc: - raise MissingXMLAttribute("The reactionArray must include a datasrc") + raise MissingXMLAttribute( + "The 'reactionArray' node must include a 'datasrc' attribute.", + reactionArray_node, + ) filter_node = reactionArray_node.find("include") if filter_node is not None: @@ -1057,11 +1073,11 @@ def get_reaction_array( if not datasrc.startswith("#"): if filter_text.lower() != "none": raise ValueError( - "Filtering reactions is not allowed with an external datasrc" + "Filtering reactions is not allowed with an external 'datasrc'" ) if skip_node is None: raise MissingXMLNode( - "Must include skip node for external data sources", + "Must include 'skip' node for external data sources", reactionArray_node, ) # This code does not handle the # character in a filename @@ -1138,24 +1154,26 @@ def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str] """ tab_thermo = BlockMap() enthalpy_node = tab_thermo_node.find("enthalpy") - if enthalpy_node is None or enthalpy_node.text is None: + if enthalpy_node is None: raise MissingXMLNode( - "Tabulated thermo must have an enthalpy node with text", tab_thermo_node + "The 'tabulatedThermo' node must have an 'enthalpy' node.", + tab_thermo_node, ) enthalpy_units = enthalpy_node.get("units", "").split("/") if not enthalpy_units: raise MissingXMLAttribute( - "The units of tabulated enthalpy must be specified", enthalpy_node + "The 'enthalpy' node must have a 'units' attribute.", enthalpy_node, ) entropy_node = tab_thermo_node.find("entropy") - if entropy_node is None or entropy_node.text is None: + if entropy_node is None: raise MissingXMLNode( - "Tabulated thermo must have an entropy node with text", tab_thermo_node + "The 'tabulatedThermo' node must have an 'entropy' node.", + tab_thermo_node, ) entropy_units = entropy_node.get("units", "").split("/") if not entropy_units: raise MissingXMLAttribute( - "The units of tabulated entropy must be specified", entropy_node + "The 'entropy' node must have a 'units' attribute.", enthalpy_node, ) if enthalpy_units[:2] != entropy_units[:2]: raise ValueError("Tabulated thermo must have the same units.") @@ -1177,9 +1195,9 @@ def get_tabulated_thermo(self, tab_thermo_node: etree.Element) -> Dict[str, str] "indicated size." ) mole_fraction_node = tab_thermo_node.find("moleFraction") - if mole_fraction_node is None or mole_fraction_node.text is None: + if mole_fraction_node is None: raise MissingXMLNode( - "Tabulated thermo must have a mole fraction node with text", + "The 'tabulatedThermo' node must have a 'moleFraction' node.", tab_thermo_node, ) mole_fraction = clean_node_text(mole_fraction_node).split(",") @@ -1207,15 +1225,18 @@ def hmw_electrolyte( A_Debye_node = activity_node.find("A_Debye") if A_Debye_node is None: raise MissingXMLNode( - "Activity coefficients for HMW must have A_Debye", activity_node + "The 'activityCoefficients' node must have an 'A_debye' node.", + activity_node, ) if A_Debye_node.get("model", "").lower() == "water": activity_data["A_Debye"] = "variable" else: # Assume the units are kg^0.5/gmol^0.5. Apparently, - # this is not handled in the same way as other units? + # this is not handled in the same way as other units. if A_Debye_node.text is None: - raise MissingNodeText("The A_Debye node must have a text value") + raise MissingNodeText( + "The 'A_Debye' node must have a text value", A_Debye_node + ) activity_data["A_Debye"] = A_Debye_node.text.strip() + " kg^0.5/gmol^0.5" interactions = [] @@ -1274,7 +1295,8 @@ def debye_huckel( activity_model = activity_node.get("model") if activity_model is None: raise MissingXMLAttribute( - "The Debye Huckel model must be specified", activity_node + "The 'activityCoefficients' node must have a 'model' attribute.", + activity_node, ) activity_data = BlockMap({"model": model_map[activity_model.lower()]}) A_Debye = activity_node.findtext("A_Debye") @@ -1456,13 +1478,15 @@ def process_polynomial( Tmin = float(node.get("Tmin", 0)) Tmax = float(node.get("Tmax", 0)) if not Tmin or not Tmax: - raise MissingXMLAttribute("Tmin and Tmax must both be specified", node) + raise MissingXMLAttribute( + "'Tmin' and 'Tmax' must both be specified.", node + ) temperature_ranges.add(Tmin) temperature_ranges.add(Tmax) float_array = node.find("floatArray") if float_array is None: raise MissingXMLNode( - "{} entry missing floatArray node".format(poly_type), node + "'{}' entry missing 'floatArray' node.".format(poly_type), node ) unsorted_data[Tmin] = FlowList( map(float, clean_node_text(float_array).split(",")) @@ -1470,7 +1494,7 @@ def process_polynomial( if len(temperature_ranges) != len(model_nodes) + 1: raise ValueError( - "The midpoint temperature is not consistent between {} " + "The midpoint temperature is not consistent between '{}' " "entries".format(poly_type) ) data = [] @@ -1531,7 +1555,7 @@ def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, "QUANTITY"]]: const_cp_node = thermo.find("const_cp") if const_cp_node is None: raise MissingXMLNode( - "The thermo node must constain a const_cp node", thermo + "The 'thermo' node must contain a 'const_cp' node", thermo ) for node in const_cp_node: tag = node.tag @@ -1553,16 +1577,18 @@ def Mu0( thermo_attribs = BlockMap({"model": "piecewise-Gibbs"}) Mu0_node = thermo.find("Mu0") if Mu0_node is None: - raise MissingXMLNode("The thermo entry must contain a Mu0 node", thermo) + raise MissingXMLNode("The 'thermo' node must contain a 'Mu0' node.", thermo) ref_pressure = Mu0_node.get("Pref") if ref_pressure is None: raise MissingXMLAttribute( - "Reference pressure for piecewise Gibbs species thermo", Mu0_node + "The 'Mu0' node must have a 'Pref' node.", Mu0_node ) thermo_attribs["reference-pressure"] = float(ref_pressure) H298_node = Mu0_node.find("H298") if H298_node is None: - raise MissingXMLNode("The Mu0 entry must contain an H298 node", Mu0_node) + raise MissingXMLNode( + "The 'Mu0' node must contain an 'H298' node.", Mu0_node + ) thermo_attribs["h0"] = get_float_or_quantity(H298_node) for float_node in Mu0_node.iterfind("floatArray"): title = float_node.get("title") @@ -1688,7 +1714,7 @@ def __init__(self, species_node: etree.Element): species_name = species_node.get("name") if species_name is None: raise MissingXMLAttribute( - "The species name must be specified", species_node + "The 'species' node must have a 'name' attribute.", species_node ) self.attribs["name"] = species_name atom_array = species_node.find("atomArray") @@ -1722,7 +1748,8 @@ def __init__(self, species_node: etree.Element): neutral_spec_mult_node = thermo.find("neutralSpeciesMultipliers") if neutral_spec_mult_node is None: raise MissingXMLNode( - "IonFromNeutral requires a neutralSpeciesMultipliers node", + "'IonFromNeutral' node requires a 'neutralSpeciesMultipliers' " + "node.", thermo, ) species_multipliers = FlowMap({}) @@ -1771,7 +1798,7 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: std_state_node = species_node.find("standardState") if thermo_node is None or std_state_node is None: raise MissingXMLNode( - "An HKFT species requires both the thermo and standardState nodes", + "An HKFT species requires both the 'thermo' and 'standardState' nodes.", species_node, ) eqn_of_state = BlockMap({"model": "HKFT"}) @@ -1789,8 +1816,7 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: node = std_state_node.find(tag) if node is None: raise MissingXMLNode( - "The HKFT standardState node requires a child node with " - "tag '{}'".format(tag), + "The HKFT 'standardState' node requires a '{}' node.".format(tag), std_state_node, ) if tag.startswith("a"): @@ -1802,8 +1828,7 @@ def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: omega_node = std_state_node.find("omega_Pr_Tr") if omega_node is None: raise MissingXMLNode( - "The HKFT standardState node requires a child node with " - "tag 'omega_Pr_Tr'", + "The HKFT 'standardState' node requires an 'omega_Pr_Tr' node.", std_state_node, ) eqn_of_state["omega"] = get_float_or_quantity(omega_node) @@ -1838,8 +1863,8 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: molar_volume_node = std_state.find("molarVolume") if molar_volume_node is None: raise MissingXMLNode( - "If the standard state model is constant_incompressible, it " - "must include a molarVolume node", + "If the standard state model is 'constant_incompressible', it " + "must include a 'molarVolume' node", std_state, ) eqn_of_state["molar-volume"] = get_float_or_quantity(molar_volume_node) @@ -1847,14 +1872,14 @@ def process_standard_state_node(self, species_node: etree.Element) -> None: poly_node = std_state.find("volumeTemperaturePolynomial") if poly_node is None: raise MissingXMLNode( - "{} standard state model requires a " - "volumeTemperaturePolynomial node".format(std_state_model), + "'{}' standard state model requires a " + "'volumeTemperaturePolynomial' node".format(std_state_model), std_state, ) poly_values_node = poly_node.find("floatArray") if poly_values_node is None: raise MissingXMLNode( - "The floatArray node must be specified", std_state + "The 'floatArray' node must be specified", std_state ) values = clean_node_text(poly_values_node).split(",") @@ -1920,7 +1945,9 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): reaction_equation = reaction.findtext("equation") if reaction_equation is None: - raise MissingNodeText("The reaction must have an equation", reaction) + raise MissingNodeText( + "The 'reaction' node must have an 'equation' node.", reaction + ) # This has to replace the reaction direction symbols separately because # species names can have [ or ] in them @@ -1931,7 +1958,9 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): reaction_type = reaction.get("type", "arrhenius").lower() rate_coeff = reaction.find("rateCoeff") if rate_coeff is None: - raise MissingXMLNode("The reaction must have a rateCoeff node.", reaction) + raise MissingXMLNode( + "The 'reaction' node must have a 'rateCoeff' node.", reaction + ) if reaction_type in ["arrhenius", "elementary"]: reaction_type = "arrhenius" elif reaction_type in ["threebody", "three_body"]: @@ -1940,12 +1969,12 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): falloff_node = rate_coeff.find("falloff") if falloff_node is None: raise MissingXMLNode( - "Falloff reaction types must have a falloff node.", rate_coeff + "Falloff reaction types must have a 'falloff' node.", rate_coeff ) falloff_type = falloff_node.get("type") if falloff_type not in ["Lindemann", "Troe", "SRI"]: raise TypeError( - "Unknown falloff type '{}' for reaction id {}".format( + "Unknown falloff type '{}' for reaction id '{}'".format( falloff_type, reaction.get("id") ) ) @@ -1960,7 +1989,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): falloff_type = falloff_node.get("type") if falloff_type != "Troe": raise TypeError( - "Unknown activation type '{}' for reaction id {}".format( + "Unknown activation type '{}' for reaction id '{}'".format( falloff_type, reaction.get("id") ) ) @@ -1994,7 +2023,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): reaction_type = "interface" else: raise TypeError( - "Unknown reaction type '{}' for reaction id {}".format( + "Unknown reaction type '{}' for reaction id '{}'".format( reaction_type, reaction.get("id") ) ) @@ -2010,7 +2039,7 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): reactants_node = reaction.find("reactants") if reactants_node is None: raise MissingXMLNode( - "The reactants must be present in the reaction", reaction + "The 'reaction' node must have a 'reactants' node.", reaction ) reactants = split_species_value_string(reactants_node) orders = {} @@ -2018,7 +2047,8 @@ def __init__(self, reaction: etree.Element, node_motz_wise: bool): species = order_node.get("species", "") if not species: raise MissingXMLAttribute( - "A reaction order node must have a species", order_node + "A reaction 'order' node must have a 'species' attribute", + order_node, ) order = get_float_or_quantity(order_node) if species not in reactants or not np.isclose(reactants[species], order): @@ -2059,7 +2089,7 @@ def sri(self, rate_coeff: etree.Element) -> "SRI_TYPE": reaction_attribs = self.lindemann((rate_coeff)) falloff_node = rate_coeff.find("falloff") if falloff_node is None: - raise MissingXMLNode("SRI reaction requires falloff node", rate_coeff) + raise MissingXMLNode("SRI reaction requires 'falloff' node", rate_coeff) SRI_names = list("ABCDE") SRI_data = FlowMap({}) for name, param in zip(SRI_names, clean_node_text(falloff_node).split()): @@ -2101,7 +2131,7 @@ def lindemann(self, rate_coeff: etree.Element) -> "LINDEMANN_TYPE": "high-P-rate-constant" ] = self.process_arrhenius_parameters(arr_coeff) else: - raise TypeError("Too many Arrhenius nodes") + raise TypeError("Too many 'Arrhenius' nodes") eff_node = rate_coeff.find("efficiencies") if eff_node is not None: reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) @@ -2120,7 +2150,7 @@ def troe(self, rate_coeff: etree.Element) -> "TROE_TYPE": troe_node = rate_coeff.find("falloff") if troe_node is None: raise MissingXMLNode( - "Troe reaction types must include a falloff node", rate_coeff + "Troe reaction types must include a 'falloff' node", rate_coeff ) troe_params = clean_node_text(troe_node).split() troe_names = ["A", "T3", "T1", "T2"] @@ -2150,7 +2180,7 @@ def chemact(self, rate_coeff: etree.Element) -> "CHEMACT_TYPE": "low-P-rate-constant" ] = self.process_arrhenius_parameters(arr_coeff) else: - raise TypeError("Too many Arrhenius nodes") + raise TypeError("Too many 'Arrhenius' nodes") eff_node = rate_coeff.find("efficiencies") if eff_node is not None: reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) @@ -2158,7 +2188,7 @@ def chemact(self, rate_coeff: etree.Element) -> "CHEMACT_TYPE": troe_node = rate_coeff.find("falloff") if troe_node is None: raise MissingXMLNode( - "Chemically activated reaction types must include a falloff node", + "Chemically activated reaction types must include a 'falloff' node", rate_coeff, ) troe_params = clean_node_text(troe_node).split() @@ -2185,7 +2215,7 @@ def plog(self, rate_coeff: etree.Element) -> "PLOG_TYPE": P_node = arr_coeff.find("P") if P_node is None: raise MissingXMLNode( - "The pressure for a plog reaction must be specified", arr_coeff + "A 'plog' reaction must have a 'P' node.", arr_coeff ) rate_constant["P"] = get_float_or_quantity(P_node) rate_constants.append(rate_constant) @@ -2210,7 +2240,8 @@ def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": range_node = rate_coeff.find(range_tag) if range_node is None: raise MissingXMLNode( - "A Chebyshev reaction must include a {} node".format(range_tag), + "A Chebyshev 'reaction' node must include a '{}' " + "node".format(range_tag), rate_coeff, ) if range_tag.startswith("T"): @@ -2224,25 +2255,26 @@ def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": data_node = rate_coeff.find("floatArray") if data_node is None: raise MissingXMLNode( - "Chebyshev reaction must include a floatArray node.", rate_coeff + "A Chebyshev 'reaction' node must include a 'floatArray' node.", + rate_coeff, ) n_p_values = int(data_node.get("degreeP", 0)) n_T_values = int(data_node.get("degreeT", 0)) if not n_p_values or not n_T_values: raise MissingXMLAttribute( - "The Chebyshev polynomial degree in pressure and temperature must be " - "specified", + "A Chebyshev 'floatArray' node is missing the 'degreeP' or 'degreeT' " + "attributes.", data_node, ) raw_data = [float(a) for a in clean_node_text(data_node).split(",")] data = [] for i in range(0, len(raw_data), n_p_values): - data.append(FlowList(raw_data[i : i + n_p_values])) # NOQA: E203 + data.append(FlowList(raw_data[i : i + n_p_values])) if len(data) != n_T_values: raise ValueError( - "The number of rows of the Chebyshev data do not match the specified " - "temperature degree." + "The number of coefficients in the Chebyshev data do not match the " + "specified temperature and pressure degrees." ) reaction_attributes["data"] = data @@ -2259,7 +2291,7 @@ def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": arr_node = rate_coeff.find("Arrhenius") if arr_node is None: raise MissingXMLNode( - "Interface reaction requires Arrhenius node", rate_coeff + "An interface 'reaction' node requires an 'Arrhenius' node", rate_coeff ) if arr_node.get("type", "").lower() == "stick": reaction_attributes = FlowMap( @@ -2282,13 +2314,19 @@ def interface(self, rate_coeff: etree.Element) -> "INTERFACE_TYPE": cov_species = cov_node.get("species") cov_a = cov_node.find("a") if cov_a is None: - raise MissingXMLNode("Coverage requires a", cov_node) + raise MissingXMLNode( + "A 'coverage' node requires an 'a' node.", cov_node + ) cov_m = cov_node.find("m") if cov_m is None: - raise MissingXMLNode("Coverage requires m", cov_node) + raise MissingXMLNode( + "A 'coverage' node requires an 'm' node.", cov_node + ) cov_e = cov_node.find("e") if cov_e is None: - raise MissingXMLNode("Coverage requires e", cov_node) + raise MissingXMLNode( + "A 'coverage' node requires an 'e' node.", cov_node + ) reaction_attributes["coverage-dependencies"] = { cov_species: { "a": get_float_or_quantity(cov_a), @@ -2331,13 +2369,14 @@ def process_arrhenius_parameters( with tags ``A``, ``b``, and ``E``. """ if arr_node is None: - raise MissingXMLNode("The Arrhenius node must be present.") + raise MissingXMLNode("The 'Arrhenius' node must be present.") A_node = arr_node.find("A") b_node = arr_node.find("b") E_node = arr_node.find("E") if A_node is None or b_node is None or E_node is None: raise MissingXMLNode( - "All of A, b, and E must be specified for the Arrhenius parameters.", + "All of 'A', 'b', and 'E' must be specified for the 'Arrhenius' " + "parameters.", arr_node, ) return FlowMap( From c3701bab89ea32153ca1d95751f4210bf6115ca9 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 11:00:14 -0500 Subject: [PATCH 92/99] [ctml2yaml] RK does not need activityCoefficients The Redlich-Kwong phase-thermo model doesn't require the activityCoefficients node. If not specified, values will be found from critProperties.xml --- interfaces/cython/cantera/ctml2yaml.py | 7 +++---- 1 file changed, 3 insertions(+), 4 deletions(-) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index 5dc98ca5944..e90c8f3449b 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -459,11 +459,10 @@ def __init__( self.move_density_to_species(species, phase_thermo, species_data) elif phase_thermo_model == "RedlichKwongMFTP": activity_coefficients = phase_thermo.find("activityCoefficients") - if activity_coefficients is None: - raise MissingXMLNode( - "Redlich-Kwong thermo model requires activity", phase_thermo + if activity_coefficients is not None: + self.move_RK_coeffs_to_species( + species, activity_coefficients, species_data ) - self.move_RK_coeffs_to_species(species, activity_coefficients, species_data) elif phase_thermo_model == "MaskellSolidSolnPhase": try: self.move_density_to_species(species, phase_thermo, species_data) From 96d9c171d27221e5458e1126e0fc56ebbd755f62 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 11:05:36 -0500 Subject: [PATCH 93/99] [ctml2yaml/Test] Remove on-the-fly ctml_writer use ctml_writer.py maintains global state between calling convert(). This change commits the resulting XML file into the test/data directory rather than converting during the test. --- .../cython/cantera/test/test_convert.py | 6 +- test/data/co2_RK_example.xml | 299 ++++++++++++++++++ 2 files changed, 301 insertions(+), 4 deletions(-) create mode 100644 test/data/co2_RK_example.xml diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 6066fe1c525..bb8fee21578 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -5,7 +5,7 @@ from . import utilities import cantera as ct -from cantera import ck2cti, ck2yaml, cti2yaml, ctml2yaml, ctml_writer +from cantera import ck2cti, ck2yaml, cti2yaml, ctml2yaml class converterTestCommon: @@ -876,9 +876,7 @@ def test_liquidvapor(self): [1.3 * ctmlPhase.min_temp, 0.7 * ctmlPhase.max_temp]) def test_Redlich_Kwong_CO2(self): - ctml_writer.convert(str(Path(self.test_data_dir).joinpath('co2_RK_example.cti')), - str(Path(self.test_work_dir).joinpath('co2_RK_example.xml'))) - ctml2yaml.convert(Path(self.test_work_dir).joinpath('co2_RK_example.xml'), + ctml2yaml.convert(Path(self.test_data_dir).joinpath('co2_RK_example.xml'), Path(self.test_work_dir).joinpath('co2_RK_example.yaml')) ctmlGas, yamlGas = self.checkConversion('co2_RK_example') for P in [1e5, 2e6, 1.3e7]: diff --git a/test/data/co2_RK_example.xml b/test/data/co2_RK_example.xml new file mode 100644 index 00000000000..d74b279c1f4 --- /dev/null +++ b/test/data/co2_RK_example.xml @@ -0,0 +1,299 @@ + + + + + + + C O H N + CO2 H2O H2 CO CH4 O2 N2 + + + 300.0 + 101325.0 + CO2:0.99, H2:0.01 + + + + + + 6.45714e+12, 0 + + + 29.65792 + + + + + 1.42674e+13, 0 + + + 21.12706 + + + + + 1.43319e+11, 0 + + + 18.42803 + + + + + 1.6202612e+12, 0 + + + 25.83591 + + + + + 3.22224e+12, 0 + + + 29.8483 + + + + + 1.73132e+12, 0 + + + 22.04783 + + + + + 1.55976e+12, 0 + + + 26.81725 + + + + + 7.897e+12, 0 + + + + + + + + + + + + + + H:2 + TPIS78 + + + + 2.344331120E+00, 7.980520750E-03, -1.947815100E-05, 2.015720940E-08, + -7.376117610E-12, -9.179351730E+02, 6.830102380E-01 + + + + 3.337279200E+00, -4.940247310E-05, 4.994567780E-07, -1.795663940E-10, + 2.002553760E-14, -9.501589220E+02, -3.205023310E+00 + + + + linear + 38.000 + 2.920 + 0.000 + 0.790 + 280.000 + 0.000 + 0.000 + + + + + + O:1 C:1 + TPIS79 + + + + 3.579533470E+00, -6.103536800E-04, 1.016814330E-06, 9.070058840E-10, + -9.044244990E-13, -1.434408600E+04, 3.508409280E+00 + + + + 2.715185610E+00, 2.062527430E-03, -9.988257710E-07, 2.300530080E-10, + -2.036477160E-14, -1.415187240E+04, 7.818687720E+00 + + + + linear + 98.100 + 3.650 + 0.000 + 1.950 + 1.800 + 0.000 + 0.000 + + + + + + N:2 + 121286 + + + + 3.298677000E+00, 1.408240400E-03, -3.963222000E-06, 5.641515000E-09, + -2.444854000E-12, -1.020899900E+03, 3.950372000E+00 + + + + 2.926640000E+00, 1.487976800E-03, -5.684760000E-07, 1.009703800E-10, + -6.753351000E-15, -9.227977000E+02, 5.980528000E+00 + + + + linear + 97.530 + 3.620 + 0.000 + 1.760 + 4.000 + 0.000 + 0.000 + + + + + + O:2 + TPIS89 + + + + 3.782456360E+00, -2.996734160E-03, 9.847302010E-06, -9.681295090E-09, + 3.243728370E-12, -1.063943560E+03, 3.657675730E+00 + + + + 3.282537840E+00, 1.483087540E-03, -7.579666690E-07, 2.094705550E-10, + -2.167177940E-14, -1.088457720E+03, 5.453231290E+00 + + + + linear + 107.400 + 3.460 + 0.000 + 1.600 + 3.800 + 0.000 + 0.000 + + + + + + O:2 C:1 + L 7/88 + + + + 2.356773520E+00, 8.984596770E-03, -7.123562690E-06, 2.459190220E-09, + -1.436995480E-13, -4.837196970E+04, 9.901052220E+00 + + + + 3.857460290E+00, 4.414370260E-03, -2.214814040E-06, 5.234901880E-10, + -4.720841640E-14, -4.875916600E+04, 2.271638060E+00 + + + + linear + 244.000 + 3.760 + 0.000 + 2.650 + 2.100 + 0.000 + 0.000 + + + + + + H:4 C:1 + L 8/88 + + + + 5.149876130E+00, -1.367097880E-02, 4.918005990E-05, -4.847430260E-08, + 1.666939560E-11, -1.024664760E+04, -4.641303760E+00 + + + + 7.485149500E-02, 1.339094670E-02, -5.732858090E-06, 1.222925350E-09, + -1.018152300E-13, -9.468344590E+03, 1.843731800E+01 + + + + nonlinear + 141.400 + 3.750 + 0.000 + 2.600 + 13.000 + 0.000 + 0.000 + + + + + + H:2 O:1 + L 8/89 + + + + 4.198640560E+00, -2.036434100E-03, 6.520402110E-06, -5.487970620E-09, + 1.771978170E-12, -3.029372670E+04, -8.490322080E-01 + + + + 3.033992490E+00, 2.176918040E-03, -1.640725180E-07, -9.704198700E-11, + 1.682009920E-14, -3.000429710E+04, 4.966770100E+00 + + + + nonlinear + 572.400 + 2.600 + 1.850 + 0.000 + 4.000 + 0.000 + 0.000 + + + + + + + + CO2 + H2 [=] CO + H2O + + + 1.200000E+00 + 0 + 0.000000 + + + H2:1 CO2:1.0 + H2O:1 CO:1.0 + + + From 4d6dcce6e0a2c44fe5b121103d71993bec0bcef1 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 11:22:33 -0500 Subject: [PATCH 94/99] [Test] Remove unnecessary enumerate The index from the enumerate is not actually used anywhere. --- interfaces/cython/cantera/test/test_convert.py | 6 ++---- 1 file changed, 2 insertions(+), 4 deletions(-) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index bb8fee21578..34c461e8278 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -528,8 +528,7 @@ def checkConversion(self, basename, cls=ct.Solution, ctiphases=(), for C, Y in zip(ctiPhase.species(), yamlPhase.species()): self.assertEqual(C.composition, Y.composition) - for i, (C, Y) in enumerate(zip(ctiPhase.reactions(), - yamlPhase.reactions())): + for C, Y in zip(ctiPhase.reactions(), yamlPhase.reactions()): self.assertEqual(C.__class__, Y.__class__) self.assertEqual(C.reactants, Y.reactants) self.assertEqual(C.products, Y.products) @@ -744,8 +743,7 @@ def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(), for C, Y in zip(ctmlPhase.species(), yamlPhase.species()): self.assertEqual(C.composition, Y.composition) - for i, (C, Y) in enumerate(zip(ctmlPhase.reactions(), - yamlPhase.reactions())): + for C, Y in zip(ctmlPhase.reactions(), yamlPhase.reactions()): self.assertEqual(C.__class__, Y.__class__) self.assertEqual(C.reactants, Y.reactants) self.assertEqual(C.products, Y.products) From 3ef2709c417028f3ca10a5ce8cb3c5908288714e Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 11:23:29 -0500 Subject: [PATCH 95/99] [cti2yaml/Test] Remove test class setup There isn't a reason to convert gri30 every time the class is instantiated. Move the conversion into the test_gri30 function. --- interfaces/cython/cantera/test/test_convert.py | 8 ++------ 1 file changed, 2 insertions(+), 6 deletions(-) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 34c461e8278..008c16cbd4d 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -511,12 +511,6 @@ def test_short_source_input(self): class cti2yamlTest(utilities.CanteraTest): - @classmethod - def setUpClass(cls): - super().setUpClass() - cti2yaml.convert(Path(cls.cantera_data).joinpath('gri30.cti'), - Path(cls.test_work_dir).joinpath('gri30.yaml')) - def checkConversion(self, basename, cls=ct.Solution, ctiphases=(), yamlphases=(), **kwargs): ctiPhase = cls(basename + '.cti', adjacent=ctiphases, **kwargs) @@ -587,6 +581,8 @@ def checkTransport(self, ctiPhase, yamlPhase, temperatures, self.assertNear(Dkm_cti[i], Dkm_yaml[i], msg=message) def test_gri30(self): + cti2yaml.convert(Path(self.cantera_data).joinpath('gri30.cti'), + Path(self.test_work_dir).joinpath('gri30.yaml')) ctiPhase, yamlPhase = self.checkConversion('gri30') X = {'O2': 0.3, 'H2': 0.1, 'CH4': 0.2, 'CO2': 0.4} ctiPhase.X = X From c544d2f5946f3e7f26dfb20945e3db494d092d07 Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 11:24:00 -0500 Subject: [PATCH 96/99] [ctml2yaml/Test] Reformat test file paths Better conform to PEP-8. --- .../cython/cantera/test/test_convert.py | 204 ++++++++++++------ 1 file changed, 136 insertions(+), 68 deletions(-) diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 008c16cbd4d..b77eccb04c1 100644 --- a/interfaces/cython/cantera/test/test_convert.py +++ b/interfaces/cython/cantera/test/test_convert.py @@ -798,8 +798,10 @@ def checkTransport(self, ctmlPhase, yamlPhase, temperatures, self.assertNear(Dkm_ctml[i], Dkm_yaml[i], msg=message) def test_gri30(self): - ctml2yaml.convert(Path(self.cantera_data).joinpath('gri30.xml'), - Path(self.test_work_dir).joinpath('gri30.yaml')) + ctml2yaml.convert( + Path(self.cantera_data).joinpath('gri30.xml'), + Path(self.test_work_dir).joinpath('gri30.yaml'), + ) ctmlPhase, yamlPhase = self.checkConversion('gri30') X = {'O2': 0.3, 'H2': 0.1, 'CH4': 0.2, 'CO2': 0.4} ctmlPhase.X = X @@ -809,15 +811,19 @@ def test_gri30(self): self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2400]) def test_pdep(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath('pdep-test.xml'), - Path(self.test_work_dir).joinpath('pdep-test.yaml')) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath('pdep-test.xml'), + Path(self.test_work_dir).joinpath('pdep-test.yaml'), + ) ctmlPhase, yamlPhase = self.checkConversion('pdep-test') self.checkKinetics(ctmlPhase, yamlPhase, [300, 1000, 2200], [100, ct.one_atm, 2e5, 2e6, 9.9e6]) def test_ptcombust(self): - ctml2yaml.convert(Path(self.cantera_data).joinpath('ptcombust.xml'), - Path(self.test_work_dir).joinpath('ptcombust.yaml')) + ctml2yaml.convert( + Path(self.cantera_data).joinpath('ptcombust.xml'), + Path(self.test_work_dir).joinpath('ptcombust.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('ptcombust') ctmlSurf, yamlSurf = self.checkConversion('ptcombust', ct.Interface, name='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) @@ -827,8 +833,10 @@ def test_ptcombust(self): self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5]) def test_ptcombust_motzwise(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath('ptcombust-motzwise.xml'), - Path(self.test_work_dir).joinpath('ptcombust-motzwise.yaml')) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath('ptcombust-motzwise.xml'), + Path(self.test_work_dir).joinpath('ptcombust-motzwise.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('ptcombust-motzwise') ctmlSurf, yamlSurf = self.checkConversion('ptcombust-motzwise', ct.Interface, name='Pt_surf', ctmlphases=[ctmlGas], yamlphases=[yamlGas]) @@ -838,8 +846,10 @@ def test_ptcombust_motzwise(self): self.checkKinetics(ctmlSurf, yamlSurf, [500, 1200], [1e4, 3e5]) def test_sofc(self): - ctml2yaml.convert(Path(self.cantera_data).joinpath('sofc.xml'), - Path(self.test_work_dir).joinpath('sofc.yaml')) + ctml2yaml.convert( + Path(self.cantera_data).joinpath('sofc.xml'), + Path(self.test_work_dir).joinpath('sofc.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('sofc') ctmlMetal, yamlMetal = self.checkConversion('sofc', name='metal') ctmlOxide, yamlOxide = self.checkConversion('sofc', name='oxide_bulk') @@ -861,8 +871,10 @@ def test_sofc(self): self.checkKinetics(ctml_tpb, yaml_tpb, [900, 1000, 1100], [1e5]) def test_liquidvapor(self): - ctml2yaml.convert(Path(self.cantera_data).joinpath('liquidvapor.xml'), - Path(self.test_work_dir).joinpath('liquidvapor.yaml')) + ctml2yaml.convert( + Path(self.cantera_data).joinpath('liquidvapor.xml'), + Path(self.test_work_dir).joinpath('liquidvapor.yaml'), + ) for name in ['water', 'nitrogen', 'methane', 'hydrogen', 'oxygen', 'hfc134a', 'carbondioxide', 'heptane']: ctmlPhase, yamlPhase = self.checkConversion('liquidvapor', name=name) @@ -870,24 +882,30 @@ def test_liquidvapor(self): [1.3 * ctmlPhase.min_temp, 0.7 * ctmlPhase.max_temp]) def test_Redlich_Kwong_CO2(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath('co2_RK_example.xml'), - Path(self.test_work_dir).joinpath('co2_RK_example.yaml')) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath('co2_RK_example.xml'), + Path(self.test_work_dir).joinpath('co2_RK_example.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('co2_RK_example') for P in [1e5, 2e6, 1.3e7]: yamlGas.TP = ctmlGas.TP = 300, P self.checkThermo(ctmlGas, yamlGas, [300, 400, 500]) def test_Redlich_Kwong_ndodecane(self): - ctml2yaml.convert(Path(self.cantera_data).joinpath('nDodecane_Reitz.xml'), - Path(self.test_work_dir).joinpath('nDodecane_Reitz.yaml')) + ctml2yaml.convert( + Path(self.cantera_data).joinpath('nDodecane_Reitz.xml'), + Path(self.test_work_dir).joinpath('nDodecane_Reitz.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('nDodecane_Reitz') self.checkThermo(ctmlGas, yamlGas, [300, 400, 500]) self.checkKinetics(ctmlGas, yamlGas, [300, 500, 1300], [1e5, 2e6, 1.4e7], 1e-6) def test_diamond(self): - ctml2yaml.convert(Path(self.cantera_data).joinpath('diamond.xml'), - Path(self.test_work_dir).joinpath('diamond.yaml')) + ctml2yaml.convert( + Path(self.cantera_data).joinpath('diamond.xml'), + Path(self.test_work_dir).joinpath('diamond.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('diamond', name='gas') ctmlSolid, yamlSolid = self.checkConversion('diamond', name='diamond') ctmlSurf, yamlSurf = self.checkConversion('diamond', @@ -899,8 +917,10 @@ def test_diamond(self): def test_lithium_ion_battery(self): name = 'lithium_ion_battery' - ctml2yaml.convert(Path(self.cantera_data).joinpath(name + ".xml"), - Path(self.test_work_dir).joinpath(name + ".yaml")) + ctml2yaml.convert( + Path(self.cantera_data).joinpath(name + ".xml"), + Path(self.test_work_dir).joinpath(name + ".yaml"), + ) ctmlAnode, yamlAnode = self.checkConversion(name, name='anode') ctmlCathode, yamlCathode = self.checkConversion(name, name='cathode') ctmlMetal, yamlMetal = self.checkConversion(name, name='electron') @@ -935,36 +955,46 @@ def test_lithium_ion_battery(self): self.checkKinetics(ctmlCathodeInt, yamlCathodeInt, [300], [1e5]) def test_noxNeg(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath('noxNeg.xml'), - Path(self.test_work_dir).joinpath('noxNeg.yaml')) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath('noxNeg.xml'), + Path(self.test_work_dir).joinpath('noxNeg.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('noxNeg') self.checkThermo(ctmlGas, yamlGas, [300, 1000]) self.checkKinetics(ctmlGas, yamlGas, [300, 1000], [1e5]) def test_ch4_ion(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("ch4_ion.xml"), - Path(self.test_work_dir).joinpath("ch4_ion.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("ch4_ion.xml"), + Path(self.test_work_dir).joinpath("ch4_ion.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("ch4_ion") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) self.checkTransport(ctmlGas, yamlGas, [298, 1001, 2400]) def test_nasa9(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("nasa9-test.xml"), - Path(self.test_work_dir).joinpath("nasa9-test.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("nasa9-test.xml"), + Path(self.test_work_dir).joinpath("nasa9-test.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("nasa9-test") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) def test_chemically_activated(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("chemically-activated-reaction.xml"), - Path(self.test_work_dir).joinpath("chemically-activated-reaction.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("chemically-activated-reaction.xml"), + Path(self.test_work_dir).joinpath("chemically-activated-reaction.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("chemically-activated-reaction") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) def test_explicit_forward_order(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("explicit-forward-order.xml"), - Path(self.test_work_dir).joinpath("explicit-forward-order.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("explicit-forward-order.xml"), + Path(self.test_work_dir).joinpath("explicit-forward-order.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("explicit-forward-order") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) @@ -977,28 +1007,36 @@ def test_explicit_reverse_rate(self): self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) def test_explicit_third_bodies(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("explicit-third-bodies.xml"), - Path(self.test_work_dir).joinpath("explicit-third-bodies.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("explicit-third-bodies.xml"), + Path(self.test_work_dir).joinpath("explicit-third-bodies.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("explicit-third-bodies") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) def test_fractional_stoich_coeffs(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("frac.xml"), - Path(self.test_work_dir).joinpath("frac.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("frac.xml"), + Path(self.test_work_dir).joinpath("frac.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("frac") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) def test_fixed_chemical_potential_thermo(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("LiFixed.xml"), - Path(self.test_work_dir).joinpath("LiFixed.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("LiFixed.xml"), + Path(self.test_work_dir).joinpath("LiFixed.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("LiFixed") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) def test_water_IAPWS95_thermo(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("liquid-water.xml"), - Path(self.test_work_dir).joinpath("liquid-water.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("liquid-water.xml"), + Path(self.test_work_dir).joinpath("liquid-water.yaml"), + ) ctmlWater, yamlWater = self.checkConversion("liquid-water") self.checkThermo(ctmlWater, yamlWater, [300, 500, 1300, 2000]) self.assertEqual(ctmlWater.transport_model, yamlWater.transport_model) @@ -1026,14 +1064,18 @@ def test_hmw_nacl_phase(self): self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_NaCl_solid_phase(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("NaCl_Solid.xml"), - Path(self.test_work_dir).joinpath("NaCl_Solid.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("NaCl_Solid.xml"), + Path(self.test_work_dir).joinpath("NaCl_Solid.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("NaCl_Solid") self.checkThermo(ctmlPhase, yamlPhase, [300, 500, 1300, 2000]) def test_DH_NaCl_phase(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("debye-huckel-all.xml"), - Path(self.test_work_dir).joinpath("debye-huckel-all.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("debye-huckel-all.xml"), + Path(self.test_work_dir).joinpath("debye-huckel-all.yaml"), + ) for name in [ "debye-huckel-dilute", "debye-huckel-B-dot-ak", @@ -1051,8 +1093,10 @@ def test_DH_NaCl_phase(self): self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_Maskell_solid_soln(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("MaskellSolidSolnPhase_valid.xml"), - Path(self.test_work_dir).joinpath("MaskellSolidSolnPhase_valid.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("MaskellSolidSolnPhase_valid.xml"), + Path(self.test_work_dir).joinpath("MaskellSolidSolnPhase_valid.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("MaskellSolidSolnPhase_valid") # Maskell phase doesn't support partial molar properties, so just check density @@ -1062,8 +1106,10 @@ def test_Maskell_solid_soln(self): self.assertNear(ctmlPhase.density, yamlPhase.density) def test_mock_ion(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("mock_ion.xml"), - Path(self.test_work_dir).joinpath("mock_ion.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("mock_ion.xml"), + Path(self.test_work_dir).joinpath("mock_ion.yaml"), + ) ctmlPhase = ct.ThermoPhase("mock_ion.xml") yamlPhase = ct.ThermoPhase("mock_ion.yaml") # Due to changes in how the species elements are specified, the composition @@ -1081,28 +1127,36 @@ def test_mock_ion(self): self.assertNear(ctmlPhase.density, yamlPhase.density) def test_Redlich_Kister(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("RedlichKisterVPSSTP_valid.xml"), - Path(self.test_work_dir).joinpath("RedlichKisterVPSSTP_valid.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("RedlichKisterVPSSTP_valid.xml"), + Path(self.test_work_dir).joinpath("RedlichKisterVPSSTP_valid.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("RedlichKisterVPSSTP_valid") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_species_names(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath('species-names.xml'), - Path(self.test_work_dir).joinpath('species-names.yaml')) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath('species-names.xml'), + Path(self.test_work_dir).joinpath('species-names.yaml'), + ) ctmlGas, yamlGas = self.checkConversion('species-names') self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) def test_sri_falloff_reaction(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("sri-falloff.xml"), - Path(self.test_work_dir).joinpath("sri-falloff.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("sri-falloff.xml"), + Path(self.test_work_dir).joinpath("sri-falloff.yaml"), + ) ctmlGas, yamlGas = self.checkConversion("sri-falloff") self.checkThermo(ctmlGas, yamlGas, [300, 500, 1300, 2000]) self.checkKinetics(ctmlGas, yamlGas, [900, 1800], [2e5, 20e5]) def test_vpss_and_hkft(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("pdss_hkft.xml"), - Path(self.test_work_dir).joinpath("pdss_hkft.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("pdss_hkft.xml"), + Path(self.test_work_dir).joinpath("pdss_hkft.yaml"), + ) for name in ["vpss_gas_pdss_hkft_phase", "vpss_soln_pdss_hkft_phase"]: ctmlPhase = ct.ThermoPhase("pdss_hkft.xml", name=name) @@ -1118,8 +1172,10 @@ def test_vpss_and_hkft(self): self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_lattice_solid(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("Li7Si3_ls.xml"), - Path(self.test_work_dir).joinpath("Li7Si3_ls.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("Li7Si3_ls.xml"), + Path(self.test_work_dir).joinpath("Li7Si3_ls.yaml"), + ) # Use ThermoPhase to avoid constructing a default Transport object which # throws an error for the LatticeSolidPhase basename = "Li7Si3_ls" @@ -1131,15 +1187,19 @@ def test_lattice_solid(self): self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_margules(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("LiKCl_liquid.xml"), - Path(self.test_work_dir).joinpath("LiKCl_liquid.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("LiKCl_liquid.xml"), + Path(self.test_work_dir).joinpath("LiKCl_liquid.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("LiKCl_liquid") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_idealsolidsoln(self): with self.assertWarnsRegex(UserWarning, "SolidKinetics type is not implemented"): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("IdealSolidSolnPhaseExample.xml"), - Path(self.test_work_dir).joinpath("IdealSolidSolnPhaseExample.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("IdealSolidSolnPhaseExample.xml"), + Path(self.test_work_dir).joinpath("IdealSolidSolnPhaseExample.yaml"), + ) # SolidKinetics is not implemented, so can't create a Kinetics class instance. basename = "IdealSolidSolnPhaseExample" @@ -1151,30 +1211,38 @@ def test_idealsolidsoln(self): self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_idealmolalsoln(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("IdealMolalSolnPhaseExample.xml"), - Path(self.test_work_dir).joinpath("IdealMolalSolnPhaseExample.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("IdealMolalSolnPhaseExample.xml"), + Path(self.test_work_dir).joinpath("IdealMolalSolnPhaseExample.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("IdealMolalSolnPhaseExample") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) def test_transport_models(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("transport_models_test.xml"), - Path(self.test_work_dir).joinpath("transport_models_test.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("transport_models_test.xml"), + Path(self.test_work_dir).joinpath("transport_models_test.yaml"), + ) for name in ["UnityLewis", "CK_Mix", "CK_Multi", "HighP"]: ctmlPhase, yamlPhase = self.checkConversion("transport_models_test", name=name) self.checkTransport(ctmlPhase, yamlPhase, [298, 1001, 2500]) def test_nonreactant_orders(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("reaction-orders.xml"), - Path(self.test_work_dir).joinpath("reaction-orders.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("reaction-orders.xml"), + Path(self.test_work_dir).joinpath("reaction-orders.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("reaction-orders") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) self.checkKinetics(ctmlPhase, yamlPhase, [300, 1001, 2500], [1e5, 10e5]) def test_species_ss_temperature_polynomials(self): - ctml2yaml.convert(Path(self.test_data_dir).joinpath("Li_Liquid.xml"), - Path(self.test_work_dir).joinpath("Li_Liquid.yaml")) + ctml2yaml.convert( + Path(self.test_data_dir).joinpath("Li_Liquid.xml"), + Path(self.test_work_dir).joinpath("Li_Liquid.yaml"), + ) ctmlPhase, yamlPhase = self.checkConversion("Li_Liquid") self.checkThermo(ctmlPhase, yamlPhase, [300, 500]) From 4c977e4a816140aaaabaceb7b7039c48db030abb Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 14:51:12 -0500 Subject: [PATCH 97/99] [Python] Add entry point scripts for converters The YAML converters are now installed as scripts from the Python package. The zip_safe flag is needed to ensure that data files are accessible by the package. This was added to resolve a build problem with Python 3.8. I'm not sure why it hasn't been a problem until now. --- interfaces/cython/cantera/ctml2yaml.py | 5 +++++ interfaces/cython/setup.py.in | 4 ++++ 2 files changed, 9 insertions(+) diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py index e90c8f3449b..4a6f8d9c0ec 100644 --- a/interfaces/cython/cantera/ctml2yaml.py +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -1,3 +1,8 @@ +#!/usr/bin/env python + +# This file is part of Cantera. See License.txt in the top-level directory or +# at https://cantera.org/license.txt for license and copyright information. + """Convert legacy CTML input to YAML format. There are two main entry points to this script, `main` and `convert`. The former is diff --git a/interfaces/cython/setup.py.in b/interfaces/cython/setup.py.in index 280b02ac71d..b69bd7d3599 100644 --- a/interfaces/cython/setup.py.in +++ b/interfaces/cython/setup.py.in @@ -71,6 +71,9 @@ setup( 'console_scripts': [ 'ck2cti=cantera.ck2cti:script_entry_point', 'ctml_writer=cantera.ctml_writer:main', + 'ck2yaml=cantera.ck2yaml:script_entry_point', + 'cti2yaml=cantera.cti2yaml:main', + 'ctml2yaml=cantera.ctml2yaml:main', ], }, classifiers=[ @@ -96,4 +99,5 @@ setup( 'cantera.examples': ['*/*.*'], 'cantera': ["@py_extension@", '*.pxd'], }, + zip_safe=False, ) From 20906b637d93e3e90208664aabcdb7b7191dd04a Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Tue, 26 Nov 2019 14:51:41 -0500 Subject: [PATCH 98/99] [SCons/Python] Minimum Python version is 3.5 Bump the minimum Python version to 3.5 to support features in ctml2yaml. --- SConstruct | 2 +- interfaces/cython/setup.py.in | 3 +-- 2 files changed, 2 insertions(+), 3 deletions(-) diff --git a/SConstruct b/SConstruct index d76bf29d627..46c88aa9961 100644 --- a/SConstruct +++ b/SConstruct @@ -1200,7 +1200,7 @@ if env['VERBOSE']: env['python_cmd_esc'] = quoted(env['python_cmd']) # Python Package Settings -python_min_version = LooseVersion('3.3') +python_min_version = LooseVersion('3.5') cython_min_version = LooseVersion('0.23') numpy_min_test_version = LooseVersion('1.8.1') diff --git a/interfaces/cython/setup.py.in b/interfaces/cython/setup.py.in index b69bd7d3599..c85e4b7766a 100644 --- a/interfaces/cython/setup.py.in +++ b/interfaces/cython/setup.py.in @@ -86,11 +86,10 @@ setup( 'Operating System :: POSIX :: Linux', 'Programming Language :: C++', 'Programming Language :: Fortran', - 'Programming Language :: Python :: 3.3', - 'Programming Language :: Python :: 3.4', 'Programming Language :: Python :: 3.5', 'Programming Language :: Python :: 3.6', 'Programming Language :: Python :: 3.7', + 'Programming Language :: Python :: 3.8', 'Topic :: Scientific/Engineering :: Chemistry', ], package_data = { From 65081792d9b6d189d6ef5e6adb994caf8ffd559f Mon Sep 17 00:00:00 2001 From: "Bryan W. Weber" Date: Sat, 30 Nov 2019 15:53:25 -0500 Subject: [PATCH 99/99] [cti2yaml] Update command line interface Use argparse to have the CLI match ctml2yaml.py --- interfaces/cython/cantera/cti2yaml.py | 47 +++++++++++++++++++-------- 1 file changed, 33 insertions(+), 14 deletions(-) diff --git a/interfaces/cython/cantera/cti2yaml.py b/interfaces/cython/cantera/cti2yaml.py index 7f153c90f21..4ac96472a74 100644 --- a/interfaces/cython/cantera/cti2yaml.py +++ b/interfaces/cython/cantera/cti2yaml.py @@ -3,14 +3,12 @@ # This file is part of Cantera. See License.txt in the top-level directory or # at https://cantera.org/license.txt for license and copyright information. -""" -cti2yaml.py: Convert legacy CTI input files to YAML - -Usage: - python cti2yaml.py mech.cti [out.yaml] +"""Convert legacy CTI input files to YAML. -This will produce the output file 'mech.yaml' if an output file name is not -specified. +There are two main entry points to this script, `main` and `convert`. The former is +used from the command line interface and parses the arguments passed. The latter +accepts either the name of the CTI input file or a string containing the CTI +content. """ import sys @@ -19,6 +17,7 @@ from collections import OrderedDict import numpy as np from email.utils import formatdate +import argparse try: import ruamel_yaml as yaml @@ -1669,13 +1668,33 @@ def convert(filename=None, output_name=None, text=None): def main(): - if len(sys.argv) == 1 or sys.argv[1] in ('-h', '--help'): - print(__doc__) - sys.exit(0) - if len(sys.argv) not in (2,3): - raise ValueError("Incorrect number of command line arguments.\n" - "See 'cti2yaml --help' for more information.") - convert(*sys.argv[1:]) + """Parse command line arguments and pass them to `convert`.""" + parser = argparse.ArgumentParser( + description="Convert legacy CTI input files to YAML format", + epilog=("The 'output' argument is optional. If it is not given, an output " + "file with the same name as the input file is used, with the extension " + "changed to '.yaml'.") + ) + parser.add_argument("input", help="The input CTI filename. Must be specified.") + parser.add_argument("output", nargs="?", help="The output YAML filename. Optional.") + if len(sys.argv) not in [2, 3]: + if len(sys.argv) > 3: + print( + "cti2yaml.py: error: unrecognized arguments:", + ' '.join(sys.argv[3:]), + file=sys.stderr, + ) + parser.print_help(sys.stderr) + sys.exit(1) + args = parser.parse_args() + input_file = pathlib.Path(args.input) + if args.output is None: + output_file = input_file.with_suffix(".yaml") + else: + output_file = pathlib.Path(args.output) + + convert(input_file, output_file) + if __name__ == "__main__": main()