diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index a3b07cb82..594caf8bc 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -59,12 +59,14 @@ public: template T& as(); - const std::type_info& type(); + const std::type_info& type() const; + std::string type_str() const; template bool is() const; explicit AnyValue(const std::string& value); + explicit AnyValue(const char* value); AnyValue& operator=(const std::string& value); AnyValue& operator=(const char* value); const std::string& asString() const; diff --git a/include/cantera/thermo/ThermoFactory.h b/include/cantera/thermo/ThermoFactory.h index 72eba6e8b..db9155e62 100644 --- a/include/cantera/thermo/ThermoFactory.h +++ b/include/cantera/thermo/ThermoFactory.h @@ -119,6 +119,17 @@ inline ThermoPhase* newThermoPhase(const std::string& model) */ ThermoPhase* newPhase(XML_Node& phase); +//! Create a new ThermoPhase object and initialize it +/*! + * @param phaseNode The node containing the phase definition (i.e. thermo + * model, list of species, and initial state) + * @param rootNode The root node of the tree containing the phase definition, + * which will be used as the default location from which to read species + * definitions. + */ +unique_ptr newPhase(const AnyMap& phaseNode, + const AnyMap& rootNode=AnyMap()); + //! Create and Initialize a ThermoPhase object from an XML input file. /*! * This routine is a wrapper around the newPhase(XML_Node) routine which does @@ -187,6 +198,18 @@ ThermoPhase* newPhase(const std::string& infile, std::string id=""); */ void importPhase(XML_Node& phase, ThermoPhase* th); +//! Initialize a ThermoPhase object +/*! + * @param phase The ThermoPhase object to be initialized + * @param phaseNode The node containing the phase definition (i.e. thermo + * model, list of species, and initial state) + * @param rootNode The root node of the tree containing the phase definition, + * which will be used as the default location from which to read species + * definitions. + */ +void setupPhase(ThermoPhase& phase, const AnyMap& phaseNode, + const AnyMap& rootNode=AnyMap()); + //! Add the elements given in an XML_Node tree to the specified phase void installElements(Phase& th, const XML_Node& phaseNode); diff --git a/interfaces/cython/.gitignore b/interfaces/cython/.gitignore index 447b2efd0..c85f7d201 100644 --- a/interfaces/cython/.gitignore +++ b/interfaces/cython/.gitignore @@ -7,6 +7,8 @@ cantera/test/data/*.xml cantera/test/data/*.inp cantera/test/data/*.dat cantera/test/data/*.csv +cantera/test/data/*.yaml +cantera/test/data/*.yml setup.py scripts/ctml_writer.py scripts/ctml_writer diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 94523b05b..4871053cf 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -287,12 +287,18 @@ bool AnyValue::hasKey(const std::string& key) const { void AnyValue::setKey(const std::string &key) { m_key = key; } -const std::type_info &AnyValue::type() { +const std::type_info &AnyValue::type() const { return m_value->type(); } +std::string AnyValue::type_str() const { + return demangle(type()); +} + AnyValue::AnyValue(const std::string& value) : m_value(new boost::any{value}) {} +AnyValue::AnyValue(const char* value) : m_value(new boost::any{std::string(value)}) {} + AnyValue &AnyValue::operator=(const std::string &value) { *m_value = value; return *this; diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index 14e1a20c3..efee3ea21 100644 --- a/src/thermo/ThermoFactory.cpp +++ b/src/thermo/ThermoFactory.cpp @@ -50,6 +50,7 @@ std::mutex ThermoFactory::thermo_mutex; ThermoFactory::ThermoFactory() { reg("IdealGas", []() { return new IdealGasPhase(); }); + m_synonyms["ideal-gas"] = "IdealGas"; reg("Incompressible", []() { return new ConstDensityThermo(); }); reg("Surface", []() { return new SurfPhase(); }); reg("Edge", []() { return new EdgePhase(); }); @@ -88,6 +89,13 @@ ThermoPhase* newPhase(XML_Node& xmlphase) return t.release(); } +unique_ptr newPhase(const AnyMap& phaseNode, const AnyMap& rootNode) +{ + unique_ptr t(newThermoPhase(phaseNode["thermo"].asString())); + setupPhase(*t, phaseNode, rootNode); + return t; +} + ThermoPhase* newPhase(const std::string& infile, std::string id) { XML_Node* root = get_XML_File(infile); @@ -353,6 +361,152 @@ void importPhase(XML_Node& phase, ThermoPhase* th) th->initThermoXML(phase, id); } +void addElements(ThermoPhase& thermo, const vector& element_names, + const unordered_map& local_elements, + bool allow_default) +{ + for (const auto& symbol : element_names) { + if (local_elements.count(symbol)) { + auto& element = *local_elements.at(symbol); + double weight = element["atomic-weight"].asDouble(); + long int number = element.getInt("atomic-number", 0); + double e298 = element.getDouble("entropy298", ENTROPY298_UNKNOWN); + thermo.addElement(symbol, weight, number, e298); + } else if (allow_default) { + thermo.addElement(symbol); + } else { + throw CanteraError("addElements", "Element '{}' not found", symbol); + } + } +} + +void addSpecies(ThermoPhase& thermo, const AnyValue& names, const AnyValue& species) +{ + if (names.is>()) { + // 'names' is a list of species names which should be found in 'species' + const auto& species_nodes = species.asMap("name"); + for (const auto& name : names.asVector()) { + thermo.addSpecies(newSpecies(*species_nodes.at(name))); + } + } else if (names.is() && names.asString() == "all") { + // The keyword 'all' means to add all species from this source + for (const auto& item : species.asVector()) { + thermo.addSpecies(newSpecies(item)); + } + } else { + throw CanteraError("addSpecies", + "Could not parse species declaration of type '{}'", names.type_str()); + } +} + +void setupPhase(ThermoPhase& thermo, const AnyMap& phaseNode, + const AnyMap& rootNode) +{ + thermo.setName(phaseNode["name"].asString()); + + // Add elements + if (phaseNode.hasKey("elements")) { + if (phaseNode.getBool("skip-undeclared-elements", false)) { + thermo.ignoreUndefinedElements(); + } else { + thermo.throwUndefinedElements(); + } + + if (phaseNode["elements"].is>()) { + // 'elements' is a list of element symbols + if (rootNode.hasKey("elements")) { + addElements(thermo, phaseNode["elements"].asVector(), + rootNode["elements"].asMap("symbol"), true); + } else { + addElements(thermo, phaseNode["elements"].asVector(), + {}, true); + } + } else if (phaseNode["elements"].is>()) { + // Each item in 'elements' is a map with one item, where the key is + // a section in this file or another YAML file, and the value is a + // list of element symbols to read from that section + for (const auto& elemNode : phaseNode["elements"].asVector()) { + const string& source = elemNode.begin()->first; + const auto& names = elemNode.begin()->second.asVector(); + const auto& slash = boost::ifind_first(source, "/"); + if (slash) { + std::string fileName(source.begin(), slash.begin()); + std::string node(slash.end(), source.end()); + const AnyMap elements = AnyMap::fromYamlFile(fileName); + addElements(thermo, names, + elements[node].asMap("symbol"), false); + } else if (rootNode.hasKey(source)) { + addElements(thermo, names, + rootNode[source].asMap("symbol"), false); + } else if (source == "default") { + addElements(thermo, names, {}, true); + } else { + throw CanteraError("setupPhase", + "Could not find elements section named '{}'", source); + } + } + } else { + throw CanteraError("setupPhase", + "Could not parse elements declaration of type '{}'", + phaseNode["elements"].type_str()); + } + } else { + // If no elements list is provided, just add elements as-needed from the + // default list. + thermo.addUndefinedElements(); + } + + // Add species + if (phaseNode.hasKey("species")) { + if (phaseNode["species"].is>()) { + // 'species' is a list of species names to be added from the current + // file's 'species' section + addSpecies(thermo, phaseNode["species"], rootNode["species"]); + } else if (phaseNode["species"].is()) { + // 'species' is a keyword applicable to the current file's 'species' + // section + addSpecies(thermo, phaseNode["species"], rootNode["species"]); + } else if (phaseNode["species"].is>()) { + // Each item in 'species' is a map with one item, where the key is + // a section in this file or another YAML file, and the value is a + // list of species names to read from that section + for (const auto& speciesNode : phaseNode["species"].asVector()) { + const string& source = speciesNode.begin()->first; + const auto& names = speciesNode.begin()->second; + const auto& slash = boost::ifind_first(source, "/"); + if (slash) { + // source is a different input file + std::string fileName(source.begin(), slash.begin()); + std::string node(slash.end(), source.end()); + AnyMap species = AnyMap::fromYamlFile(fileName); + addSpecies(thermo, names, species[node]); + } else if (rootNode.hasKey(source)) { + // source is in the current file + addSpecies(thermo, names, rootNode[source]); + } else { + throw CanteraError("setupPhase", + "Could not find species section named '{}'", source); + } + } + } else { + throw CanteraError("setupPhase", + "Could not parse species declaration of type '{}'", + phaseNode["species"].type_str()); + } + } else { + // By default, add all species from the 'species' section + addSpecies(thermo, AnyValue("all"), rootNode["species"]); + } + + thermo.initThermo(); + + if (phaseNode.hasKey("state")) { + thermo.setState(phaseNode["state"].as()); + } else { + thermo.setState_TP(298.15, OneAtm); + } +} + void installElements(Phase& th, const XML_Node& phaseNode) { // get the declared element names diff --git a/test/data/ideal-gas.yaml b/test/data/ideal-gas.yaml new file mode 100644 index 000000000..9b8aeb91c --- /dev/null +++ b/test/data/ideal-gas.yaml @@ -0,0 +1,96 @@ +units: {length: cm, time: s, quantity: mol, molar-energy: cal/mol} + +phases: +- name: simple + thermo: ideal-gas + elements: [N, O] + species: [O2, NO, N2] + state: {T: 500.0, P: 10 atm, X: {O2: 0.21, N2: 0.79}} + +- name: duplicate-species + thermo: ideal-gas + elements: [N, O] + species: [O2, NO, N2, NO] + state: {T: 300.0, P: 1 atm, X: {O2: 0.21, N2: 0.79}} + +- name: element-override + thermo: ideal-gas + elements: [N, O, Ar] + species: [O2, NO, N2, AR] + note: replace Argon with custom element + state: {T: 900.0, P: 5 atm, Y: {O2: 0.4, N2: 0.4, AR: 0.2}} + +- name: element-remote + thermo: ideal-gas + elements: + - default: [N, O] + - species-elements.yaml/isotopes: [Ar] + species: [O2, NO, N2, AR] + note: replace Argon with custom element from a different file + state: {T: 900.0, P: 5 atm, Y: {O2: 0.4, N2: 0.4, AR: 0.2}} + +- name: species-remote + thermo: ideal-gas + species: + - species: [O2, N2] + - species-elements.yaml/species: [NO2, N2O] + state: {T: 300.0, P: 1 atm, X: "N2O:0.3, O2:0.7"} + +- name: species-all + thermo: ideal-gas + state: {T: 300.0, P: 1 atm, X: {AR: 0.2, O2: 0.7, N2: 0.1}} + + +elements: +- symbol: Ar + atomic-weight: 36 + + +species: +- name: O2 + composition: {O: 2} + thermo: + model: NASA7 + temperature-ranges: [200.00, 1000.00, 3500.00] + data: + - [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] + note: "TPIS89" + +- name: NO + composition: {N: 1, O: 1} + thermo: + model: NASA7 + temperature-ranges: [200.00, 1000.00, 6000.00] + data: + - [4.218476300E+00, -4.638976000E-03, 1.104102200E-05, -9.336135400E-09, + 2.803577000E-12, 9.844623000E+03, 2.280846400E+00] + - [3.260605600E+00, 1.191104300E-03, -4.291704800E-07, 6.945766900E-11, + -4.033609900E-15, 9.920974600E+03, 6.369302700E+00] + note: "RUS 78" + +- name: N2 + composition: {N: 2} + thermo: + model: NASA7 + temperature-ranges: [300.00, 1000.00, 5000.00] + data: + - [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] + note: "121286" + +- name: AR + composition: {Ar: 1} + thermo: + model: NASA7 + temperature-ranges: [300.00, 1000.00, 5000.00] + data: + - [2.500000000E+00, 0.000000000E+00, 0.000000000E+00, 0.000000000E+00, + 0.000000000E+00, -7.453750000E+02, 4.366000000E+00] + - [2.500000000E+00, 0.000000000E+00, 0.000000000E+00, 0.000000000E+00, + 0.000000000E+00, -7.453750000E+02, 4.366000000E+00] + note: "120186" diff --git a/test/data/species-elements.yaml b/test/data/species-elements.yaml new file mode 100644 index 000000000..3382b93f3 --- /dev/null +++ b/test/data/species-elements.yaml @@ -0,0 +1,32 @@ +units: {length: cm, time: s, quantity: mol, molar-energy: cal/mol} + +isotopes: +- symbol: Ar + atomic-weight: 38 +- symbol: O18 + atomic-weight: 18 + +species: +- name: NO2 + composition: {N: 1, O: 2} + thermo: + model: NASA7 + temperature-ranges: [200.00, 1000.00, 6000.00] + data: + - [3.944031200E+00, -1.585429000E-03, 1.665781200E-05, -2.047542600E-08, + 7.835056400E-12, 2.896617900E+03, 6.311991700E+00] + - [4.884754200E+00, 2.172395600E-03, -8.280690600E-07, 1.574751000E-10, + -1.051089500E-14, 2.316498300E+03, -1.174169500E-01] + note: "L 7/88" + +- name: N2O + composition: {N: 2, O: 1} + thermo: + model: NASA7 + temperature-ranges: [200.00, 1000.00, 6000.00] + data: + - [2.257150200E+00, 1.130472800E-02, -1.367131900E-05, 9.681980600E-09, + -2.930718200E-12, 8.741774400E+03, 1.075799200E+01] + - [4.823072900E+00, 2.627025100E-03, -9.585087400E-07, 1.600071200E-10, + -9.775230300E-15, 8.073404800E+03, -2.201720700E+00] + note: "L 7/88" diff --git a/test/thermo/thermoFromYaml.cpp b/test/thermo/thermoFromYaml.cpp new file mode 100644 index 000000000..23c65d613 --- /dev/null +++ b/test/thermo/thermoFromYaml.cpp @@ -0,0 +1,69 @@ +#include "gtest/gtest.h" +#include "cantera/thermo/ThermoFactory.h" +#include "cantera/thermo/Elements.h" + +using namespace Cantera; + +TEST(ThermoFromYaml, simpleIdealGas) +{ + AnyMap infile = AnyMap::fromYamlFile("ideal-gas.yaml"); + auto phaseNodes = infile["phases"].asMap("name"); + auto thermo = newPhase(*phaseNodes.at("simple"), infile); + EXPECT_EQ(thermo->nSpecies(), (size_t) 3); + EXPECT_DOUBLE_EQ(thermo->density(), 7.031763356741983); + EXPECT_DOUBLE_EQ(thermo->cp_mass(), 1037.7632754708304); +} + +TEST(ThermoFromYaml, failDuplicateSpecies) +{ + AnyMap infile = AnyMap::fromYamlFile("ideal-gas.yaml"); + auto phaseNodes = infile["phases"].asMap("name"); + EXPECT_THROW(newPhase(*phaseNodes.at("duplicate-species"), infile), + CanteraError); +} + +TEST(ThermoFromYaml, elementOverride) +{ + AnyMap infile = AnyMap::fromYamlFile("ideal-gas.yaml"); + auto phaseNodes = infile["phases"].asMap("name"); + auto thermo = newPhase(*phaseNodes.at("element-override"), infile); + EXPECT_EQ(thermo->nElements(), (size_t) 3); + EXPECT_DOUBLE_EQ(thermo->atomicWeight(0), getElementWeight("N")); + EXPECT_DOUBLE_EQ(thermo->atomicWeight(1), getElementWeight("O")); + EXPECT_DOUBLE_EQ(thermo->atomicWeight(2), 36); +} + +TEST(ThermoFromYaml, elementFromDifferentFile) +{ + AnyMap infile = AnyMap::fromYamlFile("ideal-gas.yaml"); + auto phaseNodes = infile["phases"].asMap("name"); + auto thermo = newPhase(*phaseNodes.at("element-remote"), infile); + EXPECT_EQ(thermo->nElements(), (size_t) 3); + EXPECT_DOUBLE_EQ(thermo->atomicWeight(0), getElementWeight("N")); + EXPECT_DOUBLE_EQ(thermo->atomicWeight(1), getElementWeight("O")); + EXPECT_DOUBLE_EQ(thermo->atomicWeight(2), 38); +} + +TEST(ThermoFromYaml, speciesFromDifferentFile) +{ + AnyMap infile = AnyMap::fromYamlFile("ideal-gas.yaml"); + auto phaseNodes = infile["phases"].asMap("name"); + auto thermo = newPhase(*phaseNodes.at("species-remote"), infile); + EXPECT_EQ(thermo->nElements(), (size_t) 2); + EXPECT_EQ(thermo->nSpecies(), (size_t) 4); + EXPECT_EQ(thermo->species(0)->composition["O"], 2); + EXPECT_EQ(thermo->species(3)->composition["O"], 1); + EXPECT_EQ(thermo->species(2)->name, "NO2"); + EXPECT_DOUBLE_EQ(thermo->moleFraction(3), 0.3); +} + +TEST(ThermoFromYaml, speciesAll) +{ + AnyMap infile = AnyMap::fromYamlFile("ideal-gas.yaml"); + auto phaseNodes = infile["phases"].asMap("name"); + auto thermo = newPhase(*phaseNodes.at("species-all"), infile); + EXPECT_EQ(thermo->nElements(), (size_t) 3); + EXPECT_EQ(thermo->nSpecies(), (size_t) 4); + EXPECT_EQ(thermo->species(1)->name, "NO"); + EXPECT_EQ(thermo->species(2)->name, "N2"); +}