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
+
+
+
+
+
+