diff --git a/SConstruct b/SConstruct index 4c9f9d31207..46c88aa9961 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') @@ -1199,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/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..b0b982a7af8 --- /dev/null +++ b/doc/sphinx/yaml/ctml_conversion.rst @@ -0,0 +1,51 @@ +*********************** +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_quantity +.. 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 +.. autofunction:: main + +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 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() diff --git a/interfaces/cython/cantera/ctml2yaml.py b/interfaces/cython/cantera/ctml2yaml.py new file mode 100644 index 00000000000..4a6f8d9c0ec --- /dev/null +++ b/interfaces/cython/cantera/ctml2yaml.py @@ -0,0 +1,2636 @@ +#!/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 +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 +import warnings +import copy + +from typing import Any, Dict, Union, Iterable, Optional, List, Tuple +from typing import TYPE_CHECKING + +try: + import ruamel_yaml as yaml # type: ignore +except ImportError: + from ruamel import yaml + +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 + + QUANTITY = Union[float, str] + + RK_EOS_DICT = TypedDict( + "RK_EOS_DICT", + {"a": List[QUANTITY], "b": QUANTITY, "binary-a": Dict[str, List[QUANTITY]]}, + total=False, + ) + DH_BETA_MATRIX = TypedDict( + "DH_BETA_MATRIX", {"species": List[str], "beta": QUANTITY}, total=False + ) + 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] + SRI_PARAMS = Dict[str, float] + COVERAGE_PARAMS = Dict[str, ARRHENIUS_PARAMS] + + ARRHENIUS_TYPE = Dict[str, ARRHENIUS_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]]] + 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]] + + THERMO_POLY_TYPE = Union[List[List[float]], List[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 + + +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): + 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: + message += ": '" + node_str + "'" + else: + message = node_str + + super().__init__(message) + + +class MissingXMLAttribute(LookupError): + 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: + message += ": '" + node_str + "'" + else: + message = node_str + + super().__init__(message) + + +class MissingNodeText(LookupError): + 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: + 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") + + +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) + + 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: 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: + 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_quantity(node: etree.Element) -> "QUANTITY": + """Process an XML node into a float value or a value with units. + + :param node: + The XML node with a value in the text and optionally a units attribute. + + Given XML nodes like: + + .. code:: XML + + 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) + else: + return value + + +def split_species_value_string(node: etree.Element) -> Dict[str, float]: + """Split a string of species:value pairs into a dictionary. + + :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 + ``base/stringUtils.cpp``. + """ + 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): + try: + colon = text.index(":", left) + except ValueError: + break + + # colon + 1 here excludes the colon itself from the search + valstart_match = non_whitespace.search(text, colon + 1) + if valstart_match is None: + 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 + + +def clean_node_text(node: etree.Element) -> str: + """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) + return text.replace("\n", " ").replace("\t", " ").strip() + + +class Phase: + thermo_model_mapping = { + "IdealGas": "ideal-gas", + "Incompressible": "constant-density", + "Surface": "ideal-surface", + "Edge": "edge", + "Metal": "electron-cloud", + "StoichSubstance": "fixed-stoichiometry", + "PureFluid": "pure-fluid", + "LatticeSolid": "compound-lattice", + "Lattice": "lattice", + "HMW": "HMW-electrolyte", + "IdealSolidSolution": "ideal-condensed", + "DebyeHuckel": "Debye-Huckel", + "IdealMolalSolution": "ideal-molal-solution", + "IdealGasVPSS": "ideal-gas-VPSS", + "IdealSolnVPSS": "ideal-solution-VPSS", + "Margules": "Margules", + "IonsFromNeutralMolecule": "ions-from-neutral-molecule", + "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", + "Interface": "surface", + "none": None, + "Edge": "edge", + "None": None, + "SolidKinetics": None, + } + transport_model_mapping = { + "Mix": "mixture-averaged", + "Multi": "multicomponent", + "None": None, + "Ion": "ionized-gas", + "Water": "water", + "none": None, + None: None, + "UnityLewis": "unity-Lewis-number", + "CK_Mix": "mixture-averaged-CK", + "CK_Multi": "multicomponent-CK", + "HighP": "high-pressure", + } + + state_properties_mapping = { + "moleFractions": "X", + "massFractions": "Y", + "temperature": "T", + "pressure": "P", + "coverages": "coverages", + "soluteMolalities": "molalities", + } + + pure_fluid_mapping = { + "0": "water", + "1": "nitrogen", + "2": "methane", + "3": "hydrogen", + "4": "oxygen", + "5": "HFC134a", + "7": "carbondioxide", + "8": "heptane", + } + + def __init__( + self, + phase: etree.Element, + 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( + "The 'phase' node requires an 'id' attribute.", 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 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' 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( + "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( + "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( + "The 'DebyeHuckel' thermo model requires the " + "'activityCoefficients' node.", + phase_thermo, + ) + 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": + activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is not None: + 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_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) + 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) + elif phase_thermo_model == "Redlich-Kister": + activity_coefficients = phase_thermo.find("activityCoefficients") + if activity_coefficients is None: + raise MissingXMLNode( + "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( + "The '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( + "The '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) + 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 + 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": + 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_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_quantity(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: + self.attribs["transport"] = transport_model + + # 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[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 + # 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 "reactions" in reactions[0]: + self.attribs.update(reactions[0]) + else: + self.attribs["reactions"] = reactions + + 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", + "soluteMolalities", + ]: + composition = split_species_value_string(prop) + phase_state[property_name] = composition + else: + value = get_float_or_quantity(prop) + phase_state[property_name] = value + + if phase_state: + self.attribs["state"] = phase_state + + std_conc_node = phase.find("standardConc") + if std_conc_node is not None: + self.attribs["standard-concentration-basis"] = std_conc_node.get("model") + + self.check_elements(species, species_data) + + def ideal_molal_solution( + self, activity_coeffs: etree.Element + ) -> Dict[str, Union[str, "QUANTITY"]]: + """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. The + dictionary will be empty when there are no cutoff nodes in the + ``activityCoefficients`` node. + """ + 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") + 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_quantity(limit_node) + return cutoff + + def margules( + self, activity_coeffs: etree.Element + ) -> List[Dict[str, List[Union[str, "QUANTITY"]]]]: + """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`` 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 = [] + 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, QUANTITY]]] + 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, "QUANTITY"]]]]: + """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. The ``activityCoefficients`` node + must have a ``binaryNeutralSpeciesParameters`` child node. + """ + 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, QUANTITY]]] + 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]]], + species_data: Dict[str, List["Species"]], + ) -> None: + """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. 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: + 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]]], + activity_coeffs: etree.Element, + species_data: Dict[str, List["Species"]], + ) -> None: + """Move the Redlich-Kwong activity coefficient data from phase to species. + + :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"): + eq_of_state = BlockMap({"model": "Redlich-Kwong"}) + pure_species = pure_param.get("species") + if pure_species is None: + raise MissingXMLAttribute( + "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 'pureFluidParameters' node requires the 'a_coeff' node.", + pure_param, + ) + + 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() + 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(pure_a) + + pure_b_node = pure_param.find("b_coeff") + if pure_b_node is None: + raise MissingXMLNode( + "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 + + 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 'crossFluidParameters' node 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( + "The 'crossFluidParameters' node requires the 'a_coeff' node", + cross_param, + ) + + 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_w_units = [] + cross_a_w_units.append( + "{} {}".format(float2string(cross_a[0]), cross_a_unit + "*K^0.5") + ) + cross_a_w_units.append( + "{} {}".format(float2string(cross_a[1]), cross_a_unit + "/K^0.5") + ) + 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)}) + + 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"] = all_species_eos[ + spec.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. + + :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" + 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 'density', 'molarDensity', or 'molarVolume' " + "node.", + phase_thermo, + ) + + equation_of_state = { + "model": "constant-volume", + 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(): + 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 + ) -> Dict[str, Iterable[str]]: + """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": + new_datasrc = "species" + elif datasrc.startswith("#"): + new_datasrc = datasrc[1:] + else: + filename, location = datasrc.split("#", 1) + name = str(Path(filename).with_suffix(".yaml")) + if location == "species_data": + location = "species" + new_datasrc = "{}/{}".format(name, location) + + return {new_datasrc: species_list} + + 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. + + :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' node must include a 'datasrc' attribute.", + reactionArray_node, + ) + + 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: + # "undeclared" is the only allowed option for third_bodies and species + # here, so ignore other options + if skip_node.get("third_bodies", "").lower() == "undeclared": + self.attribs["skip-undeclared-third-bodies"] = True + if skip_node.get("species", "").lower() == "undeclared": + reaction_option = "declared-species" + else: + reaction_option = "all" + else: + reaction_option = "all" + + if not datasrc.startswith("#"): + if filter_text.lower() != "none": + raise ValueError( + "Filtering reactions is not allowed with an external 'datasrc'" + ) + if skip_node is None: + 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) + name = str(Path(filename).with_suffix(".yaml")) + if location == "reaction_data": + location = "reactions" + datasrc = "{}/{}".format(name, location) + else: + if filter_text.lower() != "none": + datasrc = self.filter_reaction_list(datasrc, filter_text, reaction_data) + elif datasrc == "#reaction_data": + datasrc = "reactions" + else: + datasrc = datasrc.lstrip("#") + + 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. + + :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 = [] + misses = [] + re_pattern = re.compile(filter_text.replace("*", ".*")) + for reaction in all_reactions: + reaction_id = 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]: + """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: + raise MissingXMLNode( + "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 'enthalpy' node must have a 'units' attribute.", enthalpy_node, + ) + entropy_node = tab_thermo_node.find("entropy") + if entropy_node is None: + raise MissingXMLNode( + "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 '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.") + tab_thermo["units"] = FlowMap( + {"energy": entropy_units[0], "quantity": entropy_units[1]} + ) + 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 = clean_node_text(entropy_node).split(",") + tab_thermo["entropy"] = FlowList(map(float, entropy)) + 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") + if mole_fraction_node is None: + raise MissingXMLNode( + "The 'tabulatedThermo' node must have a 'moleFraction' node.", + 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", 0)): + raise ValueError( + "The number of entries in the mole_fraction list is different from the " + "indicated size." + ) + + return tab_thermo + + 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: + 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: + raise MissingXMLNode( + "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. + if A_Debye_node.text is None: + 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 = [] + 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 = clean_node_text(param_node).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 + + 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, "QUANTITY", bool]]: + """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", + "bdot_with_common_a": "B-dot-with-common-a", + "pitzer_with_beta_ij": "Pitzer-with-beta_ij", + "beta_ij": "beta_ij", + "": "dilute-limit", + } + activity_model = activity_node.get("model") + if activity_model is None: + raise MissingXMLAttribute( + "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") + 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_quantity(B_dot_node) + + ionic_radius_node = activity_node.find("ionicRadius") + 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") + 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 + 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() + for r in radii: + species_name, radius = r.strip().rsplit(":", 1) + if radius_units is not None: + radius += " {}".format(radius_units) + 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 + + @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: + 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)) + func = getattr(self, thermo_type) + self.attribs = func(thermo) + + def process_polynomial( + self, thermo: etree.Element, poly_type: str + ) -> Tuple[List[List[float]], List[float]]: + """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 = {} + for node in model_nodes: + 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 + ) + 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 + ) + 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 '{}' " + "entries".format(poly_type) + ) + 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 `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 + thermo_attribs["data"] = data + return thermo_attribs + + def NASA(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: + """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 + thermo_attribs["data"] = data + return thermo_attribs + + def NASA9(self, thermo: etree.Element) -> Dict[str, Union[str, "THERMO_POLY_TYPE"]]: + """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 + thermo_attribs["data"] = data + return thermo_attribs + + def const_cp(self, thermo: etree.Element) -> Dict[str, Union[str, "QUANTITY"]]: + """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: + raise MissingXMLNode( + "The 'thermo' node must contain a 'const_cp' node", thermo + ) + for node in const_cp_node: + tag = node.tag + if tag == "t0": + tag = "T0" + thermo_attribs[tag] = get_float_or_quantity(node) + + return thermo_attribs + + def Mu0( + self, thermo: etree.Element + ) -> Dict[str, Union[str, Dict[float, Iterable]]]: + """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: + raise MissingXMLNode("The 'thermo' node must contain a 'Mu0' node.", thermo) + ref_pressure = Mu0_node.get("Pref") + if ref_pressure is None: + raise MissingXMLAttribute( + "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' 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") + if title == "Mu0Values": + dimensions = float_node.get("units") + if dimensions == "Dimensionless": + thermo_attribs["dimensionless"] = True + dimensions = "" + values = [] # type: Union[Iterable[float], Iterable[str]] + values = map(float, clean_node_text(float_node).split(",")) + if dimensions: + values = [float2string(v) + " " + dimensions for v in values] + elif title == "Mu0Temperatures": + temperatures = map(float, clean_node_text(float_node).split(",")) + + thermo_attribs["data"] = dict(zip(temperatures, values)) + + return thermo_attribs + + @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: + 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, 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: + raise TypeError( + "Unknown transport model type: '{}'".format(transport.get("model")) + ) + self.attribs["model"] = self.species_transport_mapping[transport_model] + self.attribs["geometry"] = transport.findtext("string[@title='geometry']") + 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): + """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: + standard_state_model_mapping = { + "ideal-gas": "ideal-gas", + "constant_incompressible": "constant-volume", + "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", + "chargedSpecies": "charged-species", + "strongAcidAssociated": "strong-acid-associated", + "polarNeutral": "polar-neutral", + "nonpolarNeutral": "nonpolar-neutral", + } + + 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: + raise MissingXMLAttribute( + "The 'species' node must have a 'name' attribute.", species_node + ) + self.attribs["name"] = species_name + atom_array = species_node.find("atomArray") + if atom_array is not None and atom_array.text is not None: + self.attribs["composition"] = split_species_value_string(atom_array) + 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: + thermo_model = thermo.get("model", "") + # 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) + # 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: + raise MissingXMLNode( + "'IonFromNeutral' node 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": + self.attribs["equation-of-state"] = self.hkft(species_node) + else: + if len(thermo) > 0: + 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_quantity(weak_acid_charge) + + def hkft(self, species_node: etree.Element) -> Dict[str, "HKFT_THERMO_TYPE"]: + """Process a species with HKFT thermo type. + + :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") + 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_quantity(t_node) + elif t_node.tag == "DG0_f_Pr_Tr": + eqn_of_state["g0"] = get_float_or_quantity(t_node) + elif t_node.tag == "S0_Pr_Tr": + eqn_of_state["s0"] = get_float_or_quantity(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 '{}' node.".format(tag), + std_state_node, + ) + if tag.startswith("a"): + a.append(get_float_or_quantity(node)) + elif tag.startswith("c"): + 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") + if omega_node is None: + raise MissingXMLNode( + "The HKFT 'standardState' node requires an 'omega_Pr_Tr' node.", + std_state_node, + ) + eqn_of_state["omega"] = get_float_or_quantity(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. + + :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. + """ + 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: + 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 = { + "model": self.standard_state_model_mapping[std_state_model] + } # 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: + raise MissingXMLNode( + "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) + 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 + 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 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: + # 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: + self.attribs["id"] = reaction_id + + reaction_equation = reaction.findtext("equation") + if reaction_equation is None: + 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 + self.attribs["equation"] = reaction_equation.replace("[=]", "<=>").replace( + "=]", "=>" + ) + + reaction_type = reaction.get("type", "arrhenius").lower() + rate_coeff = reaction.find("rateCoeff") + if rate_coeff is None: + 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"]: + reaction_type = "threebody" + elif reaction_type == "falloff": + falloff_node = rate_coeff.find("falloff") + if falloff_node is None: + 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", "SRI"]: + raise TypeError( + "Unknown falloff type '{}' for reaction id '{}'".format( + falloff_type, reaction.get("id") + ) + ) + else: + reaction_type = falloff_type + elif reaction_type in ["chemact", "chemically_activated"]: + falloff_node = rate_coeff.find("falloff") + if falloff_node is None: + raise MissingXMLNode( + "chemAct reaction types must have a falloff node.", rate_coeff + ) + falloff_type = falloff_node.get("type") + if falloff_type != "Troe": + raise TypeError( + "Unknown activation type '{}' for reaction id '{}'".format( + 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)) + + if node_motz_wise and self.attribs.get("Motz-Wise") is None: + self.attribs["Motz-Wise"] = True + + if reaction.get("negative_A", "").lower() == "yes": + self.attribs["negative-A"] = True + + reactants_node = reaction.find("reactants") + if reactants_node is None: + raise MissingXMLNode( + "The 'reaction' node must have a 'reactants' node.", reaction + ) + reactants = split_species_value_string(reactants_node) + orders = {} + for order_node in reaction.iterfind("order"): + species = order_node.get("species", "") + if not species: + raise MissingXMLAttribute( + "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): + orders[species] = order + if orders: + self.attribs["orders"] = orders + + if reaction.get("negative_orders", "").lower() == "yes": + self.attribs["negative-orders"] = True + + if reaction.get("nonreactant_orders", "").lower() == "yes": + self.attribs["nonreactant-orders"] = True + + if reaction.get("duplicate", "").lower() == "yes": + self.attribs["duplicate"] = True + + @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. + + :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") + 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) -> "THREEBODY_TYPE": + """Process a three-body reaction. + + :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( + rate_coeff.find("Arrhenius") + ) + eff_node = rate_coeff.find("efficiencies") + if eff_node is not None: + reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) + + return reaction_attribs + + def lindemann(self, rate_coeff: etree.Element) -> "LINDEMANN_TYPE": + """Process a Lindemann falloff reaction. + + :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"): + if 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") + eff_node = rate_coeff.find("efficiencies") + if eff_node is not None: + reaction_attribs["efficiencies"] = self.process_efficiencies(eff_node) + + return reaction_attribs + + def troe(self, rate_coeff: etree.Element) -> "TROE_TYPE": + """Process a Troe falloff reaction. + + :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) + + troe_node = rate_coeff.find("falloff") + if troe_node is None: + 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() + # 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)}) # type: ignore + + return reaction_attribs + + def chemact(self, rate_coeff: etree.Element) -> "CHEMACT_TYPE": + """Process a chemically activated falloff reaction. + + :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"): + if 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_node = rate_coeff.find("falloff") + if troe_node is None: + raise MissingXMLNode( + "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"] + 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 plog(self, rate_coeff: etree.Element) -> "PLOG_TYPE": + """Process a PLOG reaction. + + :param rate_coeff: + The XML node with rate coefficient information for this reaction. + """ + 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) + P_node = arr_coeff.find("P") + if P_node is None: + raise MissingXMLNode( + "A 'plog' reaction must have a 'P' node.", arr_coeff + ) + rate_constant["P"] = get_float_or_quantity(P_node) + rate_constants.append(rate_constant) + reaction_attributes["rate-constants"] = rate_constants + + return reaction_attributes + + def chebyshev(self, rate_coeff: etree.Element) -> "CHEBYSHEV_TYPE": + """Process a Chebyshev reaction. + + :param rate_coeff: + The XML node with rate coefficient information for this reaction. + """ + reaction_attributes = FlowMap( + { + "type": "Chebyshev", + "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 MissingXMLNode( + "A Chebyshev 'reaction' node must include a '{}' " + "node".format(range_tag), + rate_coeff, + ) + if range_tag.startswith("T"): + reaction_attributes["temperature-range"].append( + get_float_or_quantity(range_node) + ) + elif range_tag.startswith("P"): + reaction_attributes["pressure-range"].append( + get_float_or_quantity(range_node) + ) + data_node = rate_coeff.find("floatArray") + if data_node is None: + raise MissingXMLNode( + "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( + "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])) + + if len(data) != n_T_values: + raise ValueError( + "The number of coefficients in the Chebyshev data do not match the " + "specified temperature and pressure degrees." + ) + reaction_attributes["data"] = data + + return reaction_attributes + + 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. + """ + arr_node = rate_coeff.find("Arrhenius") + if arr_node is None: + raise MissingXMLNode( + "An interface 'reaction' node requires an 'Arrhenius' node", rate_coeff + ) + if arr_node.get("type", "").lower() == "stick": + 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)} + ) + 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( + "A 'coverage' node requires an 'a' node.", cov_node + ) + cov_m = cov_node.find("m") + if cov_m is None: + raise MissingXMLNode( + "A 'coverage' node requires an 'm' node.", cov_node + ) + cov_e = cov_node.find("e") + if cov_e is None: + raise MissingXMLNode( + "A 'coverage' node requires an 'e' node.", cov_node + ) + reaction_attributes["coverage-dependencies"] = { + cov_species: { + "a": get_float_or_quantity(cov_a), + "m": get_float_or_quantity(cov_m), + "E": get_float_or_quantity(cov_e), + } + } + + echem_node = rate_coeff.find("electrochem") + 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", "").lower() == "exchangecurrentdensity": + reaction_attributes["exchange-current-density-formulation"] = True + + return reaction_attributes + + def arrhenius(self, rate_coeff: etree.Element) -> "ARRHENIUS_TYPE": + """Process a standard Arrhenius-type reaction. + + :param rate_coeff: + The XML node with rate coefficient information for this reaction. + """ + return FlowMap( + { + "rate-constant": self.process_arrhenius_parameters( + rate_coeff.find("Arrhenius") + ) + } + ) + + def process_arrhenius_parameters( + self, arr_node: Optional[etree.Element] + ) -> "ARRHENIUS_PARAMS": + """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") + 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.", + arr_node, + ) + return FlowMap( + { + "A": get_float_or_quantity(A_node), + "b": get_float_or_quantity(b_node), + "Ea": get_float_or_quantity(E_node), + } + ) + + def process_efficiencies(self, eff_node: etree.Element) -> "EFFICIENCY_PARAMS": + """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]]: + """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", "") + if this_data_node_id in species: + warnings.warn( + "Duplicate 'speciesData' id found: '{}'. Only the first section will " + "be included in the output file.".format(this_data_node_id) + ) + continue + species[this_data_node_id] = [ + Species(s) for s in species_data_node.iterfind("species") + ] + + return species + + +def create_reactions_from_data_node( + ctml_tree: etree.Element, +) -> Dict[str, List[Reaction]]: + """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 + if reactionData_node.get("motz_wise", "").lower() == "true": + node_motz_wise = True + 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 + + +def create_phases_from_data_node( + ctml_tree: etree.Element, + species_data: Dict[str, List[Species]], + reaction_data: Dict[str, List[Reaction]], +) -> List[Phase]: + """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") + ] + 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] = 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. 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. + """ + 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 + # 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) + 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") + + emitter = yaml.YAML() + for cl in [Phase, Species, SpeciesThermo, SpeciesTransport, Reaction]: + emitter.register_class(cl) + + 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: + emitter.dump(metadata, output_file) + emitter.dump(output_phases, output_file) + if output_species: + emitter.dump(output_species, output_file) + if output_reactions: + 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__": + main() 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: diff --git a/interfaces/cython/cantera/test/test_convert.py b/interfaces/cython/cantera/test/test_convert.py index 37541c12a55..b77eccb04c1 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, @@ -510,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) @@ -527,8 +522,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) @@ -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 @@ -729,3 +725,536 @@ 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): + + def checkConversion(self, basename, cls=ct.Solution, ctmlphases=(), + 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) + self.assertEqual(ctmlPhase.n_reactions, yamlPhase.n_reactions) + for C, Y in zip(ctmlPhase.species(), yamlPhase.species()): + self.assertEqual(C.composition, Y.composition) + + 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) + 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(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) + + 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): + 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 + 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]) + + 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]) + + 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, + name='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_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, + name='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'), + ) + ctmlGas, yamlGas = self.checkConversion('sofc') + ctmlMetal, yamlMetal = self.checkConversion('sofc', name='metal') + ctmlOxide, yamlOxide = self.checkConversion('sofc', name='oxide_bulk') + ctmlMSurf, yamlMSurf = self.checkConversion('sofc', ct.Interface, + name='metal_surface', ctmlphases=[ctmlGas, ctmlMetal], + yamlphases=[yamlGas, yamlMetal]) + ctmlOSurf, yamlOSurf = self.checkConversion('sofc', ct.Interface, + name='oxide_surface', ctmlphases=[ctmlGas, ctmlOxide], + yamlphases=[yamlGas, yamlOxide]) + ctml_tpb, yaml_tpb = self.checkConversion('sofc', ct.Interface, + name='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]) + + 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', name=name) + self.checkThermo(ctmlPhase, yamlPhase, + [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'), + ) + 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'), + ) + 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', name='gas') + ctmlSolid, yamlSolid = self.checkConversion('diamond', name='diamond') + ctmlSurf, yamlSurf = self.checkConversion('diamond', + ct.Interface, name='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]) + + 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, 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, + name='edge_anode_electrolyte', + ctmlphases=[ctmlAnode, ctmlMetal, ctmlElyt], + yamlphases=[yamlAnode, yamlMetal, yamlElyt]) + ctmlCathodeInt, yamlCathodeInt = self.checkConversion(name, + name='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]) + + 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]) + + 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]) + + 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]) + + 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]) + + 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]) + + 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]) + + 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) + + 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"), + ) + 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 name in [ + "debye-huckel-dilute", + "debye-huckel-B-dot-ak", + "debye-huckel-B-dot-a", + "debye-huckel-pitzer-beta_ij", + "debye-huckel-beta_ij", + ]: + # 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): + 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) + + 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) + + 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]) + + 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]) + + 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]) + + 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]) + + 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]) + + 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]) + + 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]) + + def test_idealmolalsoln(self): + 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"), + ) + 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]) + + 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]) + + 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") + ) diff --git a/interfaces/cython/setup.py.in b/interfaces/cython/setup.py.in index 280b02ac71d..c85e4b7766a 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=[ @@ -83,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 = { @@ -96,4 +98,5 @@ setup( 'cantera.examples': ['*/*.*'], 'cantera': ["@py_extension@", '*.pxd'], }, + zip_safe=False, ) 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"); } } diff --git a/src/thermo/IonsFromNeutralVPSSTP.cpp b/src/thermo/IonsFromNeutralVPSSTP.cpp index 1bd988c347e..71f88118d5d 100644 --- a/src/thermo/IonsFromNeutralVPSSTP.cpp +++ b/src/thermo/IonsFromNeutralVPSSTP.cpp @@ -482,11 +482,30 @@ 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_) { + throw CanteraError( + "IonsFromNeutralVPSSTP::initThermo", + "The neutral phase has not been initialized. Are you missing the " + "'neutral-phase' key?" + ); } size_t nElementsN = neutralMoleculePhase_->nElements(); @@ -497,14 +516,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) { 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 + + + + + + + + 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. + + + + + + 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 + + + + + + + + + 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 + + + + + + + + + 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 + + + + + + + + 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 @@ - 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 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 + + + 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 + + + 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 + + + + + + + + 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 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, 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 + + + + + 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 + + + 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 + + + diff --git a/test/data/species-names.xml b/test/data/species-names.xml new file mode 100644 index 00000000000..9b394b14a57 --- /dev/null +++ b/test/data/species-names.xml @@ -0,0 +1,402 @@ + + + + + + + H C + + (Parens) @#$%^-2 co:lons: [xy2]*{.} + plus+ eq=uals plus trans_butene + co amp&ersand + + + 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 + + + + + + + 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 + + + + + + + + + (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 + + + + + amp&ersand [=] plus+ + + + 9.999000E+06 + 9.9 + 999.900000 + + + amp&ersand:1.0 + plus+:1.0 + + + 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 + + + + + +