Create a complete ThermoPhase object from a YAML input file

This commit is contained in:
Ray Speth 2018-12-21 23:23:57 -05:00
parent c8b737035d
commit f4d45f8a85
8 changed files with 386 additions and 2 deletions

View file

@ -59,12 +59,14 @@ public:
template<class T>
T& as();
const std::type_info& type();
const std::type_info& type() const;
std::string type_str() const;
template<class T>
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;

View file

@ -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<ThermoPhase> 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);

View file

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

View file

@ -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;

View file

@ -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<ThermoPhase> newPhase(const AnyMap& phaseNode, const AnyMap& rootNode)
{
unique_ptr<ThermoPhase> 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<string>& element_names,
const unordered_map<string, const AnyMap*>& 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<vector<string>>()) {
// '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<string>()) {
thermo.addSpecies(newSpecies(*species_nodes.at(name)));
}
} else if (names.is<string>() && names.asString() == "all") {
// The keyword 'all' means to add all species from this source
for (const auto& item : species.asVector<AnyMap>()) {
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<vector<string>>()) {
// 'elements' is a list of element symbols
if (rootNode.hasKey("elements")) {
addElements(thermo, phaseNode["elements"].asVector<string>(),
rootNode["elements"].asMap("symbol"), true);
} else {
addElements(thermo, phaseNode["elements"].asVector<string>(),
{}, true);
}
} else if (phaseNode["elements"].is<vector<AnyMap>>()) {
// 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<AnyMap>()) {
const string& source = elemNode.begin()->first;
const auto& names = elemNode.begin()->second.asVector<string>();
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<vector<string>>()) {
// '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<string>()) {
// 'species' is a keyword applicable to the current file's 'species'
// section
addSpecies(thermo, phaseNode["species"], rootNode["species"]);
} else if (phaseNode["species"].is<vector<AnyMap>>()) {
// 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<AnyMap>()) {
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<AnyMap>());
} else {
thermo.setState_TP(298.15, OneAtm);
}
}
void installElements(Phase& th, const XML_Node& phaseNode)
{
// get the declared element names

96
test/data/ideal-gas.yaml Normal file
View file

@ -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"

View file

@ -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"

View file

@ -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");
}