From a20e9aee3a4d2de7c56371893dc7750238935e0d Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 2 Nov 2019 23:17:26 -0400 Subject: [PATCH 01/57] [Input] Add a basic YAML emitter for AnyMap / AnyValue --- include/cantera/base/AnyMap.h | 2 + src/base/AnyMap.cpp | 73 +++++++++++++++++++++++++++++++- test/general/test_containers.cpp | 12 ++++++ 3 files changed, 85 insertions(+), 2 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 8dcdde0d0d..dfef64e65e 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -369,6 +369,8 @@ class AnyMap : public AnyBase //! Create an AnyMap from a string containing a YAML document static AnyMap fromYamlString(const std::string& yaml); + std::string toYamlString() const; + //! Get the value of the item stored in `key`. AnyValue& operator[](const std::string& key); const AnyValue& operator[](const std::string& key) const; diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 241528c302..360b24ae6c 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -22,6 +22,7 @@ namespace ba = boost::algorithm; namespace { // helper functions std::mutex yaml_cache_mutex; +using std::vector; bool isFloat(const std::string& val) { @@ -149,8 +150,13 @@ using namespace Cantera; template<> struct convert { static Node encode(const Cantera::AnyMap& rhs) { - throw NotImplementedError("AnyMap::encode"); + Node node; + for (const auto& item : rhs) { + node[item.first] = item.second; + } + return node; } + static bool decode(const Node& node, Cantera::AnyMap& target) { target.setLoc(node.Mark().line, node.Mark().column); if (node.IsSequence()) { @@ -183,7 +189,60 @@ struct convert { template<> struct convert { static Node encode(const Cantera::AnyValue& rhs) { - throw NotImplementedError("AnyValue::encode"); + Node node; + if (rhs.isScalar()) { + if (rhs.is()) { + node = rhs.asString(); + } else if (rhs.is()) { + node = rhs.asDouble(); + } else if (rhs.is()) { + node = rhs.asInt(); + } else if (rhs.is()) { + node = rhs.asBool(); + } else { + throw CanteraError("AnyValue::encode", "Don't know how to " + "encode value of type '{}'", rhs.type_str()); + } + } else if (rhs.is()) { + node = rhs.as(); + } else if (rhs.is>()) { + node = rhs.asVector(); + } else if (rhs.is>()) { + node = rhs.asVector(); + node.SetStyle(YAML::EmitterStyle::Flow); + } else if (rhs.is>()) { + node = rhs.asVector(); + node.SetStyle(YAML::EmitterStyle::Flow); + } else if (rhs.is>()) { + node = rhs.asVector(); + node.SetStyle(YAML::EmitterStyle::Flow); + } else if (rhs.is>()) { + node = rhs.asVector(); + node.SetStyle(YAML::EmitterStyle::Flow); + } else if (rhs.is>()) { + node = rhs.asVector(); + } else if (rhs.is>>()) { + node = rhs.asVector>(); + for (size_t i = 0; i < node.size(); i++) { + node[i].SetStyle(YAML::EmitterStyle::Flow); + } + } else if (rhs.is>>()) { + node = rhs.asVector>(); + for (size_t i = 0; i < node.size(); i++) { + node[i].SetStyle(YAML::EmitterStyle::Flow); + } + } else if (rhs.is>>()) { + node = rhs.asVector>(); + for (size_t i = 0; i < node.size(); i++) { + node[i].SetStyle(YAML::EmitterStyle::Flow); + } + } else if (rhs.is>>()) { + node = rhs.asVector>(); + } else { + throw CanteraError("AnyValue::encode", + "Don't know how to encode value of type '{}'", rhs.type_str()); + } + return node; } static bool decode(const Node& node, Cantera::AnyValue& target) { @@ -1213,6 +1272,16 @@ AnyMap AnyMap::fromYamlFile(const std::string& name, return cache_item.first; } +std::string AnyMap::toYamlString() const +{ + YAML::Emitter out; + YAML::Node node; + node = *this; + out << node; + out << YAML::Newline; + return out.c_str(); +} + AnyMap::Iterator begin(const AnyValue& v) { return v.as().begin(); } diff --git a/test/general/test_containers.cpp b/test/general/test_containers.cpp index 24695b026c..946a24658c 100644 --- a/test/general/test_containers.cpp +++ b/test/general/test_containers.cpp @@ -372,3 +372,15 @@ TEST(AnyMap, missingKeyAt) EXPECT_THAT(ex.what(), ::testing::HasSubstr("Key 'spam' not found")); } } + +TEST(AnyMap, dumpYamlString) +{ + AnyMap original = AnyMap::fromYamlFile("h2o2.yaml"); + std::string serialized = original.toYamlString(); + AnyMap generated = AnyMap::fromYamlString(serialized); + for (const auto& item : original) { + EXPECT_TRUE(generated.hasKey(item.first)); + } + EXPECT_EQ(original["species"].getMapWhere("name", "OH")["thermo"]["data"].asVector(), + generated["species"].getMapWhere("name", "OH")["thermo"]["data"].asVector()); +} From a74e763f31f12f4a1c198125b7c6ea8757e67290 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 11 Nov 2019 20:39:17 -0500 Subject: [PATCH 02/57] [Input] Implement serialization of species thermo objects --- include/cantera/thermo/ConstCpPoly.h | 2 + include/cantera/thermo/Mu0Poly.h | 2 + include/cantera/thermo/Nasa9Poly1.h | 2 + .../cantera/thermo/Nasa9PolyMultiTempRegion.h | 2 + include/cantera/thermo/NasaPoly1.h | 7 ++ include/cantera/thermo/NasaPoly2.h | 2 + include/cantera/thermo/ShomatePoly.h | 21 +++++ .../cantera/thermo/SpeciesThermoInterpType.h | 6 ++ src/thermo/ConstCpPoly.cpp | 11 +++ src/thermo/Mu0Poly.cpp | 13 +++ src/thermo/Nasa9Poly1.cpp | 8 ++ src/thermo/Nasa9PolyMultiTempRegion.cpp | 14 +++ src/thermo/NasaPoly2.cpp | 12 +++ src/thermo/SpeciesThermoInterpType.cpp | 7 ++ test/thermo/thermoParameterizations.cpp | 91 +++++++++++++++++++ 15 files changed, 200 insertions(+) diff --git a/include/cantera/thermo/ConstCpPoly.h b/include/cantera/thermo/ConstCpPoly.h index 4ba4b353ba..df24d4a949 100644 --- a/include/cantera/thermo/ConstCpPoly.h +++ b/include/cantera/thermo/ConstCpPoly.h @@ -95,6 +95,8 @@ class ConstCpPoly: public SpeciesThermoInterpType doublereal& pref, doublereal* const coeffs) const; + virtual void getParameters(AnyMap& thermo) const; + virtual doublereal reportHf298(doublereal* const h298 = 0) const; virtual void modifyOneHf298(const size_t k, const doublereal Hf298New); virtual void resetHf298(); diff --git a/include/cantera/thermo/Mu0Poly.h b/include/cantera/thermo/Mu0Poly.h index 42fbf65194..057a75c79c 100644 --- a/include/cantera/thermo/Mu0Poly.h +++ b/include/cantera/thermo/Mu0Poly.h @@ -136,6 +136,8 @@ class Mu0Poly: public SpeciesThermoInterpType doublereal& pref, doublereal* const coeffs) const; + virtual void getParameters(AnyMap& thermo) const; + protected: //! Number of intervals in the interpolating linear approximation. Number //! of points is one more than the number of intervals. diff --git a/include/cantera/thermo/Nasa9Poly1.h b/include/cantera/thermo/Nasa9Poly1.h index 99c0d9cf94..4012b3e6d8 100644 --- a/include/cantera/thermo/Nasa9Poly1.h +++ b/include/cantera/thermo/Nasa9Poly1.h @@ -124,6 +124,8 @@ class Nasa9Poly1 : public SpeciesThermoInterpType doublereal& pref, doublereal* const coeffs) const; + virtual void getParameters(AnyMap& thermo) const; + protected: //! array of polynomial coefficients vector_fp m_coeff; diff --git a/include/cantera/thermo/Nasa9PolyMultiTempRegion.h b/include/cantera/thermo/Nasa9PolyMultiTempRegion.h index 2bd3f36c46..e8afee4956 100644 --- a/include/cantera/thermo/Nasa9PolyMultiTempRegion.h +++ b/include/cantera/thermo/Nasa9PolyMultiTempRegion.h @@ -118,6 +118,8 @@ class Nasa9PolyMultiTempRegion : public SpeciesThermoInterpType doublereal& pref, doublereal* const coeffs) const; + virtual void getParameters(AnyMap& thermo) const; + protected: //! Lower boundaries of each temperature regions vector_fp m_lowerTempBounds; diff --git a/include/cantera/thermo/NasaPoly1.h b/include/cantera/thermo/NasaPoly1.h index 75ed200047..c0d892cf08 100644 --- a/include/cantera/thermo/NasaPoly1.h +++ b/include/cantera/thermo/NasaPoly1.h @@ -16,6 +16,7 @@ #include "SpeciesThermoInterpType.h" #include "cantera/thermo/speciesThermoTypes.h" +#include "cantera/base/AnyMap.h" namespace Cantera { @@ -140,6 +141,12 @@ class NasaPoly1 : public SpeciesThermoInterpType std::copy(m_coeff.begin(), m_coeff.end(), coeffs); } + virtual void getParameters(AnyMap& thermo) const { + // NasaPoly1 is only used as an embedded model within NasaPoly2, so all + // that needs to be added here are the polynomial coefficients + thermo["data"].asVector().push_back(m_coeff); + } + virtual doublereal reportHf298(doublereal* const h298 = 0) const { double tt[6]; double temp = 298.15; diff --git a/include/cantera/thermo/NasaPoly2.h b/include/cantera/thermo/NasaPoly2.h index d8dbf5bf89..25eb6c64f9 100644 --- a/include/cantera/thermo/NasaPoly2.h +++ b/include/cantera/thermo/NasaPoly2.h @@ -134,6 +134,8 @@ class NasaPoly2 : public SpeciesThermoInterpType type = NASA2; } + virtual void getParameters(AnyMap& thermo) const; + doublereal reportHf298(doublereal* const h298 = 0) const { double h; if (298.15 <= m_midT) { diff --git a/include/cantera/thermo/ShomatePoly.h b/include/cantera/thermo/ShomatePoly.h index c904118fc7..2c3e36209f 100644 --- a/include/cantera/thermo/ShomatePoly.h +++ b/include/cantera/thermo/ShomatePoly.h @@ -15,6 +15,7 @@ #define CT_SHOMATEPOLY1_H #include "cantera/thermo/SpeciesThermoInterpType.h" +#include "cantera/base/AnyMap.h" namespace Cantera { @@ -159,6 +160,16 @@ class ShomatePoly : public SpeciesThermoInterpType } } + virtual void getParameters(AnyMap& thermo) const { + // ShomatePoly is only used as an embedded model within ShomatePoly2, so + // all that needs to be added here are the polynomial coefficients + vector_fp dimensioned_coeffs(m_coeff.size()); + for (size_t i = 0; i < m_coeff.size(); i++) { + dimensioned_coeffs[i] = m_coeff[i] * GasConstant / 1000; + } + thermo["data"].asVector().push_back(dimensioned_coeffs); + } + virtual doublereal reportHf298(doublereal* const h298 = 0) const { double cp_R, h_RT, s_R; updatePropertiesTemp(298.15, &cp_R, &h_RT, &s_R); @@ -317,6 +328,16 @@ class ShomatePoly2 : public SpeciesThermoInterpType type = SHOMATE2; } + virtual void getParameters(AnyMap& thermo) const { + SpeciesThermoInterpType::getParameters(thermo); + thermo["model"] = "Shomate"; + vector_fp Tranges {m_lowT, m_midT, m_highT}; + thermo["temperature-ranges"] = Tranges; + thermo["data"] = std::vector(); + msp_low.getParameters(thermo); + msp_high.getParameters(thermo); + } + virtual doublereal reportHf298(doublereal* const h298 = 0) const { doublereal h; if (298.15 <= m_midT) { diff --git a/include/cantera/thermo/SpeciesThermoInterpType.h b/include/cantera/thermo/SpeciesThermoInterpType.h index 1230370124..a360a01b4a 100644 --- a/include/cantera/thermo/SpeciesThermoInterpType.h +++ b/include/cantera/thermo/SpeciesThermoInterpType.h @@ -19,6 +19,7 @@ namespace Cantera { class PDSS; +class AnyMap; /** * @defgroup spthermo Species Reference-State Thermodynamic Properties @@ -226,6 +227,11 @@ class SpeciesThermoInterpType doublereal& refPressure, doublereal* const coeffs) const; + //! Store the parameters of the species thermo object such that an identical + //! species thermo object could be reconstructed using the + //! newSpeciesThermo() function. + virtual void getParameters(AnyMap& thermo) const; + //! Report the 298 K Heat of Formation of the standard state of one species //! (J kmol-1) /*! diff --git a/src/thermo/ConstCpPoly.cpp b/src/thermo/ConstCpPoly.cpp index d398de59cb..f0b7497e50 100644 --- a/src/thermo/ConstCpPoly.cpp +++ b/src/thermo/ConstCpPoly.cpp @@ -9,6 +9,7 @@ // at https://cantera.org/license.txt for license and copyright information. #include "cantera/thermo/ConstCpPoly.h" +#include "cantera/base/AnyMap.h" namespace Cantera { @@ -81,6 +82,16 @@ void ConstCpPoly::reportParameters(size_t& n, int& type, coeffs[3] = m_cp0_R * GasConstant; } +void ConstCpPoly::getParameters(AnyMap& thermo) const +{ + SpeciesThermoInterpType::getParameters(thermo); + thermo["model"] = "constant-cp"; + thermo["T0"] = m_t0; + thermo["h0"] = m_h0_R * GasConstant; + thermo["s0"] = m_s0_R * GasConstant; + thermo["cp0"] = m_cp0_R * GasConstant; +} + doublereal ConstCpPoly::reportHf298(doublereal* const h298) const { double temp = 298.15; diff --git a/src/thermo/Mu0Poly.cpp b/src/thermo/Mu0Poly.cpp index 6f3b97bacb..d791b76885 100644 --- a/src/thermo/Mu0Poly.cpp +++ b/src/thermo/Mu0Poly.cpp @@ -12,6 +12,7 @@ #include "cantera/thermo/Mu0Poly.h" #include "cantera/base/ctml.h" #include "cantera/base/stringUtils.h" +#include "cantera/base/AnyMap.h" using namespace std; @@ -156,6 +157,18 @@ void Mu0Poly::reportParameters(size_t& n, int& type, } } +void Mu0Poly::getParameters(AnyMap& thermo) const +{ + SpeciesThermoInterpType::getParameters(thermo); + thermo["model"] = "piecewise-Gibbs"; + thermo["h0"] = m_H298 * GasConstant; + map data; + for (size_t i = 0; i < m_numIntervals+1; i++) { + data[fmt::format("{}", m_t0_int[i])] = m_mu0_R_int[i] * GasConstant; + } + thermo["data"] = data; +} + Mu0Poly* newMu0ThermoFromXML(const XML_Node& Mu0Node) { bool dimensionlessMu0Values = false; diff --git a/src/thermo/Nasa9Poly1.cpp b/src/thermo/Nasa9Poly1.cpp index e89df8e332..01c9d9090b 100644 --- a/src/thermo/Nasa9Poly1.cpp +++ b/src/thermo/Nasa9Poly1.cpp @@ -13,6 +13,7 @@ // at https://cantera.org/license.txt for license and copyright information. #include "cantera/thermo/Nasa9Poly1.h" +#include "cantera/base/AnyMap.h" namespace Cantera { @@ -106,4 +107,11 @@ void Nasa9Poly1::reportParameters(size_t& n, int& type, } } +void Nasa9Poly1::getParameters(AnyMap& thermo) const { + // Nasa9Poly1 is only used as an embedded model within + // Nasa9PolyMultiTempRegion, so all that needs to be added here are the + // polynomial coefficients + thermo["data"].asVector().push_back(m_coeff); +} + } diff --git a/src/thermo/Nasa9PolyMultiTempRegion.cpp b/src/thermo/Nasa9PolyMultiTempRegion.cpp index 9f46954ebb..9969febc5b 100644 --- a/src/thermo/Nasa9PolyMultiTempRegion.cpp +++ b/src/thermo/Nasa9PolyMultiTempRegion.cpp @@ -17,6 +17,7 @@ #include "cantera/base/ctexceptions.h" #include "cantera/thermo/Nasa9PolyMultiTempRegion.h" #include "cantera/thermo/speciesThermoTypes.h" +#include "cantera/base/AnyMap.h" using namespace std; @@ -188,4 +189,17 @@ void Nasa9PolyMultiTempRegion::reportParameters(size_t& n, int& type, } } +void Nasa9PolyMultiTempRegion::getParameters(AnyMap& thermo) const +{ + SpeciesThermoInterpType::getParameters(thermo); + thermo["model"] = "NASA9"; + auto T_ranges = m_lowerTempBounds; + T_ranges.push_back(m_highT); + thermo["temperature-ranges"] = T_ranges; + thermo["data"] = std::vector(); + for (const auto& region : m_regionPts) { + region->getParameters(thermo); + } +} + } diff --git a/src/thermo/NasaPoly2.cpp b/src/thermo/NasaPoly2.cpp index a3312c89be..64223e7b38 100644 --- a/src/thermo/NasaPoly2.cpp +++ b/src/thermo/NasaPoly2.cpp @@ -4,6 +4,7 @@ #include "cantera/thermo/NasaPoly2.h" #include "cantera/base/global.h" #include "cantera/base/stringUtils.h" +#include "cantera/base/AnyMap.h" namespace Cantera { @@ -21,6 +22,17 @@ void NasaPoly2::setParameters(double Tmid, const vector_fp& low, mnp_high.setParameters(high); } +void NasaPoly2::getParameters(AnyMap& thermo) const +{ + SpeciesThermoInterpType::getParameters(thermo); + thermo["model"] = "NASA7"; + vector_fp Tranges {m_lowT, m_midT, m_highT}; + thermo["temperature-ranges"] = Tranges; + thermo["data"] = std::vector(); + mnp_low.getParameters(thermo); + mnp_high.getParameters(thermo); +} + void NasaPoly2::validate(const std::string& name) { if (thermo_warnings_suppressed()) { diff --git a/src/thermo/SpeciesThermoInterpType.cpp b/src/thermo/SpeciesThermoInterpType.cpp index 7ef37423d6..0135805f11 100644 --- a/src/thermo/SpeciesThermoInterpType.cpp +++ b/src/thermo/SpeciesThermoInterpType.cpp @@ -52,6 +52,13 @@ void SpeciesThermoInterpType::reportParameters(size_t& index, int& type, throw NotImplementedError("SpeciesThermoInterpType::reportParameters"); } +void SpeciesThermoInterpType::getParameters(AnyMap& thermo) const +{ + if (m_Pref != OneAtm) { + thermo["reference-pressure"] = m_Pref; + } +} + doublereal SpeciesThermoInterpType::reportHf298(doublereal* const h298) const { throw NotImplementedError("SpeciesThermoInterpType::reportHf298"); diff --git a/test/thermo/thermoParameterizations.cpp b/test/thermo/thermoParameterizations.cpp index 5049ad3128..848002b7c2 100644 --- a/test/thermo/thermoParameterizations.cpp +++ b/test/thermo/thermoParameterizations.cpp @@ -8,6 +8,7 @@ #include "cantera/thermo/ShomatePoly.h" #include "cantera/thermo/PDSS_HKFT.h" #include "cantera/base/stringUtils.h" +#include "cantera/base/Solution.h" #include "thermo_data.h" #include @@ -240,3 +241,93 @@ TEST(SpeciesThermo, Mu0PolyFromYaml) { EXPECT_DOUBLE_EQ(st->minTemp(), 273.15); EXPECT_DOUBLE_EQ(st->maxTemp(), 350); } + +TEST(SpeciesThermo, NasaPoly2ToYaml) { + shared_ptr soln = newSolution("../data/simplephases.cti", "nasa1"); + auto original = soln->thermo()->species("H2O")->thermo; + AnyMap h2o_data1; + original->getParameters(h2o_data1); + AnyMap h2o_data2 = AnyMap::fromYamlString(h2o_data1.toYamlString()); + auto duplicate = newSpeciesThermo(h2o_data2); + double cp1, cp2, h1, h2, s1, s2; + for (double T : {500, 900, 1300, 2100}) { + original->updatePropertiesTemp(T, &cp1, &h1, &s1); + duplicate->updatePropertiesTemp(T, &cp2, &h2, &s2); + EXPECT_DOUBLE_EQ(cp1, cp2); + EXPECT_DOUBLE_EQ(h1, h2); + EXPECT_DOUBLE_EQ(s1, s2); + } + EXPECT_EQ(original->refPressure(), duplicate->refPressure()); +} + +TEST(SpeciesThermo, ShomatePolyToYaml) { + shared_ptr soln = newSolution("../data/simplephases.cti", "shomate1"); + auto original = soln->thermo()->species("CO2")->thermo; + AnyMap co2_data1; + original->getParameters(co2_data1); + AnyMap co2_data2 = AnyMap::fromYamlString(co2_data1.toYamlString()); + auto duplicate = newSpeciesThermo(co2_data2); + double cp1, cp2, h1, h2, s1, s2; + for (double T : {500, 900, 1300, 2100}) { + original->updatePropertiesTemp(T, &cp1, &h1, &s1); + duplicate->updatePropertiesTemp(T, &cp2, &h2, &s2); + EXPECT_DOUBLE_EQ(cp1, cp2); + EXPECT_DOUBLE_EQ(h1, h2); + EXPECT_DOUBLE_EQ(s1, s2); + } + EXPECT_EQ(original->refPressure(), duplicate->refPressure()); +} + +TEST(SpeciesThermo, ConstCpToYaml) { + shared_ptr soln = newSolution("../data/simplephases.cti", "simple1"); + auto original = soln->thermo()->species("H2O")->thermo; + AnyMap h2o_data1; + original->getParameters(h2o_data1); + AnyMap h2o_data2 = AnyMap::fromYamlString(h2o_data1.toYamlString()); + auto duplicate = newSpeciesThermo(h2o_data2); + double cp1, cp2, h1, h2, s1, s2; + for (double T : {300, 500, 900}) { + original->updatePropertiesTemp(T, &cp1, &h1, &s1); + duplicate->updatePropertiesTemp(T, &cp2, &h2, &s2); + EXPECT_DOUBLE_EQ(cp1, cp2); + EXPECT_DOUBLE_EQ(h1, h2); + EXPECT_DOUBLE_EQ(s1, s2); + } + EXPECT_EQ(original->refPressure(), duplicate->refPressure()); +} + +TEST(SpeciesThermo, PiecewiseGibbsToYaml) { + shared_ptr soln = newSolution("../data/thermo-models.yaml", + "debye-huckel-beta_ij"); + auto original = soln->thermo()->species("OH-")->thermo; + AnyMap oh_data; + original->getParameters(oh_data); + auto duplicate = newSpeciesThermo(oh_data); + double cp1, cp2, h1, h2, s1, s2; + for (double T : {274, 300, 330, 340}) { + original->updatePropertiesTemp(T, &cp1, &h1, &s1); + duplicate->updatePropertiesTemp(T, &cp2, &h2, &s2); + EXPECT_DOUBLE_EQ(cp1, cp2); + EXPECT_DOUBLE_EQ(h1, h2); + EXPECT_DOUBLE_EQ(s1, s2); + } + EXPECT_EQ(original->refPressure(), duplicate->refPressure()); +} + +TEST(SpeciesThermo, Nasa9PolyToYaml) { + shared_ptr soln = newSolution("airNASA9.cti"); + auto original = soln->thermo()->species("N2+")->thermo; + AnyMap n2p_data1; + original->getParameters(n2p_data1); + AnyMap n2p_data2 = AnyMap::fromYamlString(n2p_data1.toYamlString()); + auto duplicate = newSpeciesThermo(n2p_data2); + double cp1, cp2, h1, h2, s1, s2; + for (double T : {300, 900, 1500, 7000}) { + original->updatePropertiesTemp(T, &cp1, &h1, &s1); + duplicate->updatePropertiesTemp(T, &cp2, &h2, &s2); + EXPECT_DOUBLE_EQ(cp1, cp2); + EXPECT_DOUBLE_EQ(h1, h2); + EXPECT_DOUBLE_EQ(s1, s2); + } + EXPECT_EQ(original->refPressure(), duplicate->refPressure()); +} From d59142fa848c7c797641a1ebe31504114891079c Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 11 Nov 2019 22:18:14 -0500 Subject: [PATCH 03/57] [Input] Make simple ThermoPhase objects serializable --- include/cantera/thermo/ThermoPhase.h | 4 ++++ src/thermo/ThermoPhase.cpp | 31 ++++++++++++++++++++++++++++ test/thermo/thermoToYaml.cpp | 23 +++++++++++++++++++++ 3 files changed, 58 insertions(+) create mode 100644 test/thermo/thermoToYaml.cpp diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index 639c5ed960..4452fee565 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -1709,6 +1709,10 @@ class ThermoPhase : public Phase virtual void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); + //! Store the parameters of a ThermoPhase object such that an identical + //! one could be reconstructed using the newPhase(AnyMap&) function. + virtual void getParameters(AnyMap& phaseNode) const; + //! Access input data associated with the phase description const AnyMap& input() const; AnyMap& input(); diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 614b7ec789..81000e3a30 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1189,6 +1189,37 @@ void ThermoPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) m_input = phaseNode; } +void ThermoPhase::getParameters(AnyMap& phaseNode) const +{ + phaseNode["name"] = name(); + phaseNode["thermo"] = ThermoFactory::factory()->canonicalize(type()); + vector elementNames; + for (size_t i = 0; i < nElements(); i++) { + elementNames.push_back(elementName(i)); + } + phaseNode["elements"] = elementNames; + phaseNode["species"] = speciesNames(); + + // @TODO: Update pending resolution of PR #720 + map state; + state["T"] = temperature(); + state["density"] = density(); + map Y; + for (size_t k = 0; k < m_kk; k++) { + if (massFraction(k) > 0) { + Y[speciesName(k)] = massFraction(k); + } + } + state["Y"] = Y; + phaseNode["state"] = state; + + for (const auto& item : m_input) { + if (!phaseNode.hasKey(item.first)) { + phaseNode[item.first] = item.second; + } + } +} + const AnyMap& ThermoPhase::input() const { return m_input; diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp new file mode 100644 index 0000000000..a3d126443c --- /dev/null +++ b/test/thermo/thermoToYaml.cpp @@ -0,0 +1,23 @@ +#include "gtest/gtest.h" +#include "cantera/thermo/ThermoFactory.h" + +using namespace Cantera; + +shared_ptr newThermo(const std::string& fileName, + const std::string& phaseName) +{ + return shared_ptr(newPhase(fileName, phaseName)); +} + +TEST(ThermoToYaml, simpleIdealGas) +{ + auto thermo = newThermo("ideal-gas.yaml", "simple"); + AnyMap data; + thermo->setState_TP(1010, 2e5); + double rho = thermo->density(); + thermo->getParameters(data); + + ASSERT_EQ(data["thermo"], "ideal-gas"); + ASSERT_EQ(data["state"]["T"], 1010); + ASSERT_EQ(data["state"]["density"], rho); +} From f0cf42e7116b341c3eb149e7996a93838e647590 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 16 Nov 2019 15:33:31 -0500 Subject: [PATCH 04/57] [Input] Make Species objects serializable, including transport data --- include/cantera/thermo/Species.h | 2 ++ include/cantera/transport/TransportData.h | 5 +++ src/thermo/Species.cpp | 35 +++++++++++++++++++ src/transport/TransportData.cpp | 41 +++++++++++++++++++++++ 4 files changed, 83 insertions(+) diff --git a/include/cantera/thermo/Species.h b/include/cantera/thermo/Species.h index a113ba5130..7cba3f46d4 100644 --- a/include/cantera/thermo/Species.h +++ b/include/cantera/thermo/Species.h @@ -35,6 +35,8 @@ class Species Species& operator=(const Species& other) = delete; ~Species(); + void getParameters(AnyMap& speciesNode) const; + //! The name of the species std::string name; diff --git a/include/cantera/transport/TransportData.h b/include/cantera/transport/TransportData.h index a005571dfa..f496a34b4e 100644 --- a/include/cantera/transport/TransportData.h +++ b/include/cantera/transport/TransportData.h @@ -24,6 +24,9 @@ class TransportData virtual void validate(const Species& species) {} + //! Store the parameters needed to reconstruct a TransportData object + virtual void getParameters(AnyMap& transportNode) const; + //! Input data used for specific models AnyMap input; }; @@ -57,6 +60,8 @@ class GasTransportData : public TransportData //! rotational relaxation number. virtual void validate(const Species& species); + virtual void getParameters(AnyMap& transportNode) const; + //! A string specifying the molecular geometry. One of `atom`, `linear`, or //! `nonlinear`. std::string geometry; diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index eb43f51903..958a603ba1 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -33,6 +33,41 @@ Species::~Species() { } +void Species::getParameters(AnyMap& speciesNode) const +{ + speciesNode["name"] = name; + speciesNode["composition"] = composition; + + if (charge != 0) { + speciesNode["charge"] = charge; + } + if (size != 1) { + speciesNode["size"] = size; + } + + AnyMap thermoNode; + thermo->getParameters(thermoNode); + speciesNode["thermo"] = thermoNode; + + if (transport) { + AnyMap transportNode; + transport->getParameters(transportNode); + speciesNode["transport"] = transportNode; + } + + for (const auto& item : input) { + if (!speciesNode.hasKey(item.first)) { + speciesNode[item.first] = item.second; + } + } + + for (const auto& item : extra) { + if (!speciesNode.hasKey(item.first)) { + speciesNode[item.first] = item.second; + } + } +} + shared_ptr newSpecies(const XML_Node& species_node) { std::string name = species_node["name"]; diff --git a/src/transport/TransportData.cpp b/src/transport/TransportData.cpp index a614134e23..295a69e68b 100644 --- a/src/transport/TransportData.cpp +++ b/src/transport/TransportData.cpp @@ -13,6 +13,16 @@ namespace Cantera { + +void TransportData::getParameters(AnyMap &transportNode) const +{ + for (const auto& item : input) { + if (!transportNode.hasKey(item.first)) { + transportNode[item.first] = item.second; + } + } +} + GasTransportData::GasTransportData() : diameter(0.0) , well_depth(0.0) @@ -127,6 +137,37 @@ void GasTransportData::validate(const Species& sp) } } +void GasTransportData::getParameters(AnyMap& transportNode) const +{ + TransportData::getParameters(transportNode); + transportNode["model"] = "gas"; + transportNode["geometry"] = geometry; + transportNode["diameter"] = diameter * 1e10; // convert from m to Angstroms + transportNode["well-depth"] = well_depth / Boltzmann; // convert from J to K + if (dipole != 0) { + // convert from Debye to Coulomb-m + transportNode["dipole"] = dipole * 1e21 * lightSpeed; + } + if (polarizability != 0) { + // convert from m^3 to Angstroms^3 + transportNode["polarizability"] = 1e30 * polarizability; + } + if (rotational_relaxation != 0) { + transportNode["rotational-relaxation"] = rotational_relaxation; + } + if (acentric_factor != 0) { + transportNode["acentric-factor"] = acentric_factor; + } + if (dispersion_coefficient != 0) { + // convert from m^5 to Angstroms^5 + transportNode["dispersion-coefficient"] = dispersion_coefficient * 1e50; + } + if (quadrupole_polarizability) { + // convert from m^5 to Angstroms^5 + transportNode["quadrupole-polarizability"] = quadrupole_polarizability * 1e50; + } +} + void setupGasTransportData(GasTransportData& tr, const XML_Node& tr_node) { std::string geometry, dummy; From 26562aee970dc6e801c28e8db4837631406b78a6 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 16 Nov 2019 18:55:02 -0500 Subject: [PATCH 05/57] [Input] Introduce YamlWriter class for input file serialization Currently only handles thermo, not kinetics or transport --- include/cantera/base/YamlWriter.h | 34 +++++++++++++ src/base/YamlWriter.cpp | 75 +++++++++++++++++++++++++++++ test/general/test_serialization.cpp | 56 +++++++++++++++++++++ 3 files changed, 165 insertions(+) create mode 100644 include/cantera/base/YamlWriter.h create mode 100644 src/base/YamlWriter.cpp create mode 100644 test/general/test_serialization.cpp diff --git a/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h new file mode 100644 index 0000000000..97f74eaa18 --- /dev/null +++ b/include/cantera/base/YamlWriter.h @@ -0,0 +1,34 @@ +//! @file YamlWriter.h Declaration for class Cantera::YamlWriter. + +// 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. + +#ifndef CT_YAMLWRITER_H +#define CT_YAMLWRITER_H + +#include "cantera/base/ct_defs.h" + +namespace Cantera +{ + +class Solution; + +//! A class for generating full YAML input files from multiple data sources +class YamlWriter +{ +public: + YamlWriter() {} + + //! Include a phase definition for the specified Solution object + void addPhase(shared_ptr soln); + + std::string toYamlString() const; + void toYamlFile(const std::string& filename) const; + +protected: + std::vector> m_phases; +}; + +} + +#endif diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp new file mode 100644 index 0000000000..1a2e9f7594 --- /dev/null +++ b/src/base/YamlWriter.cpp @@ -0,0 +1,75 @@ +// 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. + +#include "cantera/base/YamlWriter.h" +#include "cantera/base/AnyMap.h" +#include "cantera/base/Solution.h" +#include "cantera/base/stringUtils.h" +#include "cantera/thermo/ThermoPhase.h" +#include "cantera/thermo/Species.h" + +#include +#include + +namespace Cantera { + +void YamlWriter::addPhase(shared_ptr soln) { + for (auto& phase : m_phases) { + if (phase->name() == soln->name()) { + throw CanteraError("YamlWriter::addPhase", + "Duplicate phase name '{}'", soln->name()); + } + } + m_phases.push_back(soln); +} + +std::string YamlWriter::toYamlString() const +{ + AnyMap output; + output["generator"] = "YamlWriter"; + output["cantera-version"] = CANTERA_VERSION; + time_t now = std::chrono::system_clock::to_time_t(std::chrono::system_clock::now()); + output["date"] = trimCopy(std::ctime(&now)); + + // Build phase definitions + std::vector phaseDefs(m_phases.size()); + size_t nspecies_total = 0; + for (size_t i = 0; i < m_phases.size(); i++) { + m_phases[i]->thermo()->getParameters(phaseDefs[i]); + nspecies_total += m_phases[i]->thermo()->nSpecies(); + } + output["phases"] = phaseDefs; + + // Build species definitions for all phases + std::vector speciesDefs; + speciesDefs.reserve(nspecies_total); + std::unordered_map speciesDefIndex; + for (const auto& phase : m_phases) { + for (const auto& name : phase->thermo()->speciesNames()) { + const auto& species = phase->thermo()->species(name); + AnyMap speciesDef; + species->getParameters(speciesDef); + if (speciesDefIndex.count(name) == 0) { + speciesDefs.emplace_back(speciesDef); + speciesDefIndex[name] = speciesDefs.size() - 1; + } else if (speciesDefs[speciesDefIndex[name]] != speciesDef) { + throw CanteraError("YamlWriter::toYamlString", + "Multiple species with different definitions are not " + "supported:\n>>>>>>\n{}\n======\n{}\n<<<<<<\n", + speciesDef.toYamlString(), + speciesDefs[speciesDefIndex[name]].toYamlString()); + } + } + } + output["species"] = speciesDefs; + + return output.toYamlString(); +} + +void YamlWriter::toYamlFile(const std::string& filename) const +{ + std::ofstream out(filename); + out << toYamlString(); +} + +} diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp new file mode 100644 index 0000000000..fd7bbea82d --- /dev/null +++ b/test/general/test_serialization.cpp @@ -0,0 +1,56 @@ +// 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. + +#include "gtest/gtest.h" +#include "cantera/base/YamlWriter.h" +#include "cantera/thermo.h" +#include "cantera/base/Solution.h" + +using namespace Cantera; + +TEST(YamlWriter, thermoDef) +{ + auto original = newSolution("ideal-gas.yaml", "simple"); + YamlWriter writer; + writer.addPhase(original); + writer.toYamlFile("generated-simple.yaml"); + + auto duplicate = newSolution("generated-simple.yaml", "simple"); + + auto thermo1 = original->thermo(); + auto thermo2 = duplicate->thermo(); + + EXPECT_EQ(thermo1->type(), thermo2->type()); + EXPECT_EQ(thermo1->speciesNames(), thermo2->speciesNames()); + EXPECT_DOUBLE_EQ(thermo1->pressure(), thermo2->pressure()); + EXPECT_DOUBLE_EQ(thermo1->enthalpy_mole(), thermo2->enthalpy_mole()); +} + +TEST(YamlWriter, sharedSpecies) +{ + auto original1 = newSolution("ideal-gas.yaml", "simple"); + auto original2 = newSolution("ideal-gas.yaml", "species-remote"); + + YamlWriter writer; + writer.addPhase(original1); + writer.addPhase(original2); + writer.toYamlFile("generated-shared-species.yaml"); + + auto duplicate = newSolution("generated-shared-species.yaml", "species-remote"); + auto thermo1 = original2->thermo(); + auto thermo2 = duplicate->thermo(); + + EXPECT_EQ(thermo1->type(), thermo2->type()); + EXPECT_EQ(thermo1->speciesNames(), thermo2->speciesNames()); + EXPECT_DOUBLE_EQ(thermo1->pressure(), thermo2->pressure()); + EXPECT_DOUBLE_EQ(thermo1->enthalpy_mole(), thermo2->enthalpy_mole()); +} + +TEST(YamlWriter, duplicateName) +{ + auto original1 = newSolution("ideal-gas.yaml", "simple"); + auto original2 = newSolution("ideal-gas.yaml", "simple"); + YamlWriter writer; + writer.addPhase(original1); + EXPECT_THROW(writer.addPhase(original2), CanteraError); +} From 8ec3fed15b51e212ad449d3fcf8bb4c357450ce5 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 16 Nov 2019 20:05:57 -0500 Subject: [PATCH 06/57] [Input] Implement serialization of gas phase reactions --- include/cantera/kinetics/Falloff.h | 10 +++ include/cantera/kinetics/Reaction.h | 12 ++++ include/cantera/kinetics/RxnRates.h | 3 + src/kinetics/Falloff.cpp | 26 ++++++++ src/kinetics/Reaction.cpp | 100 ++++++++++++++++++++++++++++ src/kinetics/RxnRates.cpp | 7 ++ test/kinetics/kineticsFromYaml.cpp | 95 ++++++++++++++++++++++++++ 7 files changed, 253 insertions(+) diff --git a/include/cantera/kinetics/Falloff.h b/include/cantera/kinetics/Falloff.h index 0adefb56c4..f31dc754c6 100644 --- a/include/cantera/kinetics/Falloff.h +++ b/include/cantera/kinetics/Falloff.h @@ -9,6 +9,8 @@ namespace Cantera { +class AnyMap; + /** * @defgroup falloffGroup Falloff Parameterizations * @@ -88,6 +90,10 @@ class Falloff //! Get the values of the parameters for this object. *params* must be an //! array of at least nParameters() elements. virtual void getParameters(double* params) const {} + + //! Store the falloff-related parameters needed to reconstruct an identical + //! Reaction using the newReaction(AnyMap&, Kinetics&) function. + virtual void getParameters(AnyMap& reactionNode) const {} }; @@ -157,6 +163,8 @@ class Troe : public Falloff //! Sets params to contain, in order, \f[ (A, T_3, T_1, T_2) \f] virtual void getParameters(double* params) const; + virtual void getParameters(AnyMap& reactionNode) const; + protected: //! parameter a in the 4-parameter Troe falloff function. Dimensionless doublereal m_a; @@ -231,6 +239,8 @@ class SRI : public Falloff //! Sets params to contain, in order, \f[ (a, b, c, d, e) \f] virtual void getParameters(double* params) const; + virtual void getParameters(AnyMap& reactionNode) const; + protected: //! parameter a in the 5-parameter SRI falloff function. Dimensionless. doublereal m_a; diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index f19b3cf69d..b5ecd83b32 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -60,6 +60,10 @@ class Reaction m_valid = valid; } + //! Store the parameters of a Reaction needed to reconstruct an identical + //! object using the newReaction(AnyMap&, Kinetics&) function. + virtual void getParameters(AnyMap& reactionNode) const; + //! Type of the reaction. The valid types are listed in the file, //! reaction_defs.h, with constants ending in `RXN`. /*! @@ -139,6 +143,7 @@ class ElementaryReaction : public Reaction ElementaryReaction(const Composition& reactants, const Composition products, const Arrhenius& rate); virtual void validate(); + virtual void getParameters(AnyMap& reactionNode) const; virtual std::string type() const { return "elementary"; @@ -180,6 +185,7 @@ class ThreeBodyReaction : public ElementaryReaction virtual std::string reactantString() const; virtual std::string productString() const; + virtual void getParameters(AnyMap& reactionNode) const; //! Relative efficiencies of third-body species in enhancing the reaction //! rate. @@ -203,6 +209,7 @@ class FalloffReaction : public Reaction virtual std::string reactantString() const; virtual std::string productString() const; virtual void validate(); + virtual void getParameters(AnyMap& reactionNode) const; //! The rate constant in the low-pressure limit Arrhenius low_rate; @@ -235,6 +242,8 @@ class ChemicallyActivatedReaction : public FalloffReaction virtual std::string type() const { return "chemically-activated"; } + + virtual void getParameters(AnyMap& reactionNode) const; }; //! A pressure-dependent reaction parameterized by logarithmically interpolating @@ -251,6 +260,8 @@ class PlogReaction : public Reaction } virtual void validate(); + virtual void getParameters(AnyMap& reactionNode) const; + Plog rate; }; @@ -262,6 +273,7 @@ class ChebyshevReaction : public Reaction ChebyshevReaction(); ChebyshevReaction(const Composition& reactants, const Composition& products, const ChebyshevRate& rate); + virtual void getParameters(AnyMap& reactionNode) const; virtual std::string type() const { return "Chebyshev"; diff --git a/include/cantera/kinetics/RxnRates.h b/include/cantera/kinetics/RxnRates.h index 7205eb6226..35c963c25c 100644 --- a/include/cantera/kinetics/RxnRates.h +++ b/include/cantera/kinetics/RxnRates.h @@ -18,6 +18,7 @@ class Array2D; class AnyValue; class UnitSystem; class Units; +class AnyMap; //! Arrhenius reaction rate type depends only on temperature /** @@ -45,6 +46,8 @@ class Arrhenius void setParameters(const AnyValue& rate, const UnitSystem& units, const Units& rate_units); + void getParameters(AnyMap& rateNode) const; + //! Update concentration-dependent parts of the rate coefficient. /*! * For this class, there are no concentration-dependent parts, so this diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index 3958e59acf..aafdf4b59b 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -9,6 +9,7 @@ #include "cantera/base/stringUtils.h" #include "cantera/base/ctexceptions.h" #include "cantera/base/global.h" +#include "cantera/base/AnyMap.h" #include "cantera/kinetics/Falloff.h" namespace Cantera @@ -83,6 +84,18 @@ void Troe::getParameters(double* params) const { params[3] = m_t2; } +void Troe::getParameters(AnyMap& reactionNode) const +{ + AnyMap params; + params["A"]= m_a; + params["T3"] = 1.0 / m_rt3; + params["T1"] = 1.0 / m_rt1; + if (std::abs(m_t2) > SmallNumber) { + params["T2"] = m_t2; + } + reactionNode["Troe"] = std::move(params); +} + void SRI::init(const vector_fp& c) { if (c.size() != 3 && c.size() != 5) { @@ -137,4 +150,17 @@ void SRI::getParameters(double* params) const params[4] = m_e; } +void SRI::getParameters(AnyMap& reactionNode) const +{ + AnyMap params; + params["A"]= m_a; + params["B"] = m_b; + params["C"] = m_c; + if (m_d != 1.0 || m_e != 0.0) { + params["D"] = m_d; + params["E"] = m_e; + } + reactionNode["SRI"] = std::move(params); +} + } diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 23b5047ba7..f052959f53 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -98,6 +98,29 @@ void Reaction::validate() } } +void Reaction::getParameters(AnyMap& reactionNode) const +{ + reactionNode["equation"] = equation(); + if (duplicate) { + reactionNode["duplicate"] = true; + } + if (orders.size()) { + reactionNode["orders"] = orders; + } + if (allow_negative_orders) { + reactionNode["negative-orders"] = true; + } + if (allow_nonreactant_orders) { + reactionNode["nonreactant-orders"] = true; + } + + for (const auto& item : input) { + if (!reactionNode.hasKey(item.first)) { + reactionNode[item.first] = item.second; + } + } +} + std::string Reaction::reactantString() const { std::ostringstream result; @@ -165,6 +188,17 @@ void ElementaryReaction::validate() } } +void ElementaryReaction::getParameters(AnyMap& reactionNode) const +{ + Reaction::getParameters(reactionNode); + if (allow_negative_pre_exponential_factor) { + reactionNode["negative-A"] = true; + } + AnyMap rateNode; + rate.getParameters(rateNode); + reactionNode["rate-constant"] = std::move(rateNode); +} + ThirdBody::ThirdBody(double default_eff) : default_efficiency(default_eff) { @@ -198,6 +232,16 @@ std::string ThreeBodyReaction::productString() const { return ElementaryReaction::productString() + " + M"; } +void ThreeBodyReaction::getParameters(AnyMap& reactionNode) const +{ + ElementaryReaction::getParameters(reactionNode); + reactionNode["type"] = "three-body"; + reactionNode["efficiencies"] = third_body.efficiencies; + if (third_body.default_efficiency != 1.0) { + reactionNode["default-efficiency"] = third_body.default_efficiency; + } +} + FalloffReaction::FalloffReaction() : Reaction() , falloff(new Falloff()) @@ -254,6 +298,24 @@ void FalloffReaction::validate() { } } +void FalloffReaction::getParameters(AnyMap& reactionNode) const +{ + Reaction::getParameters(reactionNode); + reactionNode["type"] = "falloff"; + AnyMap lowRateNode; + low_rate.getParameters(lowRateNode); + reactionNode["low-P-rate-constant"] = std::move(lowRateNode); + AnyMap highRateNode; + high_rate.getParameters(highRateNode); + reactionNode["high-P-rate-constant"] = std::move(highRateNode); + falloff->getParameters(reactionNode); + + reactionNode["efficiencies"] = third_body.efficiencies; + if (third_body.default_efficiency != 1.0) { + reactionNode["default-efficiency"] = third_body.default_efficiency; + } +} + ChemicallyActivatedReaction::ChemicallyActivatedReaction() { reaction_type = CHEMACT_RXN; @@ -268,6 +330,12 @@ ChemicallyActivatedReaction::ChemicallyActivatedReaction( reaction_type = CHEMACT_RXN; } +void ChemicallyActivatedReaction::getParameters(AnyMap& reactionNode) const +{ + FalloffReaction::getParameters(reactionNode); + reactionNode["type"] = "chemically-activated"; +} + PlogReaction::PlogReaction() : Reaction() { @@ -282,6 +350,20 @@ PlogReaction::PlogReaction(const Composition& reactants_, reaction_type = PLOG_RXN; } +void PlogReaction::getParameters(AnyMap& reactionNode) const +{ + Reaction::getParameters(reactionNode); + reactionNode["type"] = "pressure-dependent-Arrhenius"; + std::vector rateList; + for (const auto& r : rate.rates()) { + AnyMap rateNode; + r.second.getParameters(rateNode); + rateNode["P"] = r.first; + rateList.push_back(std::move(rateNode)); + } + reactionNode["rate-constants"] = std::move(rateList); +} + ChebyshevReaction::ChebyshevReaction() : Reaction() { @@ -352,6 +434,24 @@ void TestReaction::setParameters(const AnyMap& node, const Kinetics& kin) allow_negative_pre_exponential_factor = node.getBool("negative-A", false); } +void ChebyshevReaction::getParameters(AnyMap& reactionNode) const +{ + Reaction::getParameters(reactionNode); + reactionNode["type"] = "Chebyshev"; + reactionNode["temperature-range"] = vector_fp{rate.Tmin(), rate.Tmax()}; + reactionNode["pressure-range"] = vector_fp{rate.Pmin(), rate.Pmax()}; + const auto& coeffs1d = rate.coeffs(); + size_t nT = rate.nTemperature(); + size_t nP = rate.nPressure(); + std::vector coeffs2d(nT, vector_fp(nP)); + for (size_t i = 0; i < nT; i++) { + for (size_t j = 0; j < nP; j++) { + coeffs2d[i][j] = coeffs1d[nP*i + j]; + } + } + reactionNode["data"] = std::move(coeffs2d); +} + InterfaceReaction::InterfaceReaction() : is_sticking_coefficient(false) , use_motz_wise_correction(false) diff --git a/src/kinetics/RxnRates.cpp b/src/kinetics/RxnRates.cpp index 0b72288158..349580b552 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -51,6 +51,13 @@ void Arrhenius::setParameters(const AnyValue& rate, } } +void Arrhenius::getParameters(AnyMap& rateNode) const +{ + rateNode["A"] = preExponentialFactor(); + rateNode["b"] = temperatureExponent(); + rateNode["Ea"] = activationEnergy_R() * GasConstant; +} + SurfaceArrhenius::SurfaceArrhenius() : m_b(0.0) , m_E(0.0) diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index 5ce804a7d7..b7016e05df 100644 --- a/test/kinetics/kineticsFromYaml.cpp +++ b/test/kinetics/kineticsFromYaml.cpp @@ -322,3 +322,98 @@ TEST(KineticsFromYaml, ReactionsFieldWithoutKineticsModel2) "nokinetics-reactions"), InputFileError); } + +class ReactionToYaml : public testing::Test +{ +public: + void duplicateReaction(size_t i) { + auto kin = soln->kinetics(); + iOld = i; + AnyMap rdata1; + kin->reaction(iOld)->getParameters(rdata1); + AnyMap rdata2 = AnyMap::fromYamlString(rdata1.toYamlString()); + duplicate = newReaction(rdata2, *kin); + kin->addReaction(duplicate); + iNew = kin->nReactions() - 1; + } + + void compareReactions() { + auto kin = soln->kinetics(); + EXPECT_EQ(kin->reactionString(iOld), kin->reactionString(iNew)); + EXPECT_EQ(kin->reactionTypeStr(iOld), kin->reactionTypeStr(iNew)); + EXPECT_EQ(kin->isReversible(iOld), kin->isReversible(iNew)); + + vector_fp kf(kin->nReactions()), kr(kin->nReactions()); + vector_fp ropf(kin->nReactions()), ropr(kin->nReactions()); + kin->getFwdRateConstants(kf.data()); + kin->getRevRateConstants(kr.data()); + kin->getFwdRatesOfProgress(ropf.data()); + kin->getRevRatesOfProgress(ropr.data()); + EXPECT_DOUBLE_EQ(kf[iOld], kf[iNew]); + EXPECT_DOUBLE_EQ(kr[iOld], kr[iNew]); + EXPECT_DOUBLE_EQ(ropf[iOld], ropf[iNew]); + EXPECT_DOUBLE_EQ(ropr[iOld], ropr[iNew]); + } + + shared_ptr soln; + shared_ptr duplicate; + + size_t iOld; + size_t iNew; +}; + +TEST_F(ReactionToYaml, elementary) +{ + soln = newSolution("h2o2.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "H2:1.0, O2:0.5, O:1e-8, OH:3e-8"); + duplicateReaction(2); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); +} + +TEST_F(ReactionToYaml, threeBody) +{ + soln = newSolution("h2o2.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "H2:1.0, O2:0.5, O:1e-8, OH:3e-8, H:2e-7"); + duplicateReaction(1); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); +} + +TEST_F(ReactionToYaml, TroeFalloff) +{ + soln = newSolution("h2o2.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "H2:1.0, O2:0.5, H2O2:1e-8, OH:3e-8"); + duplicateReaction(20); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); +} + +TEST_F(ReactionToYaml, chemicallyActivated) +{ + soln = newSolution("chemically-activated-reaction.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "H2:1.0, ch2o:0.1, ch3:1e-8, oh:3e-6"); + duplicateReaction(0); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); +} + +TEST_F(ReactionToYaml, pdepArrhenius) +{ + soln = newSolution("pdep-test.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "R2:1, H:0.1, P2A:2, P2B:0.3"); + duplicateReaction(1); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); + soln->thermo()->setState_TPY(1100, 1e3, "R2:1, H:0.2, P2A:2, P2B:0.3"); + compareReactions(); +} + +TEST_F(ReactionToYaml, Chebyshev) +{ + soln = newSolution("pdep-test.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "R6:1, P6A:2, P6B:0.3"); + duplicateReaction(5); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); +} From ec83f3032374fe83220f2611bff93a4d049cfb75 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 23 Nov 2019 19:16:46 -0500 Subject: [PATCH 07/57] [Input] Implement serialization of interface reactions --- include/cantera/kinetics/Reaction.h | 2 ++ src/kinetics/Reaction.cpp | 37 +++++++++++++++++++++++++++++ test/kinetics/kineticsFromYaml.cpp | 33 +++++++++++++++++++++++++ 3 files changed, 72 insertions(+) diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index b5ecd83b32..67f258b0c3 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -346,6 +346,7 @@ class InterfaceReaction : public ElementaryReaction InterfaceReaction(); InterfaceReaction(const Composition& reactants, const Composition& products, const Arrhenius& rate, bool isStick=false); + virtual void getParameters(AnyMap& reactionNode) const; virtual std::string type() const { return "interface"; @@ -379,6 +380,7 @@ class ElectrochemicalReaction : public InterfaceReaction ElectrochemicalReaction(); ElectrochemicalReaction(const Composition& reactants, const Composition& products, const Arrhenius& rate); + virtual void getParameters(AnyMap& reactionNode) const; //! Forward value of the apparent Electrochemical transfer coefficient doublereal beta; diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index f052959f53..d951b94803 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -470,6 +470,32 @@ InterfaceReaction::InterfaceReaction(const Composition& reactants_, reaction_type = INTERFACE_RXN; } +void InterfaceReaction::getParameters(AnyMap& reactionNode) const +{ + ElementaryReaction::getParameters(reactionNode); + if (is_sticking_coefficient) { + reactionNode["sticking-coefficient"] = std::move(reactionNode["rate-constant"]); + reactionNode.erase("rate-constant"); + } + if (use_motz_wise_correction) { + reactionNode["Motz-Wise"] = true; + } + if (!sticking_species.empty()) { + reactionNode["sticking-species"] = sticking_species; + } + if (!coverage_deps.empty()) { + AnyMap deps; + for (const auto& d : coverage_deps) { + AnyMap dep; + dep["a"] = d.second.a; + dep["m"] = d.second.m; + dep["E"] = d.second.E; + deps[d.first] = std::move(dep); + } + reactionNode["coverage-dependencies"] = std::move(deps); + } +} + ElectrochemicalReaction::ElectrochemicalReaction() : beta(0.5) , exchange_current_density_formulation(false) @@ -485,6 +511,17 @@ ElectrochemicalReaction::ElectrochemicalReaction(const Composition& reactants_, { } +void ElectrochemicalReaction::getParameters(AnyMap& reactionNode) const +{ + InterfaceReaction::getParameters(reactionNode); + if (beta != 0.5) { + reactionNode["beta"] = beta; + } + if (exchange_current_density_formulation) { + reactionNode["exchange-current-density-formulation"] = true; + } +} + Arrhenius readArrhenius(const XML_Node& arrhenius_node) { return Arrhenius(getFloat(arrhenius_node, "A", "toSI"), diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index b7016e05df..3641ce21ea 100644 --- a/test/kinetics/kineticsFromYaml.cpp +++ b/test/kinetics/kineticsFromYaml.cpp @@ -417,3 +417,36 @@ TEST_F(ReactionToYaml, Chebyshev) EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); compareReactions(); } + +TEST_F(ReactionToYaml, surface) +{ + auto gas = newSolution("diamond.yaml", "gas"); + auto solid = newSolution("diamond.yaml", "diamond"); + soln = newSolution("diamond.yaml", "diamond_100", "None", {gas, solid}); + auto surf = std::dynamic_pointer_cast(soln->thermo()); + surf->setCoveragesByName("c6HH:0.1, c6H*:0.6, c6**:0.1"); + gas->thermo()->setMassFractionsByName("H2:0.7, CH4:0.3"); + duplicateReaction(0); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); +} + +TEST_F(ReactionToYaml, electrochemical) +{ + auto gas = newSolution("sofc.yaml", "gas"); + auto metal = newSolution("sofc.yaml", "metal"); + auto oxide_bulk = newSolution("sofc.yaml", "oxide_bulk"); + auto metal_surf = newSolution("sofc.yaml", "metal_surface", "None", {gas}); + auto oxide_surf = newSolution("sofc.yaml", "oxide_surface", "None", + {gas, oxide_bulk}); + soln = newSolution("sofc.yaml", "tpb", "None", + {metal, metal_surf, oxide_surf}); + auto ox_surf = std::dynamic_pointer_cast(oxide_surf->thermo()); + oxide_bulk->thermo()->setElectricPotential(-3.4); + oxide_surf->thermo()->setElectricPotential(-3.4); + ox_surf->setCoveragesByName("O''(ox):0.2, OH'(ox):0.3, H2O(ox):0.5"); + duplicateReaction(0); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); + compareReactions(); +} From af0576d0754db2ce0c3823e9c78ae365d99bafa6 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 25 Nov 2019 09:08:57 -0500 Subject: [PATCH 08/57] [Input] Add reactions/kinetics to YamlWriter output --- include/cantera/kinetics/Kinetics.h | 6 +++ src/base/YamlWriter.cpp | 72 +++++++++++++++++++++++++++++ src/kinetics/Kinetics.cpp | 9 ++++ src/kinetics/KineticsFactory.cpp | 4 ++ test/general/test_serialization.cpp | 54 ++++++++++++++++++++++ 5 files changed, 145 insertions(+) diff --git a/include/cantera/kinetics/Kinetics.h b/include/cantera/kinetics/Kinetics.h index f448243f93..39838ec903 100644 --- a/include/cantera/kinetics/Kinetics.h +++ b/include/cantera/kinetics/Kinetics.h @@ -21,6 +21,7 @@ namespace Cantera class ThermoPhase; class Reaction; class Solution; +class AnyMap; /** * @defgroup chemkinetics Chemical Kinetics @@ -703,6 +704,11 @@ class Kinetics */ virtual void init() {} + //! Store the parameters for a phase definition which are needed to + //! reconstruct an identical object using the newKinetics function. This + //! excludes the reaction definitions, which are handled separately. + virtual void getParameters(AnyMap& phaseNode); + /** * Resize arrays with sizes that depend on the total number of species. * Automatically called before adding each Reaction and Phase. diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 1a2e9f7594..881b2eaeca 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -7,6 +7,8 @@ #include "cantera/base/stringUtils.h" #include "cantera/thermo/ThermoPhase.h" #include "cantera/thermo/Species.h" +#include "cantera/kinetics/Kinetics.h" +#include "cantera/kinetics/Reaction.h" #include #include @@ -37,6 +39,13 @@ std::string YamlWriter::toYamlString() const for (size_t i = 0; i < m_phases.size(); i++) { m_phases[i]->thermo()->getParameters(phaseDefs[i]); nspecies_total += m_phases[i]->thermo()->nSpecies(); + const auto& kin = m_phases[i]->kinetics(); + if (kin) { + kin->getParameters(phaseDefs[i]); + if (phaseDefs[i].hasKey("kinetics") && kin->nReactions() == 0) { + phaseDefs[i]["reactions"] = "none"; + } + } } output["phases"] = phaseDefs; @@ -63,6 +72,69 @@ std::string YamlWriter::toYamlString() const } output["species"] = speciesDefs; + // build reaction definitions for all phases + std::map> allReactions; + for (const auto& phase : m_phases) { + const auto kin = phase->kinetics(); + if (!kin || !kin->nReactions()) { + continue; + } + std::vector reactions; + for (size_t i = 0; i < kin->nReactions(); i++) { + const auto reaction = kin->reaction(i); + AnyMap reactionDef; + reaction->getParameters(reactionDef); + reactions.push_back(std::move(reactionDef)); + } + allReactions[phase->name()] = std::move(reactions); + } + + // Figure out which phase definitions have identical sets of reactions, + // and can share a reaction definition section + + // key: canonical phase in allReactions + // value: phases using this reaction set + std::map> phaseGroups; + + for (const auto& phase : m_phases) { + const auto kin = phase->kinetics(); + std::string name = phase->name(); + if (!kin || !kin->nReactions()) { + continue; + } + bool match = false; + for (auto& group : phaseGroups) { + if (allReactions[group.first] == allReactions[name]) { + group.second.push_back(name); + allReactions.erase(name); + match = true; + break; + } + } + if (!match) { + phaseGroups[name].push_back(name); + } + } + + // Generate the reactions section(s) in the output file + if (phaseGroups.size() == 1) { + output["reactions"] = std::move(allReactions[phaseGroups.begin()->first]); + } else { + for (const auto& group : phaseGroups) { + std::string groupName; + for (auto& name : group.second) { + groupName += name + "-"; + } + groupName += "reactions"; + output[groupName] = std::move(allReactions[group.first]); + + for (auto& name : group.second) { + AnyMap& phaseDef = output["phases"].getMapWhere("name", name); + phaseDef["reactions"] = std::vector{groupName}; + } + } + } + return output.toYamlString(); } diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index 9f883583a5..3e00f5bdee 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -10,6 +10,7 @@ // at https://cantera.org/license.txt for license and copyright information. #include "cantera/kinetics/Kinetics.h" +#include "cantera/kinetics/KineticsFactory.h" #include "cantera/kinetics/Reaction.h" #include "cantera/thermo/ThermoPhase.h" #include "cantera/base/stringUtils.h" @@ -493,6 +494,14 @@ void Kinetics::addPhase(ThermoPhase& thermo) resizeSpecies(); } +void Kinetics::getParameters(AnyMap& phaseNode) +{ + string name = KineticsFactory::factory()->canonicalize(kineticsType()); + if (name != "none") { + phaseNode["kinetics"] = name; + } +} + void Kinetics::resizeSpecies() { m_kk = 0; diff --git a/src/kinetics/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index e45b887fff..6c96d6f356 100644 --- a/src/kinetics/KineticsFactory.cpp +++ b/src/kinetics/KineticsFactory.cpp @@ -41,11 +41,15 @@ Kinetics* KineticsFactory::newKinetics(XML_Node& phaseData, KineticsFactory::KineticsFactory() { reg("none", []() { return new Kinetics(); }); + addAlias("none", "Kinetics"); reg("gas", []() { return new GasKinetics(); }); addAlias("gas", "gaskinetics"); + addAlias("gas", "Gas"); reg("surface", []() { return new InterfaceKinetics(); }); addAlias("surface", "interface"); + addAlias("surface", "Surf"); reg("edge", []() { return new EdgeKinetics(); }); + addAlias("edge", "Edge"); } Kinetics* KineticsFactory::newKinetics(const string& model) diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index fd7bbea82d..31c9f69ef6 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -5,6 +5,7 @@ #include "cantera/base/YamlWriter.h" #include "cantera/thermo.h" #include "cantera/base/Solution.h" +#include "cantera/kinetics.h" using namespace Cantera; @@ -54,3 +55,56 @@ TEST(YamlWriter, duplicateName) writer.addPhase(original1); EXPECT_THROW(writer.addPhase(original2), CanteraError); } + +TEST(YamlWriter, reactions) +{ + auto original = newSolution("h2o2.yaml"); + YamlWriter writer; + writer.addPhase(original); + writer.toYamlFile("generated-h2o2.yaml"); + auto duplicate = newSolution("generated-h2o2.yaml"); + + auto kin1 = original->kinetics(); + auto kin2 = duplicate->kinetics(); + + ASSERT_EQ(kin1->nReactions(), kin2->nReactions()); + vector_fp kf1(kin1->nReactions()), kf2(kin1->nReactions()); + kin1->getFwdRateConstants(kf1.data()); + kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < kin1->nReactions(); i++) { + EXPECT_DOUBLE_EQ(kf1[i], kf2[i]) << "for reaction i = " << i; + } + + AnyMap m = AnyMap::fromYamlFile("generated-h2o2.yaml"); + auto& reactions = m["reactions"].asVector(); +} + +TEST(YamlWriter, multipleReactionSections) +{ + auto original1 = newSolution("h2o2.yaml"); + auto original2 = newSolution("h2o2.yaml"); + auto original3 = newSolution("h2o2.yaml"); + // this phase will require its own "reactions" section + auto R = original3->kinetics()->reaction(3); + R->duplicate = true; + original3->kinetics()->addReaction(R); + original2->setName("ohmech2"); + original3->setName("ohmech3"); + YamlWriter writer; + writer.addPhase(original1); + writer.addPhase(original2); + writer.addPhase(original3); + writer.toYamlFile("generated-multi-rxn-secs.yaml"); + + auto duplicate1 = newSolution("generated-multi-rxn-secs.yaml", "ohmech"); + auto duplicate2 = newSolution("generated-multi-rxn-secs.yaml", "ohmech2"); + auto duplicate3 = newSolution("generated-multi-rxn-secs.yaml", "ohmech3"); + auto kin1 = duplicate1->kinetics(); + auto kin2 = duplicate2->kinetics(); + auto kin3 = duplicate3->kinetics(); + + ASSERT_EQ(kin1->nReactions(), kin2->nReactions()); + ASSERT_EQ(kin2->nReactions() + 1, kin3->nReactions()); + ASSERT_EQ(kin2->reactionString(3), + kin3->reactionString(kin3->nReactions() - 1)); +} From 51a32b4dcfadaa7d591a3aba3efba6b3defa4b83 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 25 Nov 2019 14:56:30 -0500 Subject: [PATCH 09/57] [Input] Add transport model specification to YamlWriter output --- include/cantera/transport/TransportBase.h | 7 +++++++ src/base/YamlWriter.cpp | 5 +++++ src/transport/TransportBase.cpp | 9 +++++++++ src/transport/TransportFactory.cpp | 1 + 4 files changed, 22 insertions(+) diff --git a/include/cantera/transport/TransportBase.h b/include/cantera/transport/TransportBase.h index 86dfc9e356..ace86afb3b 100644 --- a/include/cantera/transport/TransportBase.h +++ b/include/cantera/transport/TransportBase.h @@ -27,6 +27,7 @@ namespace Cantera class ThermoPhase; class Solution; +class AnyMap; /*! * \addtogroup tranprops @@ -601,6 +602,12 @@ class Transport */ virtual void setParameters(const int type, const int k, const doublereal* const p); + //! Store the parameters for a phase definition which are needed to + //! reconstruct an identical object using the newTransport function. This + //! excludes the individual species transport properties, which are handled + //! separately. + virtual void getParameters(AnyMap& phaseNode); + //! Sets the velocity basis /*! * What the transport object does with this parameter is up to the diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 881b2eaeca..a18f30a8bb 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -9,6 +9,7 @@ #include "cantera/thermo/Species.h" #include "cantera/kinetics/Kinetics.h" #include "cantera/kinetics/Reaction.h" +#include "cantera/transport/TransportBase.h" #include #include @@ -46,6 +47,10 @@ std::string YamlWriter::toYamlString() const phaseDefs[i]["reactions"] = "none"; } } + const auto& tran = m_phases[i]->transport(); + if (tran) { + tran->getParameters(phaseDefs[i]); + } } output["phases"] = phaseDefs; diff --git a/src/transport/TransportBase.cpp b/src/transport/TransportBase.cpp index ee9ca950c7..5af328b9d7 100644 --- a/src/transport/TransportBase.cpp +++ b/src/transport/TransportBase.cpp @@ -8,6 +8,7 @@ #include "cantera/transport/TransportBase.h" #include "cantera/thermo/ThermoPhase.h" +#include "cantera/transport/TransportFactory.h" using namespace std; @@ -52,6 +53,14 @@ void Transport::setParameters(const int type, const int k, throw NotImplementedError("Transport::setParameters"); } +void Transport::getParameters(AnyMap& phaseNode) +{ + string name = TransportFactory::factory()->canonicalize(transportType()); + if (name != "") { + phaseNode["transport"] = name; + } +} + void Transport::setThermo(ThermoPhase& thermo) { if (!ready()) { diff --git a/src/transport/TransportFactory.cpp b/src/transport/TransportFactory.cpp index 7d8f70e618..73f964f656 100644 --- a/src/transport/TransportFactory.cpp +++ b/src/transport/TransportFactory.cpp @@ -31,6 +31,7 @@ TransportFactory::TransportFactory() { reg("", []() { return new Transport(); }); addAlias("", "None"); + addAlias("", "Transport"); reg("unity-Lewis-number", []() { return new UnityLewisTransport(); }); addAlias("unity-Lewis-number", "UnityLewis"); reg("mixture-averaged", []() { return new MixTransport(); }); From c09f7e3daf445bf05bae620e72979030e6eb00be Mon Sep 17 00:00:00 2001 From: Raymond Speth Date: Sat, 30 Jan 2021 18:02:31 -0500 Subject: [PATCH 10/57] [Input] Implement serialization of more complex phase types --- .../thermo/BinarySolutionTabulatedThermo.h | 1 + include/cantera/thermo/DebyeHuckel.h | 4 + include/cantera/thermo/HMWSoln.h | 1 + include/cantera/thermo/IdealMolalSoln.h | 2 + include/cantera/thermo/IdealSolidSolnPhase.h | 1 + include/cantera/thermo/IdealSolnGasVPSS.h | 5 +- .../cantera/thermo/IonsFromNeutralVPSSTP.h | 1 + include/cantera/thermo/LatticePhase.h | 1 + include/cantera/thermo/LatticeSolidPhase.h | 1 + include/cantera/thermo/MargulesVPSSTP.h | 1 + .../cantera/thermo/MaskellSolidSolnPhase.h | 1 + include/cantera/thermo/MetalPhase.h | 5 + include/cantera/thermo/PureFluidPhase.h | 1 + include/cantera/thermo/RedlichKisterVPSSTP.h | 1 + include/cantera/thermo/SurfPhase.h | 1 + src/thermo/BinarySolutionTabulatedThermo.cpp | 11 + src/thermo/DebyeHuckel.cpp | 86 +++++- src/thermo/HMWSoln.cpp | 282 +++++++++++++++++- src/thermo/IdealMolalSoln.cpp | 101 +++++-- src/thermo/IdealSolidSolnPhase.cpp | 10 + src/thermo/IdealSolnGasVPSS.cpp | 11 + src/thermo/IonsFromNeutralVPSSTP.cpp | 8 + src/thermo/LatticePhase.cpp | 6 + src/thermo/LatticeSolidPhase.cpp | 23 ++ src/thermo/MargulesVPSSTP.cpp | 29 ++ src/thermo/MaskellSolidSolnPhase.cpp | 6 + src/thermo/PureFluidPhase.cpp | 6 + src/thermo/RedlichKisterVPSSTP.cpp | 22 ++ src/thermo/SurfPhase.cpp | 6 + src/thermo/ThermoFactory.cpp | 9 + test/data/thermo-models.yaml | 34 ++- test/thermo/thermoToYaml.cpp | 219 +++++++++++++- 32 files changed, 833 insertions(+), 63 deletions(-) diff --git a/include/cantera/thermo/BinarySolutionTabulatedThermo.h b/include/cantera/thermo/BinarySolutionTabulatedThermo.h index d1f05b7eda..1a242a9ee2 100644 --- a/include/cantera/thermo/BinarySolutionTabulatedThermo.h +++ b/include/cantera/thermo/BinarySolutionTabulatedThermo.h @@ -144,6 +144,7 @@ class BinarySolutionTabulatedThermo : public IdealSolidSolnPhase } virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id_); protected: diff --git a/include/cantera/thermo/DebyeHuckel.h b/include/cantera/thermo/DebyeHuckel.h index d9a81c9673..b240caba73 100644 --- a/include/cantera/thermo/DebyeHuckel.h +++ b/include/cantera/thermo/DebyeHuckel.h @@ -778,6 +778,7 @@ class DebyeHuckel : public MolalityVPSSTP virtual bool addSpecies(shared_ptr spec); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); //! Return the Debye Huckel constant as a function of temperature @@ -966,6 +967,9 @@ class DebyeHuckel : public MolalityVPSSTP //! a_k = Size of the ionic species in the DH formulation. units = meters vector_fp m_Aionic; + //! Default ionic radius for species where it is not specified + double m_Aionic_default; + //! Current value of the ionic strength on the molality scale mutable double m_IionicMolality; diff --git a/include/cantera/thermo/HMWSoln.h b/include/cantera/thermo/HMWSoln.h index 4b9a509af3..a1c57ea9a5 100644 --- a/include/cantera/thermo/HMWSoln.h +++ b/include/cantera/thermo/HMWSoln.h @@ -1468,6 +1468,7 @@ class HMWSoln : public MolalityVPSSTP double ln_gamma_o_min, double ln_gamma_o_max); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; //! Initialize the phase parameters from an XML file. /*! diff --git a/include/cantera/thermo/IdealMolalSoln.h b/include/cantera/thermo/IdealMolalSoln.h index 03c3457b4e..7900f6074f 100644 --- a/include/cantera/thermo/IdealMolalSoln.h +++ b/include/cantera/thermo/IdealMolalSoln.h @@ -388,6 +388,8 @@ class IdealMolalSoln : public MolalityVPSSTP virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; + //! Set the standard concentration model. /*! * Must be one of 'unity', 'molar_volume', or 'solvent_volume'. diff --git a/include/cantera/thermo/IdealSolidSolnPhase.h b/include/cantera/thermo/IdealSolidSolnPhase.h index 2a991402df..9a93d673b5 100644 --- a/include/cantera/thermo/IdealSolidSolnPhase.h +++ b/include/cantera/thermo/IdealSolidSolnPhase.h @@ -558,6 +558,7 @@ class IdealSolidSolnPhase : public ThermoPhase virtual bool addSpecies(shared_ptr spec); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); virtual void setToEquilState(const doublereal* mu_RT); diff --git a/include/cantera/thermo/IdealSolnGasVPSS.h b/include/cantera/thermo/IdealSolnGasVPSS.h index 3deae06b0c..840fbbb382 100644 --- a/include/cantera/thermo/IdealSolnGasVPSS.h +++ b/include/cantera/thermo/IdealSolnGasVPSS.h @@ -35,12 +35,12 @@ class IdealSolnGasVPSS : public VPStandardStateTP //@{ virtual std::string type() const { - return "IdealSolnGas"; + return "ideal-solution-VPSS"; } //! Set the standard concentration model /* - * Must be one of 'unity', 'molar_volume', or 'solvent_volume'. + * Must be one of 'unity', 'species-molar-volume', or 'solvent-molar-volume'. */ void setStandardConcentrationModel(const std::string& model); @@ -130,6 +130,7 @@ class IdealSolnGasVPSS : public VPStandardStateTP virtual bool addSpecies(shared_ptr spec); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void setToEquilState(const doublereal* lambda_RT); virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); diff --git a/include/cantera/thermo/IonsFromNeutralVPSSTP.h b/include/cantera/thermo/IonsFromNeutralVPSSTP.h index 1526b110b2..df8750ca09 100644 --- a/include/cantera/thermo/IonsFromNeutralVPSSTP.h +++ b/include/cantera/thermo/IonsFromNeutralVPSSTP.h @@ -272,6 +272,7 @@ class IonsFromNeutralVPSSTP : public GibbsExcessVPSSTP virtual void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void setParametersFromXML(const XML_Node& thermoNode); private: diff --git a/include/cantera/thermo/LatticePhase.h b/include/cantera/thermo/LatticePhase.h index 725d772616..7122fddce2 100644 --- a/include/cantera/thermo/LatticePhase.h +++ b/include/cantera/thermo/LatticePhase.h @@ -618,6 +618,7 @@ class LatticePhase : public ThermoPhase void setSiteDensity(double sitedens); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; //! Set equation of state parameter values from XML entries. /*! diff --git a/include/cantera/thermo/LatticeSolidPhase.h b/include/cantera/thermo/LatticeSolidPhase.h index 9cae772b81..d51b156b43 100644 --- a/include/cantera/thermo/LatticeSolidPhase.h +++ b/include/cantera/thermo/LatticeSolidPhase.h @@ -434,6 +434,7 @@ class LatticeSolidPhase : public ThermoPhase virtual void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void setParametersFromXML(const XML_Node& eosdata); diff --git a/include/cantera/thermo/MargulesVPSSTP.h b/include/cantera/thermo/MargulesVPSSTP.h index def54ea821..353bb72589 100644 --- a/include/cantera/thermo/MargulesVPSSTP.h +++ b/include/cantera/thermo/MargulesVPSSTP.h @@ -349,6 +349,7 @@ class MargulesVPSSTP : public GibbsExcessVPSSTP /// @{ virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); //! Add a binary species interaction with the specified parameters diff --git a/include/cantera/thermo/MaskellSolidSolnPhase.h b/include/cantera/thermo/MaskellSolidSolnPhase.h index 18eda59eb9..cdfc0bd817 100644 --- a/include/cantera/thermo/MaskellSolidSolnPhase.h +++ b/include/cantera/thermo/MaskellSolidSolnPhase.h @@ -102,6 +102,7 @@ class MaskellSolidSolnPhase : public VPStandardStateTP //@{ virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); void set_h_mix(const doublereal hmix) { h_mixing = hmix; } diff --git a/include/cantera/thermo/MetalPhase.h b/include/cantera/thermo/MetalPhase.h index b59ea91189..3df291e9f9 100644 --- a/include/cantera/thermo/MetalPhase.h +++ b/include/cantera/thermo/MetalPhase.h @@ -113,6 +113,11 @@ class MetalPhase : public ThermoPhase } } + virtual void getParameters(AnyMap& phaseNode) const { + ThermoPhase::getParameters(phaseNode); + phaseNode["density"] = density(); + } + virtual void setParametersFromXML(const XML_Node& eosdata) { eosdata._require("model","Metal"); doublereal rho = getFloat(eosdata, "density", "density"); diff --git a/include/cantera/thermo/PureFluidPhase.h b/include/cantera/thermo/PureFluidPhase.h index a350ca544e..7071666b6f 100644 --- a/include/cantera/thermo/PureFluidPhase.h +++ b/include/cantera/thermo/PureFluidPhase.h @@ -191,6 +191,7 @@ class PureFluidPhase : public ThermoPhase //@} virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void setParametersFromXML(const XML_Node& eosdata); virtual std::string report(bool show_thermo=true, diff --git a/include/cantera/thermo/RedlichKisterVPSSTP.h b/include/cantera/thermo/RedlichKisterVPSSTP.h index c0a49fe289..6e8d4a4aec 100644 --- a/include/cantera/thermo/RedlichKisterVPSSTP.h +++ b/include/cantera/thermo/RedlichKisterVPSSTP.h @@ -352,6 +352,7 @@ class RedlichKisterVPSSTP : public GibbsExcessVPSSTP /// To see how they are used, see importPhase(). virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); //! Add a binary species interaction with the specified parameters diff --git a/include/cantera/thermo/SurfPhase.h b/include/cantera/thermo/SurfPhase.h index 185b3e3a86..a514e51604 100644 --- a/include/cantera/thermo/SurfPhase.h +++ b/include/cantera/thermo/SurfPhase.h @@ -293,6 +293,7 @@ class SurfPhase : public ThermoPhase */ virtual void setParametersFromXML(const XML_Node& thermoData); virtual void initThermo(); + virtual void getParameters(AnyMap& phaseNode) const; virtual bool addSpecies(shared_ptr spec); diff --git a/src/thermo/BinarySolutionTabulatedThermo.cpp b/src/thermo/BinarySolutionTabulatedThermo.cpp index 1312a1dfdd..91bd09d014 100644 --- a/src/thermo/BinarySolutionTabulatedThermo.cpp +++ b/src/thermo/BinarySolutionTabulatedThermo.cpp @@ -113,6 +113,17 @@ void BinarySolutionTabulatedThermo::initThermo() IdealSolidSolnPhase::initThermo(); } +void BinarySolutionTabulatedThermo::getParameters(AnyMap& phaseNode) const +{ + IdealSolidSolnPhase::getParameters(phaseNode); + phaseNode["tabulated-species"] = speciesName(m_kk_tab); + AnyMap tabThermo; + tabThermo["mole-fractions"] = m_molefrac_tab; + tabThermo["enthalpy"] = m_enthalpy_tab; + tabThermo["entropy"] = m_entropy_tab; + phaseNode["tabulated-thermo"] = std::move(tabThermo); +} + void BinarySolutionTabulatedThermo::initThermoXML(XML_Node& phaseNode, const std::string& id_) { vector_fp x, h, s; diff --git a/src/thermo/DebyeHuckel.cpp b/src/thermo/DebyeHuckel.cpp index b3a610ac0e..e37de29279 100644 --- a/src/thermo/DebyeHuckel.cpp +++ b/src/thermo/DebyeHuckel.cpp @@ -27,16 +27,23 @@ using namespace std; namespace Cantera { +namespace { +double A_Debye_default = 1.172576; // units = sqrt(kg/gmol) +double B_Debye_default = 3.28640E9; // units = sqrt(kg/gmol) / m +double maxIionicStrength_default = 30.0; +} + DebyeHuckel::DebyeHuckel(const std::string& inputFile, const std::string& id_) : m_formDH(DHFORM_DILUTE_LIMIT), + m_Aionic_default(NAN), m_IionicMolality(0.0), - m_maxIionicStrength(30.0), + m_maxIionicStrength(maxIionicStrength_default), m_useHelgesonFixedForm(false), m_IionicMolalityStoich(0.0), m_form_A_Debye(A_DEBYE_CONST), - m_A_Debye(1.172576), // units = sqrt(kg/gmol) - m_B_Debye(3.28640E9), // units = sqrt(kg/gmol) / m + m_A_Debye(A_Debye_default), + m_B_Debye(B_Debye_default), m_waterSS(0), m_densWaterSS(1000.) { @@ -45,13 +52,14 @@ DebyeHuckel::DebyeHuckel(const std::string& inputFile, DebyeHuckel::DebyeHuckel(XML_Node& phaseRoot, const std::string& id_) : m_formDH(DHFORM_DILUTE_LIMIT), + m_Aionic_default(NAN), m_IionicMolality(0.0), - m_maxIionicStrength(3.0), + m_maxIionicStrength(maxIionicStrength_default), m_useHelgesonFixedForm(false), m_IionicMolalityStoich(0.0), m_form_A_Debye(A_DEBYE_CONST), - m_A_Debye(1.172576), // units = sqrt(kg/gmol) - m_B_Debye(3.28640E9), // units = sqrt(kg/gmol) / m + m_A_Debye(A_Debye_default), + m_B_Debye(B_Debye_default), m_waterSS(0), m_densWaterSS(1000.) { @@ -350,6 +358,7 @@ void DebyeHuckel::setB_dot(double bdot) void DebyeHuckel::setDefaultIonicRadius(double value) { + m_Aionic_default = value; for (size_t k = 0; k < m_kk; k++) { if (std::isnan(m_Aionic[k])) { m_Aionic[k] = value; @@ -591,6 +600,71 @@ void DebyeHuckel::initThermo() } } +void DebyeHuckel::getParameters(AnyMap& phaseNode) const +{ + MolalityVPSSTP::getParameters(phaseNode); + AnyMap activityNode; + + switch (m_formDH) { + case DHFORM_DILUTE_LIMIT: + activityNode["model"] = "dilute-limit"; + break; + case DHFORM_BDOT_AK: + activityNode["model"] = "B-dot-with-variable-a"; + break; + case DHFORM_BDOT_ACOMMON: + activityNode["model"] = "B-dot-with-common-a"; + break; + case DHFORM_BETAIJ: + activityNode["model"] = "beta_ij"; + break; + case DHFORM_PITZER_BETAIJ: + activityNode["model"] = "Pitzer-with-beta_ij"; + break; + } + + if (m_form_A_Debye == A_DEBYE_WATER) { + activityNode["A_Debye"] = "variable"; + } else if (m_A_Debye != A_Debye_default) { + activityNode["A_Debye"] = fmt::format("{} kg^0.5/gmol^0.5", m_A_Debye); + } + + if (m_B_Debye != B_Debye_default) { + activityNode["B_Debye"] = fmt::format("{} kg^0.5/gmol^0.5/m", m_B_Debye); + } + if (m_maxIionicStrength != maxIionicStrength_default) { + activityNode["max-ionic-strength"] = m_maxIionicStrength; + } + if (m_useHelgesonFixedForm) { + activityNode["use-Helgeson-fixed-form"] = true; + } + if (!isnan(m_Aionic_default)) { + activityNode["default-ionic-radius"] = m_Aionic_default; + } + for (double B_dot : m_B_Dot) { + if (B_dot != 0.0) { + activityNode["B-dot"] = B_dot; + break; + } + } + if (m_Beta_ij.nRows() && m_Beta_ij.nColumns()) { + std::vector beta; + for (size_t i = 0; i < m_kk; i++) { + for (size_t j = i; j < m_kk; j++) { + if (m_Beta_ij(i, j) != 0) { + AnyMap entry; + entry["species"] = vector{ + speciesName(i), speciesName(j)}; + entry["beta"] = m_Beta_ij(i, j); + beta.push_back(std::move(entry)); + } + } + } + activityNode["beta"] = std::move(beta); + } + phaseNode["activity-data"] = std::move(activityNode); +} + double DebyeHuckel::A_Debye_TP(double tempArg, double presArg) const { double T = temperature(); diff --git a/src/thermo/HMWSoln.cpp b/src/thermo/HMWSoln.cpp index a3bb53b57e..ac89b12452 100644 --- a/src/thermo/HMWSoln.cpp +++ b/src/thermo/HMWSoln.cpp @@ -27,6 +27,15 @@ using namespace std; namespace Cantera { +namespace { +double A_Debye_default = 1.172576; // units = sqrt(kg/gmol) +double maxIionicStrength_default = 100.0; +double crop_ln_gamma_o_min_default = -6.0; +double crop_ln_gamma_o_max_default = 3.0; +double crop_ln_gamma_k_min_default = -5.0; +double crop_ln_gamma_k_max_default = 15.0; +} + HMWSoln::~HMWSoln() { } @@ -34,10 +43,10 @@ HMWSoln::~HMWSoln() HMWSoln::HMWSoln(const std::string& inputFile, const std::string& id_) : m_formPitzerTemp(PITZER_TEMP_CONSTANT), m_IionicMolality(0.0), - m_maxIionicStrength(100.0), + m_maxIionicStrength(maxIionicStrength_default), m_TempPitzerRef(298.15), m_form_A_Debye(A_DEBYE_CONST), - m_A_Debye(1.172576), // units = sqrt(kg/gmol) + m_A_Debye(A_Debye_default), m_waterSS(0), m_molalitiesAreCropped(false), IMS_X_o_cutoff_(0.2), @@ -57,10 +66,10 @@ HMWSoln::HMWSoln(const std::string& inputFile, const std::string& id_) : MC_apCut_(0.0), MC_bpCut_(0.0), MC_cpCut_(0.0), - CROP_ln_gamma_o_min(-6.0), - CROP_ln_gamma_o_max(3.0), - CROP_ln_gamma_k_min(-5.0), - CROP_ln_gamma_k_max(15.0), + CROP_ln_gamma_o_min(crop_ln_gamma_o_min_default), + CROP_ln_gamma_o_max(crop_ln_gamma_o_max_default), + CROP_ln_gamma_k_min(crop_ln_gamma_k_min_default), + CROP_ln_gamma_k_max(crop_ln_gamma_k_max_default), m_last_is(-1.0) { initThermoFile(inputFile, id_); @@ -69,10 +78,10 @@ HMWSoln::HMWSoln(const std::string& inputFile, const std::string& id_) : HMWSoln::HMWSoln(XML_Node& phaseRoot, const std::string& id_) : m_formPitzerTemp(PITZER_TEMP_CONSTANT), m_IionicMolality(0.0), - m_maxIionicStrength(100.0), + m_maxIionicStrength(maxIionicStrength_default), m_TempPitzerRef(298.15), m_form_A_Debye(A_DEBYE_CONST), - m_A_Debye(1.172576), // units = sqrt(kg/gmol) + m_A_Debye(A_Debye_default), m_waterSS(0), m_molalitiesAreCropped(false), IMS_X_o_cutoff_(0.2), @@ -92,10 +101,10 @@ HMWSoln::HMWSoln(XML_Node& phaseRoot, const std::string& id_) : MC_apCut_(0.0), MC_bpCut_(0.0), MC_cpCut_(0.0), - CROP_ln_gamma_o_min(-6.0), - CROP_ln_gamma_o_max(3.0), - CROP_ln_gamma_k_min(-5.0), - CROP_ln_gamma_k_max(15.0), + CROP_ln_gamma_o_min(crop_ln_gamma_o_min_default), + CROP_ln_gamma_o_max(crop_ln_gamma_o_max_default), + CROP_ln_gamma_k_min(crop_ln_gamma_k_min_default), + CROP_ln_gamma_k_max(crop_ln_gamma_k_max_default), m_last_is(-1.0) { importPhase(phaseRoot, this); @@ -701,10 +710,10 @@ void HMWSoln::initThermo() if (actData.hasKey("cropping-coefficients")) { auto& crop = actData["cropping-coefficients"].as(); setCroppingCoefficients( - crop.getDouble("ln_gamma_k_min", -5.0), - crop.getDouble("ln_gamma_k_max", 15.0), - crop.getDouble("ln_gamma_o_min", -6.0), - crop.getDouble("ln_gamma_o_max", 3.0)); + crop.getDouble("ln_gamma_k_min", crop_ln_gamma_k_min_default), + crop.getDouble("ln_gamma_k_max", crop_ln_gamma_k_max_default), + crop.getDouble("ln_gamma_o_min", crop_ln_gamma_o_min_default), + crop.getDouble("ln_gamma_o_max", crop_ln_gamma_o_max_default)); } } else { initLengths(); @@ -791,6 +800,247 @@ void HMWSoln::initThermo() setMoleFSolventMin(1.0E-5); } +void assignTrimmed(AnyMap& interaction, const std::string& key, vector_fp& values) { + while (values.size() > 1 && values.back() == 0) { + values.pop_back(); + } + if (values.size() == 1) { + interaction[key] = values[0]; + } else { + interaction[key] = values; + } +} + +void HMWSoln::getParameters(AnyMap& phaseNode) const +{ + MolalityVPSSTP::getParameters(phaseNode); + AnyMap activityNode; + size_t nParams = 1; + if (m_formPitzerTemp == PITZER_TEMP_LINEAR) { + activityNode["temperature-model"] = "linear"; + nParams = 2; + } else if (m_formPitzerTemp == PITZER_TEMP_COMPLEX1) { + activityNode["temperature-model"] = "complex"; + nParams = 5; + } + + if (m_form_A_Debye == A_DEBYE_WATER) { + activityNode["A_Debye"] = "variable"; + } else if (m_A_Debye != A_Debye_default) { + activityNode["A_Debye"] = fmt::format("{} kg^0.5/gmol^0.5", m_A_Debye); + } + if (m_maxIionicStrength != maxIionicStrength_default) { + activityNode["max-ionic-strength"] = m_maxIionicStrength; + } + + vector interactions; + + // Binary interactions + for (size_t i = 1; i < m_kk; i++) { + for (size_t j = 1; j < m_kk; j++) { + size_t c = i*m_kk + j; + // lambda: neutral-charged / neutral-neutral interactions + bool lambda_found = false; + for (size_t n = 0; n < nParams; n++) { + if (m_Lambda_nj_coeff(n, c)) { + lambda_found = true; + break; + } + } + if (lambda_found) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(i), speciesName(j)}; + vector_fp lambda(nParams); + for (size_t n = 0; n < nParams; n++) { + lambda[n] = m_Lambda_nj_coeff(n, c); + } + assignTrimmed(interaction, "lambda", lambda); + interactions.push_back(std::move(interaction)); + continue; + } + + c = static_cast(m_CounterIJ[m_kk * i + j]); + if (c == 0 || i > j) { + continue; + } + + // beta: opposite charged interactions + bool salt_found = false; + for (size_t n = 0; n < nParams; n++) { + if (m_Beta0MX_ij_coeff(n, c) || m_Beta1MX_ij_coeff(n, c) || + m_Beta2MX_ij_coeff(n, c) || m_CphiMX_ij_coeff(n, c)) + { + salt_found = true; + break; + } + } + if (salt_found) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(i), speciesName(j)}; + vector_fp beta0(nParams), beta1(nParams), beta2(nParams), Cphi(nParams); + size_t last_nonzero = 0; + for (size_t n = 0; n < nParams; n++) { + beta0[n] = m_Beta0MX_ij_coeff(n, c); + beta1[n] = m_Beta1MX_ij_coeff(n, c); + beta2[n] = m_Beta2MX_ij_coeff(n, c); + Cphi[n] = m_CphiMX_ij_coeff(n, c); + if (beta0[n] || beta1[n] || beta2[n] || Cphi[n]) { + last_nonzero = n; + } + } + if (last_nonzero == 0) { + interaction["beta0"] = beta0[0]; + interaction["beta1"] = beta1[0]; + interaction["beta2"] = beta2[0]; + interaction["Cphi"] = Cphi[0]; + } else { + beta0.resize(1 + last_nonzero); + beta1.resize(1 + last_nonzero); + beta2.resize(1 + last_nonzero); + Cphi.resize(1 + last_nonzero); + interaction["beta0"] = beta0; + interaction["beta1"] = beta1; + interaction["beta2"] = beta2; + interaction["Cphi"] = Cphi; + } + interaction["alpha1"] = m_Alpha1MX_ij[c]; + if (m_Alpha2MX_ij[c]) { + interaction["alpha2"] = m_Alpha2MX_ij[c]; + } + interactions.push_back(std::move(interaction)); + continue; + } + + // theta: like-charge interactions + bool theta_found = false; + for (size_t n = 0; n < nParams; n++) { + if (m_Theta_ij_coeff(n, c)) { + theta_found = true; + break; + } + } + if (theta_found) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(i), speciesName(j)}; + vector_fp theta(nParams); + for (size_t n = 0; n < nParams; n++) { + theta[n] = m_Theta_ij_coeff(n, c); + } + assignTrimmed(interaction, "theta", theta); + interactions.push_back(std::move(interaction)); + continue; + } + } + } + + // psi: ternary charged species interactions + // Need to check species charges because both psi and zeta parameters + // are stored in m_Psi_ijk_coeff + for (size_t i = 1; i < m_kk; i++) { + if (charge(i) == 0) { + continue; + } + for (size_t j = i + 1; j < m_kk; j++) { + if (charge(j) == 0) { + continue; + } + for (size_t k = j + 1; k < m_kk; k++) { + if (charge(k) == 0) { + continue; + } + size_t c = i*m_kk*m_kk + j*m_kk + k; + for (size_t n = 0; n < nParams; n++) { + if (m_Psi_ijk_coeff(n, c) != 0) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(i), speciesName(j), speciesName(k)}; + vector_fp psi(nParams); + for (size_t m = 0; m < nParams; m++) { + psi[m] = m_Psi_ijk_coeff(m, c); + } + assignTrimmed(interaction, "psi", psi); + interactions.push_back(std::move(interaction)); + break; + } + } + } + } + } + + // zeta: neutral-cation-anion interactions + for (size_t i = 1; i < m_kk; i++) { + if (charge(i) != 0) { + continue; // first species must be neutral + } + for (size_t j = 1; j < m_kk; j++) { + if (charge(j) <= 0) { + continue; // second species must be cation + } + for (size_t k = 1; k < m_kk; k++) { + if (charge(k) >= 0) { + continue; // third species must be anion + } + size_t c = i*m_kk*m_kk + j*m_kk + k; + for (size_t n = 0; n < nParams; n++) { + if (m_Psi_ijk_coeff(n, c) != 0) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(i), speciesName(j), speciesName(k)}; + vector_fp zeta(nParams); + for (size_t m = 0; m < nParams; m++) { + zeta[m] = m_Psi_ijk_coeff(m, c); + } + assignTrimmed(interaction, "zeta", zeta); + interactions.push_back(std::move(interaction)); + break; + } + } + } + } + } + + // mu: neutral self-interaction + for (size_t i = 1; i < m_kk; i++) { + for (size_t n = 0; n < nParams; n++) { + if (m_Mu_nnn_coeff(n, i) != 0) { + AnyMap interaction; + interaction["species"] = vector{speciesName(i)}; + vector_fp mu(nParams); + for (size_t m = 0; m < nParams; m++) { + mu[m] = m_Mu_nnn_coeff(m, i); + } + assignTrimmed(interaction, "mu", mu); + interactions.push_back(std::move(interaction)); + break; + } + } + } + + activityNode["interactions"] = std::move(interactions); + + AnyMap croppingCoeffs; + if (CROP_ln_gamma_k_min != crop_ln_gamma_k_min_default) { + croppingCoeffs["ln_gamma_k_min"] = CROP_ln_gamma_k_min; + } + if (CROP_ln_gamma_k_max != crop_ln_gamma_k_max_default) { + croppingCoeffs["ln_gamma_k_max"] = CROP_ln_gamma_k_max; + } + if (CROP_ln_gamma_o_min != crop_ln_gamma_o_min_default) { + croppingCoeffs["ln_gamma_o_min"] = CROP_ln_gamma_o_min; + } + if (CROP_ln_gamma_o_max != crop_ln_gamma_o_max_default) { + croppingCoeffs["ln_gamma_o_max"] = CROP_ln_gamma_o_max; + } + if (croppingCoeffs.size()) { + activityNode["cropping-coefficients"] = std::move(croppingCoeffs); + } + + phaseNode["activity-data"] = std::move(activityNode); +} + void HMWSoln::initThermoXML(XML_Node& phaseNode, const std::string& id_) { if (id_.size() > 0) { diff --git a/src/thermo/IdealMolalSoln.cpp b/src/thermo/IdealMolalSoln.cpp index d601632289..e1e50482f4 100644 --- a/src/thermo/IdealMolalSoln.cpp +++ b/src/thermo/IdealMolalSoln.cpp @@ -21,6 +21,17 @@ #include "cantera/base/ctml.h" #include "cantera/base/stringUtils.h" +#include + +namespace { +double X_o_cutoff_default = 0.20; +double gamma_o_min_default = 0.00001; +double gamma_k_min_default = 10.0; +double slopefCut_default = 0.6; +double slopegCut_default = 0.0; +double cCut_default = .05; +} + namespace Cantera { @@ -29,12 +40,12 @@ IdealMolalSoln::IdealMolalSoln(const std::string& inputFile, MolalityVPSSTP(), m_formGC(2), IMS_typeCutoff_(0), - IMS_X_o_cutoff_(0.2), - IMS_gamma_o_min_(0.00001), - IMS_gamma_k_min_(10.0), - IMS_slopefCut_(0.6), - IMS_slopegCut_(0.0), - IMS_cCut_(.05), + IMS_X_o_cutoff_(X_o_cutoff_default), + IMS_gamma_o_min_(gamma_o_min_default), + IMS_gamma_k_min_(gamma_k_min_default), + IMS_slopefCut_(slopefCut_default), + IMS_slopegCut_(slopegCut_default), + IMS_cCut_(cCut_default), IMS_dfCut_(0.0), IMS_efCut_(0.0), IMS_afCut_(0.0), @@ -51,12 +62,12 @@ IdealMolalSoln::IdealMolalSoln(XML_Node& root, const std::string& id_) : MolalityVPSSTP(), m_formGC(2), IMS_typeCutoff_(0), - IMS_X_o_cutoff_(0.2), - IMS_gamma_o_min_(0.00001), - IMS_gamma_k_min_(10.0), - IMS_slopefCut_(0.6), - IMS_slopegCut_(0.0), - IMS_cCut_(.05), + IMS_X_o_cutoff_(X_o_cutoff_default), + IMS_gamma_o_min_(gamma_o_min_default), + IMS_gamma_k_min_(gamma_k_min_default), + IMS_slopefCut_(slopefCut_default), + IMS_slopegCut_(slopegCut_default), + IMS_cCut_(cCut_default), IMS_dfCut_(0.0), IMS_efCut_(0.0), IMS_afCut_(0.0), @@ -386,24 +397,12 @@ void IdealMolalSoln::initThermo() if (m_input.hasKey("cutoff")) { auto& cutoff = m_input["cutoff"].as(); setCutoffModel(cutoff.getString("model", "none")); - if (cutoff.hasKey("gamma_o")) { - IMS_gamma_o_min_ = cutoff["gamma_o"].asDouble(); - } - if (cutoff.hasKey("gamma_k")) { - IMS_gamma_k_min_ = cutoff["gamma_k"].asDouble(); - } - if (cutoff.hasKey("X_o")) { - IMS_X_o_cutoff_ = cutoff["X_o"].asDouble(); - } - if (cutoff.hasKey("c_0")) { - IMS_cCut_ = cutoff["c_0"].asDouble(); - } - if (cutoff.hasKey("slope_f")) { - IMS_slopefCut_ = cutoff["slope_f"].asDouble(); - } - if (cutoff.hasKey("slope_g")) { - IMS_slopegCut_ = cutoff["slope_g"].asDouble(); - } + IMS_gamma_o_min_ = cutoff.getDouble("gamma_o", gamma_o_min_default); + IMS_gamma_k_min_ = cutoff.getDouble("gamma_k", gamma_k_min_default); + IMS_X_o_cutoff_ = cutoff.getDouble("X_o", X_o_cutoff_default); + IMS_cCut_ = cutoff.getDouble("c_0", cCut_default); + IMS_slopefCut_ = cutoff.getDouble("slope_f", slopefCut_default); + IMS_slopegCut_ = cutoff.getDouble("slope_g", slopegCut_default); } for (size_t k = 0; k < nSpecies(); k++) { @@ -415,6 +414,48 @@ void IdealMolalSoln::initThermo() setMoleFSolventMin(1.0E-5); } +void IdealMolalSoln::getParameters(AnyMap& phaseNode) const +{ + MolalityVPSSTP::getParameters(phaseNode); + + // "solvent-molar-volume" (m_formGC == 2) is the default, and can be omitted + if (m_formGC == 0) { + phaseNode["standard-concentration-basis"] = "unity"; + } else if (m_formGC == 1) { + phaseNode["standard-concentration-basis"] = "species-molar-volume"; + } + + AnyMap cutoff; + if (IMS_typeCutoff_ == 1) { + cutoff["model"] = "poly"; + } else if (IMS_typeCutoff_ == 2) { + cutoff["model"] = "polyexp"; + } + + if (IMS_gamma_o_min_ != gamma_o_min_default) { + cutoff["gamma_o"] = IMS_gamma_o_min_; + } + if (IMS_gamma_k_min_ != gamma_k_min_default) { + cutoff["gamma_k"] = IMS_gamma_k_min_; + } + if (IMS_X_o_cutoff_ != X_o_cutoff_default) { + cutoff["X_o"] = IMS_X_o_cutoff_; + } + if (IMS_cCut_ != cCut_default) { + cutoff["c_0"] = IMS_cCut_; + } + if (IMS_slopefCut_ != slopefCut_default) { + cutoff["slope_f"] = IMS_slopefCut_; + } + if (IMS_slopegCut_ != slopegCut_default) { + cutoff["slope_g"] = IMS_slopegCut_; + } + + if (cutoff.size()) { + phaseNode["cutoff"] = std::move(cutoff); + } +} + void IdealMolalSoln::setStandardConcentrationModel(const std::string& model) { if (caseInsensitiveEquals(model, "unity")) { diff --git a/src/thermo/IdealSolidSolnPhase.cpp b/src/thermo/IdealSolidSolnPhase.cpp index 79ccb7fc68..0576cc4b9a 100644 --- a/src/thermo/IdealSolidSolnPhase.cpp +++ b/src/thermo/IdealSolidSolnPhase.cpp @@ -418,6 +418,16 @@ void IdealSolidSolnPhase::initThermo() ThermoPhase::initThermo(); } +void IdealSolidSolnPhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + if (m_formGC == 1) { + phaseNode["standard-concentration-basis"] = "species-molar-volume"; + } else if (m_formGC == 2) { + phaseNode["standard-concentration-basis"] = "solvent-molar-volume"; + } +} + void IdealSolidSolnPhase::initThermoXML(XML_Node& phaseNode, const std::string& id_) { if (id_.size() > 0 && phaseNode.id() != id_) { diff --git a/src/thermo/IdealSolnGasVPSS.cpp b/src/thermo/IdealSolnGasVPSS.cpp index f73f21e5e6..293e9f5636 100644 --- a/src/thermo/IdealSolnGasVPSS.cpp +++ b/src/thermo/IdealSolnGasVPSS.cpp @@ -226,6 +226,17 @@ void IdealSolnGasVPSS::initThermo() } } +void IdealSolnGasVPSS::getParameters(AnyMap& phaseNode) const +{ + VPStandardStateTP::getParameters(phaseNode); + // "unity" (m_formGC == 0) is the default, and can be omitted + if (m_formGC == 1) { + phaseNode["standard-concentration-basis"] = "species-molar-volume"; + } else if (m_formGC == 2) { + phaseNode["standard-concentration-basis"] = "solvent-molar-volume"; + } +} + void IdealSolnGasVPSS::initThermoXML(XML_Node& phaseNode, const std::string& id_) { // Form of the standard concentrations. Must have one of: diff --git a/src/thermo/IonsFromNeutralVPSSTP.cpp b/src/thermo/IonsFromNeutralVPSSTP.cpp index 93220c2370..05740d00da 100644 --- a/src/thermo/IonsFromNeutralVPSSTP.cpp +++ b/src/thermo/IonsFromNeutralVPSSTP.cpp @@ -581,6 +581,14 @@ void IonsFromNeutralVPSSTP::initThermo() GibbsExcessVPSSTP::initThermo(); } +void IonsFromNeutralVPSSTP::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + if (neutralMoleculePhase_) { + phaseNode["neutral-phase"] = neutralMoleculePhase_->name(); + } +} + void IonsFromNeutralVPSSTP::setNeutralMoleculePhase(shared_ptr neutral) { neutralMoleculePhase_ = neutral; diff --git a/src/thermo/LatticePhase.cpp b/src/thermo/LatticePhase.cpp index ee81249eca..6b92f5a75f 100644 --- a/src/thermo/LatticePhase.cpp +++ b/src/thermo/LatticePhase.cpp @@ -297,6 +297,12 @@ void LatticePhase::initThermo() } } +void LatticePhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + phaseNode["site-density"] = m_site_density; +} + void LatticePhase::setParametersFromXML(const XML_Node& eosdata) { eosdata._require("model", "Lattice"); diff --git a/src/thermo/LatticeSolidPhase.cpp b/src/thermo/LatticeSolidPhase.cpp index 9166e822ba..5a74756335 100644 --- a/src/thermo/LatticeSolidPhase.cpp +++ b/src/thermo/LatticeSolidPhase.cpp @@ -17,7 +17,10 @@ #include "cantera/base/stringUtils.h" #include "cantera/base/utilities.h" +#include + using namespace std; +namespace ba = boost::algorithm; namespace Cantera { @@ -313,6 +316,26 @@ void LatticeSolidPhase::initThermo() ThermoPhase::initThermo(); } +void LatticeSolidPhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + AnyMap composition; + for (size_t i = 0; i < m_lattice.size(); i++) { + composition[m_lattice[i]->name()] = theta_[i]; + } + phaseNode["composition"] = std::move(composition); + + // Remove fields not used in this model + phaseNode.erase("species"); + vector elements; + for (auto& el : phaseNode["elements"].asVector()) { + if (!ba::starts_with(el, "LC_")) { + elements.push_back(el); + } + } + phaseNode["elements"] = elements; +} + bool LatticeSolidPhase::addSpecies(shared_ptr spec) { // Species are added from component phases in addLattice() diff --git a/src/thermo/MargulesVPSSTP.cpp b/src/thermo/MargulesVPSSTP.cpp index b86ed83d08..0b5adf07b6 100644 --- a/src/thermo/MargulesVPSSTP.cpp +++ b/src/thermo/MargulesVPSSTP.cpp @@ -216,6 +216,35 @@ void MargulesVPSSTP::initThermo() GibbsExcessVPSSTP::initThermo(); } +void MargulesVPSSTP::getParameters(AnyMap& phaseNode) const +{ + GibbsExcessVPSSTP::getParameters(phaseNode); + vector interactions; + for (size_t n = 0; n < m_pSpecies_A_ij.size(); n++) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(m_pSpecies_A_ij[n]), speciesName(m_pSpecies_B_ij[n])}; + if (m_HE_b_ij[n] != 0 || m_HE_c_ij[n] != 0) { + interaction["excess-enthalpy"] = + vector_fp{m_HE_b_ij[n], m_HE_c_ij[n]}; + } + if (m_SE_b_ij[n] != 0 || m_SE_c_ij[n] != 0) { + interaction["excess-entropy"] = + vector_fp{m_SE_b_ij[n], m_SE_c_ij[n]}; + } + if (m_VHE_b_ij[n] != 0 || m_VHE_c_ij[n] != 0) { + interaction["excess-volume-enthalpy"] = + vector_fp{m_VHE_b_ij[n], m_VHE_c_ij[n]}; + } + if (m_VSE_b_ij[n] != 0 || m_VSE_c_ij[n] != 0) { + interaction["excess-volume-entropy"] = + vector_fp{m_VSE_b_ij[n], m_VSE_c_ij[n]}; + } + interactions.push_back(std::move(interaction)); + } + phaseNode["interactions"] = std::move(interactions); +} + void MargulesVPSSTP::initLengths() { dlnActCoeffdlnN_.resize(m_kk, m_kk); diff --git a/src/thermo/MaskellSolidSolnPhase.cpp b/src/thermo/MaskellSolidSolnPhase.cpp index 32403feddc..0e29e8e16b 100644 --- a/src/thermo/MaskellSolidSolnPhase.cpp +++ b/src/thermo/MaskellSolidSolnPhase.cpp @@ -168,6 +168,12 @@ void MaskellSolidSolnPhase::initThermo() VPStandardStateTP::initThermo(); } +void MaskellSolidSolnPhase::getParameters(AnyMap& phaseNode) const +{ + VPStandardStateTP::getParameters(phaseNode); + phaseNode["excess-enthalpy"] = h_mixing; + phaseNode["product-species"] = speciesName(product_species_index); +} void MaskellSolidSolnPhase::initThermoXML(XML_Node& phaseNode, const std::string& id_) { diff --git a/src/thermo/PureFluidPhase.cpp b/src/thermo/PureFluidPhase.cpp index a12412de15..24e99d9d5c 100644 --- a/src/thermo/PureFluidPhase.cpp +++ b/src/thermo/PureFluidPhase.cpp @@ -62,6 +62,12 @@ void PureFluidPhase::initThermo() +name()+"\n", m_verbose); } +void PureFluidPhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + phaseNode["pure-fluid-name"] = m_sub->name(); +} + void PureFluidPhase::setParametersFromXML(const XML_Node& eosdata) { eosdata._require("model","PureFluid"); diff --git a/src/thermo/RedlichKisterVPSSTP.cpp b/src/thermo/RedlichKisterVPSSTP.cpp index de093c7d0a..b334ac8298 100644 --- a/src/thermo/RedlichKisterVPSSTP.cpp +++ b/src/thermo/RedlichKisterVPSSTP.cpp @@ -186,6 +186,28 @@ void RedlichKisterVPSSTP::initThermo() GibbsExcessVPSSTP::initThermo(); } +void RedlichKisterVPSSTP::getParameters(AnyMap& phaseNode) const +{ + GibbsExcessVPSSTP::getParameters(phaseNode); + vector interactions; + for (size_t n = 0; n < m_pSpecies_A_ij.size(); n++) { + AnyMap interaction; + interaction["species"] = vector{ + speciesName(m_pSpecies_A_ij[n]), speciesName(m_pSpecies_B_ij[n])}; + vector_fp h = m_HE_m_ij[n]; + vector_fp s = m_SE_m_ij[n]; + while (h.size() > 1 && h.back() == 0) { + h.pop_back(); + } + while (s.size() > 1 && s.back() == 0) { + s.pop_back(); + } + interaction["excess-enthalpy"] = std::move(h); + interaction["excess-entropy"] = std::move(s); + interactions.push_back(std::move(interaction)); + } + phaseNode["interactions"] = std::move(interactions);} + void RedlichKisterVPSSTP::initLengths() { dlnActCoeffdlnN_.resize(m_kk, m_kk); diff --git a/src/thermo/SurfPhase.cpp b/src/thermo/SurfPhase.cpp index 8bbb30ee50..8377c5b0f5 100644 --- a/src/thermo/SurfPhase.cpp +++ b/src/thermo/SurfPhase.cpp @@ -343,6 +343,12 @@ void SurfPhase::initThermo() } } +void SurfPhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + phaseNode["site-density"] = m_n0; +} + void SurfPhase::setStateFromXML(const XML_Node& state) { double t; diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index f1a4c52a98..364aae9ee7 100644 --- a/src/thermo/ThermoFactory.cpp +++ b/src/thermo/ThermoFactory.cpp @@ -51,6 +51,7 @@ ThermoFactory::ThermoFactory() addAlias("ideal-gas", "IdealGas"); reg("ideal-surface", []() { return new SurfPhase(); }); addAlias("ideal-surface", "Surface"); + addAlias("ideal-surface", "Surf"); reg("edge", []() { return new EdgePhase(); }); addAlias("edge", "Edge"); reg("electron-cloud", []() { return new MetalPhase(); }); @@ -65,27 +66,35 @@ ThermoFactory::ThermoFactory() addAlias("lattice", "Lattice"); reg("HMW-electrolyte", []() { return new HMWSoln(); }); addAlias("HMW-electrolyte", "HMW"); + addAlias("HMW-electrolyte", "HMWSoln"); reg("ideal-condensed", []() { return new IdealSolidSolnPhase(); }); addAlias("ideal-condensed", "IdealSolidSolution"); + addAlias("ideal-condensed", "IdealSolidSoln"); reg("Debye-Huckel", []() { return new DebyeHuckel(); }); addAlias("Debye-Huckel", "DebyeHuckel"); reg("ideal-molal-solution", []() { return new IdealMolalSoln(); }); addAlias("ideal-molal-solution", "IdealMolalSolution"); + addAlias("ideal-molal-solution", "IdealMolalSoln"); reg("ideal-solution-VPSS", []() { return new IdealSolnGasVPSS(); }); reg("ideal-gas-VPSS", []() { return new IdealSolnGasVPSS(); }); addAlias("ideal-solution-VPSS", "IdealSolnVPSS"); + addAlias("ideal-solution-VPSS", "IdealSolnGas"); addAlias("ideal-gas-VPSS", "IdealGasVPSS"); reg("Margules", []() { return new MargulesVPSSTP(); }); reg("ions-from-neutral-molecule", []() { return new IonsFromNeutralVPSSTP(); }); addAlias("ions-from-neutral-molecule", "IonsFromNeutralMolecule"); + addAlias("ions-from-neutral-molecule", "IonsFromNeutral"); reg("Redlich-Kister", []() { return new RedlichKisterVPSSTP(); }); + addAlias("Redlich-Kister", "RedlichKister"); reg("Redlich-Kwong", []() { return new RedlichKwongMFTP(); }); addAlias("Redlich-Kwong", "RedlichKwongMFTP"); addAlias("Redlich-Kwong", "RedlichKwong"); reg("Maskell-solid-solution", []() { return new MaskellSolidSolnPhase(); }); addAlias("Maskell-solid-solution", "MaskellSolidSolnPhase"); + addAlias("Maskell-solid-solution", "MaskellSolidsoln"); reg("liquid-water-IAPWS95", []() { return new WaterSSTP(); }); addAlias("liquid-water-IAPWS95", "PureLiquidWater"); + addAlias("liquid-water-IAPWS95", "Water"); reg("binary-solution-tabulated", []() { return new BinarySolutionTabulatedThermo(); }); addAlias("binary-solution-tabulated", "BinarySolutionTabulatedThermo"); } diff --git a/test/data/thermo-models.yaml b/test/data/thermo-models.yaml index 7c1f998ceb..2fad3a70a1 100644 --- a/test/data/thermo-models.yaml +++ b/test/data/thermo-models.yaml @@ -120,7 +120,7 @@ phases: activity-data: temperature-model: complex # "constant" or "linear" are the other options A_Debye: 1.175930 kg^0.5/gmol^0.5 - interactions: &hmw-ineractions + interactions: &hmw-interactions - species: [Na+, Cl-] beta0: [0.0765, 0.008946, -3.3158E-6, -777.03, -4.4706] beta1: [0.2664, 6.1608E-5, 1.0715E-6, 0.0, 0.0] @@ -157,7 +157,31 @@ phases: activity-data: temperature-model: complex # "constant" or "linear" are the other options A_Debye: variable - interactions: *hmw-ineractions + interactions: *hmw-interactions + +- name: HMW-bogus + species: + - {HMW-species: [H2O(L), Cl-, H+, Na+, OH-]} + - {dh-electrolyte-species: [NaCl(aq)]} + thermo: HMW-electrolyte + state: + T: 423.15 + P: 101325 + molalities: {Na+: 6.0997, Cl-: 6.0997, H+: 2.16e-9, OH-: 2.16e-9} + activity-data: + temperature-model: linear + interactions: + - species: [NaCl(aq), Cl-] + lambda: [0.3, 0.01] + - species: [NaCl(aq), Na+] + lambda: [0.2, 0.02] + - species: [NaCl(aq), Na+, OH-] + zeta: [0.5, 0.2] + - species: [NaCl(aq)] + mu: [0.0, 0.3] + cropping-coefficients: + ln_gamma_k_min: -8.0 + ln_gamma_k_max: 20 - name: CO2-RK species: [{rk-species: [CO2, H2O, H2]}] @@ -181,6 +205,12 @@ phases: species: [{ISSP-species: all}] state: {T: 500, P: 2 bar, X: {sp1: 0.1, sp2: 0.89, sp3: 0.01}} +- name: IdealSolidSolnPhase2 + thermo: ideal-condensed + standard-concentration-basis: solvent-molar-volume + species: [{ISSP-species: all}] + state: {T: 500, P: 2 bar, X: {sp1: 0.1, sp2: 0.89, sp3: 0.01}} + - name: Li7Si3(s) species: [{lattice-species: [Li7Si3(s)]}] thermo: fixed-stoichiometry diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index a3d126443c..41d6a88082 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -2,17 +2,27 @@ #include "cantera/thermo/ThermoFactory.h" using namespace Cantera; +typedef std::vector strvec; -shared_ptr newThermo(const std::string& fileName, - const std::string& phaseName) +class ThermoToYaml : public testing::Test { - return shared_ptr(newPhase(fileName, phaseName)); -} +public: + void setup(const std::string& fileName, const std::string& phaseName="") { + thermo.reset(newPhase(fileName, phaseName)); + // Because ThermoPhase::input may already contain the data we are trying + // to check for here, clear it so that the only parameters are those + // added by the overrides of getParameters. + thermo->input().clear(); + thermo->getParameters(data); + } -TEST(ThermoToYaml, simpleIdealGas) -{ - auto thermo = newThermo("ideal-gas.yaml", "simple"); + shared_ptr thermo; AnyMap data; +}; + +TEST_F(ThermoToYaml, simpleIdealGas) +{ + setup("ideal-gas.yaml", "simple"); thermo->setState_TP(1010, 2e5); double rho = thermo->density(); thermo->getParameters(data); @@ -21,3 +31,198 @@ TEST(ThermoToYaml, simpleIdealGas) ASSERT_EQ(data["state"]["T"], 1010); ASSERT_EQ(data["state"]["density"], rho); } + +TEST_F(ThermoToYaml, IdealSolidSoln) +{ + setup("thermo-models.yaml", "IdealSolidSolnPhase2"); + EXPECT_EQ(data["name"], "IdealSolidSolnPhase2"); + EXPECT_EQ(data["species"].asVector().size(), thermo->nSpecies()); + EXPECT_EQ(data["standard-concentration-basis"], "solvent-molar-volume"); +} + +TEST_F(ThermoToYaml, BinarySolutionTabulated) +{ + setup("thermo-models.yaml", "graphite-anode"); + EXPECT_EQ(data["tabulated-species"], "Li[anode]"); + auto& tabThermo = data["tabulated-thermo"].as(); + auto& X = tabThermo["mole-fractions"].asVector(); + auto& h = tabThermo["enthalpy"].asVector(); + auto& s = tabThermo["entropy"].asVector(); + EXPECT_DOUBLE_EQ(X[0], 5.75e-3); + EXPECT_DOUBLE_EQ(h[1], -9.69664e6); + EXPECT_DOUBLE_EQ(s[2], 1.27000e4); +} + +TEST_F(ThermoToYaml, Lattice) +{ + setup("thermo-models.yaml", "Li7Si3-interstitial"); + EXPECT_DOUBLE_EQ(data["site-density"].asDouble(), 1.046344e+01); +} + +TEST_F(ThermoToYaml, LatticeSolid) +{ + setup("thermo-models.yaml", "Li7Si3_and_interstitials"); + EXPECT_DOUBLE_EQ(data["composition"]["Li7Si3(s)"].asDouble(), 1.0); + EXPECT_DOUBLE_EQ(data["composition"]["Li7Si3-interstitial"].asDouble(), 1.0); +} + +TEST_F(ThermoToYaml, Metal) +{ + setup("thermo-models.yaml", "Metal"); + EXPECT_EQ(data["thermo"], "electron-cloud"); + EXPECT_DOUBLE_EQ(data["density"].asDouble(), 9.0); +} + +TEST_F(ThermoToYaml, PureFluid) +{ + setup("thermo-models.yaml", "nitrogen"); + EXPECT_EQ(data["thermo"], "pure-fluid"); + EXPECT_EQ(data["pure-fluid-name"], "nitrogen"); +} + +TEST_F(ThermoToYaml, Surface) +{ + setup("surface-phases.yaml", "Pt-surf"); + EXPECT_EQ(data["thermo"], "ideal-surface"); + EXPECT_DOUBLE_EQ(data["site-density"].asDouble(), 2.7063e-8); +} + +TEST_F(ThermoToYaml, Edge) +{ + setup("surface-phases.yaml", "TPB"); + EXPECT_EQ(data["thermo"], "edge"); + EXPECT_DOUBLE_EQ(data["site-density"].asDouble(), 5e-18); +} + +TEST_F(ThermoToYaml, IonsFromNeutral) +{ + setup("thermo-models.yaml", "ions-from-neutral-molecule"); + EXPECT_EQ(data["neutral-phase"], "KCl-neutral"); +} + +TEST_F(ThermoToYaml, Margules) +{ + setup("thermo-models.yaml", "molten-salt-Margules"); + auto& interactions = data["interactions"].asVector(); + EXPECT_EQ(interactions.size(), (size_t) 1); + EXPECT_EQ(interactions[0]["species"].asVector()[0], "KCl(l)"); + EXPECT_EQ(interactions[0]["excess-enthalpy"].asVector()[1], -377e3); +} + +TEST_F(ThermoToYaml, RedlichKister) +{ + setup("thermo-models.yaml", "Redlich-Kister-LiC6"); + auto& interactions = data["interactions"].asVector(); + EXPECT_EQ(interactions.size(), (size_t) 1); + auto& I = interactions[0]; + EXPECT_EQ(I["excess-enthalpy"].asVector().size(), (size_t) 15); + EXPECT_EQ(I["excess-entropy"].asVector().size(), (size_t) 1); +} + +TEST_F(ThermoToYaml, MaskellSolidSolution) +{ + setup("thermo-models.yaml", "MaskellSolidSoln"); + EXPECT_EQ(data["product-species"], "H(s)"); + EXPECT_DOUBLE_EQ(data["excess-enthalpy"].asDouble(), 5e3); +} + +TEST_F(ThermoToYaml, DebyeHuckel_B_dot_ak) +{ + setup("thermo-models.yaml", "debye-huckel-B-dot-ak"); + auto& ac = data["activity-data"]; + EXPECT_EQ(ac["model"], "B-dot-with-variable-a"); + EXPECT_DOUBLE_EQ(ac["B-dot"].asDouble(), 0.0410); + EXPECT_DOUBLE_EQ(ac["max-ionic-strength"].asDouble(), 50.0); + EXPECT_DOUBLE_EQ(ac["default-ionic-radius"].asDouble(), 4e-10); + EXPECT_FALSE(ac.as().hasKey("A_Debye")); + EXPECT_FALSE(ac.as().hasKey("B_Debye")); +} + +TEST_F(ThermoToYaml, DebyeHuckel_beta_ij) +{ + setup("thermo-models.yaml", "debye-huckel-beta_ij"); + EXPECT_EQ(data["activity-data"]["model"], "beta_ij"); + EXPECT_TRUE(data["activity-data"]["use-Helgeson-fixed-form"].asBool()); + auto& beta = data["activity-data"]["beta"].asVector(); + ASSERT_EQ(beta.size(), (size_t) 3); + for (size_t i = 0; i < 3; i++) { + auto species = beta[i]["species"].asVector(); + std::sort(species.begin(), species.end()); + if (species[0] == "Cl-" && species[1] == "H+") { + EXPECT_DOUBLE_EQ(beta[i]["beta"].asDouble(), 0.27); + } else if (species[0] == "Cl-" && species[1] == "Na+") { + EXPECT_DOUBLE_EQ(beta[i]["beta"].asDouble(), 0.15); + } else { + EXPECT_EQ(species[0], "Na+"); + EXPECT_EQ(species[1], "OH-"); + EXPECT_DOUBLE_EQ(beta[i]["beta"].asDouble(), 0.06); + } + } +} + +TEST_F(ThermoToYaml, HMWSoln1) +{ + setup("thermo-models.yaml", "HMW-NaCl-electrolyte"); + EXPECT_EQ(data["activity-data"]["temperature-model"], "complex"); + auto& interactions = data["activity-data"]["interactions"].asVector(); + EXPECT_EQ(interactions.size(), (size_t) 7); + for (auto& item : interactions) { + auto species = item["species"].asVector(); + std::sort(species.begin(), species.end()); + if (species == strvec{"Cl-", "Na+"}) { + auto& beta0 = item["beta0"].asVector(); + EXPECT_EQ(beta0.size(), (size_t) 5); + EXPECT_DOUBLE_EQ(beta0[1], 0.008946); + } else if (species == strvec{"Cl-", "H+"}) { + EXPECT_TRUE(item.hasKey("beta2")); + EXPECT_TRUE(item.hasKey("Cphi")); + } else if (species == strvec{"Na+", "OH-"}) { + EXPECT_DOUBLE_EQ(item["beta2"].asDouble(), 0.0); + } else if (species == strvec{"Cl-", "OH-"}) { + EXPECT_DOUBLE_EQ(item["theta"].asDouble(), -0.05); + } else if (species == strvec{"Cl-", "Na+", "OH-"}) { + EXPECT_DOUBLE_EQ(item["psi"].asDouble(), -0.006); + } else if (species == strvec{"H+", "Na+"}) { + EXPECT_DOUBLE_EQ(item["theta"].asDouble(), 0.036); + } else if (species == strvec{"Cl-", "H+", "Na+"}) { + EXPECT_DOUBLE_EQ(item["psi"].asDouble(), -0.004); + } else { + FAIL(); // unexpected set of species + } + } +} + +TEST_F(ThermoToYaml, HMWSoln2) +{ + setup("thermo-models.yaml", "HMW-bogus"); + EXPECT_EQ(data["activity-data"]["temperature-model"], "linear"); + auto& interactions = data["activity-data"]["interactions"].asVector(); + EXPECT_EQ(interactions.size(), (size_t) 4); + for (auto& item : interactions) { + auto species = item["species"].asVector(); + std::sort(species.begin(), species.end()); + if (species == strvec{"Cl-", "NaCl(aq)"}) { + EXPECT_DOUBLE_EQ(item["lambda"].asVector()[0], 0.3); + } else if (species == strvec{"Na+", "NaCl(aq)"}) { + EXPECT_DOUBLE_EQ(item["lambda"].asVector()[1], 0.02); + } else if (species == strvec{"Na+", "NaCl(aq)", "OH-"}) { + EXPECT_DOUBLE_EQ(item["zeta"].asVector()[0], 0.5); + } else if (species == strvec{"NaCl(aq)"}) { + EXPECT_DOUBLE_EQ(item["mu"].asVector()[1], 0.3); + } else { + FAIL(); // unexpected set of species + } + } + auto& crop = data["activity-data"]["cropping-coefficients"]; + EXPECT_DOUBLE_EQ(crop["ln_gamma_k_min"].asDouble(), -8.0); + EXPECT_DOUBLE_EQ(crop["ln_gamma_k_max"].asDouble(), 20); +} + +TEST_F(ThermoToYaml, IdealMolalSolution) +{ + setup("thermo-models.yaml", "ideal-molal-aqueous"); + auto& cutoff = data["cutoff"]; + EXPECT_EQ(cutoff["model"], "polyexp"); + EXPECT_EQ(cutoff.as().size(), (size_t) 2); // other values are defaults + EXPECT_DOUBLE_EQ(cutoff["gamma_o"].asDouble(), 0.0001); +} From 65dbcd3db74a625557b414d725112f3c35f25024 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 13 Dec 2019 17:12:39 -0500 Subject: [PATCH 11/57] [Input] Improve float formatting in YAML output --- include/cantera/base/YamlWriter.h | 10 ++++- src/base/AnyMap.cpp | 57 ++++++++++++++++++++++++++--- src/base/YamlWriter.cpp | 6 +++ test/general/test_serialization.cpp | 6 +-- 4 files changed, 69 insertions(+), 10 deletions(-) diff --git a/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h index 97f74eaa18..bf80aca91c 100644 --- a/include/cantera/base/YamlWriter.h +++ b/include/cantera/base/YamlWriter.h @@ -17,7 +17,7 @@ class Solution; class YamlWriter { public: - YamlWriter() {} + YamlWriter(); //! Include a phase definition for the specified Solution object void addPhase(shared_ptr soln); @@ -25,8 +25,16 @@ class YamlWriter std::string toYamlString() const; void toYamlFile(const std::string& filename) const; + //! For output floating point values, set the maximum number of digits to + //! the right of the decimal point. The default is 15 digits. + void setPrecision(long int n) { + m_float_precision = n; + } + protected: std::vector> m_phases; + + long int m_float_precision; }; } diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 360b24ae6c..3a52846f94 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -23,6 +23,7 @@ namespace { // helper functions std::mutex yaml_cache_mutex; using std::vector; +using std::string; bool isFloat(const std::string& val) { @@ -139,6 +140,45 @@ Type elementTypes(const YAML::Node& node) return types; } +string formatDouble(double x, const Cantera::AnyValue& precisionSource) +{ + long int precision = 15; + auto& userPrecision = precisionSource.getMetadata("precision"); + if (userPrecision.is()) { + precision = userPrecision.asInt(); + } + if (x == 0.0) { + return "0.0"; + } else if (std::abs(x) >= 0.01 && std::abs(x) < 10000) { + string s = fmt::format("{:.{}f}", x, precision); + // Trim trailing zeros, keeping at least one digit to the right of + // the decimal point + size_t last = s.size() - 1; + for (; last != 0; last--) { + if (s[last] != '0' || s[last-1] == '.') { + break; + } + } + s.resize(last + 1); + return s; + } else { + string s = fmt::format("{:.{}e}", x, precision); + // Trim trailing zeros, keeping at least one digit to the right of + // the decimal point + size_t eloc = s.find('e'); + size_t last = eloc - 1; + for (; last != 0; last--) { + if (s[last] != '0' || s[last-1] == '.') { + break; + } + } + if (last != eloc - 1) { + s = string(s, 0, last + 1) + string(s.begin() + eloc, s.end()); + } + return s; + } +} + Cantera::AnyValue Empty; } // end anonymous namespace @@ -194,7 +234,7 @@ struct convert { if (rhs.is()) { node = rhs.asString(); } else if (rhs.is()) { - node = rhs.asDouble(); + node = formatDouble(rhs.asDouble(), rhs); } else if (rhs.is()) { node = rhs.asInt(); } else if (rhs.is()) { @@ -208,7 +248,9 @@ struct convert { } else if (rhs.is>()) { node = rhs.asVector(); } else if (rhs.is>()) { - node = rhs.asVector(); + for (double x : rhs.asVector()) { + node.push_back(formatDouble(x, rhs)); + } node.SetStyle(YAML::EmitterStyle::Flow); } else if (rhs.is>()) { node = rhs.asVector(); @@ -222,9 +264,14 @@ struct convert { } else if (rhs.is>()) { node = rhs.asVector(); } else if (rhs.is>>()) { - node = rhs.asVector>(); - for (size_t i = 0; i < node.size(); i++) { - node[i].SetStyle(YAML::EmitterStyle::Flow); + const auto& doubleVals = rhs.asVector>(); + for (size_t i = 0; i < doubleVals.size(); i++) { + YAML::Node inner; + for (size_t j = 0; j < doubleVals[i].size(); j++) { + inner.push_back(formatDouble(doubleVals[i][j], rhs)); + } + inner.SetStyle(YAML::EmitterStyle::Flow); + node.push_back(inner); } } else if (rhs.is>>()) { node = rhs.asVector>(); diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index a18f30a8bb..eb9cb324f8 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -16,6 +16,11 @@ namespace Cantera { +YamlWriter::YamlWriter() + : m_float_precision(15) +{ +} + void YamlWriter::addPhase(shared_ptr soln) { for (auto& phase : m_phases) { if (phase->name() == soln->name()) { @@ -140,6 +145,7 @@ std::string YamlWriter::toYamlString() const } } + output.setMetadata("precision", AnyValue(m_float_precision)); return output.toYamlString(); } diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index 31c9f69ef6..f5d08a9c7e 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -61,6 +61,7 @@ TEST(YamlWriter, reactions) auto original = newSolution("h2o2.yaml"); YamlWriter writer; writer.addPhase(original); + writer.setPrecision(14); writer.toYamlFile("generated-h2o2.yaml"); auto duplicate = newSolution("generated-h2o2.yaml"); @@ -72,11 +73,8 @@ TEST(YamlWriter, reactions) kin1->getFwdRateConstants(kf1.data()); kin2->getFwdRateConstants(kf2.data()); for (size_t i = 0; i < kin1->nReactions(); i++) { - EXPECT_DOUBLE_EQ(kf1[i], kf2[i]) << "for reaction i = " << i; + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for reaction i = " << i; } - - AnyMap m = AnyMap::fromYamlFile("generated-h2o2.yaml"); - auto& reactions = m["reactions"].asVector(); } TEST(YamlWriter, multipleReactionSections) From 07e8ef5f3e69c867dbef1079edf0bfd004d7d9ae Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 15 Dec 2019 21:05:00 -0500 Subject: [PATCH 12/57] [Input] Use stream-based YAML output method to allow formatting control --- src/base/AnyMap.cpp | 203 ++++++++++++++++++++++++++++---------------- 1 file changed, 130 insertions(+), 73 deletions(-) diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 3a52846f94..994d2116c7 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -18,12 +18,12 @@ #include namespace ba = boost::algorithm; +using std::vector; +using std::string; namespace { // helper functions std::mutex yaml_cache_mutex; -using std::vector; -using std::string; bool isFloat(const std::string& val) { @@ -140,16 +140,25 @@ Type elementTypes(const YAML::Node& node) return types; } -string formatDouble(double x, const Cantera::AnyValue& precisionSource) +long int getPrecision(const Cantera::AnyValue& precisionSource) { long int precision = 15; auto& userPrecision = precisionSource.getMetadata("precision"); if (userPrecision.is()) { precision = userPrecision.asInt(); } + return precision; +} + +string formatDouble(double x, long int precision) +{ + int log10x = static_cast(std::floor(std::log10(std::abs(x)))); if (x == 0.0) { return "0.0"; - } else if (std::abs(x) >= 0.01 && std::abs(x) < 10000) { + } else if (log10x >= -2 && log10x <= 3) { + // Adjust precision to account for leading zeros or digits left of the + // decimal point + precision -= log10x; string s = fmt::format("{:.{}f}", x, precision); // Trim trailing zeros, keeping at least one digit to the right of // the decimal point @@ -190,11 +199,7 @@ using namespace Cantera; template<> struct convert { static Node encode(const Cantera::AnyMap& rhs) { - Node node; - for (const auto& item : rhs) { - node[item.first] = item.second; - } - return node; + throw NotImplementedError("AnyMap::encode"); } static bool decode(const Node& node, Cantera::AnyMap& target) { @@ -226,70 +231,124 @@ struct convert { } }; -template<> -struct convert { - static Node encode(const Cantera::AnyValue& rhs) { - Node node; - if (rhs.isScalar()) { - if (rhs.is()) { - node = rhs.asString(); - } else if (rhs.is()) { - node = formatDouble(rhs.asDouble(), rhs); - } else if (rhs.is()) { - node = rhs.asInt(); - } else if (rhs.is()) { - node = rhs.asBool(); - } else { - throw CanteraError("AnyValue::encode", "Don't know how to " - "encode value of type '{}'", rhs.type_str()); - } - } else if (rhs.is()) { - node = rhs.as(); - } else if (rhs.is>()) { - node = rhs.asVector(); - } else if (rhs.is>()) { - for (double x : rhs.asVector()) { - node.push_back(formatDouble(x, rhs)); - } - node.SetStyle(YAML::EmitterStyle::Flow); - } else if (rhs.is>()) { - node = rhs.asVector(); - node.SetStyle(YAML::EmitterStyle::Flow); - } else if (rhs.is>()) { - node = rhs.asVector(); - node.SetStyle(YAML::EmitterStyle::Flow); - } else if (rhs.is>()) { - node = rhs.asVector(); - node.SetStyle(YAML::EmitterStyle::Flow); - } else if (rhs.is>()) { - node = rhs.asVector(); - } else if (rhs.is>>()) { - const auto& doubleVals = rhs.asVector>(); - for (size_t i = 0; i < doubleVals.size(); i++) { - YAML::Node inner; - for (size_t j = 0; j < doubleVals[i].size(); j++) { - inner.push_back(formatDouble(doubleVals[i][j], rhs)); - } - inner.SetStyle(YAML::EmitterStyle::Flow); - node.push_back(inner); - } - } else if (rhs.is>>()) { - node = rhs.asVector>(); - for (size_t i = 0; i < node.size(); i++) { - node[i].SetStyle(YAML::EmitterStyle::Flow); - } - } else if (rhs.is>>()) { - node = rhs.asVector>(); - for (size_t i = 0; i < node.size(); i++) { - node[i].SetStyle(YAML::EmitterStyle::Flow); - } - } else if (rhs.is>>()) { - node = rhs.asVector>(); +YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs); + +YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) +{ + out << YAML::BeginMap; + for (const auto& item : rhs) { + out << item.first; + out << item.second; + } + out << YAML::EndMap; + return out; +} + +void emitFlowVector(YAML::Emitter& out, const vector& v, double precision) +{ + out << YAML::Flow; + out << YAML::BeginSeq; + size_t width = 15; // wild guess, but no better value is available + for (auto& x : v) { + string xstr = formatDouble(x, precision); + if (width + xstr.size() > 79) { + out << YAML::Newline; + width = 15; + } + out << xstr; + width += xstr.size() + 2; + } + out << YAML::EndSeq; +} + +template +void emitFlowVector(YAML::Emitter& out, const vector& v) +{ + out << YAML::Flow; + out << YAML::BeginSeq; + size_t width = 15; // wild guess, but no better value is available + for (const auto& x : v) { + string xstr = fmt::format("{}", x); + if (width + xstr.size() > 79) { + out << YAML::Newline; + width = 15; + } + out << xstr; + width += xstr.size() + 2; + } + out << YAML::EndSeq; +} + +YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs) +{ + if (rhs.isScalar()) { + if (rhs.is()) { + out << rhs.asString(); + } else if (rhs.is()) { + out << formatDouble(rhs.asDouble(), getPrecision(rhs)); + } else if (rhs.is()) { + out << rhs.asInt(); + } else if (rhs.is()) { + out << rhs.asBool(); } else { - throw CanteraError("AnyValue::encode", + throw CanteraError("operator<<(YAML::Emitter&, AnyValue&)", "Don't know how to encode value of type '{}'", rhs.type_str()); } - return node; + } else if (rhs.is()) { + out << rhs.as(); + } else if (rhs.is>()) { + out << rhs.asVector(); + } else if (rhs.is>()) { + emitFlowVector(out, rhs.asVector(), getPrecision(rhs)); + } else if (rhs.is>()) { + emitFlowVector(out, rhs.asVector()); + } else if (rhs.is>()) { + emitFlowVector(out, rhs.asVector()); + } else if (rhs.is>()) { + emitFlowVector(out, rhs.asVector()); + } else if (rhs.is>()) { + out << rhs.asVector(); + } else if (rhs.is>>()) { + const auto& v = rhs.asVector>(); + long int precision = getPrecision(rhs); + out << YAML::BeginSeq; + for (const auto& u : v) { + emitFlowVector(out, u, precision); + } + out << YAML::EndSeq; + } else if (rhs.is>>()) { + const auto& v = rhs.asVector>(); + out << YAML::BeginSeq; + for (const auto& u : v) { + emitFlowVector(out, u); + } + out << YAML::EndSeq; + } else if (rhs.is>>()) { + const auto& v = rhs.asVector>(); + out << YAML::BeginSeq; + for (const auto& u : v) { + emitFlowVector(out, u); + } + out << YAML::EndSeq; + } else if (rhs.is>>()) { + const auto& v = rhs.asVector>(); + out << YAML::BeginSeq; + for (const auto& u : v) { + emitFlowVector(out, u); + } + out << YAML::EndSeq; + } else { + throw CanteraError("operator<<(YAML::Emitter&, AnyValue&)", + "Don't know how to encode value of type '{}'", rhs.type_str()); + } + return out; +} + + +template<> +struct convert { + static Node encode(const Cantera::AnyValue& rhs) { + throw NotImplementedError(""); } static bool decode(const Node& node, Cantera::AnyValue& target) { @@ -1322,9 +1381,7 @@ AnyMap AnyMap::fromYamlFile(const std::string& name, std::string AnyMap::toYamlString() const { YAML::Emitter out; - YAML::Node node; - node = *this; - out << node; + out << *this; out << YAML::Newline; return out.c_str(); } From 2d9bd81e4f92ba7b2917af2854af473e8649b5b9 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 4 Jan 2020 21:51:24 -0500 Subject: [PATCH 13/57] [Input] Enable optional use of flow mode for outputting YAML maps --- include/cantera/base/AnyMap.h | 6 ++++ src/base/AnyMap.cpp | 57 ++++++++++++++++++++++++++++++++--- src/kinetics/Falloff.cpp | 2 ++ src/kinetics/Reaction.cpp | 2 ++ src/kinetics/RxnRates.cpp | 1 + src/thermo/Species.cpp | 1 + 6 files changed, 64 insertions(+), 5 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index dfef64e65e..80d86b7715 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -220,6 +220,9 @@ class AnyValue : public AnyBase //! @see AnyMap::applyUnits void applyUnits(const UnitSystem& units); + //! @see AnyMap::setFlowStyle + void setFlowStyle(bool flow=true); + private: std::string demangle(const std::type_info& type) const; @@ -507,6 +510,9 @@ class AnyMap : public AnyBase */ void applyUnits(const UnitSystem& units); + //! Use "flow" style when outputting this AnyMap to YAML + void setFlowStyle(bool flow=true); + private: //! The stored data std::unordered_map m_data; diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 994d2116c7..1673ece2d8 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -235,16 +235,55 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs); YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) { - out << YAML::BeginMap; - for (const auto& item : rhs) { - out << item.first; - out << item.second; + bool flow = rhs.getBool("__flow__", false); + if (flow) { + out << YAML::Flow; + out << YAML::BeginMap; + size_t width = 15; + for (const auto& item : rhs) { + string value; + bool foundType = true; + if (item.second.is()) { + value = formatDouble(item.second.asDouble(), + getPrecision(item.second)); + } else if (item.second.is()) { + value = item.second.asString(); + } else if (item.second.is()) { + value = fmt::format("{}", item.second.asInt()); + } else if (item.second.is()) { + value = fmt::format("{}", item.second.asBool()); + } else { + foundType = false; + } + + if (foundType) { + if (width + item.first.size() + value.size() + 4 > 79) { + out << YAML::Newline; + width = 15; + } + out << item.first; + out << value; + width += item.first.size() + value.size() + 4; + } else { + // Put items of an unknown (compound) type on a line alone + out << YAML::Newline; + out << item.first; + out << item.second; + width = 99; // Force newline after this item as well + } + } + } else { + out << YAML::BeginMap; + for (const auto& item : rhs) { + out << item.first; + out << item.second; + } } out << YAML::EndMap; return out; } -void emitFlowVector(YAML::Emitter& out, const vector& v, double precision) +void emitFlowVector(YAML::Emitter& out, const vector& v, long int precision) { out << YAML::Flow; out << YAML::BeginSeq; @@ -946,7 +985,11 @@ void AnyValue::applyUnits(const UnitSystem& units) } } } +} +void AnyValue::setFlowStyle(bool flow) +{ + as().setFlowStyle(); } std::string AnyValue::demangle(const std::type_info& type) const @@ -1303,6 +1346,10 @@ void AnyMap::applyUnits(const UnitSystem& units) { } } +void AnyMap::setFlowStyle(bool flow) { + (*this)["__flow__"] = flow; +} + AnyMap AnyMap::fromYamlString(const std::string& yaml) { AnyMap amap; try { diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index aafdf4b59b..698b63d36a 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -93,6 +93,7 @@ void Troe::getParameters(AnyMap& reactionNode) const if (std::abs(m_t2) > SmallNumber) { params["T2"] = m_t2; } + params.setFlowStyle(); reactionNode["Troe"] = std::move(params); } @@ -160,6 +161,7 @@ void SRI::getParameters(AnyMap& reactionNode) const params["D"] = m_d; params["E"] = m_e; } + params.setFlowStyle(); reactionNode["SRI"] = std::move(params); } diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index d951b94803..c50db7f4aa 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -237,6 +237,7 @@ void ThreeBodyReaction::getParameters(AnyMap& reactionNode) const ElementaryReaction::getParameters(reactionNode); reactionNode["type"] = "three-body"; reactionNode["efficiencies"] = third_body.efficiencies; + reactionNode["efficiencies"].setFlowStyle(); if (third_body.default_efficiency != 1.0) { reactionNode["default-efficiency"] = third_body.default_efficiency; } @@ -311,6 +312,7 @@ void FalloffReaction::getParameters(AnyMap& reactionNode) const falloff->getParameters(reactionNode); reactionNode["efficiencies"] = third_body.efficiencies; + reactionNode["efficiencies"].setFlowStyle(); if (third_body.default_efficiency != 1.0) { reactionNode["default-efficiency"] = third_body.default_efficiency; } diff --git a/src/kinetics/RxnRates.cpp b/src/kinetics/RxnRates.cpp index 349580b552..155b0bf7ee 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -56,6 +56,7 @@ void Arrhenius::getParameters(AnyMap& rateNode) const rateNode["A"] = preExponentialFactor(); rateNode["b"] = temperatureExponent(); rateNode["Ea"] = activationEnergy_R() * GasConstant; + rateNode.setFlowStyle(); } SurfaceArrhenius::SurfaceArrhenius() diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 958a603ba1..8fdba4ff6d 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -37,6 +37,7 @@ void Species::getParameters(AnyMap& speciesNode) const { speciesNode["name"] = name; speciesNode["composition"] = composition; + speciesNode["composition"].setFlowStyle(); if (charge != 0) { speciesNode["charge"] = charge; From c5c8bdf9c23b818713ce26d493f8b37c98affe57 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 5 Jan 2020 15:30:14 -0500 Subject: [PATCH 14/57] [Input] Preserve order of items when outputting YAML maps The default order is the order in which items are added. Items originating from an input file come after programmatically added items, and retain their existing ordering. --- include/cantera/base/AnyMap.h | 11 +++- include/cantera/kinetics/Reaction.h | 3 +- include/cantera/thermo/ThermoPhase.h | 3 +- include/cantera/transport/TransportData.h | 3 +- src/base/AnyMap.cpp | 78 ++++++++++++++++------- src/base/YamlWriter.cpp | 23 +++++++ src/kinetics/Falloff.cpp | 4 +- src/kinetics/Reaction.cpp | 7 +- src/thermo/ConstCpPoly.cpp | 2 +- src/thermo/Nasa9PolyMultiTempRegion.cpp | 2 +- src/thermo/NasaPoly2.cpp | 2 +- src/thermo/Species.cpp | 15 ++--- src/thermo/ThermoPhase.cpp | 6 -- src/transport/TransportData.cpp | 5 -- test/data/ideal-gas.yaml | 4 ++ 15 files changed, 105 insertions(+), 63 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 80d86b7715..39fc752df3 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -38,10 +38,12 @@ class AnyBase { const AnyValue& getMetadata(const std::string& key) const; protected: - //! Line where this node occurs in the input file + //! The line where this value occurs in the input file. Set to -1 for values + //! that weren't created from an input file. int m_line; - //! Column where this node occurs in the input file + //! If m_line >= 0, the column where this value occurs in the input file. + //! If m_line == -1, a value used for determining output ordering int m_column; //! Metadata relevant to an entire AnyMap tree, such as information about @@ -217,6 +219,9 @@ class AnyValue : public AnyBase //! Returns `true` when getMapWhere() would succeed bool hasMapWhere(const std::string& key, const std::string& value) const; + //! Return values used to determine the sort order when outputting to YAML + std::pair order() const; + //! @see AnyMap::applyUnits void applyUnits(const UnitSystem& units); @@ -358,7 +363,7 @@ std::vector& AnyValue::asVector(size_t nMin, size_t nMax); class AnyMap : public AnyBase { public: - AnyMap(): m_units() {}; + AnyMap(); //! Create an AnyMap from a YAML file. /*! diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index 67f258b0c3..d33c4b2e04 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -61,7 +61,8 @@ class Reaction } //! Store the parameters of a Reaction needed to reconstruct an identical - //! object using the newReaction(AnyMap&, Kinetics&) function. + //! object using the newReaction(AnyMap&, Kinetics&) function. Does not + //! include user-defined fields available in the #input map. virtual void getParameters(AnyMap& reactionNode) const; //! Type of the reaction. The valid types are listed in the file, diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index 4452fee565..ce4ecf9993 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -1710,7 +1710,8 @@ class ThermoPhase : public Phase const AnyMap& rootNode=AnyMap()); //! Store the parameters of a ThermoPhase object such that an identical - //! one could be reconstructed using the newPhase(AnyMap&) function. + //! one could be reconstructed using the newPhase(AnyMap&) function. This + //! does not include user-defined fields available in input(). virtual void getParameters(AnyMap& phaseNode) const; //! Access input data associated with the phase description diff --git a/include/cantera/transport/TransportData.h b/include/cantera/transport/TransportData.h index f496a34b4e..715de3a28c 100644 --- a/include/cantera/transport/TransportData.h +++ b/include/cantera/transport/TransportData.h @@ -24,7 +24,8 @@ class TransportData virtual void validate(const Species& species) {} - //! Store the parameters needed to reconstruct a TransportData object + //! Store the parameters needed to reconstruct a TransportData object. Does + //! not include user-defined fields available in #input. virtual void getParameters(AnyMap& transportNode) const; //! Input data used for specific models diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 1673ece2d8..0a93b24373 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -8,6 +8,7 @@ #include "cantera/base/yaml.h" #include "cantera/base/stringUtils.h" #include "cantera/base/global.h" +#include "cantera/base/utilities.h" #ifdef CT_USE_DEMANGLE #include #endif @@ -24,6 +25,7 @@ using std::string; namespace { // helper functions std::mutex yaml_cache_mutex; +using namespace Cantera; bool isFloat(const std::string& val) { @@ -235,48 +237,56 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs); YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) { + vector, std::string, const AnyValue*>> ordered; + for (const auto& item : rhs) { + ordered.emplace_back(item.second.order(), item.first, &item.second); + } + std::sort(ordered.begin(), ordered.end()); + bool flow = rhs.getBool("__flow__", false); if (flow) { out << YAML::Flow; out << YAML::BeginMap; size_t width = 15; - for (const auto& item : rhs) { - string value; + for (const auto& item : ordered) { + const auto& name = std::get<1>(item); + const auto& value = *std::get<2>(item); + string valueStr; bool foundType = true; - if (item.second.is()) { - value = formatDouble(item.second.asDouble(), - getPrecision(item.second)); - } else if (item.second.is()) { - value = item.second.asString(); - } else if (item.second.is()) { - value = fmt::format("{}", item.second.asInt()); - } else if (item.second.is()) { - value = fmt::format("{}", item.second.asBool()); + if (value.is()) { + valueStr = formatDouble(value.asDouble(), + getPrecision(value)); + } else if (value.is()) { + valueStr = value.asString(); + } else if (value.is()) { + valueStr = fmt::format("{}", value.asInt()); + } else if (value.is()) { + valueStr = fmt::format("{}", value.asBool()); } else { foundType = false; } if (foundType) { - if (width + item.first.size() + value.size() + 4 > 79) { + if (width + name.size() + valueStr.size() + 4 > 79) { out << YAML::Newline; width = 15; } - out << item.first; - out << value; - width += item.first.size() + value.size() + 4; + out << name; + out << valueStr; + width += name.size() + valueStr.size() + 4; } else { // Put items of an unknown (compound) type on a line alone out << YAML::Newline; - out << item.first; - out << item.second; + out << name; + out << value; width = 99; // Force newline after this item as well } } } else { out << YAML::BeginMap; - for (const auto& item : rhs) { - out << item.first; - out << item.second; + for (const auto& item : ordered) { + out << std::get<1>(item); + out << *std::get<2>(item); } } out << YAML::EndMap; @@ -482,7 +492,7 @@ std::unordered_map> AnyMap::s_cache; AnyBase::AnyBase() : m_line(-1) - , m_column(-1) + , m_column(0) {} void AnyBase::setLoc(int line, int column) @@ -950,6 +960,11 @@ bool AnyValue::hasMapWhere(const std::string& key, const std::string& value) con } } +std::pair AnyValue::order() const +{ + return {m_line, m_column}; +} + void AnyValue::applyUnits(const UnitSystem& units) { if (is()) { @@ -1148,6 +1163,11 @@ std::vector& AnyValue::asVector(size_t nMin, size_t nMax) // Methods of class AnyMap +AnyMap::AnyMap() + : m_units() +{ +} + AnyValue& AnyMap::operator[](const std::string& key) { const auto& iter = m_data.find(key); @@ -1158,11 +1178,21 @@ AnyValue& AnyMap::operator[](const std::string& key) AnyValue& value = m_data.insert({key, AnyValue()}).first->second; value.setKey(key); if (m_metadata) { - // Approximate location, useful mainly if this insertion is going to - // immediately result in an error that needs to be reported. - value.setLoc(m_line, m_column); value.propagateMetadata(m_metadata); } + + // If the AnyMap is being created from an input file, this is an + // approximate location, useful mainly if this insertion is going to + // immediately result in an error that needs to be reported. Otherwise, + // this is the location used to set the ordering when outputting to + // YAML. + value.setLoc(m_line, m_column); + + if (m_line == -1) { + // use m_column as a surrogate for ordering the next item added + m_column += 10; + } + return value; } else { // Return an already-existing item diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index eb9cb324f8..241b87197e 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -56,6 +56,13 @@ std::string YamlWriter::toYamlString() const if (tran) { tran->getParameters(phaseDefs[i]); } + + // user-defined fields + for (const auto& item : m_phases[i]->thermo()->input()) { + if (!phaseDefs[i].hasKey(item.first)) { + phaseDefs[i][item.first] = item.second; + } + } } output["phases"] = phaseDefs; @@ -68,6 +75,14 @@ std::string YamlWriter::toYamlString() const const auto& species = phase->thermo()->species(name); AnyMap speciesDef; species->getParameters(speciesDef); + + // user-defined fields + for (const auto& item : species->input) { + if (!speciesDef.hasKey(item.first)) { + speciesDef[item.first] = item.second; + } + } + if (speciesDefIndex.count(name) == 0) { speciesDefs.emplace_back(speciesDef); speciesDefIndex[name] = speciesDefs.size() - 1; @@ -94,6 +109,14 @@ std::string YamlWriter::toYamlString() const const auto reaction = kin->reaction(i); AnyMap reactionDef; reaction->getParameters(reactionDef); + + // user-defined fields + for (const auto& item : reaction->input) { + if (!reactionDef.hasKey(item.first)) { + reactionDef[item.first] = item.second; + } + } + reactions.push_back(std::move(reactionDef)); } allReactions[phase->name()] = std::move(reactions); diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index 698b63d36a..309b31c935 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -87,7 +87,7 @@ void Troe::getParameters(double* params) const { void Troe::getParameters(AnyMap& reactionNode) const { AnyMap params; - params["A"]= m_a; + params["A"] = m_a; params["T3"] = 1.0 / m_rt3; params["T1"] = 1.0 / m_rt1; if (std::abs(m_t2) > SmallNumber) { @@ -154,7 +154,7 @@ void SRI::getParameters(double* params) const void SRI::getParameters(AnyMap& reactionNode) const { AnyMap params; - params["A"]= m_a; + params["A"] = m_a; params["B"] = m_b; params["C"] = m_c; if (m_d != 1.0 || m_e != 0.0) { diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index c50db7f4aa..a55760a7a8 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -101,6 +101,7 @@ void Reaction::validate() void Reaction::getParameters(AnyMap& reactionNode) const { reactionNode["equation"] = equation(); + if (duplicate) { reactionNode["duplicate"] = true; } @@ -113,12 +114,6 @@ void Reaction::getParameters(AnyMap& reactionNode) const if (allow_nonreactant_orders) { reactionNode["nonreactant-orders"] = true; } - - for (const auto& item : input) { - if (!reactionNode.hasKey(item.first)) { - reactionNode[item.first] = item.second; - } - } } std::string Reaction::reactantString() const diff --git a/src/thermo/ConstCpPoly.cpp b/src/thermo/ConstCpPoly.cpp index f0b7497e50..92ed311057 100644 --- a/src/thermo/ConstCpPoly.cpp +++ b/src/thermo/ConstCpPoly.cpp @@ -84,8 +84,8 @@ void ConstCpPoly::reportParameters(size_t& n, int& type, void ConstCpPoly::getParameters(AnyMap& thermo) const { - SpeciesThermoInterpType::getParameters(thermo); thermo["model"] = "constant-cp"; + SpeciesThermoInterpType::getParameters(thermo); thermo["T0"] = m_t0; thermo["h0"] = m_h0_R * GasConstant; thermo["s0"] = m_s0_R * GasConstant; diff --git a/src/thermo/Nasa9PolyMultiTempRegion.cpp b/src/thermo/Nasa9PolyMultiTempRegion.cpp index 9969febc5b..a209ec46ac 100644 --- a/src/thermo/Nasa9PolyMultiTempRegion.cpp +++ b/src/thermo/Nasa9PolyMultiTempRegion.cpp @@ -191,8 +191,8 @@ void Nasa9PolyMultiTempRegion::reportParameters(size_t& n, int& type, void Nasa9PolyMultiTempRegion::getParameters(AnyMap& thermo) const { - SpeciesThermoInterpType::getParameters(thermo); thermo["model"] = "NASA9"; + SpeciesThermoInterpType::getParameters(thermo); auto T_ranges = m_lowerTempBounds; T_ranges.push_back(m_highT); thermo["temperature-ranges"] = T_ranges; diff --git a/src/thermo/NasaPoly2.cpp b/src/thermo/NasaPoly2.cpp index 64223e7b38..bfb38c43ac 100644 --- a/src/thermo/NasaPoly2.cpp +++ b/src/thermo/NasaPoly2.cpp @@ -24,8 +24,8 @@ void NasaPoly2::setParameters(double Tmid, const vector_fp& low, void NasaPoly2::getParameters(AnyMap& thermo) const { - SpeciesThermoInterpType::getParameters(thermo); thermo["model"] = "NASA7"; + SpeciesThermoInterpType::getParameters(thermo); vector_fp Tranges {m_lowT, m_midT, m_highT}; thermo["temperature-ranges"] = Tranges; thermo["data"] = std::vector(); diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 8fdba4ff6d..93aebdf127 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -54,17 +54,10 @@ void Species::getParameters(AnyMap& speciesNode) const AnyMap transportNode; transport->getParameters(transportNode); speciesNode["transport"] = transportNode; - } - - for (const auto& item : input) { - if (!speciesNode.hasKey(item.first)) { - speciesNode[item.first] = item.second; - } - } - - for (const auto& item : extra) { - if (!speciesNode.hasKey(item.first)) { - speciesNode[item.first] = item.second; + for (const auto& item : transport->input) { + if (!transportNode.hasKey(item.first)) { + transportNode[item.first] = item.second; + } } } } diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 81000e3a30..a5bcc5288a 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1212,12 +1212,6 @@ void ThermoPhase::getParameters(AnyMap& phaseNode) const } state["Y"] = Y; phaseNode["state"] = state; - - for (const auto& item : m_input) { - if (!phaseNode.hasKey(item.first)) { - phaseNode[item.first] = item.second; - } - } } const AnyMap& ThermoPhase::input() const diff --git a/src/transport/TransportData.cpp b/src/transport/TransportData.cpp index 295a69e68b..03886c723a 100644 --- a/src/transport/TransportData.cpp +++ b/src/transport/TransportData.cpp @@ -16,11 +16,6 @@ namespace Cantera void TransportData::getParameters(AnyMap &transportNode) const { - for (const auto& item : input) { - if (!transportNode.hasKey(item.first)) { - transportNode[item.first] = item.second; - } - } } GasTransportData::GasTransportData() diff --git a/test/data/ideal-gas.yaml b/test/data/ideal-gas.yaml index ee83a98d68..a3371caa3e 100644 --- a/test/data/ideal-gas.yaml +++ b/test/data/ideal-gas.yaml @@ -4,6 +4,10 @@ phases: - name: simple thermo: ideal-gas elements: [N, O] + custom-field: + first: true + second: [3.0, 5.0] + last: [100, 200, 300] species: [O2, NO, N2] state: {T: 500.0, P: 10 atm, X: {O2: 0.21, N2: 0.79}} From 19f4ed9eef542439076f2c8c5c95c3ff634ecd47 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 6 Jan 2020 23:12:03 -0500 Subject: [PATCH 15/57] [Input] Retain input species thermo data --- .../cantera/thermo/SpeciesThermoInterpType.h | 8 +++++++- src/thermo/Mu0Poly.cpp | 10 +++++++++- src/thermo/Species.cpp | 9 +++++++-- src/thermo/SpeciesThermoFactory.cpp | 1 + src/thermo/SpeciesThermoInterpType.cpp | 10 ++++++++++ test/data/ideal-gas.yaml | 10 ++++++++++ test/general/test_serialization.cpp | 18 ++++++++++++++++++ test/thermo/thermoParameterizations.cpp | 8 ++++---- 8 files changed, 66 insertions(+), 8 deletions(-) diff --git a/include/cantera/thermo/SpeciesThermoInterpType.h b/include/cantera/thermo/SpeciesThermoInterpType.h index a360a01b4a..60baab1dc9 100644 --- a/include/cantera/thermo/SpeciesThermoInterpType.h +++ b/include/cantera/thermo/SpeciesThermoInterpType.h @@ -14,12 +14,12 @@ #include "cantera/base/ct_defs.h" #include "cantera/base/ctexceptions.h" +#include "cantera/base/AnyMap.h" namespace Cantera { class PDSS; -class AnyMap; /** * @defgroup spthermo Species Reference-State Thermodynamic Properties @@ -268,6 +268,10 @@ class SpeciesThermoInterpType throw NotImplementedError("SpeciesThermoInterpType::resetHf298"); } + //! Access input data associated with the species thermo definition + const AnyMap& input() const; + AnyMap& input(); + protected: //! lowest valid temperature doublereal m_lowT; @@ -275,6 +279,8 @@ class SpeciesThermoInterpType doublereal m_highT; //! Reference state pressure doublereal m_Pref; + + AnyMap m_input; }; } diff --git a/src/thermo/Mu0Poly.cpp b/src/thermo/Mu0Poly.cpp index d791b76885..8fb98bfc68 100644 --- a/src/thermo/Mu0Poly.cpp +++ b/src/thermo/Mu0Poly.cpp @@ -163,8 +163,16 @@ void Mu0Poly::getParameters(AnyMap& thermo) const thermo["model"] = "piecewise-Gibbs"; thermo["h0"] = m_H298 * GasConstant; map data; + bool dimensionless = m_input.getBool("dimensionless", false); + if (dimensionless) { + thermo["dimensionless"] = true; + } for (size_t i = 0; i < m_numIntervals+1; i++) { - data[fmt::format("{}", m_t0_int[i])] = m_mu0_R_int[i] * GasConstant; + if (dimensionless) { + data[fmt::format("{}", m_t0_int[i])] = m_mu0_R_int[i] / m_t0_int[i]; + } else { + data[fmt::format("{}", m_t0_int[i])] = m_mu0_R_int[i] * GasConstant; + } } thermo["data"] = data; } diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 93aebdf127..1a7d65aceb 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -48,7 +48,12 @@ void Species::getParameters(AnyMap& speciesNode) const AnyMap thermoNode; thermo->getParameters(thermoNode); - speciesNode["thermo"] = thermoNode; + for (const auto& item : thermo->input()) { + if (!thermoNode.hasKey(item.first)) { + thermoNode[item.first] = item.second; + } + } + speciesNode["thermo"] = std::move(thermoNode); if (transport) { AnyMap transportNode; @@ -139,7 +144,7 @@ unique_ptr newSpecies(const AnyMap& node) // Store input parameters in the "input" map, unless they are stored in a // child object const static std::set known_keys{ - "transport" + "thermo", "transport" }; s->input.applyUnits(node.units()); for (const auto& item : node) { diff --git a/src/thermo/SpeciesThermoFactory.cpp b/src/thermo/SpeciesThermoFactory.cpp index 48ec0b9cab..14d141bbdf 100644 --- a/src/thermo/SpeciesThermoFactory.cpp +++ b/src/thermo/SpeciesThermoFactory.cpp @@ -149,6 +149,7 @@ void setupSpeciesThermo(SpeciesThermoInterpType& thermo, { double Pref = node.convert("reference-pressure", "Pa", OneAtm); thermo.setRefPressure(Pref); + thermo.input() = node; } void setupNasaPoly(NasaPoly2& thermo, const AnyMap& node) diff --git a/src/thermo/SpeciesThermoInterpType.cpp b/src/thermo/SpeciesThermoInterpType.cpp index 0135805f11..a051e57c68 100644 --- a/src/thermo/SpeciesThermoInterpType.cpp +++ b/src/thermo/SpeciesThermoInterpType.cpp @@ -70,4 +70,14 @@ void SpeciesThermoInterpType::modifyOneHf298(const size_t k, throw NotImplementedError("SpeciesThermoInterpType::modifyOneHf298"); } +const AnyMap& SpeciesThermoInterpType::input() const +{ + return m_input; +} + +AnyMap& SpeciesThermoInterpType::input() +{ + return m_input; +} + } diff --git a/test/data/ideal-gas.yaml b/test/data/ideal-gas.yaml index a3371caa3e..448c69d68d 100644 --- a/test/data/ideal-gas.yaml +++ b/test/data/ideal-gas.yaml @@ -99,6 +99,7 @@ species: composition: {N: 1, O: 1} thermo: model: NASA7 + bonus-field: green temperature-ranges: [200.00, 1000.00, 6000.00] data: - [4.218476300E+00, -4.638976000E-03, 1.104102200E-05, -9.336135400E-09, @@ -106,6 +107,15 @@ species: - [3.260605600E+00, 1.191104300E-03, -4.291704800E-07, 6.945766900E-11, -4.033609900E-15, 9.920974600E+03, 6.369302700E+00] note: "RUS 78" + extra-field: blue + transport: + model: gas + geometry: linear + well-depth: 97.53 + diameter: 3.621 + polarizability: 1.76 + rotational-relaxation: 4.0 + bogus-field: red - name: N2 composition: {N: 2} diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index f5d08a9c7e..e057274316 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -27,6 +27,24 @@ TEST(YamlWriter, thermoDef) EXPECT_DOUBLE_EQ(thermo1->enthalpy_mole(), thermo2->enthalpy_mole()); } +TEST(YamlWriter, userDefinedFields) +{ + auto original = newSolution("ideal-gas.yaml", "simple"); + YamlWriter writer; + writer.addPhase(original); + + AnyMap input1 = AnyMap::fromYamlString(writer.toYamlString()); + auto thermo1 = newPhase(input1["phases"].getMapWhere("name", "simple"), + input1); + + // user-defined fields should be in place + EXPECT_TRUE(thermo1->input()["custom-field"]["second"].is()); + auto spec1 = thermo1->species("NO"); + EXPECT_EQ(spec1->input["extra-field"], "blue"); + EXPECT_EQ(spec1->thermo->input()["bonus-field"], "green"); + EXPECT_EQ(spec1->transport->input["bogus-field"], "red"); +} + TEST(YamlWriter, sharedSpecies) { auto original1 = newSolution("ideal-gas.yaml", "simple"); diff --git a/test/thermo/thermoParameterizations.cpp b/test/thermo/thermoParameterizations.cpp index 848002b7c2..68ac34b8b0 100644 --- a/test/thermo/thermoParameterizations.cpp +++ b/test/thermo/thermoParameterizations.cpp @@ -302,14 +302,14 @@ TEST(SpeciesThermo, PiecewiseGibbsToYaml) { auto original = soln->thermo()->species("OH-")->thermo; AnyMap oh_data; original->getParameters(oh_data); - auto duplicate = newSpeciesThermo(oh_data); + auto duplicate = newSpeciesThermo(AnyMap::fromYamlString(oh_data.toYamlString())); double cp1, cp2, h1, h2, s1, s2; for (double T : {274, 300, 330, 340}) { original->updatePropertiesTemp(T, &cp1, &h1, &s1); duplicate->updatePropertiesTemp(T, &cp2, &h2, &s2); - EXPECT_DOUBLE_EQ(cp1, cp2); - EXPECT_DOUBLE_EQ(h1, h2); - EXPECT_DOUBLE_EQ(s1, s2); + EXPECT_DOUBLE_EQ(cp1, cp2) << T; + EXPECT_DOUBLE_EQ(h1, h2) << T; + EXPECT_DOUBLE_EQ(s1, s2) << T; } EXPECT_EQ(original->refPressure(), duplicate->refPressure()); } From 57d4392513cb757ddbfaf5a6c0d980afbc8a48d8 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 6 Jan 2020 23:16:26 -0500 Subject: [PATCH 16/57] [Input] Add option to skip user-defined fields when writing to YAML --- include/cantera/base/YamlWriter.h | 11 ++++++++++ include/cantera/thermo/Species.h | 2 +- src/base/YamlWriter.cpp | 32 +++++++++++++++-------------- src/thermo/Species.cpp | 20 ++++++++++-------- test/general/test_serialization.cpp | 12 +++++++++++ 5 files changed, 53 insertions(+), 24 deletions(-) diff --git a/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h index bf80aca91c..4b0dca83d5 100644 --- a/include/cantera/base/YamlWriter.h +++ b/include/cantera/base/YamlWriter.h @@ -31,10 +31,21 @@ class YamlWriter m_float_precision = n; } + //! By default user-defined data present in the input is preserved on + //! output. This method can be used to skip output of user-defined data + //! fields which are not directly used by Cantera. + void skipUserDefined(bool skip=true) { + m_skip_user_defined = skip; + } + protected: std::vector> m_phases; + //! @see setPrecision() long int m_float_precision; + + //! @see skipUserDefined() + bool m_skip_user_defined; }; } diff --git a/include/cantera/thermo/Species.h b/include/cantera/thermo/Species.h index 7cba3f46d4..c2120a4c5b 100644 --- a/include/cantera/thermo/Species.h +++ b/include/cantera/thermo/Species.h @@ -35,7 +35,7 @@ class Species Species& operator=(const Species& other) = delete; ~Species(); - void getParameters(AnyMap& speciesNode) const; + void getParameters(AnyMap& speciesNode, bool withInput=true) const; //! The name of the species std::string name; diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 241b87197e..2b57f31363 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -18,6 +18,7 @@ namespace Cantera { YamlWriter::YamlWriter() : m_float_precision(15) + , m_skip_user_defined(false) { } @@ -57,10 +58,11 @@ std::string YamlWriter::toYamlString() const tran->getParameters(phaseDefs[i]); } - // user-defined fields - for (const auto& item : m_phases[i]->thermo()->input()) { - if (!phaseDefs[i].hasKey(item.first)) { - phaseDefs[i][item.first] = item.second; + if (!m_skip_user_defined) { + for (const auto& item : m_phases[i]->thermo()->input()) { + if (!phaseDefs[i].hasKey(item.first)) { + phaseDefs[i][item.first] = item.second; + } } } } @@ -74,12 +76,12 @@ std::string YamlWriter::toYamlString() const for (const auto& name : phase->thermo()->speciesNames()) { const auto& species = phase->thermo()->species(name); AnyMap speciesDef; - species->getParameters(speciesDef); - - // user-defined fields - for (const auto& item : species->input) { - if (!speciesDef.hasKey(item.first)) { - speciesDef[item.first] = item.second; + species->getParameters(speciesDef, !m_skip_user_defined); + if (!m_skip_user_defined) { + for (const auto& item : species->input) { + if (!speciesDef.hasKey(item.first)) { + speciesDef[item.first] = item.second; + } } } @@ -109,11 +111,11 @@ std::string YamlWriter::toYamlString() const const auto reaction = kin->reaction(i); AnyMap reactionDef; reaction->getParameters(reactionDef); - - // user-defined fields - for (const auto& item : reaction->input) { - if (!reactionDef.hasKey(item.first)) { - reactionDef[item.first] = item.second; + if (!m_skip_user_defined) { + for (const auto& item : reaction->input) { + if (!reactionDef.hasKey(item.first)) { + reactionDef[item.first] = item.second; + } } } diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 1a7d65aceb..1c52e401f8 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -33,7 +33,7 @@ Species::~Species() { } -void Species::getParameters(AnyMap& speciesNode) const +void Species::getParameters(AnyMap& speciesNode, bool withInput) const { speciesNode["name"] = name; speciesNode["composition"] = composition; @@ -48,9 +48,11 @@ void Species::getParameters(AnyMap& speciesNode) const AnyMap thermoNode; thermo->getParameters(thermoNode); - for (const auto& item : thermo->input()) { - if (!thermoNode.hasKey(item.first)) { - thermoNode[item.first] = item.second; + if (withInput) { + for (const auto& item : thermo->input()) { + if (!thermoNode.hasKey(item.first)) { + thermoNode[item.first] = item.second; + } } } speciesNode["thermo"] = std::move(thermoNode); @@ -58,12 +60,14 @@ void Species::getParameters(AnyMap& speciesNode) const if (transport) { AnyMap transportNode; transport->getParameters(transportNode); - speciesNode["transport"] = transportNode; - for (const auto& item : transport->input) { - if (!transportNode.hasKey(item.first)) { - transportNode[item.first] = item.second; + if (withInput) { + for (const auto& item : transport->input) { + if (!transportNode.hasKey(item.first)) { + transportNode[item.first] = item.second; + } } } + speciesNode["transport"] = std::move(transportNode); } } diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index e057274316..32d8c661b3 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -6,6 +6,7 @@ #include "cantera/thermo.h" #include "cantera/base/Solution.h" #include "cantera/kinetics.h" +#include "cantera/transport/TransportData.h" using namespace Cantera; @@ -43,6 +44,17 @@ TEST(YamlWriter, userDefinedFields) EXPECT_EQ(spec1->input["extra-field"], "blue"); EXPECT_EQ(spec1->thermo->input()["bonus-field"], "green"); EXPECT_EQ(spec1->transport->input["bogus-field"], "red"); + + writer.skipUserDefined(); + AnyMap input2 = AnyMap::fromYamlString(writer.toYamlString()); + auto thermo2 = newPhase(input2["phases"].getMapWhere("name", "simple"), + input2); + // user-defined fields should have been removed + EXPECT_FALSE(thermo2->input().hasKey("custom-field")); + auto spec2 = thermo2->species("NO"); + EXPECT_FALSE(spec2->input.hasKey("extra-field")); + EXPECT_FALSE(spec2->thermo->input().hasKey("bonus-field")); + EXPECT_FALSE(spec2->transport->input.hasKey("bogus-field")); } TEST(YamlWriter, sharedSpecies) From 8ac4271fde83b9028ba59bf1fcebdd7489d8f6a8 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 11:10:18 -0500 Subject: [PATCH 17/57] [Input] Add pathway for serialization of equation-of-state data --- include/cantera/thermo/ThermoPhase.h | 9 +++++++++ src/base/YamlWriter.cpp | 21 +++++++++++++++++++-- 2 files changed, 28 insertions(+), 2 deletions(-) diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index ce4ecf9993..2c5171d110 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -1714,6 +1714,15 @@ class ThermoPhase : public Phase //! does not include user-defined fields available in input(). virtual void getParameters(AnyMap& phaseNode) const; + //! Get phase-specific parameters of a Species object such that an + //! identical one could be reconstructed and added to this phase. + /*! + * @param name Name of the species + * @param eosNode Mapping to be populated with parameters + */ + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const {} + //! Access input data associated with the phase description const AnyMap& input() const; AnyMap& input(); diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 2b57f31363..ec90b0f73d 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -73,10 +73,27 @@ std::string YamlWriter::toYamlString() const speciesDefs.reserve(nspecies_total); std::unordered_map speciesDefIndex; for (const auto& phase : m_phases) { - for (const auto& name : phase->thermo()->speciesNames()) { - const auto& species = phase->thermo()->species(name); + const auto thermo = phase->thermo(); + for (const auto& name : thermo->speciesNames()) { + const auto& species = thermo->species(name); AnyMap speciesDef; species->getParameters(speciesDef, !m_skip_user_defined); + + thermo->getSpeciesParameters(name, speciesDef); + if (!m_skip_user_defined + && species->input.hasKey("equation-of-state")) { + auto& eosIn = species->input["equation-of-state"].asVector(); + for (const auto& eos : eosIn) { + auto& out = speciesDef["equation-of-state"].getMapWhere( + "model", eos["model"].asString(), true); + for (const auto& item : eos) { + if (!out.hasKey(item.first)) { + out[item.first] = item.second; + } + } + } + } + if (!m_skip_user_defined) { for (const auto& item : species->input) { if (!speciesDef.hasKey(item.first)) { From af89ec59014732698cddf2d6840708010cf12a68 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 13:22:00 -0500 Subject: [PATCH 18/57] [Input] Serialize equation-of-state data for IdealSolidSolnPhase --- include/cantera/thermo/IdealSolidSolnPhase.h | 2 ++ src/thermo/IdealSolidSolnPhase.cpp | 23 ++++++++++++++++++++ test/thermo/thermoToYaml.cpp | 15 +++++++++++++ 3 files changed, 40 insertions(+) diff --git a/include/cantera/thermo/IdealSolidSolnPhase.h b/include/cantera/thermo/IdealSolidSolnPhase.h index 9a93d673b5..d40aa902fc 100644 --- a/include/cantera/thermo/IdealSolidSolnPhase.h +++ b/include/cantera/thermo/IdealSolidSolnPhase.h @@ -559,6 +559,8 @@ class IdealSolidSolnPhase : public ThermoPhase virtual bool addSpecies(shared_ptr spec); virtual void initThermo(); virtual void getParameters(AnyMap& phaseNode) const; + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); virtual void setToEquilState(const doublereal* mu_RT); diff --git a/src/thermo/IdealSolidSolnPhase.cpp b/src/thermo/IdealSolidSolnPhase.cpp index 0576cc4b9a..a2bf24731a 100644 --- a/src/thermo/IdealSolidSolnPhase.cpp +++ b/src/thermo/IdealSolidSolnPhase.cpp @@ -428,6 +428,29 @@ void IdealSolidSolnPhase::getParameters(AnyMap& phaseNode) const } } +void IdealSolidSolnPhase::getSpeciesParameters(const std::string &name, + AnyMap& speciesNode) const +{ + ThermoPhase::getSpeciesParameters(name, speciesNode); + size_t k = speciesIndex(name); + const auto S = species(k); + auto& eosNode = speciesNode["equation-of-state"].getMapWhere( + "model", "constant-volume", true); + // Output volume information in a form consistent with the input + if (S->input.hasKey("equation-of-state")) { + auto& eosIn = S->input["equation-of-state"]; + if (eosIn.hasKey("density")) { + eosNode["density"] = molecularWeight(k) / m_speciesMolarVolume[k]; + } else if (eosIn.hasKey("molar-density")) { + eosNode["molar-density"] = 1.0 / m_speciesMolarVolume[k]; + } else { + eosNode["molar-volume"] = m_speciesMolarVolume[k]; + } + } else { + eosNode["molar-volume"] = m_speciesMolarVolume[k]; + } +} + void IdealSolidSolnPhase::initThermoXML(XML_Node& phaseNode, const std::string& id_) { if (id_.size() > 0 && phaseNode.id() != id_) { diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index 41d6a88082..aa78ea2775 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -14,10 +14,22 @@ class ThermoToYaml : public testing::Test // added by the overrides of getParameters. thermo->input().clear(); thermo->getParameters(data); + + speciesData.resize(thermo->nSpecies()); + eosData.resize(thermo->nSpecies()); + for (size_t k = 0; k < thermo->nSpecies(); k++) { + thermo->getSpeciesParameters(thermo->speciesName(k), speciesData[k]); + if (speciesData[k].hasKey("equation-of-state")) { + // Get the first EOS node, for convenience + eosData[k] = speciesData[k]["equation-of-state"].asVector()[0]; + } + } } shared_ptr thermo; AnyMap data; + std::vector speciesData; + std::vector eosData; }; TEST_F(ThermoToYaml, simpleIdealGas) @@ -38,6 +50,9 @@ TEST_F(ThermoToYaml, IdealSolidSoln) EXPECT_EQ(data["name"], "IdealSolidSolnPhase2"); EXPECT_EQ(data["species"].asVector().size(), thermo->nSpecies()); EXPECT_EQ(data["standard-concentration-basis"], "solvent-molar-volume"); + + EXPECT_DOUBLE_EQ(eosData[0]["molar-volume"].asDouble(), 1.5); + EXPECT_DOUBLE_EQ(eosData[2]["molar-volume"].asDouble(), 0.1); } TEST_F(ThermoToYaml, BinarySolutionTabulated) From a6ad50bc0939ab3aa4dc9aa4afa4a62d181c14a6 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 15:28:27 -0500 Subject: [PATCH 19/57] [Input] Serialize equation-of-state data for LatticePhase/LatticeSolidPhase --- include/cantera/thermo/LatticePhase.h | 2 ++ include/cantera/thermo/LatticeSolidPhase.h | 2 ++ src/thermo/LatticePhase.cpp | 34 ++++++++++++++++++++++ src/thermo/LatticeSolidPhase.cpp | 13 +++++++++ test/thermo/thermoToYaml.cpp | 2 ++ 5 files changed, 53 insertions(+) diff --git a/include/cantera/thermo/LatticePhase.h b/include/cantera/thermo/LatticePhase.h index 7122fddce2..29c6aa9b31 100644 --- a/include/cantera/thermo/LatticePhase.h +++ b/include/cantera/thermo/LatticePhase.h @@ -619,6 +619,8 @@ class LatticePhase : public ThermoPhase virtual void initThermo(); virtual void getParameters(AnyMap& phaseNode) const; + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; //! Set equation of state parameter values from XML entries. /*! diff --git a/include/cantera/thermo/LatticeSolidPhase.h b/include/cantera/thermo/LatticeSolidPhase.h index d51b156b43..c91846480c 100644 --- a/include/cantera/thermo/LatticeSolidPhase.h +++ b/include/cantera/thermo/LatticeSolidPhase.h @@ -435,6 +435,8 @@ class LatticeSolidPhase : public ThermoPhase const AnyMap& rootNode=AnyMap()); virtual void initThermo(); virtual void getParameters(AnyMap& phaseNode) const; + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; virtual void setParametersFromXML(const XML_Node& eosdata); diff --git a/src/thermo/LatticePhase.cpp b/src/thermo/LatticePhase.cpp index 6b92f5a75f..5f3c57e214 100644 --- a/src/thermo/LatticePhase.cpp +++ b/src/thermo/LatticePhase.cpp @@ -303,6 +303,40 @@ void LatticePhase::getParameters(AnyMap& phaseNode) const phaseNode["site-density"] = m_site_density; } +void LatticePhase::getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const +{ + ThermoPhase::getSpeciesParameters(name, speciesNode); + size_t k = speciesIndex(name); + // Output volume information in a form consistent with the input + const auto S = species(k); + if (S->input.hasKey("equation-of-state")) { + auto& eosIn = S->input["equation-of-state"].getMapWhere( + "model", "constant-volume"); + auto& eosOut = speciesNode["equation-of-state"].getMapWhere( + "model", "constant-volume", true); + + if (eosIn.hasKey("density")) { + eosOut["model"] = "constant-volume"; + eosOut["density"] = molecularWeight(k) / m_speciesMolarVolume[k]; + } else if (eosIn.hasKey("molar-density")) { + eosOut["model"] = "constant-volume"; + eosOut["molar-density"] = 1.0 / m_speciesMolarVolume[k]; + } else if (eosIn.hasKey("molar-volume")) { + eosOut["model"] = "constant-volume"; + eosOut["molar-volume"] = m_speciesMolarVolume[k]; + } + } else if (S->input.hasKey("molar_volume")) { + // Species came from XML + auto& eosOut = speciesNode["equation-of-state"].getMapWhere( + "model", "constant-volume", true); + eosOut["model"] = "constant-volume"; + eosOut["molar-volume"] = m_speciesMolarVolume[k]; + } + // Otherwise, species volume is determined by the phase-level site density +} + + void LatticePhase::setParametersFromXML(const XML_Node& eosdata) { eosdata._require("model", "Lattice"); diff --git a/src/thermo/LatticeSolidPhase.cpp b/src/thermo/LatticeSolidPhase.cpp index 5a74756335..20d9c0170d 100644 --- a/src/thermo/LatticeSolidPhase.cpp +++ b/src/thermo/LatticeSolidPhase.cpp @@ -336,6 +336,19 @@ void LatticeSolidPhase::getParameters(AnyMap& phaseNode) const phaseNode["elements"] = elements; } +void LatticeSolidPhase::getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const +{ + // Use child lattice phases to determine species parameters so that these + // are set consistently + for (const auto& phase : m_lattice) { + if (phase->speciesIndex(name) != npos) { + phase->getSpeciesParameters(name, speciesNode); + break; + } + } +} + bool LatticeSolidPhase::addSpecies(shared_ptr spec) { // Species are added from component phases in addLattice() diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index aa78ea2775..0af6a26980 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -72,6 +72,8 @@ TEST_F(ThermoToYaml, Lattice) { setup("thermo-models.yaml", "Li7Si3-interstitial"); EXPECT_DOUBLE_EQ(data["site-density"].asDouble(), 1.046344e+01); + EXPECT_DOUBLE_EQ(eosData[0]["molar-volume"].asDouble(), 0.2); + EXPECT_EQ(eosData[1].size(), (size_t) 0); } TEST_F(ThermoToYaml, LatticeSolid) From 5cd275085bd8abf794fe305481d41ce39306bec3 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 15:52:20 -0500 Subject: [PATCH 20/57] [Input] Serialize equation-of-state data for StoichSubstance --- include/cantera/thermo/StoichSubstance.h | 2 ++ src/thermo/StoichSubstance.cpp | 23 +++++++++++++++++++++++ test/thermo/thermoToYaml.cpp | 14 ++++++++++++++ 3 files changed, 39 insertions(+) diff --git a/include/cantera/thermo/StoichSubstance.h b/include/cantera/thermo/StoichSubstance.h index 06f4ad7145..f0b53ff912 100644 --- a/include/cantera/thermo/StoichSubstance.h +++ b/include/cantera/thermo/StoichSubstance.h @@ -299,6 +299,8 @@ class StoichSubstance : public SingleSpeciesTP // @} virtual void initThermo(); + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); //! Set the equation of state parameters diff --git a/src/thermo/StoichSubstance.cpp b/src/thermo/StoichSubstance.cpp index 35a48caeec..c94a4510bd 100644 --- a/src/thermo/StoichSubstance.cpp +++ b/src/thermo/StoichSubstance.cpp @@ -156,6 +156,29 @@ void StoichSubstance::initThermo() SingleSpeciesTP::initThermo(); } +void StoichSubstance::getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const +{ + SingleSpeciesTP::getSpeciesParameters(name, speciesNode); + size_t k = speciesIndex(name); + const auto S = species(k); + auto& eosNode = speciesNode["equation-of-state"].getMapWhere( + "model", "constant-volume", true); + // Output volume information in a form consistent with the input + if (S->input.hasKey("equation-of-state")) { + auto& eosIn = S->input["equation-of-state"]; + if (eosIn.hasKey("density")) { + eosNode["density"] = density(); + } else if (eosIn.hasKey("molar-density")) { + eosNode["molar-density"] = density() / meanMolecularWeight(); + } else { + eosNode["molar-volume"] = meanMolecularWeight() / density(); + } + } else { + eosNode["molar-volume"] = meanMolecularWeight() / density(); + } +} + void StoichSubstance::initThermoXML(XML_Node& phaseNode, const std::string& id_) { // Find the Thermo XML node diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index 0af6a26980..b55757602a 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -68,6 +68,20 @@ TEST_F(ThermoToYaml, BinarySolutionTabulated) EXPECT_DOUBLE_EQ(s[2], 1.27000e4); } +TEST_F(ThermoToYaml, StoichSubstance1) +{ + setup("thermo-models.yaml", "NaCl(s)"); + EXPECT_EQ(eosData[0]["model"], "constant-volume"); + EXPECT_DOUBLE_EQ(eosData[0]["density"].asDouble(), 2165.0); +} + +TEST_F(ThermoToYaml, StoichSubstance2) +{ + setup("thermo-models.yaml", "KCl(s)"); + EXPECT_EQ(eosData[0]["model"], "constant-volume"); + EXPECT_DOUBLE_EQ(eosData[0]["molar-volume"].asDouble(), 0.0376521717); +} + TEST_F(ThermoToYaml, Lattice) { setup("thermo-models.yaml", "Li7Si3-interstitial"); From 747624e4318b12424a358c0423b48bf5899979d5 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 11:11:13 -0500 Subject: [PATCH 21/57] [Input] Enable serialization of Redlich-Kwong species --- include/cantera/thermo/RedlichKwongMFTP.h | 6 +++++ src/thermo/RedlichKwongMFTP.cpp | 30 +++++++++++++++++++++++ test/thermo/thermoToYaml.cpp | 9 +++++++ 3 files changed, 45 insertions(+) diff --git a/include/cantera/thermo/RedlichKwongMFTP.h b/include/cantera/thermo/RedlichKwongMFTP.h index d6317e33d9..5c2f40aa92 100644 --- a/include/cantera/thermo/RedlichKwongMFTP.h +++ b/include/cantera/thermo/RedlichKwongMFTP.h @@ -174,6 +174,8 @@ class RedlichKwongMFTP : public MixtureFugacityTP virtual void setParametersFromXML(const XML_Node& thermoNode); virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); virtual void initThermo(); + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; //! Retrieve a and b coefficients by looking up tabulated critical parameters /*! @@ -324,6 +326,10 @@ class RedlichKwongMFTP : public MixtureFugacityTP Array2D a_coeff_vec; + //! For each species, true if the a and b coefficients were determined from + //! the critical properties database + std::vector m_coeffs_from_db; + int NSolns_; doublereal Vroot_[3]; diff --git a/src/thermo/RedlichKwongMFTP.cpp b/src/thermo/RedlichKwongMFTP.cpp index 50ca199855..5b89262090 100644 --- a/src/thermo/RedlichKwongMFTP.cpp +++ b/src/thermo/RedlichKwongMFTP.cpp @@ -520,6 +520,7 @@ bool RedlichKwongMFTP::addSpecies(shared_ptr spec) a_coeff_vec.resize(2, m_kk * m_kk, NAN); m_pp.push_back(0.0); + m_coeffs_from_db.push_back(false); m_tmpV.push_back(0.0); m_partialMolarVolumes.push_back(0.0); dpdni_.push_back(0.0); @@ -591,6 +592,7 @@ void RedlichKwongMFTP::initThermoXML(XML_Node& phaseNode, const std::string& id) if (!isnan(coeffArray[0])) { //Assuming no temperature dependence (i,e a1 = 0) setSpeciesCoeffs(iName, coeffArray[0], 0.0, coeffArray[1]); + m_coeffs_from_db[i] = true; } } } @@ -617,6 +619,7 @@ void RedlichKwongMFTP::initThermo() } double b = eos.convert("b", "m^3/kmol"); setSpeciesCoeffs(item.first, a0, a1, b); + m_coeffs_from_db[speciesIndex(item.first)] = false; if (eos.hasKey("binary-a")) { AnyMap& binary_a = eos["binary-a"].as(); const UnitSystem& units = binary_a.units(); @@ -646,12 +649,38 @@ void RedlichKwongMFTP::initThermo() if (!isnan(coeffs[0])) { // Assuming no temperature dependence (i.e. a1 = 0) setSpeciesCoeffs(item.first, coeffs[0], 0.0, coeffs[1]); + m_coeffs_from_db[k] = true; } } } } } +void RedlichKwongMFTP::getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const +{ + MixtureFugacityTP::getSpeciesParameters(name, speciesNode); + size_t k = speciesIndex(name); + checkSpeciesIndex(k); + if (m_coeffs_from_db[k]) { + // No equation-of-state node is needed, since the coefficients will be + // determined from the critical properties database + return; + } + + auto& eosNode = speciesNode["equation-of-state"].getMapWhere( + "model", "Redlich-Kwong", true); + + size_t counter = k + m_kk * k; + if (a_coeff_vec(1, counter) != 0.0) { + eosNode["a"] = vector_fp{a_coeff_vec(0, counter), a_coeff_vec(1, counter)}; + } else { + eosNode["a"] = a_coeff_vec(0, counter); + } + eosNode["b"] = b_vec_Curr_[k]; +} + + vector RedlichKwongMFTP::getCoeff(const std::string& iName) { vector_fp spCoeff{NAN, NAN}; @@ -759,6 +788,7 @@ void RedlichKwongMFTP::readXMLPureFluid(XML_Node& pureFluidParam) } } setSpeciesCoeffs(pureFluidParam.attrib("species"), a0, a1, b); + m_coeffs_from_db[speciesIndex(pureFluidParam.attrib("species"))] = false; } void RedlichKwongMFTP::readXMLCrossFluid(XML_Node& CrossFluidParam) diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index b55757602a..c630f8a905 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -111,6 +111,15 @@ TEST_F(ThermoToYaml, PureFluid) EXPECT_EQ(data["pure-fluid-name"], "nitrogen"); } +TEST_F(ThermoToYaml, RedlichKwong) +{ + setup("thermo-models.yaml", "CO2-RK"); + auto a = eosData[0]["a"].asVector(); + EXPECT_DOUBLE_EQ(a[0], 7.54e6); + EXPECT_DOUBLE_EQ(a[1], -4.13e3); + EXPECT_DOUBLE_EQ(eosData[0]["b"].asDouble(), 27.80e-3); +} + TEST_F(ThermoToYaml, Surface) { setup("surface-phases.yaml", "Pt-surf"); From 9e7e9c429ce6be39d7dc9b9f0719d702cbfaa826 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 17:22:42 -0500 Subject: [PATCH 22/57] [Input] Serialize PDSS_IonsFromNeutral --- include/cantera/thermo/PDSS.h | 3 +++ include/cantera/thermo/PDSS_IonsFromNeutral.h | 1 + include/cantera/thermo/VPStandardStateTP.h | 2 ++ src/thermo/PDSS_IonsFromNeutral.cpp | 12 ++++++++++++ src/thermo/VPStandardStateTP.cpp | 9 +++++++++ test/thermo/thermoToYaml.cpp | 5 +++++ 6 files changed, 32 insertions(+) diff --git a/include/cantera/thermo/PDSS.h b/include/cantera/thermo/PDSS.h index 52fdf3c1b2..d208762500 100644 --- a/include/cantera/thermo/PDSS.h +++ b/include/cantera/thermo/PDSS.h @@ -432,6 +432,9 @@ class PDSS m_input = node; } + //! Store the parameters needed to reconstruct a copy of this PDSS object + virtual void getParameters(AnyMap& eosNode) const {} + //! Initialization routine for the PDSS object based on the speciesNode /*! * This is a cascading call, where each level should call the the parent diff --git a/include/cantera/thermo/PDSS_IonsFromNeutral.h b/include/cantera/thermo/PDSS_IonsFromNeutral.h index 34ae662082..fe242003c0 100644 --- a/include/cantera/thermo/PDSS_IonsFromNeutral.h +++ b/include/cantera/thermo/PDSS_IonsFromNeutral.h @@ -90,6 +90,7 @@ class PDSS_IonsFromNeutral : public PDSS_Nondimensional void setNeutralSpeciesMultiplier(const std::string& species, double mult); void setSpecialSpecies(bool special=true); void setParametersFromXML(const XML_Node& speciesNode); + virtual void getParameters(AnyMap& eosNode) const; virtual void initThermo(); //@} diff --git a/include/cantera/thermo/VPStandardStateTP.h b/include/cantera/thermo/VPStandardStateTP.h index cec8e4882a..1038ca14d4 100644 --- a/include/cantera/thermo/VPStandardStateTP.h +++ b/include/cantera/thermo/VPStandardStateTP.h @@ -250,6 +250,8 @@ class VPStandardStateTP : public ThermoPhase //@{ virtual void initThermo(); + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; using Phase::addSpecies; virtual bool addSpecies(shared_ptr spec); diff --git a/src/thermo/PDSS_IonsFromNeutral.cpp b/src/thermo/PDSS_IonsFromNeutral.cpp index fa424c9fd3..34124face0 100644 --- a/src/thermo/PDSS_IonsFromNeutral.cpp +++ b/src/thermo/PDSS_IonsFromNeutral.cpp @@ -68,6 +68,18 @@ void PDSS_IonsFromNeutral::setParametersFromXML(const XML_Node& speciesNode) } } +void PDSS_IonsFromNeutral::getParameters(AnyMap& eosNode) const +{ + PDSS::getParameters(eosNode); + eosNode["model"] = "ions-from-neutral-molecule"; + if (!add2RTln2_) { + eosNode["special-species"] = true; + } + if (!neutralSpeciesMultipliers_.empty()) { + eosNode["multipliers"] = neutralSpeciesMultipliers_; + } +} + void PDSS_IonsFromNeutral::initThermo() { PDSS::initThermo(); diff --git a/src/thermo/VPStandardStateTP.cpp b/src/thermo/VPStandardStateTP.cpp index f0f05a77ba..a277b860c3 100644 --- a/src/thermo/VPStandardStateTP.cpp +++ b/src/thermo/VPStandardStateTP.cpp @@ -164,6 +164,15 @@ void VPStandardStateTP::initThermo() } } +void VPStandardStateTP::getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const +{ + AnyMap eos; + providePDSS(speciesIndex(name))->getParameters(eos); + speciesNode["equation-of-state"].getMapWhere( + "model", eos.getString("model", ""), true) = std::move(eos); +} + bool VPStandardStateTP::addSpecies(shared_ptr spec) { // Specifically skip ThermoPhase::addSpecies since the Species object diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index c630f8a905..ee94ffce2c 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -138,6 +138,11 @@ TEST_F(ThermoToYaml, IonsFromNeutral) { setup("thermo-models.yaml", "ions-from-neutral-molecule"); EXPECT_EQ(data["neutral-phase"], "KCl-neutral"); + EXPECT_FALSE(eosData[0].hasKey("special-species")); + EXPECT_TRUE(eosData[1]["special-species"].asBool()); + auto multipliers = eosData[1]["multipliers"].asMap(); + EXPECT_EQ(multipliers.size(), (size_t) 1); + EXPECT_DOUBLE_EQ(multipliers["KCl(l)"], 1.5); } TEST_F(ThermoToYaml, Margules) From 7c77d32291ec744975a2b524f0da7d5d8c4ce86d Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 7 Jan 2020 17:46:28 -0500 Subject: [PATCH 23/57] [Input] Serialize PDSS_ConstVol, PDSS_IdealGas, PDSS_Water --- include/cantera/thermo/PDSS_ConstVol.h | 1 + include/cantera/thermo/PDSS_IdealGas.h | 1 + include/cantera/thermo/PDSS_Water.h | 2 ++ src/thermo/PDSS_ConstVol.cpp | 7 +++++++ src/thermo/PDSS_IdealGas.cpp | 6 ++++++ src/thermo/PDSS_Water.cpp | 5 +++++ test/thermo/thermoToYaml.cpp | 10 ++++++++++ 7 files changed, 32 insertions(+) diff --git a/include/cantera/thermo/PDSS_ConstVol.h b/include/cantera/thermo/PDSS_ConstVol.h index ab322983f0..9c48bfd068 100644 --- a/include/cantera/thermo/PDSS_ConstVol.h +++ b/include/cantera/thermo/PDSS_ConstVol.h @@ -54,6 +54,7 @@ class PDSS_ConstVol : public PDSS_Nondimensional virtual void initThermo(); virtual void setParametersFromXML(const XML_Node& speciesNode); + virtual void getParameters(AnyMap& eosNode) const; //! Set the (constant) molar volume [m3/kmol] of the species. Must be called before //! initThermo(). diff --git a/include/cantera/thermo/PDSS_IdealGas.h b/include/cantera/thermo/PDSS_IdealGas.h index 46de3bdbf6..3e44d366d6 100644 --- a/include/cantera/thermo/PDSS_IdealGas.h +++ b/include/cantera/thermo/PDSS_IdealGas.h @@ -50,6 +50,7 @@ class PDSS_IdealGas : public PDSS_Nondimensional //! @{ virtual void initThermo(); + virtual void getParameters(AnyMap& eosNode) const; //@} }; } diff --git a/include/cantera/thermo/PDSS_Water.h b/include/cantera/thermo/PDSS_Water.h index 60b1e81653..f6a8c2482f 100644 --- a/include/cantera/thermo/PDSS_Water.h +++ b/include/cantera/thermo/PDSS_Water.h @@ -148,6 +148,8 @@ class PDSS_Water : public PDSS_Molar return &m_waterProps; } + virtual void getParameters(AnyMap& eosNode) const; + //! @} private: diff --git a/src/thermo/PDSS_ConstVol.cpp b/src/thermo/PDSS_ConstVol.cpp index 25f4b3495e..e8edfb1206 100644 --- a/src/thermo/PDSS_ConstVol.cpp +++ b/src/thermo/PDSS_ConstVol.cpp @@ -52,6 +52,13 @@ void PDSS_ConstVol::initThermo() m_Vss = m_constMolarVolume; } +void PDSS_ConstVol::getParameters(AnyMap &eosNode) const +{ + PDSS::getParameters(eosNode); + eosNode["model"] = "constant-volume"; + eosNode["molar-volume"] = m_constMolarVolume; +} + doublereal PDSS_ConstVol::intEnergy_mole() const { doublereal pV = (m_pres * m_Vss); diff --git a/src/thermo/PDSS_IdealGas.cpp b/src/thermo/PDSS_IdealGas.cpp index 9933f8bc81..a9f4602a89 100644 --- a/src/thermo/PDSS_IdealGas.cpp +++ b/src/thermo/PDSS_IdealGas.cpp @@ -28,6 +28,12 @@ void PDSS_IdealGas::initThermo() m_maxTemp = m_spthermo->maxTemp(); } +void PDSS_IdealGas::getParameters(AnyMap &eosNode) const +{ + PDSS::getParameters(eosNode); + eosNode["model"] = "ideal-gas"; +} + doublereal PDSS_IdealGas::intEnergy_mole() const { return (m_h0_RT - 1.0) * GasConstant * m_temp; diff --git a/src/thermo/PDSS_Water.cpp b/src/thermo/PDSS_Water.cpp index 2ee58ee10f..a685bd440e 100644 --- a/src/thermo/PDSS_Water.cpp +++ b/src/thermo/PDSS_Water.cpp @@ -259,4 +259,9 @@ doublereal PDSS_Water::satPressure(doublereal t) return pp; } +void PDSS_Water::getParameters(AnyMap& eosNode) const +{ + eosNode["model"] = "liquid-water-IAPWS95"; +} + } diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index ee94ffce2c..870b5e2750 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -181,6 +181,10 @@ TEST_F(ThermoToYaml, DebyeHuckel_B_dot_ak) EXPECT_DOUBLE_EQ(ac["default-ionic-radius"].asDouble(), 4e-10); EXPECT_FALSE(ac.as().hasKey("A_Debye")); EXPECT_FALSE(ac.as().hasKey("B_Debye")); + + EXPECT_EQ(eosData[0]["model"], "liquid-water-IAPWS95"); + EXPECT_EQ(eosData[1]["model"], "constant-volume"); + EXPECT_DOUBLE_EQ(eosData[1]["molar-volume"].asDouble(), 1.3); } TEST_F(ThermoToYaml, DebyeHuckel_beta_ij) @@ -235,6 +239,9 @@ TEST_F(ThermoToYaml, HMWSoln1) FAIL(); // unexpected set of species } } + EXPECT_EQ(eosData[0]["model"], "liquid-water-IAPWS95"); + EXPECT_EQ(eosData[1]["model"], "constant-volume"); + EXPECT_DOUBLE_EQ(eosData[2]["molar-volume"].asDouble(), 1.3); } TEST_F(ThermoToYaml, HMWSoln2) @@ -270,4 +277,7 @@ TEST_F(ThermoToYaml, IdealMolalSolution) EXPECT_EQ(cutoff["model"], "polyexp"); EXPECT_EQ(cutoff.as().size(), (size_t) 2); // other values are defaults EXPECT_DOUBLE_EQ(cutoff["gamma_o"].asDouble(), 0.0001); + + EXPECT_EQ(eosData[2]["model"], "constant-volume"); + EXPECT_DOUBLE_EQ(eosData[2]["molar-volume"].asDouble(), 0.1); } From ee9d9eba8c44332c8d2b3e291055c13e5ca0f3e3 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 12 Jan 2020 16:00:37 -0500 Subject: [PATCH 24/57] [Input] Serialize PDSS_HKFT --- include/cantera/thermo/PDSS_HKFT.h | 1 + src/thermo/PDSS_HKFT.cpp | 25 +++++++++++++++++++++++++ test/thermo/thermoToYaml.cpp | 10 ++++++++++ 3 files changed, 36 insertions(+) diff --git a/include/cantera/thermo/PDSS_HKFT.h b/include/cantera/thermo/PDSS_HKFT.h index 42a81a90af..9959d30c81 100644 --- a/include/cantera/thermo/PDSS_HKFT.h +++ b/include/cantera/thermo/PDSS_HKFT.h @@ -105,6 +105,7 @@ class PDSS_HKFT : public PDSS_Molar void setOmega(double omega); //!< Set omega [J/kmol] void setParametersFromXML(const XML_Node& speciesNode); + virtual void getParameters(AnyMap& eosNode) const; //! This utility function reports back the type of parameterization and //! all of the parameters for the species, index. diff --git a/src/thermo/PDSS_HKFT.cpp b/src/thermo/PDSS_HKFT.cpp index 3a8d04cf0d..9c78760cdb 100644 --- a/src/thermo/PDSS_HKFT.cpp +++ b/src/thermo/PDSS_HKFT.cpp @@ -454,6 +454,31 @@ void PDSS_HKFT::setParametersFromXML(const XML_Node& speciesNode) } } +void PDSS_HKFT::getParameters(AnyMap& eosNode) const +{ + PDSS::getParameters(eosNode); + eosNode["model"] = "HKFT"; + UnitSystem U; + U.setDefaults({"cal", "gmol", "bar"}); + eosNode["h0"] = U.convert(m_deltaH_formation_tr_pr, "J/kmol"); + eosNode["g0"] = U.convert(m_deltaG_formation_tr_pr, "J/kmol"); + eosNode["s0"] = U.convert(m_Entrop_tr_pr, "J/kmol/K"); + + eosNode["a"] = vector_fp{ + U.convert(m_a1, "J/kmol/Pa"), + U.convert(m_a2, "J/kmol"), + U.convert(m_a3, "J*K/kmol/Pa"), + U.convert(m_a4, "J*K/kmol") + }; + + eosNode["c"] = vector_fp{ + U.convert(m_c1, "J/kmol/K"), + U.convert(m_c2, "J*K/kmol") + }; + + eosNode["omega"] = U.convert(m_omega_pr_tr, "J/kmol"); +} + doublereal PDSS_HKFT::deltaH() const { doublereal pbar = m_pres * 1.0E-5; diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index 870b5e2750..072426e4f3 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -270,6 +270,16 @@ TEST_F(ThermoToYaml, HMWSoln2) EXPECT_DOUBLE_EQ(crop["ln_gamma_k_max"].asDouble(), 20); } +TEST_F(ThermoToYaml, HMWSoln_HKFT) +{ + setup("thermo-models.yaml", "HMW-NaCl-HKFT"); + EXPECT_DOUBLE_EQ(eosData[1]["h0"].asDouble(), -57433 * 4184); + EXPECT_DOUBLE_EQ(eosData[1]["s0"].asDouble(), 13.96 * 4184); + EXPECT_DOUBLE_EQ(eosData[2]["a"].asVector()[2], 5.563 * 4184 / 1e5); + EXPECT_DOUBLE_EQ(eosData[4]["c"].asVector()[1], -103460 * 4184); + EXPECT_DOUBLE_EQ(eosData[4]["omega"].asDouble(), 172460 * 4184); +} + TEST_F(ThermoToYaml, IdealMolalSolution) { setup("thermo-models.yaml", "ideal-molal-aqueous"); From af08cd88e2de98237da352739efdd960c2c158ae Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 12 Jan 2020 16:43:20 -0500 Subject: [PATCH 25/57] [Input] Implement serialization of PDSS_SSVol --- include/cantera/thermo/PDSS_SSVol.h | 2 ++ src/thermo/PDSS_SSVol.cpp | 11 +++++++++++ 2 files changed, 13 insertions(+) diff --git a/include/cantera/thermo/PDSS_SSVol.h b/include/cantera/thermo/PDSS_SSVol.h index d38bb56adb..a044c441aa 100644 --- a/include/cantera/thermo/PDSS_SSVol.h +++ b/include/cantera/thermo/PDSS_SSVol.h @@ -176,6 +176,8 @@ class PDSS_SSVol : public PDSS_Nondimensional void setDensityPolynomial(double* coeffs); virtual void setParametersFromXML(const XML_Node& speciesNode); + virtual void getParameters(AnyMap& eosNode) const; + //@} private: diff --git a/src/thermo/PDSS_SSVol.cpp b/src/thermo/PDSS_SSVol.cpp index 9297cd38e9..681c809a84 100644 --- a/src/thermo/PDSS_SSVol.cpp +++ b/src/thermo/PDSS_SSVol.cpp @@ -65,6 +65,17 @@ void PDSS_SSVol::setDensityPolynomial(double* coeffs) { volumeModel_ = SSVolume_Model::density_tpoly; } +void PDSS_SSVol::getParameters(AnyMap& eosNode) const +{ + PDSS::getParameters(eosNode); + if (volumeModel_ == SSVolume_Model::density_tpoly) { + eosNode["model"] = "density-temperature-polynomial"; + } else { + eosNode["model"] = "molar-volume-temperature-polynomial"; + } + eosNode["data"] = TCoeff_; +} + void PDSS_SSVol::initThermo() { PDSS::initThermo(); From 472e89f0d24deebcf975ffba1d703fe0581cd32a Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 13 Jan 2020 00:06:12 -0500 Subject: [PATCH 26/57] [Input] Serialize species-specific data for DebyeHuckel model --- include/cantera/thermo/DebyeHuckel.h | 2 + src/thermo/DebyeHuckel.cpp | 58 ++++++++++++++++++++++++++++ test/thermo/thermoToYaml.cpp | 5 +++ 3 files changed, 65 insertions(+) diff --git a/include/cantera/thermo/DebyeHuckel.h b/include/cantera/thermo/DebyeHuckel.h index b240caba73..6617b6a0e0 100644 --- a/include/cantera/thermo/DebyeHuckel.h +++ b/include/cantera/thermo/DebyeHuckel.h @@ -779,6 +779,8 @@ class DebyeHuckel : public MolalityVPSSTP virtual bool addSpecies(shared_ptr spec); virtual void initThermo(); virtual void getParameters(AnyMap& phaseNode) const; + virtual void getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; virtual void initThermoXML(XML_Node& phaseNode, const std::string& id); //! Return the Debye Huckel constant as a function of temperature diff --git a/src/thermo/DebyeHuckel.cpp b/src/thermo/DebyeHuckel.cpp index e37de29279..3b95abc4bd 100644 --- a/src/thermo/DebyeHuckel.cpp +++ b/src/thermo/DebyeHuckel.cpp @@ -665,6 +665,64 @@ void DebyeHuckel::getParameters(AnyMap& phaseNode) const phaseNode["activity-data"] = std::move(activityNode); } +void DebyeHuckel::getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const +{ + MolalityVPSSTP::getSpeciesParameters(name, speciesNode); + size_t k = speciesIndex(name); + checkSpeciesIndex(k); + AnyMap dhNode; + if (m_Aionic[k] != m_Aionic_default) { + dhNode["ionic-radius"] = m_Aionic[k]; + } + + int estDefault = cEST_nonpolarNeutral; + if (k == 0) { + estDefault = cEST_solvent; + } + + if (m_speciesCharge_Stoich[k] != charge(k)) { + dhNode["weak-acid-charge"] = m_speciesCharge_Stoich[k]; + estDefault = cEST_weakAcidAssociated; + } else if (fabs(charge(k)) > 0.0001) { + estDefault = cEST_chargedSpecies; + } + + if (m_electrolyteSpeciesType[k] != estDefault) { + string estType; + switch (m_electrolyteSpeciesType[k]) { + case cEST_solvent: + estType = "solvent"; + break; + case cEST_chargedSpecies: + estType = "charged-species"; + break; + case cEST_weakAcidAssociated: + estType = "weak-acid-associated"; + break; + case cEST_strongAcidAssociated: + estType = "strong-acid-associated"; + break; + case cEST_polarNeutral: + estType = "polar-neutral"; + break; + case cEST_nonpolarNeutral: + estType = "nonpolar-neutral"; + break; + default: + throw CanteraError("DebyeHuckel::getSpeciesParameters", + "Unknown electrolyte species type ({}) for species '{}'", + m_electrolyteSpeciesType[k], name); + } + dhNode["electrolyte-species-type"] = estType; + } + + if (dhNode.size()) { + speciesNode["Debye-Huckel"] = std::move(dhNode); + } +} + + double DebyeHuckel::A_Debye_TP(double tempArg, double presArg) const { double T = temperature(); diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index 072426e4f3..576f3b8af5 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -185,6 +185,11 @@ TEST_F(ThermoToYaml, DebyeHuckel_B_dot_ak) EXPECT_EQ(eosData[0]["model"], "liquid-water-IAPWS95"); EXPECT_EQ(eosData[1]["model"], "constant-volume"); EXPECT_DOUBLE_EQ(eosData[1]["molar-volume"].asDouble(), 1.3); + + EXPECT_FALSE(speciesData[0].hasKey("Debye-Huckel")); + EXPECT_FALSE(speciesData[1].hasKey("Debye-Huckel")); // defaults are ok + EXPECT_DOUBLE_EQ(speciesData[2]["Debye-Huckel"]["ionic-radius"].asDouble(), 3e-10); + EXPECT_DOUBLE_EQ(speciesData[5]["Debye-Huckel"]["weak-acid-charge"].asDouble(), -1); } TEST_F(ThermoToYaml, DebyeHuckel_beta_ij) From ad3cd4f826a7f20333c6ea976235e764be98aaee Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 13 Jan 2020 14:18:28 -0500 Subject: [PATCH 27/57] [Input] YamlWriter can use separate Thermo/Kinetics/Transport objects --- include/cantera/base/YamlWriter.h | 8 ++++++++ src/base/YamlWriter.cpp | 10 ++++++++++ 2 files changed, 18 insertions(+) diff --git a/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h index 4b0dca83d5..2d328b5612 100644 --- a/include/cantera/base/YamlWriter.h +++ b/include/cantera/base/YamlWriter.h @@ -12,6 +12,9 @@ namespace Cantera { class Solution; +class ThermoPhase; +class Kinetics; +class Transport; //! A class for generating full YAML input files from multiple data sources class YamlWriter @@ -22,6 +25,11 @@ class YamlWriter //! Include a phase definition for the specified Solution object void addPhase(shared_ptr soln); + //! Include a phase definition using the specified ThermoPhase, (optional) + //! Kinetics, and (optional) Transport objects + void addPhase(shared_ptr thermo, shared_ptr kin={}, + shared_ptr tran={}); + std::string toYamlString() const; void toYamlFile(const std::string& filename) const; diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index ec90b0f73d..6379266116 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -32,6 +32,16 @@ void YamlWriter::addPhase(shared_ptr soln) { m_phases.push_back(soln); } +void YamlWriter::addPhase(shared_ptr thermo, + shared_ptr kin, + shared_ptr tran) { + auto soln = Solution::create(); + soln->setThermo(thermo); + soln->setKinetics(kin); + soln->setTransport(tran); + addPhase(soln); +} + std::string YamlWriter::toYamlString() const { AnyMap output; From 8b15f4d3ed324b34c3567f3bd72f3bc1f1b2ad9e Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 13 Jan 2020 14:47:44 -0500 Subject: [PATCH 28/57] [Input] Serialize phase state using native state variables --- src/thermo/ThermoPhase.cpp | 43 +++++++++++++++++++++++++++++--------- 1 file changed, 33 insertions(+), 10 deletions(-) diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index a5bcc5288a..17905bcd21 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1200,18 +1200,41 @@ void ThermoPhase::getParameters(AnyMap& phaseNode) const phaseNode["elements"] = elementNames; phaseNode["species"] = speciesNames(); - // @TODO: Update pending resolution of PR #720 - map state; - state["T"] = temperature(); - state["density"] = density(); - map Y; - for (size_t k = 0; k < m_kk; k++) { - if (massFraction(k) > 0) { - Y[speciesName(k)] = massFraction(k); + AnyMap state; + auto stateVars = nativeState(); + if (stateVars.count("T")) { + state["T"] = temperature(); + } + + if (stateVars.count("D")) { + state["density"] = density(); + } else if (stateVars.count("P")) { + state["P"] = pressure(); + } + + if (stateVars.count("Y")) { + map Y; + for (size_t k = 0; k < m_kk; k++) { + double Yk = massFraction(k); + if (Yk > 0) { + Y[speciesName(k)] = Yk; + } } + state["Y"] = Y; + state["Y"].setFlowStyle(); + } else if (stateVars.count("X")) { + map X; + for (size_t k = 0; k < m_kk; k++) { + double Xk = moleFraction(k); + if (Xk > 0) { + X[speciesName(k)] = Xk; + } + } + state["X"] = X; + state["X"].setFlowStyle(); } - state["Y"] = Y; - phaseNode["state"] = state; + + phaseNode["state"] = std::move(state); } const AnyMap& ThermoPhase::input() const From e314cfe81e425411cbf2aa0b2eb10753728745da Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 13 Jan 2020 18:28:04 -0500 Subject: [PATCH 29/57] [Input] Fix serialization when species has no thermo model --- src/thermo/Species.cpp | 10 +++++++--- 1 file changed, 7 insertions(+), 3 deletions(-) diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 1c52e401f8..2a51ac5f64 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -47,15 +47,19 @@ void Species::getParameters(AnyMap& speciesNode, bool withInput) const } AnyMap thermoNode; - thermo->getParameters(thermoNode); - if (withInput) { + if (thermo && thermo->reportType() != 0) { + thermo->getParameters(thermoNode); + } + if (thermo && withInput) { for (const auto& item : thermo->input()) { if (!thermoNode.hasKey(item.first)) { thermoNode[item.first] = item.second; } } } - speciesNode["thermo"] = std::move(thermoNode); + if (thermoNode.size()) { + speciesNode["thermo"] = std::move(thermoNode); + } if (transport) { AnyMap transportNode; From 1059fe32feaca18b3da571508cd025015a01bb06 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 13 Jan 2020 18:26:10 -0500 Subject: [PATCH 30/57] [Test] Add round-trip serialization tests for thermo models --- test/thermo/thermoToYaml.cpp | 156 +++++++++++++++++++++++++++++++++++ 1 file changed, 156 insertions(+) diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index 576f3b8af5..d701b4dddb 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -1,5 +1,7 @@ #include "gtest/gtest.h" #include "cantera/thermo/ThermoFactory.h" +#include "cantera/thermo/SurfPhase.h" +#include "cantera/base/YamlWriter.h" using namespace Cantera; typedef std::vector strvec; @@ -296,3 +298,157 @@ TEST_F(ThermoToYaml, IdealMolalSolution) EXPECT_EQ(eosData[2]["model"], "constant-volume"); EXPECT_DOUBLE_EQ(eosData[2]["molar-volume"].asDouble(), 0.1); } + + +class ThermoYamlRoundTrip : public testing::Test +{ +public: + void roundtrip(const std::string& fileName, const std::string& phaseName="", + const std::vector extraPhases={}) { + original.reset(newPhase(fileName, phaseName)); + YamlWriter writer; + writer.addPhase(original); + for (const auto& name : extraPhases) { + shared_ptr p(newPhase(fileName, name)); + writer.addPhase(p); + } + writer.skipUserDefined(); + AnyMap input1 = AnyMap::fromYamlString(writer.toYamlString()); + duplicate = newPhase(input1["phases"].getMapWhere("name", phaseName), + input1); + skip_cp = false; + skip_activities = false; + rtol = 1e-14; + } + + void compareThermo(double T, double P, const std::string& X="") { + size_t kk = original->nSpecies(); + ASSERT_EQ(original->nSpecies(), duplicate->nSpecies()); + + if (X.empty()) { + original->setState_TP(T, P); + duplicate->setState_TP(T, P); + } else { + original->setState_TPX(T, P, X); + duplicate->setState_TPX(T, P, X); + } + + EXPECT_NEAR(original->density(), duplicate->density(), + rtol * original->density()); + if (!skip_cp) { + EXPECT_NEAR(original->cp_mass(), duplicate->cp_mass(), + rtol * original->cp_mass()); + } + EXPECT_NEAR(original->entropy_mass(), duplicate->entropy_mass(), + rtol * fabs(original->entropy_mass())); + EXPECT_NEAR(original->enthalpy_mole(), duplicate->enthalpy_mole(), + rtol * fabs(original->enthalpy_mole())); + + vector_fp Y1(kk), Y2(kk), h1(kk), h2(kk), s1(kk), s2(kk); + vector_fp mu1(kk), mu2(kk), v1(kk), v2(kk), a1(kk), a2(kk); + original->getMassFractions(Y1.data()); + duplicate->getMassFractions(Y2.data()); + original->getPartialMolarEnthalpies(h1.data()); + duplicate->getPartialMolarEnthalpies(h2.data()); + original->getPartialMolarEntropies(s1.data()); + duplicate->getPartialMolarEntropies(s2.data()); + original->getChemPotentials(mu1.data()); + duplicate->getChemPotentials(mu2.data()); + original->getPartialMolarVolumes(v1.data()); + duplicate->getPartialMolarVolumes(v2.data()); + if (!skip_activities) { + original->getActivityCoefficients(a1.data()); + duplicate->getActivityCoefficients(a2.data()); + } + + for (size_t k = 0; k < kk; k++) { + EXPECT_NEAR(Y1[k], Y2[k], 1e-20 + rtol*fabs(Y1[k])) << k; + EXPECT_NEAR(h1[k], h2[k], 1e-20 + rtol*fabs(h1[k])) << k; + EXPECT_NEAR(s1[k], s2[k], 1e-20 + rtol*fabs(s1[k])) << k; + EXPECT_NEAR(mu1[k], mu2[k], 1e-20 + rtol*fabs(mu1[k])) << k; + EXPECT_NEAR(v1[k], v2[k], 1e-20 + rtol*fabs(v1[k])) << k; + EXPECT_NEAR(a1[k], a2[k], 1e-20 + rtol*fabs(a1[k])) << k; + } + } + + shared_ptr original; + shared_ptr duplicate; + bool skip_cp; + bool skip_activities; + double rtol; +}; + +TEST_F(ThermoYamlRoundTrip, RedlichKwong) +{ + roundtrip("nDodecane_Reitz.xml", "nDodecane_RK"); + compareThermo(500, 6e5, "c12h26: 0.2, o2: 0.1, co2: 0.4, c2h2: 0.3"); +} + +TEST_F(ThermoYamlRoundTrip, BinarySolutionTabulated) +{ + roundtrip("lithium_ion_battery.xml", "cathode"); + compareThermo(310, 2e5, "Li[cathode]:0.4, V[cathode]:0.6"); +} + +TEST_F(ThermoYamlRoundTrip, Margules) +{ + roundtrip("LiKCl_liquid.xml", "MoltenSalt_electrolyte"); + compareThermo(920, 3e5, "KCl(L):0.35, LiCl(L):0.65"); +} + +TEST_F(ThermoYamlRoundTrip, DebyeHuckel) +{ + roundtrip("thermo-models.yaml", "debye-huckel-B-dot-ak"); + compareThermo(305, 2e5); +} + +TEST_F(ThermoYamlRoundTrip, IdealMolalSolution) +{ + roundtrip("thermo-models.yaml", "ideal-molal-aqueous"); + compareThermo(308, 1.1e5, "H2O(l): 0.95, H2S(aq): 0.01, CO2(aq): 0.04"); +} + +TEST_F(ThermoYamlRoundTrip, IonsFromNeutral) +{ + roundtrip("thermo-models.yaml", "ions-from-neutral-molecule", + {"KCl-neutral"}); + skip_cp = true; // Not implemented for IonsFromNeutral + compareThermo(500, 3e5); +} + +TEST_F(ThermoYamlRoundTrip, LatticeSolid) +{ + roundtrip("thermo-models.yaml", "Li7Si3_and_interstitials", + {"Li7Si3(s)", "Li7Si3-interstitial"}); + compareThermo(710, 10e5); +} + +TEST_F(ThermoYamlRoundTrip, HMWSoln) +{ + roundtrip("thermo-models.yaml", "HMW-NaCl-electrolyte"); + rtol = 1e-10; // @TODO: Determine why more stringent tolerances can't be met + compareThermo(350.15, 101325, + "H2O(L): 0.8198, Na+:0.09, Cl-:0.09, H+:4.4e-6, OH-:4.4e-6"); +} + +TEST_F(ThermoYamlRoundTrip, PureFluid_Nitrogen) +{ + roundtrip("thermo-models.yaml", "nitrogen"); + compareThermo(90, 19e5); +} + +TEST_F(ThermoYamlRoundTrip, RedlichKister) +{ + roundtrip("thermo-models.yaml", "Redlich-Kister-LiC6"); + compareThermo(310, 2e5); +} + +TEST_F(ThermoYamlRoundTrip, Surface) +{ + roundtrip("surface-phases.yaml", "Pt-surf"); + skip_activities = true; + compareThermo(800, 2*OneAtm); + auto origSurf = std::dynamic_pointer_cast(original); + auto duplSurf = std::dynamic_pointer_cast(duplicate); + EXPECT_DOUBLE_EQ(origSurf->siteDensity(), duplSurf->siteDensity()); +} From c023796307d68c1c3489661d93f61cfeba9feecf Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 14 Jan 2020 23:07:58 -0500 Subject: [PATCH 31/57] [Python] Add conversion of AnyMap to native Python types --- include/cantera/base/AnyMap.h | 1 + interfaces/cython/cantera/_cantera.pxd | 12 +++++ interfaces/cython/cantera/utils.pyx | 73 ++++++++++++++++++++++++++ 3 files changed, 86 insertions(+) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 39fc752df3..a660bb0248 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -457,6 +457,7 @@ class AnyMap : public AnyBase //! skips over keys that start and end with double underscores. class Iterator { public: + Iterator() {} Iterator(const std::unordered_map::const_iterator& start, const std::unordered_map::const_iterator& stop); diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 4b267fa4bc..786ce7c5f6 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -53,14 +53,26 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": cdef cppclass CxxAnyValue "Cantera::AnyValue" cdef cppclass CxxAnyMap "Cantera::AnyMap": + cppclass Iterator: + pair[string, CxxAnyValue]& operator*() + Iterator& operator++() + cbool operator!=(Iterator&) + CxxAnyMap() + Iterator begin() + Iterator end() CxxAnyValue& operator[](string) except +translate_exception + cbool hasKey(string) string keys_str() cdef cppclass CxxAnyValue "Cantera::AnyValue": CxxAnyValue() unordered_map[string, CxxAnyMap*] asMap(string) except +translate_exception CxxAnyMap& getMapWhere(string, string) except +translate_exception + T& asType "as" [T]() except +translate_exception + cbool isType "is" [T]() + cbool isScalar() + pair[int, int] order() CxxAnyMap AnyMapFromYamlFile "Cantera::AnyMap::fromYamlFile" (string) except +translate_exception CxxAnyMap AnyMapFromYamlString "Cantera::AnyMap::fromYamlString" (string) except +translate_exception diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 963112d086..63913f04b8 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -72,3 +72,76 @@ class CanteraError(RuntimeError): pass cdef public PyObject* pyCanteraError = CanteraError + +cdef anyvalueToPython(string name, CxxAnyValue& v): + cdef CxxAnyMap a + cdef CxxAnyValue b + if v.isScalar(): + if v.isType[string](): + return pystr(v.asType[string]()) + elif v.isType[double](): + return v.asType[double]() + elif v.isType[long](): + return v.asType[long]() + elif v.isType[cbool](): + return v.asType[cbool]() + elif v.isType[CxxAnyMap](): + return anymapToPython(v.asType[CxxAnyMap]()) + elif v.isType[vector[CxxAnyMap]](): + return [anymapToPython(a) for a in v.asType[vector[CxxAnyMap]]()] + elif v.isType[vector[double]](): + return v.asType[vector[double]]() + elif v.isType[vector[string]](): + return [pystr(s) for s in v.asType[vector[string]]()] + elif v.isType[vector[long]](): + return v.asType[vector[long]]() + elif v.isType[vector[cbool]](): + return v.asType[vector[cbool]]() + elif v.isType[vector[CxxAnyValue]](): + return [anyvalueToPython(name, b) + for b in v.asType[vector[CxxAnyValue]]()] + elif v.isType[vector[vector[double]]](): + return v.asType[vector[vector[double]]]() + elif v.isType[vector[vector[string]]](): + return [[pystr(s) for s in row] + for row in v.asType[vector[vector[string]]]()] + elif v.isType[vector[vector[long]]](): + return v.asType[vector[vector[long]]]() + elif v.isType[vector[vector[cbool]]](): + return v.asType[vector[vector[cbool]]]() + else: + raise TypeError("Unable to convert value with key '{}' " + "from AnyValue".format(pystr(name))) + + +cdef anymapToPython(CxxAnyMap& m): + py_items = [] + for item in m: + py_items.append((item.second.order(), + pystr(item.first), + anyvalueToPython(item.first, item.second))) + py_items.sort() + return {key: value for (_, key, value) in py_items} + + +cdef mergeAnyMap(CxxAnyMap& primary, CxxAnyMap& extra): + """ + Combine two AnyMaps into a single Python dict. Items from the second map + are included only if there is no corresponding key in the first map. + + Used to combine generated data representing the current state of the object + (primary) with user-supplied fields (extra) not directly used by Cantera. + """ + py_items = [] + for item in primary: + py_items.append((item.second.order(), + pystr(item.first), + anyvalueToPython(item.first, item.second))) + for item in extra: + if not primary.hasKey(item.first): + py_items.append((item.second.order(), + pystr(item.first), + anyvalueToPython(item.first, item.second))) + + py_items.sort() + return {key: value for (_, key, value) in py_items} From ae8d0b5304cef7b59a4e52fd818caf0c9566c598 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 21 Jan 2020 17:51:54 -0500 Subject: [PATCH 32/57] [Python] Add access to input data for Solution objects --- interfaces/cython/cantera/_cantera.pxd | 4 ++ interfaces/cython/cantera/base.pyx | 11 ++++++ .../cython/cantera/test/test_composite.py | 37 +++++++++++++++++++ 3 files changed, 52 insertions(+) diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 786ce7c5f6..b28a196564 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -163,6 +163,8 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera": # miscellaneous string type() string phaseOfMatter() except +translate_exception + CxxAnyMap& input() + void getParameters(CxxAnyMap&) except +translate_exception string report(cbool, double) except +translate_exception cbool hasPhaseTransition() cbool isPure() @@ -470,6 +472,7 @@ cdef extern from "cantera/kinetics/Kinetics.h" namespace "Cantera": void addReaction(shared_ptr[CxxReaction]) except +translate_exception void modifyReaction(int, shared_ptr[CxxReaction]) except +translate_exception void invalidateCache() except +translate_exception + void getParameters(CxxAnyMap&) except +translate_exception shared_ptr[CxxReaction] reaction(size_t) except +translate_exception cbool isReversible(int) except +translate_exception @@ -495,6 +498,7 @@ cdef extern from "cantera/transport/TransportBase.h" namespace "Cantera": cdef cppclass CxxTransport "Cantera::Transport": CxxTransport(CxxThermoPhase*) string transportType() + void getParameters(CxxAnyMap&) except +translate_exception double viscosity() except +translate_exception double thermalConductivity() except +translate_exception double electricalConductivity() except +translate_exception diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index c92b008a0c..7e824696e5 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -223,6 +223,17 @@ cdef class _SolutionBase: for reaction in reactions: self.kinetics.addReaction(reaction._reaction) + property input_data: + def __get__(self): + cdef CxxAnyMap params + if self.thermo: + self.thermo.getParameters(params) + if self.kinetics: + self.kinetics.getParameters(params) + if self.transport: + self.transport.getParameters(params) + return mergeAnyMap(params, self.thermo.input()) + def __getitem__(self, selection): copy = self.__class__(origin=self) if isinstance(selection, slice): diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index 06e1742d73..2639e5eff2 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -422,3 +422,40 @@ def check(a, b): b = ct.SolutionArray(self.water) b.restore_data(data) check(a, b) + + +class TestSolutionSerialization(utilities.CanteraTest): + def test_input_data_simple(self): + gas = ct.Solution('h2o2.yaml') + data = gas.input_data + self.assertEqual(data['name'], 'ohmech') + self.assertEqual(data['thermo'], 'ideal-gas') + self.assertEqual(data['kinetics'], 'gas') + self.assertEqual(data['transport'], 'mixture-averaged') + + def test_input_data_state(self): + gas = ct.Solution('h2o2.yaml') + data = gas.input_data + self.assertEqual(gas.T, data['state']['T']) + self.assertEqual(gas.density, data['state']['density']) + + gas.TP = 500, 3.14e5 + data = gas.input_data + self.assertEqual(gas.T, data['state']['T']) + self.assertEqual(gas.density, data['state']['density']) + + def test_input_data_custom(self): + gas = ct.Solution('ideal-gas.yaml') + data = gas.input_data + self.assertEqual(data['custom-field']['first'], True) + self.assertEqual(data['custom-field']['last'], [100, 200, 300]) + + def test_input_data_debye_huckel(self): + soln = ct.Solution('thermo-models.yaml', 'debye-huckel-B-dot-ak') + data = soln.input_data + self.assertEqual(data['thermo'], 'Debye-Huckel') + act_data = data['activity-data'] + self.assertEqual(act_data['model'], 'B-dot-with-variable-a') + self.assertEqual(act_data['default-ionic-radius'], 4e-10) + self.assertNotIn('kinetics', data) + self.assertNotIn('transport', data) From 55c63604b84d8a5545a1d05320fb61353e1bc3de Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 21 Jan 2020 18:34:18 -0500 Subject: [PATCH 33/57] [Python] Add access to input data for Species objects --- interfaces/cython/cantera/_cantera.pxd | 20 ++++++++++++----- interfaces/cython/cantera/base.pyx | 5 +++++ interfaces/cython/cantera/speciesthermo.pyx | 10 +++++++-- .../cython/cantera/test/test_composite.py | 22 +++++++++++++++++++ interfaces/cython/cantera/thermo.pyx | 10 +++++++++ interfaces/cython/cantera/transport.pyx | 6 +++++ 6 files changed, 65 insertions(+), 8 deletions(-) diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index b28a196564..38a5f7331e 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -114,6 +114,8 @@ cdef extern from "cantera/thermo/SpeciesThermoInterpType.h": double refPressure() void reportParameters(size_t&, int&, double&, double&, double&, double* const) except +translate_exception int nCoeffs() except +translate_exception + void getParameters(CxxAnyMap&) except +translate_exception + CxxAnyMap& input() cdef extern from "cantera/thermo/SpeciesThermoFactory.h": cdef CxxSpeciesThermo* CxxNewSpeciesThermo "Cantera::newSpeciesThermoInterpType"\ @@ -132,6 +134,8 @@ cdef extern from "cantera/thermo/Species.h" namespace "Cantera": Composition composition double charge double size + void getParameters(CxxAnyMap&) except +translate_exception + CxxAnyMap input cdef shared_ptr[CxxSpecies] CxxNewSpecies "newSpecies" (XML_Node&) cdef vector[shared_ptr[CxxSpecies]] CxxGetSpecies "getSpecies" (XML_Node&) @@ -165,6 +169,7 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera": string phaseOfMatter() except +translate_exception CxxAnyMap& input() void getParameters(CxxAnyMap&) except +translate_exception + void getSpeciesParameters(string, CxxAnyMap&) except +translate_exception string report(cbool, double) except +translate_exception cbool hasPhaseTransition() cbool isPure() @@ -518,6 +523,8 @@ cdef extern from "cantera/transport/DustyGasTransport.h" namespace "Cantera": cdef extern from "cantera/transport/TransportData.h" namespace "Cantera": cdef cppclass CxxTransportData "Cantera::TransportData": CxxTransportData() + CxxAnyMap input + void getParameters(CxxAnyMap&) except +translate_exception cdef cppclass CxxGasTransportData "Cantera::GasTransportData" (CxxTransportData): CxxGasTransportData() @@ -1009,12 +1016,6 @@ ctypedef void (*transportMethod2d)(CxxTransport*, size_t, double*) except +trans ctypedef void (*kineticsMethod1d)(CxxKinetics*, double*) except +translate_exception # classes -cdef class Species: - cdef shared_ptr[CxxSpecies] _species - cdef CxxSpecies* species - - cdef _assign(self, shared_ptr[CxxSpecies] other) - cdef class SpeciesThermo: cdef shared_ptr[CxxSpeciesThermo] _spthermo cdef CxxSpeciesThermo* spthermo @@ -1039,6 +1040,13 @@ cdef class _SolutionBase: cdef np.ndarray _selected_species cdef object parent +cdef class Species: + cdef shared_ptr[CxxSpecies] _species + cdef CxxSpecies* species + cdef _SolutionBase _phase + + cdef _assign(self, shared_ptr[CxxSpecies] other) + cdef class ThermoPhase(_SolutionBase): cdef double _mass_factor(self) cdef double _mole_factor(self) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 7e824696e5..cc09905354 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -224,6 +224,11 @@ cdef class _SolutionBase: self.kinetics.addReaction(reaction._reaction) property input_data: + """ + Get input data corresponding to the current state of this Solution, + along with any user-specified data provided with its input (YAML) + definition. + """ def __get__(self): cdef CxxAnyMap params if self.thermo: diff --git a/interfaces/cython/cantera/speciesthermo.pyx b/interfaces/cython/cantera/speciesthermo.pyx index dee15c72f2..f3c72ecb5e 100644 --- a/interfaces/cython/cantera/speciesthermo.pyx +++ b/interfaces/cython/cantera/speciesthermo.pyx @@ -80,12 +80,18 @@ cdef class SpeciesThermo: return data def _check_n_coeffs(self, n): - """ - Check whether number of coefficients is compatible with a given + """ + Check whether number of coefficients is compatible with a given parameterization prior to instantiation of the underlying C++ object. """ raise NotImplementedError('Needs to be overloaded') + property input_data: + def __get__(self): + cdef CxxAnyMap params + self.spthermo.getParameters(params) + return mergeAnyMap(params, self.spthermo.input()) + def cp(self, T): """ Molar heat capacity at constant pressure [J/kmol/K] at temperature *T*. diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index 2639e5eff2..e3ac050bd5 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -459,3 +459,25 @@ def test_input_data_debye_huckel(self): self.assertEqual(act_data['default-ionic-radius'], 4e-10) self.assertNotIn('kinetics', data) self.assertNotIn('transport', data) + + +class TestSpeciesSerialization(utilities.CanteraTest): + def test_species_simple(self): + gas = ct.Solution('h2o2.yaml') + data = gas.species('H2O').input_data + self.assertEqual(data['name'], 'H2O') + self.assertEqual(data['composition'], {'H': 2, 'O': 1}) + + def test_species_thermo(self): + gas = ct.Solution('h2o2.yaml') + data = gas.species('H2O').input_data['thermo'] + self.assertEqual(data['model'], 'NASA7') + self.assertEqual(data['temperature-ranges'], [200, 1000, 3500]) + self.assertEqual(data['note'], 'L8/89') + + def test_species_transport(self): + gas = ct.Solution('h2o2.yaml') + data = gas.species('H2O').input_data['transport'] + self.assertEqual(data['model'], 'gas') + self.assertEqual(data['geometry'], 'nonlinear') + self.assertNear(data['dipole'], 1.844) diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 0daad5d049..688888a0e2 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -262,6 +262,14 @@ cdef class Species: def __set__(self, GasTransportData tran): self.species.transport = tran._data + property input_data: + def __get__(self): + cdef CxxAnyMap params + self.species.getParameters(params) + if self._phase: + self._phase.thermo.getSpeciesParameters(self.species.name, params) + return mergeAnyMap(params, self.species.input) + def __repr__(self): return ''.format(self.name) @@ -548,6 +556,7 @@ cdef class ThermoPhase(_SolutionBase): return [self.species(i) for i in range(self.n_species)] s = Species(init=False) + s._phase = self if isinstance(k, (str, bytes)): s._assign(self.thermo.species(stringify(k))) elif isinstance(k, (int, float)): @@ -572,6 +581,7 @@ cdef class ThermoPhase(_SolutionBase): ' is linked to a Reactor, Domain1D (flame), or Mixture object.') self.thermo.addUndefinedElements() self.thermo.addSpecies(species._species) + species._phase = self self.thermo.initThermo() if self.kinetics: self.kinetics.invalidateCache() diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index b9b0bb24e8..e3f06f164d 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -56,6 +56,12 @@ cdef class GasTransportData: dipole, polarizability, rotational_relaxation, acentric_factor, dispersion_coefficient, quadrupole_polarizability) + property input_data: + def __get__(self): + cdef CxxAnyMap params + self.data.getParameters(params) + return mergeAnyMap(params, self.data.input) + property geometry: """ Get/Set the string specifying the molecular geometry. One of `atom`, From 5b0069505a2769eb0e9a8c8279f74de44818dfb4 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 22 Jan 2020 10:57:37 -0500 Subject: [PATCH 34/57] Add extra information to AnyValue::getMapWhere error messages --- src/base/AnyMap.cpp | 6 ++++-- 1 file changed, 4 insertions(+), 2 deletions(-) diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 0a93b24373..9e7cdf2d88 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -883,7 +883,8 @@ const AnyMap& AnyValue::getMapWhere(const std::string& key, const std::string& v "Key '{}' not found", m_key); } else { throw InputFileError("AnyValue::getMapWhere", *this, - "Element is not a mapping or list of mappings"); + "Element is not a mapping or list of mappings.\n" + "Looking for a mapping with key '{}' = '{}'", key, value); } } @@ -933,7 +934,8 @@ AnyMap& AnyValue::getMapWhere(const std::string& key, const std::string& value, "Key '{}' not found", m_key); } else { throw InputFileError("AnyValue::getMapWhere", *this, - "Element is not a mapping or list of mappings"); + "Element is not a mapping or list of mappings.\n" + "Looking for a mapping with key '{}' = '{}'", key, value); } } From c117ece7a6a264cda1ed2f88c31ec39cedfd52b2 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 22 Jan 2020 11:54:54 -0500 Subject: [PATCH 35/57] [Python] Add access to input data for Reaction objects --- interfaces/cython/cantera/_cantera.pxd | 3 +++ interfaces/cython/cantera/reaction.pyx | 11 +++++++++++ .../cython/cantera/test/test_kinetics.py | 18 ++++++++++++++++++ 3 files changed, 32 insertions(+) diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 38a5f7331e..13b7854323 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -358,6 +358,8 @@ cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera": string equation() string type() void validate() except +translate_exception + void getParameters(CxxAnyMap&) except +translate_exception + int reaction_type Composition reactants Composition products Composition orders @@ -366,6 +368,7 @@ cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera": cbool duplicate cbool allow_nonreactant_orders cbool allow_negative_orders + CxxAnyMap input cdef cppclass CxxReaction2 "Cantera::Reaction2" (CxxReaction): CxxReaction2() diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index ee2370c784..6b351f44e2 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -320,6 +320,17 @@ cdef class Reaction: def __set__(self, allow): self.reaction.allow_negative_orders = allow + property input_data: + """ + Get input data for this reaction with its current parameter values, + along with any user-specified data provided with its input (YAML) + definition. + """ + def __get__(self): + cdef CxxAnyMap params + self.reaction.getParameters(params) + return mergeAnyMap(params, self.reaction.input) + def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self.equation) diff --git a/interfaces/cython/cantera/test/test_kinetics.py b/interfaces/cython/cantera/test/test_kinetics.py index 7a21bce410..ce0d21f268 100644 --- a/interfaces/cython/cantera/test/test_kinetics.py +++ b/interfaces/cython/cantera/test/test_kinetics.py @@ -842,6 +842,24 @@ def test_listFromYaml(self): self.assertIn('HO2', R[2].products) self.assertEqual(R[0].rate.temperature_exponent, 2.7) + def test_input_data_from_file(self): + R = self.gas.reaction(0) + data = R.input_data + self.assertEqual(data['type'], 'three-body') + self.assertEqual(data['efficiencies'], + {'H2': 2.4, 'H2O': 15.4, 'AR': 0.83}) + self.assertEqual(data['equation'], R.equation) + + def test_input_data_from_scratch(self): + r = ct.ElementaryReaction({'O':1, 'H2':1}, {'H':1, 'OH':1}) + r.rate = ct.Arrhenius(3.87e1, 2.7, 2.6e7) + data = r.input_data + self.assertEqual(data['rate-constant'], + {'A': 3.87e1, 'b': 2.7, 'Ea': 2.6e7}) + terms = data['equation'].split() + self.assertIn('O', terms) + self.assertIn('OH', terms) + def test_elementary(self): r = ct.ElementaryReaction({'O':1, 'H2':1}, {'H':1, 'OH':1}) r.rate = ct.Arrhenius(3.87e1, 2.7, 6260*1000*4.184) From 1f6e690b0ab2f47152992d91e6bb53a7670aa7d1 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 22 Jan 2020 18:51:04 -0500 Subject: [PATCH 36/57] Add functions for conversion from given units into a UnitSystem --- include/cantera/base/Units.h | 29 ++++++------ src/base/Units.cpp | 48 ++++++++++++++++---- src/kinetics/Reaction.cpp | 2 +- src/thermo/PDSS_HKFT.cpp | 20 ++++----- src/thermo/SpeciesThermoFactory.cpp | 2 +- test/general/test_units.cpp | 70 +++++++++++++++-------------- 6 files changed, 105 insertions(+), 66 deletions(-) diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h index 4e21201582..b9827b5a12 100644 --- a/include/cantera/base/Units.h +++ b/include/cantera/base/Units.h @@ -140,13 +140,15 @@ class UnitSystem const std::string& dest) const; double convert(double value, const Units& src, const Units& dest) const; - //! Convert `value` from this unit system (defined by `setDefaults`) to the - //! specified units. - //! - //! @warning This function is an experimental part of the %Cantera API and - //! may be changed or removed without notice. - double convert(double value, const std::string& dest) const; - double convert(double value, const Units& dest) const; + //! Convert `value` to the specified `dest` units from the appropriate units + //! for this unit system (defined by `setDefaults`) + double convertTo(double value, const std::string& dest) const; + double convertTo(double value, const Units& dest) const; + + //! Convert `value` from the specified `src` units to units appropriate for + //! this unit system (defined by `setDefaults`) + double convertFrom(double value, const std::string& src) const; + double convertFrom(double value, const Units& src) const; //! Convert a generic AnyValue node to the units specified in `dest`. If the //! input is a double, convert it using the default units. If the input is a @@ -168,12 +170,13 @@ class UnitSystem double convertActivationEnergy(double value, const std::string& src, const std::string& dest) const; - //! Convert `value` from the default activation energy units to the - //! specified units - //! - //! @warning This function is an experimental part of the %Cantera API and - //! may be changed or removed without notice. - double convertActivationEnergy(double value, const std::string& dest) const; + //! Convert `value` to the units specified by `dest` from the default + //! activation energy units + double convertActivationEnergyTo(double value, const std::string& dest) const; + + //! Convert `value` from the units specified by `src` to the default + //! activation energy units + double convertActivationEnergyFrom(double value, const std::string& src) const; //! Convert a generic AnyValue node to the units specified in `dest`. If the //! input is a double, convert it using the default units. If the input is a diff --git a/src/base/Units.cpp b/src/base/Units.cpp index dfb97f7fdf..df4da215b4 100644 --- a/src/base/Units.cpp +++ b/src/base/Units.cpp @@ -335,12 +335,12 @@ double UnitSystem::convert(double value, const Units& src, return value * src.factor() / dest.factor(); } -double UnitSystem::convert(double value, const std::string& dest) const +double UnitSystem::convertTo(double value, const std::string& dest) const { - return convert(value, Units(dest)); + return convertTo(value, Units(dest)); } -double UnitSystem::convert(double value, const Units& dest) const +double UnitSystem::convertTo(double value, const Units& dest) const { return value / dest.factor() * pow(m_mass_factor, dest.m_mass_dim - dest.m_pressure_dim - dest.m_energy_dim) @@ -351,6 +351,22 @@ double UnitSystem::convert(double value, const Units& dest) const * pow(m_energy_factor, dest.m_energy_dim); } +double UnitSystem::convertFrom(double value, const std::string& dest) const +{ + return convertFrom(value, Units(dest)); +} + +double UnitSystem::convertFrom(double value, const Units& src) const +{ + return value * src.factor() + * pow(m_mass_factor, -src.m_mass_dim + src.m_pressure_dim + src.m_energy_dim) + * pow(m_length_factor, -src.m_length_dim - src.m_pressure_dim + 2*src.m_energy_dim) + * pow(m_time_factor, -src.m_time_dim - 2*src.m_pressure_dim - 2*src.m_energy_dim) + * pow(m_quantity_factor, -src.m_quantity_dim) + * pow(m_pressure_factor, -src.m_pressure_dim) + * pow(m_energy_factor, -src.m_energy_dim); +} + static std::pair split_unit(const AnyValue& v) { if (v.is()) { // Should be a value and units, separated by a space, e.g. '2e4 J/kmol' @@ -379,7 +395,7 @@ double UnitSystem::convert(const AnyValue& v, const Units& dest) const auto val_units = split_unit(v); if (val_units.second.empty()) { // Just a value, so convert using default units - return convert(val_units.first, dest); + return convertTo(val_units.first, dest); } else { // Both source and destination units are explicit return convert(val_units.first, Units(val_units.second), dest); @@ -434,8 +450,8 @@ double UnitSystem::convertActivationEnergy(double value, const std::string& src, return value; } -double UnitSystem::convertActivationEnergy(double value, - const std::string& dest) const +double UnitSystem::convertActivationEnergyTo(double value, + const std::string& dest) const { Units udest(dest); if (udest.convertible(Units("J/kmol"))) { @@ -445,18 +461,34 @@ double UnitSystem::convertActivationEnergy(double value, } else if (udest.convertible(knownUnits.at("eV"))) { return value * m_activation_energy_factor / (Avogadro * udest.factor()); } else { - throw CanteraError("UnitSystem::convertActivationEnergy", + throw CanteraError("UnitSystem::convertActivationEnergyTo", "'{}' is not a unit of activation energy", dest); } } +double UnitSystem::convertActivationEnergyFrom(double value, + const std::string& src) const +{ + Units usrc(src); + if (usrc.convertible(Units("J/kmol"))) { + return value * usrc.factor() / m_activation_energy_factor; + } else if (usrc.convertible(knownUnits.at("K"))) { + return value * GasConstant / m_activation_energy_factor; + } else if (usrc.convertible(knownUnits.at("eV"))) { + return value * Avogadro * usrc.factor() / m_activation_energy_factor; + } else { + throw CanteraError("UnitSystem::convertActivationEnergyFrom", + "'{}' is not a unit of activation energy", src); + } +} + double UnitSystem::convertActivationEnergy(const AnyValue& v, const std::string& dest) const { auto val_units = split_unit(v); if (val_units.second.empty()) { // Just a value, so convert using default units - return convertActivationEnergy(val_units.first, dest); + return convertActivationEnergyTo(val_units.first, dest); } else { // Both source and destination units are explicit return convertActivationEnergy(val_units.first, val_units.second, dest); diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index a55760a7a8..fae6ba4437 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -1009,7 +1009,7 @@ void setupChebyshevReaction(ChebyshevReaction&R, const AnyMap& node, } const UnitSystem& units = node.units(); Units rcUnits = rateCoeffUnits(R, kin); - coeffs(0, 0) += std::log10(units.convert(1.0, rcUnits)); + coeffs(0, 0) += std::log10(units.convertTo(1.0, rcUnits)); R.rate = ChebyshevRate(units.convert(T_range[0], "K"), units.convert(T_range[1], "K"), units.convert(P_range[0], "Pa"), diff --git a/src/thermo/PDSS_HKFT.cpp b/src/thermo/PDSS_HKFT.cpp index 9c78760cdb..5c71422e1a 100644 --- a/src/thermo/PDSS_HKFT.cpp +++ b/src/thermo/PDSS_HKFT.cpp @@ -460,23 +460,23 @@ void PDSS_HKFT::getParameters(AnyMap& eosNode) const eosNode["model"] = "HKFT"; UnitSystem U; U.setDefaults({"cal", "gmol", "bar"}); - eosNode["h0"] = U.convert(m_deltaH_formation_tr_pr, "J/kmol"); - eosNode["g0"] = U.convert(m_deltaG_formation_tr_pr, "J/kmol"); - eosNode["s0"] = U.convert(m_Entrop_tr_pr, "J/kmol/K"); + eosNode["h0"] = U.convertTo(m_deltaH_formation_tr_pr, "J/kmol"); + eosNode["g0"] = U.convertTo(m_deltaG_formation_tr_pr, "J/kmol"); + eosNode["s0"] = U.convertTo(m_Entrop_tr_pr, "J/kmol/K"); eosNode["a"] = vector_fp{ - U.convert(m_a1, "J/kmol/Pa"), - U.convert(m_a2, "J/kmol"), - U.convert(m_a3, "J*K/kmol/Pa"), - U.convert(m_a4, "J*K/kmol") + U.convertTo(m_a1, "J/kmol/Pa"), + U.convertTo(m_a2, "J/kmol"), + U.convertTo(m_a3, "J*K/kmol/Pa"), + U.convertTo(m_a4, "J*K/kmol") }; eosNode["c"] = vector_fp{ - U.convert(m_c1, "J/kmol/K"), - U.convert(m_c2, "J*K/kmol") + U.convertTo(m_c1, "J/kmol/K"), + U.convertTo(m_c2, "J*K/kmol") }; - eosNode["omega"] = U.convert(m_omega_pr_tr, "J/kmol"); + eosNode["omega"] = U.convertTo(m_omega_pr_tr, "J/kmol"); } doublereal PDSS_HKFT::deltaH() const diff --git a/src/thermo/SpeciesThermoFactory.cpp b/src/thermo/SpeciesThermoFactory.cpp index 14d141bbdf..23620cec6a 100644 --- a/src/thermo/SpeciesThermoFactory.cpp +++ b/src/thermo/SpeciesThermoFactory.cpp @@ -387,7 +387,7 @@ void setupMu0(Mu0Poly& thermo, const AnyMap& node) double h0 = node.convert("h0", "J/kmol", 0.0); map T_mu; for (const auto& item : node["data"]) { - double T = node.units().convert(fpValueCheck(item.first), "K"); + double T = node.units().convertTo(fpValueCheck(item.first), "K"); if (dimensionless) { T_mu[T] = item.second.asDouble() * GasConstant * T; } else { diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp index 951b060c26..c76e9d0b30 100644 --- a/test/general/test_units.cpp +++ b/test/general/test_units.cpp @@ -42,20 +42,22 @@ TEST(Units, prefixes) { TEST(Units, with_defaults1) { UnitSystem U({"cm", "g", "mol", "atm", "kcal"}); - EXPECT_DOUBLE_EQ(U.convert(1.0, "m"), 0.01); - EXPECT_DOUBLE_EQ(U.convert(1.0, "kmol/m^3"), 1000); - EXPECT_DOUBLE_EQ(U.convert(1.0, "kg/kmol"), 1.0); - EXPECT_DOUBLE_EQ(U.convert(1.0, "cm^2"), 1.0); - EXPECT_DOUBLE_EQ(U.convert(1.0, "Pa"), 101325); - EXPECT_DOUBLE_EQ(U.convert(1.0, "hPa"), 1013.25); - EXPECT_DOUBLE_EQ(U.convert(1.0, "Pa*m^6/kmol"), 101325*1e-12*1000); - EXPECT_DOUBLE_EQ(U.convert(1.0, "J"), 4184); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "m"), 0.01); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "kmol/m^3"), 1000); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "kg/kmol"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "cm^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "Pa"), 101325); + EXPECT_DOUBLE_EQ(U.convertFrom(101325, "Pa"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "hPa"), 1013.25); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "Pa*m^6/kmol"), 101325*1e-12*1000); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "J"), 4184); } TEST(Units, with_defaults2) { UnitSystem U({"dyn/cm^2"}); - EXPECT_DOUBLE_EQ(U.convert(1.0, "Pa"), 0.1); - EXPECT_DOUBLE_EQ(U.convert(1.0, "N/m^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "Pa"), 0.1); + EXPECT_DOUBLE_EQ(U.convertFrom(1.0, "Pa"), 10); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "N/m^2"), 1.0); } TEST(Units, with_defaults_map) { @@ -65,14 +67,14 @@ TEST(Units, with_defaults_map) { }; UnitSystem U; U.setDefaults(defaults); - EXPECT_DOUBLE_EQ(U.convert(1.0, "m"), 0.01); - EXPECT_DOUBLE_EQ(U.convert(1.0, "kmol/m^3"), 1000); - EXPECT_DOUBLE_EQ(U.convert(1.0, "kg/kmol"), 1.0); - EXPECT_DOUBLE_EQ(U.convert(1.0, "cm^2"), 1.0); - EXPECT_DOUBLE_EQ(U.convert(1.0, "Pa"), 101325); - EXPECT_DOUBLE_EQ(U.convert(1.0, "hPa"), 1013.25); - EXPECT_DOUBLE_EQ(U.convert(1.0, "Pa*m^6/kmol"), 101325*1e-12*1000); - EXPECT_DOUBLE_EQ(U.convert(1.0, "J/cm^3"), 1.0); + EXPECT_DOUBLE_EQ(U.convertFrom(0.01, "m"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "kmol/m^3"), 1000); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "kg/kmol"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "cm^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "Pa"), 101325); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "hPa"), 1013.25); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "Pa*m^6/kmol"), 101325*1e-12*1000); + EXPECT_DOUBLE_EQ(U.convertTo(1.0, "J/cm^3"), 1.0); } TEST(Units, bad_defaults) { @@ -96,23 +98,25 @@ TEST(Units, activation_energies2) { UnitSystem U; U.setDefaultActivationEnergy("cal/mol"); U.setDefaults({"cm", "g", "J"}); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1000, "cal/mol"), 1000); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1000, "J/kmol"), 4184e3); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1000, "K"), 4184e3 / GasConstant); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1000, "cal/mol"), 1000); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1000, "J/kmol"), 4184e3); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyFrom(4184e3, "J/kmol"), 1000); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1000, "K"), 4184e3 / GasConstant); } TEST(Units, activation_energies3) { UnitSystem U({"cal", "mol"}); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1000, "cal/mol"), 1000); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1000, "J/kmol"), 4184e3); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1000, "K"), 4184e3 / GasConstant); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1000, "cal/mol"), 1000); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1000, "J/kmol"), 4184e3); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1000, "K"), 4184e3 / GasConstant); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyFrom(4184e3 / GasConstant, "K"), 1000); } TEST(Units, activation_energies4) { UnitSystem U; U.setDefaultActivationEnergy("K"); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(2000, "K"), 2000); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(2000, "J/kmol"), 2000 * GasConstant); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(2000, "K"), 2000); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(2000, "J/kmol"), 2000 * GasConstant); } TEST(Units, activation_energies5) { @@ -121,8 +125,8 @@ TEST(Units, activation_energies5) { {"quantity", "mol"}, {"energy", "cal"}, {"activation-energy", "K"} }; U.setDefaults(defaults); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(2000, "K"), 2000); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(2000, "J/kmol"), 2000 * GasConstant); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(2000, "K"), 2000); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(2000, "J/kmol"), 2000 * GasConstant); } TEST(Units, activation_energies6) { @@ -131,8 +135,8 @@ TEST(Units, activation_energies6) { {"activation-energy", "eV"} }; U.setDefaults(defaults); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1, "J/kmol"), ElectronCharge * Avogadro); - EXPECT_DOUBLE_EQ(U.convertActivationEnergy(1, "eV"), 1.0); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1, "J/kmol"), ElectronCharge * Avogadro); + EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1, "eV"), 1.0); } TEST(Units, from_anymap) { @@ -172,10 +176,10 @@ TEST(Units, from_yaml) { ); EXPECT_FALSE(m.hasKey("units")); - EXPECT_DOUBLE_EQ(m.units().convert(1, "m"), 1000); + EXPECT_DOUBLE_EQ(m.units().convertTo(1, "m"), 1000); auto& foo = m["foo"].asVector(); - EXPECT_DOUBLE_EQ(foo[0].units().convert(1, "m"), 0.01); - EXPECT_DOUBLE_EQ(foo[1].units().convert(1, "m"), 0.001); + EXPECT_DOUBLE_EQ(foo[0].units().convertTo(1, "m"), 0.01); + EXPECT_DOUBLE_EQ(foo[1].units().convertTo(1, "m"), 0.001); EXPECT_DOUBLE_EQ(foo[0].convert("bar", "m"), 0.006); auto& spam = m["spam"].asVector(); EXPECT_DOUBLE_EQ(spam[0].convert("eggs", "m"), 3000); From 0e85f20525dfec3fdc5efb27d26d2aaa38609c55 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Thu, 23 Jan 2020 00:13:26 -0500 Subject: [PATCH 37/57] Make AnyMap::applyUnits idempotent --- src/base/AnyMap.cpp | 38 +++++++++++++++++++++++++++++-------- test/general/test_units.cpp | 5 +++++ 2 files changed, 35 insertions(+), 8 deletions(-) diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 9e7cdf2d88..4a71ae5777 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -977,16 +977,35 @@ void AnyValue::applyUnits(const UnitSystem& units) if (list.size() && list[0].hasKey("units") && list[0].size() == 1) { // First item in the list is a units declaration, which applies to // the items in the list - UnitSystem newUnits = units; - newUnits.setDefaults(list[0]["units"].asMap()); + auto deltaUnits = list[0]["units"]; list[0].m_data.erase("units"); for (auto& item : list) { - // Any additional units declarations are errors - if (item.size() == 1 && item.hasKey("units")) { - throw InputFileError("AnyValue::applyUnits", item, - "Found units entry as not the first item in a list."); + if (item.hasKey("units")) { + if (item.size() == 1) { + // Any additional units declarations are errors + throw InputFileError("AnyValue::applyUnits", item, + "Found units entry as not the first item in a list."); + } else { + // Merge with a child units declaration + auto& childUnits = item["units"].as(); + for (auto& jtem : deltaUnits) { + if (!childUnits.hasKey(jtem.first)) { + childUnits[jtem.first] = jtem.second; + } + } + } + } else if (item.hasKey("__units__")) { + // Merge with a child units declaration + auto& childUnits = item["__units__"].as(); + for (auto& jtem : deltaUnits) { + if (!childUnits.hasKey(jtem.first)) { + childUnits[jtem.first] = jtem.second; + } + } + } else { + item["__units__"] = deltaUnits; } - item.applyUnits(newUnits); + item.applyUnits(units); } // Remove the "units" map after it has been applied list.erase(list.begin()); @@ -1370,9 +1389,12 @@ void AnyMap::applyUnits(const UnitSystem& units) { m_units = units; if (hasKey("units")) { - m_units.setDefaults(at("units").asMap()); + m_data["__units__"] = std::move(m_data["units"]); m_data.erase("units"); } + if (hasKey("__units__")) { + m_units.setDefaults(m_data["__units__"].asMap()); + } for (auto& item : m_data) { item.second.applyUnits(m_units); } diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp index c76e9d0b30..374b5df887 100644 --- a/test/general/test_units.cpp +++ b/test/general/test_units.cpp @@ -152,6 +152,11 @@ TEST(Units, from_anymap) { auto k1 = m["k1"].asVector(); EXPECT_DOUBLE_EQ(U.convert(k1[0], "m^3/kmol"), 1e-9*5e2); EXPECT_DOUBLE_EQ(U.convertActivationEnergy(k1[2], "J/kmol"), 29000); + + // calling applyUnits again should not affect results + m.applyUnits(U); + EXPECT_DOUBLE_EQ(m.convert("p", "Pa"), 12e5); + EXPECT_DOUBLE_EQ(U.convert(k1[0], "m^3/kmol"), 1e-9*5e2); } TEST(Units, from_anymap_default) { From 697d45cd61261590c3ae488d4c70127dfa191c0a Mon Sep 17 00:00:00 2001 From: Raymond Speth Date: Wed, 3 Feb 2021 12:31:37 -0500 Subject: [PATCH 38/57] [Input] Provide control over ordering of YAML fields --- include/cantera/base/AnyMap.h | 54 +++++++++++++++ src/base/AnyMap.cpp | 113 ++++++++++++++++++++++++++----- src/kinetics/Reaction.cpp | 14 +++- src/thermo/ThermoPhase.cpp | 5 ++ test/general/test_containers.cpp | 26 +++++++ 5 files changed, 194 insertions(+), 18 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index a660bb0248..4f6301a9b9 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -18,6 +18,12 @@ namespace boost class any; } +namespace YAML +{ +class Emitter; +Emitter& operator<<(Emitter& out, const Cantera::AnyMap& rhs); +} + namespace Cantera { @@ -383,6 +389,11 @@ class AnyMap : public AnyBase AnyValue& operator[](const std::string& key); const AnyValue& operator[](const std::string& key) const; + //! Used to create a new item which will be populated from a YAML input + //! string, where the item with `key` occurs at the specified line and + //! column within the string. + AnyValue& createForYaml(const std::string& key, int line, int column); + //! Get the value of the item stored in `key`. Raises an exception if the //! value does not exist. const AnyValue& at(const std::string& key) const; @@ -519,6 +530,38 @@ class AnyMap : public AnyBase //! Use "flow" style when outputting this AnyMap to YAML void setFlowStyle(bool flow=true); + //! Add global rules for setting the order of elements when outputting + //! AnyMap objects to YAML + /*! + * Enables specifying keys that should appear at either the beginning + * or end of the generated YAML mapping. Only programmatically-added keys + * are rearranged. Keys which come from YAML input retain their existing + * ordering, and are output after programmatically-added keys. + * + * This function should be called exactly once for any given spec that + * is to be added. To facilitate this, the method returns a bool so that + * it can be called as part of initializing a static variable. To avoid + * spurious compiler warnings about unused variables, the following + * structure can be used: + * + * ``` + * static bool reg = AnyMap::addOrderingRules("Reaction", + * {{"head", "equation"}, {"tail", "duplicate"}}); + * if (reg) { + * reactionMap["__type__"] = "Reaction"; + * } + * ``` + * + * @param objectType Apply rules to maps where the hidden `__type__` key + * has the corresponding value. + * @param specs A list of rule specifications. Each rule consists of + * two strings. The first string is either "head" or "tail", and the + * second string is the name of a key + * @returns ``true``, to facilitate static initialization + */ + static bool addOrderingRules(const std::string& objectType, + const std::vector>& specs); + private: //! The stored data std::unordered_map m_data; @@ -531,7 +574,18 @@ class AnyMap : public AnyBase //! time for the file, which is used to enable change detection. static std::unordered_map> s_cache; + //! Information about fields that should appear first when outputting to + //! YAML. Keys in this map are matched to `__type__` keys in AnyMap + //! objects, and values are a list of field names. + static std::unordered_map> s_headFields; + + //! Information about fields that should appear last when outputting to + //! YAML. Keys in this map are matched to `__type__` keys in AnyMap + //! objects, and values are a list of field names. + static std::unordered_map> s_tailFields; + friend class AnyValue; + friend YAML::Emitter& YAML::operator<<(YAML::Emitter& out, const AnyMap& rhs); }; // Define begin() and end() to allow use with range-based for loops diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 4a71ae5777..b1829518c9 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -25,6 +25,7 @@ using std::string; namespace { // helper functions std::mutex yaml_cache_mutex; +std::mutex yaml_field_order_mutex; using namespace Cantera; bool isFloat(const std::string& val) @@ -221,12 +222,12 @@ struct convert { for (const auto& child : node) { std::string key = child.first.as(); const auto& loc = child.second.Mark(); - target[key].setLoc(loc.line, loc.column); + AnyValue& value = target.createForYaml(key, loc.line, loc.column); if (child.second.IsMap()) { - target[key] = child.second.as(); + value = child.second.as(); } else { - target[key] = child.second.as(); - target[key].setKey(key); + value = child.second.as(); + value.setKey(key); } } return true; @@ -237,12 +238,62 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs); YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) { + // Initial sort based on the order in which items are added vector, std::string, const AnyValue*>> ordered; + int head = 0; // sort key of the first programmatically-added item + int tail = 0; // sort key of the last programmatically-added item for (const auto& item : rhs) { - ordered.emplace_back(item.second.order(), item.first, &item.second); + const auto& order = item.second.order(); + if (order.first == -1) { // Item is not from an input file + head = std::min(head, order.second); + tail = std::max(tail, order.second); + } + ordered.emplace_back(order, item.first, &item.second); } std::sort(ordered.begin(), ordered.end()); + // Adjust sort keys for items that should moved to the beginning or end of + // the list + if (rhs.hasKey("__type__")) { + bool order_changed = false; + const auto& itemType = rhs["__type__"].asString(); + std::unique_lock lock(yaml_field_order_mutex); + if (AnyMap::s_headFields.count(itemType)) { + for (const auto& key : AnyMap::s_headFields[itemType]) { + for (auto& item : ordered) { + if (std::get<0>(item).first >= 0) { + // This and following items come from an input file and + // should not be re-ordered + break; + } + if (std::get<1>(item) == key) { + std::get<0>(item).second = --head; + order_changed = true; + } + } + } + } + if (AnyMap::s_tailFields.count(itemType)) { + for (const auto& key : AnyMap::s_tailFields[itemType]) { + for (auto& item : ordered) { + if (std::get<0>(item).first >= 0) { + // This and following items come from an input file and + // should not be re-ordered + break; + } + if (std::get<1>(item) == key) { + std::get<0>(item).second = ++tail; + order_changed = true; + } + } + } + } + + if (order_changed) { + std::sort(ordered.begin(), ordered.end()); + } + } + bool flow = rhs.getBool("__flow__", false); if (flow) { out << YAML::Flow; @@ -488,6 +539,9 @@ std::map AnyValue::s_typenames = { std::unordered_map> AnyMap::s_cache; +std::unordered_map> AnyMap::s_headFields; +std::unordered_map> AnyMap::s_tailFields; + // Methods of class AnyBase AnyBase::AnyBase() @@ -1193,7 +1247,7 @@ AnyValue& AnyMap::operator[](const std::string& key) { const auto& iter = m_data.find(key); if (iter == m_data.end()) { - // Create a new key return it + // Create a new key to return // NOTE: 'insert' can be replaced with 'emplace' after support for // G++ 4.7 is dropped. AnyValue& value = m_data.insert({key, AnyValue()}).first->second; @@ -1202,17 +1256,11 @@ AnyValue& AnyMap::operator[](const std::string& key) value.propagateMetadata(m_metadata); } - // If the AnyMap is being created from an input file, this is an - // approximate location, useful mainly if this insertion is going to - // immediately result in an error that needs to be reported. Otherwise, - // this is the location used to set the ordering when outputting to - // YAML. - value.setLoc(m_line, m_column); - - if (m_line == -1) { - // use m_column as a surrogate for ordering the next item added - m_column += 10; - } + // A pseudo-location used to set the ordering when outputting to + // YAML so nodes added this way will come before nodes from YAML, + // with insertion order preserved. + value.setLoc(-1, m_column); + m_column += 10; return value; } else { @@ -1231,6 +1279,20 @@ const AnyValue& AnyMap::operator[](const std::string& key) const } } +AnyValue& AnyMap::createForYaml(const std::string& key, int line, int column) +{ + // NOTE: 'insert' can be replaced with 'emplace' after support for + // G++ 4.7 is dropped. + AnyValue& value = m_data.insert({key, AnyValue()}).first->second; + value.setKey(key); + if (m_metadata) { + value.propagateMetadata(m_metadata); + } + + value.setLoc(line, column); + return value; +} + const AnyValue& AnyMap::at(const std::string& key) const { try { @@ -1404,6 +1466,23 @@ void AnyMap::setFlowStyle(bool flow) { (*this)["__flow__"] = flow; } +bool AnyMap::addOrderingRules(const string& objectType, + const vector>& specs) +{ + std::unique_lock lock(yaml_field_order_mutex); + for (const auto& spec : specs) { + if (spec.at(0) == "head") { + s_headFields[objectType].push_back(spec.at(1)); + } else if (spec.at(0) == "tail") { + s_tailFields[objectType].push_back(spec.at(1)); + } else { + throw CanteraError("AnyMap::addOrderingRules", + "Unknown ordering rule '{}'", spec.at(0)); + } + } + return true; +} + AnyMap AnyMap::fromYamlString(const std::string& yaml) { AnyMap amap; try { diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index fae6ba4437..9882f05ae1 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -114,6 +114,18 @@ void Reaction::getParameters(AnyMap& reactionNode) const if (allow_nonreactant_orders) { reactionNode["nonreactant-orders"] = true; } + + static bool reg = AnyMap::addOrderingRules("Reaction", + {{"head", "type"}, + {"head", "equation"}, + {"tail", "duplicate"}, + {"tail", "orders"}, + {"tail", "negative-orders"}, + {"tail", "nonreactant-orders"} + }); + if (reg) { + reactionNode["__type__"] = "Reaction"; + } } std::string Reaction::reactantString() const @@ -354,8 +366,8 @@ void PlogReaction::getParameters(AnyMap& reactionNode) const std::vector rateList; for (const auto& r : rate.rates()) { AnyMap rateNode; - r.second.getParameters(rateNode); rateNode["P"] = r.first; + r.second.getParameters(rateNode); rateList.push_back(std::move(rateNode)); } reactionNode["rate-constants"] = std::move(rateList); diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 17905bcd21..15e66081e8 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1235,6 +1235,11 @@ void ThermoPhase::getParameters(AnyMap& phaseNode) const } phaseNode["state"] = std::move(state); + + static bool reg = AnyMap::addOrderingRules("Phase", {{"tail", "state"}}); + if (reg) { + phaseNode["__type__"] = "Phase"; + } } const AnyMap& ThermoPhase::input() const diff --git a/test/general/test_containers.cpp b/test/general/test_containers.cpp index 946a24658c..88c754597d 100644 --- a/test/general/test_containers.cpp +++ b/test/general/test_containers.cpp @@ -384,3 +384,29 @@ TEST(AnyMap, dumpYamlString) EXPECT_EQ(original["species"].getMapWhere("name", "OH")["thermo"]["data"].asVector(), generated["species"].getMapWhere("name", "OH")["thermo"]["data"].asVector()); } + +TEST(AnyMap, definedKeyOrdering) +{ + AnyMap m = AnyMap::fromYamlString("{zero: 1, half: 2}"); + m["one"] = 1; + m["two"] = 2; + m["three"] = 3; + m["four"] = 4; + m["__type__"] = "Test"; + + AnyMap::addOrderingRules("Test", { + {"head", "three"}, + {"tail", "one"} + }); + + std::string result = m.toYamlString(); + std::unordered_map loc; + for (auto& item : m) { + loc[item.first] = result.find(item.first); + } + EXPECT_LT(loc["three"], loc["one"]); + EXPECT_LT(loc["three"], loc["half"]); + EXPECT_LT(loc["four"], loc["one"]); + EXPECT_LT(loc["one"], loc["half"]); + EXPECT_LT(loc["zero"], loc["half"]); +} From b92d6ae58bc79a1b0d8548ec04122a77e1168f0c Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 5 Mar 2021 22:17:34 -0500 Subject: [PATCH 39/57] [Input] Enable conversion of added data to units of the parent AnyMap --- include/cantera/base/AnyMap.h | 3 ++ include/cantera/base/Units.h | 3 ++ interfaces/cython/cantera/_cantera.pxd | 1 + interfaces/cython/cantera/utils.pyx | 3 +- src/base/AnyMap.cpp | 45 ++++++++++++++++++++++++-- src/base/Units.cpp | 32 ++++++++++++++---- test/general/test_units.cpp | 10 ++++++ 7 files changed, 87 insertions(+), 10 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 4f6301a9b9..4913518b32 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -137,6 +137,9 @@ class AnyValue : public AnyBase friend bool operator==(const std::string& lhs, const AnyValue& rhs); friend bool operator!=(const std::string& lhs, const AnyValue& rhs); + void setQuantity(double value, const std::string& units, bool is_act_energy=false); + void setQuantity(const vector_fp& values, const std::string& units); + explicit AnyValue(double value); AnyValue& operator=(double value); //! Return the held value as a `double`, if it is a `double` or a `long diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h index b9827b5a12..3b7288a49b 100644 --- a/include/cantera/base/Units.h +++ b/include/cantera/base/Units.h @@ -55,6 +55,8 @@ class Units //! the dimensions of these Units. Units pow(double expoonent) const; + bool operator==(const Units& other) const; + private: //! Scale the unit by the factor `k` void scale(double k) { m_factor *= k; } @@ -173,6 +175,7 @@ class UnitSystem //! Convert `value` to the units specified by `dest` from the default //! activation energy units double convertActivationEnergyTo(double value, const std::string& dest) const; + double convertActivationEnergyTo(double value, const Units& dest) const; //! Convert `value` from the units specified by `src` to the default //! activation energy units diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 13b7854323..f3d800da64 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -70,6 +70,7 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": unordered_map[string, CxxAnyMap*] asMap(string) except +translate_exception CxxAnyMap& getMapWhere(string, string) except +translate_exception T& asType "as" [T]() except +translate_exception + string type_str() cbool isType "is" [T]() cbool isScalar() pair[int, int] order() diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 63913f04b8..34abdffbdf 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -111,7 +111,8 @@ cdef anyvalueToPython(string name, CxxAnyValue& v): return v.asType[vector[vector[cbool]]]() else: raise TypeError("Unable to convert value with key '{}' " - "from AnyValue".format(pystr(name))) + "from AnyValue of held type '{}'".format( + pystr(name), v.type_str())) cdef anymapToPython(CxxAnyMap& m): diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index b1829518c9..cb516cfeb1 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -191,6 +191,18 @@ string formatDouble(double x, long int precision) } } +struct Quantity +{ + AnyValue value; + Units units; + bool isActivationEnergy; + + bool operator==(const Quantity& other) const { + return value == other.value && units == other.units + && isActivationEnergy == other.isActivationEnergy; + } +}; + Cantera::AnyValue Empty; } // end anonymous namespace @@ -526,8 +538,7 @@ struct convert { } -namespace Cantera -{ +namespace Cantera { std::map AnyValue::s_typenames = { {typeid(double).name(), "double"}, @@ -718,6 +729,20 @@ bool operator!=(const std::string& lhs, const AnyValue& rhs) return rhs != lhs; } +// Specialization for "Quantity" + +void AnyValue::setQuantity(double value, const std::string& units, bool is_act_energy) { + *m_value = Quantity{AnyValue(value), Units(units), is_act_energy}; + m_equals = eq_comparer; +} + +void AnyValue::setQuantity(const vector_fp& values, const std::string& units) { + AnyValue v; + v = values; + *m_value = Quantity{v, Units(units), false}; + m_equals = eq_comparer; +} + // Specializations for "double" AnyValue::AnyValue(double value) @@ -1074,6 +1099,21 @@ void AnyValue::applyUnits(const UnitSystem& units) item.applyUnits(units); } } + } else if (is()) { + auto& Q = as(); + if (Q.value.is()) { + if (Q.isActivationEnergy) { + *this = Q.value.as() / units.convertActivationEnergyTo(1.0, Q.units); + } else { + *this = Q.value.as() / units.convertTo(1.0, Q.units); + } + } else if (Q.value.is()) { + double factor = 1.0 / units.convertTo(1.0, Q.units); + auto& old = Q.value.asVector(); + vector_fp converted(old.size()); + scale(old.begin(), old.end(), converted.begin(), factor); + *this = std::move(converted); + } } } @@ -1561,6 +1601,7 @@ AnyMap AnyMap::fromYamlFile(const std::string& name, std::string AnyMap::toYamlString() const { YAML::Emitter out; + const_cast(this)->applyUnits(m_units); out << *this; out << YAML::Newline; return out.c_str(); diff --git a/src/base/Units.cpp b/src/base/Units.cpp index df4da215b4..e646efbe7c 100644 --- a/src/base/Units.cpp +++ b/src/base/Units.cpp @@ -225,6 +225,19 @@ std::string Units::str() const { m_temperature_dim, m_current_dim, m_quantity_dim); } +bool Units::operator==(const Units& other) const +{ + return m_factor == other.m_factor + && m_mass_dim == other.m_mass_dim + && m_length_dim == other.m_length_dim + && m_time_dim == other.m_time_dim + && m_temperature_dim == other.m_temperature_dim + && m_current_dim == other.m_current_dim + && m_quantity_dim == other.m_quantity_dim + && m_pressure_dim == other.m_pressure_dim + && m_energy_dim == other.m_energy_dim; +} + UnitSystem::UnitSystem(std::initializer_list units) : m_mass_factor(1.0) , m_length_factor(1.0) @@ -453,16 +466,21 @@ double UnitSystem::convertActivationEnergy(double value, const std::string& src, double UnitSystem::convertActivationEnergyTo(double value, const std::string& dest) const { - Units udest(dest); - if (udest.convertible(Units("J/kmol"))) { - return value * m_activation_energy_factor / udest.factor(); - } else if (udest.convertible(knownUnits.at("K"))) { + return convertActivationEnergyTo(value, Units(dest)); +} + +double UnitSystem::convertActivationEnergyTo(double value, + const Units& dest) const +{ + if (dest.convertible(Units("J/kmol"))) { + return value * m_activation_energy_factor / dest.factor(); + } else if (dest.convertible(knownUnits.at("K"))) { return value * m_activation_energy_factor / GasConstant; - } else if (udest.convertible(knownUnits.at("eV"))) { - return value * m_activation_energy_factor / (Avogadro * udest.factor()); + } else if (dest.convertible(knownUnits.at("eV"))) { + return value * m_activation_energy_factor / (Avogadro * dest.factor()); } else { throw CanteraError("UnitSystem::convertActivationEnergyTo", - "'{}' is not a unit of activation energy", dest); + "'{}' is not a unit of activation energy", dest.str()); } } diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp index 374b5df887..cb5fcc0586 100644 --- a/test/general/test_units.cpp +++ b/test/general/test_units.cpp @@ -167,6 +167,16 @@ TEST(Units, from_anymap_default) { EXPECT_DOUBLE_EQ(m.convert("h1", "J/kmol", 999), 999); } +TEST(Units, to_anymap) { + UnitSystem U{"kcal", "mol", "cm"}; + AnyMap m; + m["h0"].setQuantity(90, "kJ/kg"); + m["density"].setQuantity({10, 20}, "kg/m^3"); + m.applyUnits(U); + EXPECT_DOUBLE_EQ(m["h0"].asDouble(), 90e3 / 4184); + EXPECT_DOUBLE_EQ(m["density"].asVector()[1], 20.0 * 1e-6); +} + TEST(Units, from_yaml) { AnyMap m = AnyMap::fromYamlString( "units: {length: km}\n" From cdc7241ed1db1b4724abe53dedaaf3c3a3a5816a Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Fri, 5 Mar 2021 23:35:46 -0500 Subject: [PATCH 40/57] [Input] Share UnitSystems among nested AnyMaps Since nested maps will often have the same units as their parents, they can share the same UnitSystem instance until a change is made to one map, at which point a new UnitSystem will be created for that map and its children. This reduces the memory footprint of storing UnitSystem objects, and will become more important as the UnitSystem object starts to carry additional data. --- include/cantera/base/AnyMap.h | 19 +++++++++++-------- src/base/AnyMap.cpp | 28 +++++++++++++++++----------- 2 files changed, 28 insertions(+), 19 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 4913518b32..ddd1d95fee 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -231,8 +231,8 @@ class AnyValue : public AnyBase //! Return values used to determine the sort order when outputting to YAML std::pair order() const; - //! @see AnyMap::applyUnits - void applyUnits(const UnitSystem& units); + //! @see AnyMap::applyUnits(const UnitSystem&) + void applyUnits(shared_ptr& units); //! @see AnyMap::setFlowStyle void setFlowStyle(bool flow=true); @@ -510,7 +510,7 @@ class AnyMap : public AnyBase bool operator!=(const AnyMap& other) const; //! Return the default units that should be used to convert stored values - const UnitSystem& units() const { return m_units; } + const UnitSystem& units() const { return *m_units; } //! Use the supplied UnitSystem to set the default units, and recursively //! process overrides from nodes named `units`. @@ -521,14 +521,17 @@ class AnyMap : public AnyBase * then the specified units are taken to be the defaults for all the maps in * the list. * - * After being processed, the `units` nodes are removed, so this function - * should be called only once, on the root AnyMap. This function is called - * automatically by the fromYamlFile() and fromYamlString() constructors. + * After being processed, the `units` nodes are removed. This function is + * called automatically by the fromYamlFile() and fromYamlString() + * constructors. * * @warning This function is an experimental part of the %Cantera API and * may be changed or removed without notice. */ - void applyUnits(const UnitSystem& units); + void applyUnits(); + + //! @see applyUnits(const UnitSystem&) + void applyUnits(shared_ptr& units); //! Use "flow" style when outputting this AnyMap to YAML void setFlowStyle(bool flow=true); @@ -570,7 +573,7 @@ class AnyMap : public AnyBase std::unordered_map m_data; //! The default units that are used to convert stored values - UnitSystem m_units; + std::shared_ptr m_units; //! Cache for previously-parsed input (YAML) files. The key is the full path //! to the file, and the second element of the value is the last-modified diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index cb516cfeb1..4415307b82 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -1046,7 +1046,7 @@ std::pair AnyValue::order() const return {m_line, m_column}; } -void AnyValue::applyUnits(const UnitSystem& units) +void AnyValue::applyUnits(shared_ptr& units) { if (is()) { // Units declaration applicable to this map @@ -1103,12 +1103,12 @@ void AnyValue::applyUnits(const UnitSystem& units) auto& Q = as(); if (Q.value.is()) { if (Q.isActivationEnergy) { - *this = Q.value.as() / units.convertActivationEnergyTo(1.0, Q.units); + *this = Q.value.as() / units->convertActivationEnergyTo(1.0, Q.units); } else { - *this = Q.value.as() / units.convertTo(1.0, Q.units); + *this = Q.value.as() / units->convertTo(1.0, Q.units); } } else if (Q.value.is()) { - double factor = 1.0 / units.convertTo(1.0, Q.units); + double factor = 1.0 / units->convertTo(1.0, Q.units); auto& old = Q.value.asVector(); vector_fp converted(old.size()); scale(old.begin(), old.end(), converted.begin(), factor); @@ -1279,7 +1279,7 @@ std::vector& AnyValue::asVector(size_t nMin, size_t nMax) // Methods of class AnyMap AnyMap::AnyMap() - : m_units() + : m_units(new UnitSystem()) { } @@ -1487,15 +1487,21 @@ bool AnyMap::operator!=(const AnyMap& other) const return m_data != other.m_data; } -void AnyMap::applyUnits(const UnitSystem& units) { - m_units = units; +void AnyMap::applyUnits() +{ + applyUnits(m_units); +} +void AnyMap::applyUnits(shared_ptr& units) { if (hasKey("units")) { m_data["__units__"] = std::move(m_data["units"]); m_data.erase("units"); } if (hasKey("__units__")) { - m_units.setDefaults(m_data["__units__"].asMap()); + m_units.reset(new UnitSystem(*units)); + m_units->setDefaults(m_data["__units__"].asMap()); + } else { + m_units = units; } for (auto& item : m_data) { item.second.applyUnits(m_units); @@ -1535,7 +1541,7 @@ AnyMap AnyMap::fromYamlString(const std::string& yaml) { throw InputFileError("AnyMap::fromYamlString", fake, err.msg); } amap.setMetadata("file-contents", AnyValue(yaml)); - amap.applyUnits(UnitSystem()); + amap.applyUnits(); return amap; } @@ -1577,7 +1583,7 @@ AnyMap AnyMap::fromYamlFile(const std::string& name, YAML::Node node = YAML::LoadFile(fullName); cache_item.first = node.as(); cache_item.first.setMetadata("filename", AnyValue(fullName)); - cache_item.first.applyUnits(UnitSystem()); + cache_item.first.applyUnits(); } catch (YAML::Exception& err) { s_cache.erase(fullName); AnyMap fake; @@ -1601,7 +1607,7 @@ AnyMap AnyMap::fromYamlFile(const std::string& name, std::string AnyMap::toYamlString() const { YAML::Emitter out; - const_cast(this)->applyUnits(m_units); + const_cast(this)->applyUnits(); out << *this; out << YAML::Newline; return out.c_str(); From 51ad58e00b87c7e739a5c00f6b22c8246d294c7d Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 7 Mar 2021 22:45:32 -0500 Subject: [PATCH 41/57] [Input] Add control over units of constructed AnyMap objects The specified units will be used when converting quantities to scalar values with the applyUnits() function, and will be output to a 'units' section when serializing to YAML. --- include/cantera/base/AnyMap.h | 5 ++++ include/cantera/base/Units.h | 7 ++++++ src/base/AnyMap.cpp | 20 +++++++++++++++ src/base/Units.cpp | 47 +++++++++++++++++++++++++++++++++++ src/thermo/Species.cpp | 3 ++- test/general/test_units.cpp | 44 +++++++++++++++++++++++++++++--- 6 files changed, 122 insertions(+), 4 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index ddd1d95fee..2e70e12380 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -533,6 +533,11 @@ class AnyMap : public AnyBase //! @see applyUnits(const UnitSystem&) void applyUnits(shared_ptr& units); + //! Set the unit system for this AnyMap. The applyUnits() method should be + //! called on the root AnyMap object after all desired calls to setUnits() + //! in the tree have been made. + void setUnits(const UnitSystem& units); + //! Use "flow" style when outputting this AnyMap to YAML void setFlowStyle(bool flow=true); diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h index 3b7288a49b..2591d48754 100644 --- a/include/cantera/base/Units.h +++ b/include/cantera/base/Units.h @@ -188,6 +188,9 @@ class UnitSystem double convertActivationEnergy(const AnyValue& val, const std::string& dest) const; + //! Get the changes to the defaults from `other` to this UnitSystem + AnyMap getDelta(const UnitSystem& other) const; + private: //! Factor to convert mass from this unit system to kg double m_mass_factor; @@ -213,6 +216,10 @@ class UnitSystem //! True if activation energy units are set explicitly, rather than as a //! combination of energy and quantity units bool m_explicit_activation_energy; + + //! Map of dimensions (mass, length, etc.) to names of specified default + //! units + std::map m_defaults; }; } diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 4415307b82..41a3324a2a 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -252,6 +252,14 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) { // Initial sort based on the order in which items are added vector, std::string, const AnyValue*>> ordered; + // Units always come first + AnyValue units; + if (rhs.hasKey("__units__") && rhs["__units__"].as().size()) { + units = rhs["__units__"]; + units.setFlowStyle(); + ordered.emplace_back(std::pair{-2, 0}, std::string("units"), &units); + } + int head = 0; // sort key of the first programmatically-added item int tail = 0; // sort key of the last programmatically-added item for (const auto& item : rhs) { @@ -1508,6 +1516,18 @@ void AnyMap::applyUnits(shared_ptr& units) { } } +void AnyMap::setUnits(const UnitSystem& units) +{ + if (hasKey("__units__")) { + for (const auto& item : units.getDelta(*m_units)) { + m_data["__units__"][item.first] = item.second; + } + } else { + m_data["__units__"] = units.getDelta(*m_units); + } + m_units.reset(new UnitSystem(units)); +} + void AnyMap::setFlowStyle(bool flow) { (*this)["__flow__"] = flow; } diff --git a/src/base/Units.cpp b/src/base/Units.cpp index e646efbe7c..ef21472c11 100644 --- a/src/base/Units.cpp +++ b/src/base/Units.cpp @@ -8,6 +8,7 @@ #include "cantera/base/global.h" #include "cantera/base/stringUtils.h" #include "cantera/base/AnyMap.h" +#include "cantera/base/utilities.h" namespace { using namespace Cantera; @@ -257,16 +258,22 @@ void UnitSystem::setDefaults(std::initializer_list units) auto unit = Units(name); if (unit.convertible(knownUnits.at("kg"))) { m_mass_factor = unit.factor(); + m_defaults["mass"] = name; } else if (unit.convertible(knownUnits.at("m"))) { m_length_factor = unit.factor(); + m_defaults["length"] = name; } else if (unit.convertible(knownUnits.at("s"))) { m_time_factor = unit.factor(); + m_defaults["time"] = name; } else if (unit.convertible(knownUnits.at("kmol"))) { m_quantity_factor = unit.factor(); + m_defaults["quantity"] = name; } else if (unit.convertible(knownUnits.at("Pa"))) { m_pressure_factor = unit.factor(); + m_defaults["pressure"] = name; } else if (unit.convertible(knownUnits.at("J"))) { m_energy_factor = unit.factor(); + m_defaults["energy"] = name; } else if (unit.convertible(knownUnits.at("K")) || unit.convertible(knownUnits.at("A"))) { // Do nothing -- no other scales are supported for temperature and current @@ -287,20 +294,26 @@ void UnitSystem::setDefaults(const std::map& units) Units unit(item.second); if (name == "mass" && unit.convertible(knownUnits.at("kg"))) { m_mass_factor = unit.factor(); + m_defaults["mass"] = item.second; } else if (name == "length" && unit.convertible(knownUnits.at("m"))) { m_length_factor = unit.factor(); + m_defaults["length"] = item.second; } else if (name == "time" && unit.convertible(knownUnits.at("s"))) { m_time_factor = unit.factor(); + m_defaults["time"] = item.second; } else if (name == "temperature" && item.second == "K") { // do nothing - no other temperature scales are supported } else if (name == "current" && item.second == "A") { // do nothing - no other current scales are supported } else if (name == "quantity" && unit.convertible(knownUnits.at("kmol"))) { m_quantity_factor = unit.factor(); + m_defaults["quantity"] = item.second; } else if (name == "pressure" && unit.convertible(knownUnits.at("Pa"))) { m_pressure_factor = unit.factor(); + m_defaults["pressure"] = item.second; } else if (name == "energy" && unit.convertible(knownUnits.at("J"))) { m_energy_factor = unit.factor(); + m_defaults["energy"] = item.second; } else if (name == "activation-energy") { // handled separately to allow override } else { @@ -319,6 +332,7 @@ void UnitSystem::setDefaults(const std::map& units) void UnitSystem::setDefaultActivationEnergy(const std::string& e_units) { Units u(e_units); + m_defaults["activation-energy"] = e_units; if (u.convertible(Units("J/kmol"))) { m_activation_energy_factor = u.factor(); } else if (u.convertible(knownUnits.at("K"))) { @@ -513,4 +527,37 @@ double UnitSystem::convertActivationEnergy(const AnyValue& v, } } +AnyMap UnitSystem::getDelta(const UnitSystem& other) const +{ + AnyMap delta; + // Create a local alias because the template specialization can't be deduced + // automatically + const auto& get = getValue; + if (m_mass_factor != other.m_mass_factor) { + delta["mass"] = get(m_defaults, "mass", "kg"); + } + if (m_length_factor != other.m_length_factor) { + delta["length"] = get(m_defaults, "length", "m"); + } + if (m_time_factor != other.m_time_factor) { + delta["time"] = get(m_defaults, "time", "s"); + } + if (m_pressure_factor != other.m_pressure_factor) { + delta["pressure"] = get(m_defaults, "pressure", "Pa"); + } + if (m_energy_factor != other.m_energy_factor) { + delta["energy"] = get(m_defaults, "energy", "J"); + } + if (m_quantity_factor != other.m_quantity_factor) { + delta["quantity"] = get(m_defaults, "quantity", "kmol"); + } + if (m_explicit_activation_energy + || (other.m_explicit_activation_energy + && m_activation_energy_factor != m_energy_factor / m_quantity_factor)) + { + delta["activation-energy"] = get(m_defaults, "activation-energy", "J/kmol"); + } + return delta; +} + } diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 2a51ac5f64..6e6359bae0 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -154,12 +154,13 @@ unique_ptr newSpecies(const AnyMap& node) const static std::set known_keys{ "thermo", "transport" }; - s->input.applyUnits(node.units()); + s->input.setUnits(node.units()); for (const auto& item : node) { if (known_keys.count(item.first) == 0) { s->input[item.first] = item.second; } } + s->input.applyUnits(); return s; } diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp index cb5fcc0586..cb3779cf51 100644 --- a/test/general/test_units.cpp +++ b/test/general/test_units.cpp @@ -144,7 +144,8 @@ TEST(Units, from_anymap) { "{p: 12 bar, v: 10, A: 1 cm^2, V: 1," " k1: [5e2, 2, 29000], k2: [1e14, -1, 1300 cal/kmol]}"); UnitSystem U({"mm", "min", "atm"}); - m.applyUnits(U); + m.setUnits(U); + m.applyUnits(); EXPECT_DOUBLE_EQ(m.convert("p", "Pa"), 12e5); EXPECT_DOUBLE_EQ(m.convert("v", "cm/min"), 1.0); EXPECT_DOUBLE_EQ(m.convert("A", "mm^2"), 100); @@ -154,7 +155,8 @@ TEST(Units, from_anymap) { EXPECT_DOUBLE_EQ(U.convertActivationEnergy(k1[2], "J/kmol"), 29000); // calling applyUnits again should not affect results - m.applyUnits(U); + m.setUnits(U); + m.applyUnits(); EXPECT_DOUBLE_EQ(m.convert("p", "Pa"), 12e5); EXPECT_DOUBLE_EQ(U.convert(k1[0], "m^3/kmol"), 1e-9*5e2); } @@ -172,11 +174,47 @@ TEST(Units, to_anymap) { AnyMap m; m["h0"].setQuantity(90, "kJ/kg"); m["density"].setQuantity({10, 20}, "kg/m^3"); - m.applyUnits(U); + m.setUnits(U); + m.applyUnits(); EXPECT_DOUBLE_EQ(m["h0"].asDouble(), 90e3 / 4184); EXPECT_DOUBLE_EQ(m["density"].asVector()[1], 20.0 * 1e-6); } +TEST(Units, to_anymap_nested) { + UnitSystem U1{"g", "cm", "mol"}; + UnitSystem U2{"mg", "km"}; + for (int i = 0; i < 4; i++) { + AnyMap m; + m["A"].setQuantity(90, "kg/m"); + m["nested"]["B"].setQuantity(12, "m^2"); + auto C = std::vector(2); + C[0]["foo"].setQuantity(17, "m^2"); + C[1]["bar"].setQuantity(19, "kmol"); + m["nested"]["C"] = C; + // Test different orders of setting units, and repeated calls to setUnits + if (i == 0) { + m.setUnits(U1); + m["nested"].as().setUnits(U2); + } else if (i == 1) { + m["nested"].as().setUnits(U2); + m.setUnits(U1); + } else if (i == 2) { + m.setUnits(U1); + m["nested"].as().setUnits(U2); + m.setUnits(U1); + } else if (i == 3) { + m["nested"].as().setUnits(U2); + m.setUnits(U1); + m["nested"].as().setUnits(U2); + } + m.applyUnits(); + EXPECT_DOUBLE_EQ(m["A"].asDouble(), 900) << "case " << i; + EXPECT_DOUBLE_EQ(m["nested"]["B"].asDouble(), 12e-6) << "case " << i; + EXPECT_DOUBLE_EQ(m["nested"]["C"].asVector()[0]["foo"].asDouble(), 17e-6); + EXPECT_DOUBLE_EQ(m["nested"]["C"].asVector()[1]["bar"].asDouble(), 19000); + } +} + TEST(Units, from_yaml) { AnyMap m = AnyMap::fromYamlString( "units: {length: km}\n" From 2cd7e9a001873762f4e4cdf7815c791fcdf322b1 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 7 Mar 2021 22:58:25 -0500 Subject: [PATCH 42/57] [Input] Add option of setting units to YamlWriter --- include/cantera/base/YamlWriter.h | 10 ++++++++ interfaces/cython/cantera/_cantera.pxd | 1 + .../cython/cantera/test/test_kinetics.py | 5 ++-- interfaces/cython/cantera/utils.pyx | 1 + src/base/YamlWriter.cpp | 1 + src/kinetics/RxnRates.cpp | 2 +- test/general/test_serialization.cpp | 25 +++++++++++++++++++ 7 files changed, 42 insertions(+), 3 deletions(-) diff --git a/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h index 2d328b5612..45b8b354ad 100644 --- a/include/cantera/base/YamlWriter.h +++ b/include/cantera/base/YamlWriter.h @@ -7,6 +7,7 @@ #define CT_YAMLWRITER_H #include "cantera/base/ct_defs.h" +#include "cantera/base/Units.h" namespace Cantera { @@ -46,6 +47,11 @@ class YamlWriter m_skip_user_defined = skip; } + //! Set the units to be used in the output file + void setUnits(const UnitSystem& units) { + m_output_units = units; + } + protected: std::vector> m_phases; @@ -54,6 +60,10 @@ class YamlWriter //! @see skipUserDefined() bool m_skip_user_defined; + + //! Top-level units directive for the output file. Defaults to Cantera's + //! native SI+kmol system. + UnitSystem m_output_units; }; } diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index f3d800da64..ca5761ae94 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -64,6 +64,7 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": CxxAnyValue& operator[](string) except +translate_exception cbool hasKey(string) string keys_str() + void applyUnits() cdef cppclass CxxAnyValue "Cantera::AnyValue": CxxAnyValue() diff --git a/interfaces/cython/cantera/test/test_kinetics.py b/interfaces/cython/cantera/test/test_kinetics.py index ce0d21f268..b3c6b7ecf1 100644 --- a/interfaces/cython/cantera/test/test_kinetics.py +++ b/interfaces/cython/cantera/test/test_kinetics.py @@ -854,8 +854,9 @@ def test_input_data_from_scratch(self): r = ct.ElementaryReaction({'O':1, 'H2':1}, {'H':1, 'OH':1}) r.rate = ct.Arrhenius(3.87e1, 2.7, 2.6e7) data = r.input_data - self.assertEqual(data['rate-constant'], - {'A': 3.87e1, 'b': 2.7, 'Ea': 2.6e7}) + self.assertNear(data['rate-constant']['A'], 3.87e1) + self.assertNear(data['rate-constant']['b'], 2.7) + self.assertNear(data['rate-constant']['Ea'], 2.6e7) terms = data['equation'].split() self.assertIn('O', terms) self.assertIn('OH', terms) diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 34abdffbdf..1360273d9b 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -117,6 +117,7 @@ cdef anyvalueToPython(string name, CxxAnyValue& v): cdef anymapToPython(CxxAnyMap& m): py_items = [] + m.applyUnits() for item in m: py_items.append((item.second.order(), pystr(item.first), diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 6379266116..53ae8b1e09 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -198,6 +198,7 @@ std::string YamlWriter::toYamlString() const } output.setMetadata("precision", AnyValue(m_float_precision)); + output.setUnits(m_output_units); return output.toYamlString(); } diff --git a/src/kinetics/RxnRates.cpp b/src/kinetics/RxnRates.cpp index 155b0bf7ee..216ca1dd2a 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -55,7 +55,7 @@ void Arrhenius::getParameters(AnyMap& rateNode) const { rateNode["A"] = preExponentialFactor(); rateNode["b"] = temperatureExponent(); - rateNode["Ea"] = activationEnergy_R() * GasConstant; + rateNode["Ea"].setQuantity(activationEnergy_R(), "K", true); rateNode.setFlowStyle(); } diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index 32d8c661b3..ea057a6bf7 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -107,6 +107,31 @@ TEST(YamlWriter, reactions) } } +TEST(YamlWriter, reactionUnits) +{ + auto original = newSolution("h2o2.yaml"); + YamlWriter writer; + writer.addPhase(original); + writer.setPrecision(14); + UnitSystem outUnits; + std::map defaults = {{"activation-energy", "K"}}; + outUnits.setDefaults(defaults); + writer.setUnits(outUnits); + writer.toYamlFile("generated-h2o2-K.yaml"); + auto duplicate = newSolution("generated-h2o2-K.yaml"); + + auto kin1 = original->kinetics(); + auto kin2 = duplicate->kinetics(); + + ASSERT_EQ(kin1->nReactions(), kin2->nReactions()); + vector_fp kf1(kin1->nReactions()), kf2(kin1->nReactions()); + kin1->getFwdRateConstants(kf1.data()); + kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < kin1->nReactions(); i++) { + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for reaction i = " << i; + } +} + TEST(YamlWriter, multipleReactionSections) { auto original1 = newSolution("h2o2.yaml"); From 6d60e9c22c5e966d2cce0e576ee31cc349fffa65 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 9 Mar 2021 18:02:32 -0500 Subject: [PATCH 43/57] [Input] Handle variable output units for all reaction types --- include/cantera/base/AnyMap.h | 27 +++ include/cantera/kinetics/Reaction.h | 14 +- include/cantera/kinetics/RxnRates.h | 3 +- src/base/AnyMap.cpp | 41 +++- src/kinetics/Falloff.cpp | 10 +- src/kinetics/Reaction.cpp | 79 +++++--- src/kinetics/RxnRates.cpp | 12 +- test/data/pdep-test.yaml | 304 ++++++++++++++++++++++++++++ test/general/test_serialization.cpp | 76 ++++++- 9 files changed, 517 insertions(+), 49 deletions(-) create mode 100644 test/data/pdep-test.yaml diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 2e70e12380..199f556892 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -22,6 +22,7 @@ namespace YAML { class Emitter; Emitter& operator<<(Emitter& out, const Cantera::AnyMap& rhs); +Emitter& operator<<(Emitter& out, const Cantera::AnyValue& rhs); } namespace Cantera @@ -137,9 +138,33 @@ class AnyValue : public AnyBase friend bool operator==(const std::string& lhs, const AnyValue& rhs); friend bool operator!=(const std::string& lhs, const AnyValue& rhs); + //! @name Quantity conversions + //! Assign a quantity consisting of one or more values and their + //! corresponding units, which will be converted to a target unit system + //! when the applyUnits() function is later called on the root of the + //! AnyMap. + //! @{ + + //! Assign a scalar quantity with units as a string, for example + //! `{3.0, "m^2"}`. If the `is_act_energy` flag is set to `true`, the units + //! will be converted using the special rules for activation energies. void setQuantity(double value, const std::string& units, bool is_act_energy=false); + + //! Assign a scalar quantity with units as a Units object, for cases where + //! the units vary and are determined dynamically, such as reaction + //! pre-exponential factors + void setQuantity(double value, const Units& units); + + //! Assign a vector where all the values have the same units void setQuantity(const vector_fp& values, const std::string& units); + typedef std::function unitConverter; + + //! Assign a value of any type where the unit conversion requires a + //! different behavior besides scaling all values by the same factor + void setQuantity(const AnyValue& value, const unitConverter& converter); + //! @} end group quantity conversions + explicit AnyValue(double value); AnyValue& operator=(double value); //! Return the held value as a `double`, if it is a `double` or a `long @@ -271,6 +296,8 @@ class AnyValue : public AnyBase static bool vector2_eq(const boost::any& lhs, const boost::any& rhs); mutable Comparer m_equals; + + friend YAML::Emitter& YAML::operator<<(YAML::Emitter& out, const AnyValue& rhs); }; //! Implicit conversion to vector diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index d33c4b2e04..58d1f69269 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -10,6 +10,8 @@ #include "cantera/base/AnyMap.h" #include "cantera/kinetics/ReactionRate.h" +#include "cantera/kinetics/RxnRates.h" +#include "cantera/base/Units.h" namespace Cantera { @@ -104,6 +106,11 @@ class Reaction //! Input data used for specific models AnyMap input; + //! The units of the rate constant. These are determined by the units of the + //! standard concentration of the reactant species' phases of the phase + //! where the reaction occurs. + Units rate_units; + protected: //! Flag indicating whether reaction is set up correctly bool m_valid; @@ -226,6 +233,10 @@ class FalloffReaction : public Reaction shared_ptr falloff; bool allow_negative_pre_exponential_factor; + + //! The units of the low-pressure rate constant. The units of the + //! high-pressure rate constant are stored in #rate_units. + Units low_rate_units; }; //! A reaction where the rate decreases as pressure increases due to collisional @@ -428,8 +439,7 @@ void parseReactionEquation(Reaction& R, const AnyValue& equation, //! //! @todo Rate units will become available as `rate_units` after serialization //! is implemented. -Units rateCoeffUnits(const Reaction& R, const Kinetics& kin, - int pressure_dependence=0); +Units rateCoeffUnits(const Reaction& R, const Kinetics& kin); // declarations of setup functions void setupElementaryReaction(ElementaryReaction&, const XML_Node&); diff --git a/include/cantera/kinetics/RxnRates.h b/include/cantera/kinetics/RxnRates.h index 35c963c25c..e2a90951e9 100644 --- a/include/cantera/kinetics/RxnRates.h +++ b/include/cantera/kinetics/RxnRates.h @@ -19,6 +19,7 @@ class AnyValue; class UnitSystem; class Units; class AnyMap; +class Units; //! Arrhenius reaction rate type depends only on temperature /** @@ -46,7 +47,7 @@ class Arrhenius void setParameters(const AnyValue& rate, const UnitSystem& units, const Units& rate_units); - void getParameters(AnyMap& rateNode) const; + void getParameters(AnyMap& rateNode, const Units& rate_units) const; //! Update concentration-dependent parts of the rate coefficient. /*! diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 41a3324a2a..d97814a133 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -196,6 +196,7 @@ struct Quantity AnyValue value; Units units; bool isActivationEnergy; + AnyValue::unitConverter converter; bool operator==(const Quantity& other) const { return value == other.value && units == other.units @@ -246,8 +247,6 @@ struct convert { } }; -YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs); - YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) { // Initial sort based on the order in which items are added @@ -412,7 +411,8 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs) out << rhs.asBool(); } else { throw CanteraError("operator<<(YAML::Emitter&, AnyValue&)", - "Don't know how to encode value of type '{}'", rhs.type_str()); + "Don't know how to encode value of type '{}' with key '{}'", + rhs.type_str(), rhs.m_key); } } else if (rhs.is()) { out << rhs.as(); @@ -459,7 +459,8 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyValue& rhs) out << YAML::EndSeq; } else { throw CanteraError("operator<<(YAML::Emitter&, AnyValue&)", - "Don't know how to encode value of type '{}'", rhs.type_str()); + "Don't know how to encode value of type '{}' with key '{}'", + rhs.type_str(), rhs.m_key); } return out; } @@ -744,6 +745,11 @@ void AnyValue::setQuantity(double value, const std::string& units, bool is_act_e m_equals = eq_comparer; } +void AnyValue::setQuantity(double value, const Units& units) { + *m_value = Quantity{AnyValue(value), units, false}; + m_equals = eq_comparer; +} + void AnyValue::setQuantity(const vector_fp& values, const std::string& units) { AnyValue v; v = values; @@ -751,6 +757,13 @@ void AnyValue::setQuantity(const vector_fp& values, const std::string& units) { m_equals = eq_comparer; } +void AnyValue::setQuantity(const AnyValue& value, const unitConverter& converter) +{ + *m_value = Quantity{value, Units(0.0), false, converter}; + m_equals = eq_comparer; +} + + // Specializations for "double" AnyValue::AnyValue(double value) @@ -1057,8 +1070,21 @@ std::pair AnyValue::order() const void AnyValue::applyUnits(shared_ptr& units) { if (is()) { + AnyMap& m = as(); + + //! @todo Remove this check after CTI/XML support is removed in Cantera 3.0. + if (m.getBool("__unconvertible__", false)) { + AnyMap delta = units->getDelta(UnitSystem()); + if (delta.hasKey("length") || delta.hasKey("quantity") + || delta.hasKey("time")) + { + throw CanteraError("AnyValue::applyUnits", "AnyMap contains" + " values that cannot be converted to non-default unit" + " systems (probably reaction rates read from XML files)"); + } + } // Units declaration applicable to this map - as().applyUnits(units); + m.applyUnits(units); } else if (is>()) { auto& list = as>(); if (list.size() && list[0].hasKey("units") && list[0].size() == 1) { @@ -1109,7 +1135,10 @@ void AnyValue::applyUnits(shared_ptr& units) } } else if (is()) { auto& Q = as(); - if (Q.value.is()) { + if (Q.converter) { + Q.converter(Q.value, *units); + *this = std::move(Q.value); + } else if (Q.value.is()) { if (Q.isActivationEnergy) { *this = Q.value.as() / units->convertActivationEnergyTo(1.0, Q.units); } else { diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index 309b31c935..c94496f508 100644 --- a/src/kinetics/Falloff.cpp +++ b/src/kinetics/Falloff.cpp @@ -88,10 +88,10 @@ void Troe::getParameters(AnyMap& reactionNode) const { AnyMap params; params["A"] = m_a; - params["T3"] = 1.0 / m_rt3; - params["T1"] = 1.0 / m_rt1; + params["T3"].setQuantity(1.0 / m_rt3, "K"); + params["T1"].setQuantity(1.0 / m_rt1, "K"); if (std::abs(m_t2) > SmallNumber) { - params["T2"] = m_t2; + params["T2"].setQuantity(m_t2, "K"); } params.setFlowStyle(); reactionNode["Troe"] = std::move(params); @@ -155,8 +155,8 @@ void SRI::getParameters(AnyMap& reactionNode) const { AnyMap params; params["A"] = m_a; - params["B"] = m_b; - params["C"] = m_c; + params["B"].setQuantity(m_b, "K"); + params["C"].setQuantity(m_c, "K"); if (m_d != 1.0 || m_e != 0.0) { params["D"] = m_d; params["E"] = m_e; diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 9882f05ae1..6317283d6b 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -202,7 +202,7 @@ void ElementaryReaction::getParameters(AnyMap& reactionNode) const reactionNode["negative-A"] = true; } AnyMap rateNode; - rate.getParameters(rateNode); + rate.getParameters(rateNode, rate_units); reactionNode["rate-constant"] = std::move(rateNode); } @@ -311,10 +311,10 @@ void FalloffReaction::getParameters(AnyMap& reactionNode) const Reaction::getParameters(reactionNode); reactionNode["type"] = "falloff"; AnyMap lowRateNode; - low_rate.getParameters(lowRateNode); + low_rate.getParameters(lowRateNode, low_rate_units); reactionNode["low-P-rate-constant"] = std::move(lowRateNode); AnyMap highRateNode; - high_rate.getParameters(highRateNode); + high_rate.getParameters(highRateNode, rate_units); reactionNode["high-P-rate-constant"] = std::move(highRateNode); falloff->getParameters(reactionNode); @@ -366,8 +366,8 @@ void PlogReaction::getParameters(AnyMap& reactionNode) const std::vector rateList; for (const auto& r : rate.rates()) { AnyMap rateNode; - rateNode["P"] = r.first; - r.second.getParameters(rateNode); + rateNode["P"].setQuantity(r.first, "Pa"); + r.second.getParameters(rateNode, rate_units); rateList.push_back(std::move(rateNode)); } reactionNode["rate-constants"] = std::move(rateList); @@ -420,7 +420,6 @@ CustomFunc1Reaction::CustomFunc1Reaction() void CustomFunc1Reaction::setParameters(const AnyMap& node, const Kinetics& kin) { Reaction2::setParameters(node, kin); - Units rate_units; // @todo Not needed once `rate_units` is implemented. setRate( std::shared_ptr(new CustomFunc1Rate(node, rate_units))); } @@ -435,9 +434,6 @@ void TestReaction::setParameters(const AnyMap& node, const Kinetics& kin) { Reaction2::setParameters(node, kin); - // @todo Rate units will become available as `rate_units` after - // serialization is fully implemented. - Units rate_units = rateCoeffUnits(*this, kin); setRate( std::shared_ptr(new ArrheniusRate(node, rate_units))); allow_negative_pre_exponential_factor = node.getBool("negative-A", false); @@ -447,8 +443,8 @@ void ChebyshevReaction::getParameters(AnyMap& reactionNode) const { Reaction::getParameters(reactionNode); reactionNode["type"] = "Chebyshev"; - reactionNode["temperature-range"] = vector_fp{rate.Tmin(), rate.Tmax()}; - reactionNode["pressure-range"] = vector_fp{rate.Pmin(), rate.Pmax()}; + reactionNode["temperature-range"].setQuantity({rate.Tmin(), rate.Tmax()}, "K"); + reactionNode["pressure-range"].setQuantity({rate.Pmin(), rate.Pmax()}, "Pa"); const auto& coeffs1d = rate.coeffs(); size_t nT = rate.nTemperature(); size_t nP = rate.nPressure(); @@ -458,7 +454,15 @@ void ChebyshevReaction::getParameters(AnyMap& reactionNode) const coeffs2d[i][j] = coeffs1d[nP*i + j]; } } - reactionNode["data"] = std::move(coeffs2d); + // Unit conversions must take place later, after the destination unit system + // is known. A lambda function is used here to override the default behavior + Units rate_units2 = rate_units; + auto converter = [rate_units2](AnyValue& coeffs, const UnitSystem& units) { + coeffs.asVector()[0][0] += std::log10(units.convertFrom(1.0, rate_units2)); + }; + AnyValue coeffs; + coeffs = std::move(coeffs2d); + reactionNode["data"].setQuantity(coeffs, converter); } InterfaceReaction::InterfaceReaction() @@ -498,7 +502,7 @@ void InterfaceReaction::getParameters(AnyMap& reactionNode) const AnyMap dep; dep["a"] = d.second.a; dep["m"] = d.second.m; - dep["E"] = d.second.E; + dep["E"].setQuantity(d.second.E, "K", true); deps[d.first] = std::move(dep); } reactionNode["coverage-dependencies"] = std::move(deps); @@ -538,17 +542,12 @@ Arrhenius readArrhenius(const XML_Node& arrhenius_node) getFloat(arrhenius_node, "E", "actEnergy") / GasConstant); } -Units rateCoeffUnits(const Reaction& R, const Kinetics& kin, - int pressure_dependence) +Units rateCoeffUnits(const Reaction& R, const Kinetics& kin) { if (!R.valid()) { // If a reaction is invalid because of missing species in the Kinetics // object, determining the units of the rate coefficient is impossible. return Units(); - } else if (R.type() == "interface" - && dynamic_cast(R).is_sticking_coefficient) { - // Sticking coefficients are dimensionless - return Units(); } // Determine the units of the rate coefficient @@ -564,15 +563,13 @@ Units rateCoeffUnits(const Reaction& R, const Kinetics& kin, // unless already overridden by user-specified orders if (stoich.first == "M") { rcUnits *= rxn_phase_units.pow(-1); + } else if (ba::starts_with(stoich.first, "(+")) { + continue; } else if (R.orders.find(stoich.first) == R.orders.end()) { const auto& phase = kin.speciesPhase(stoich.first); rcUnits *= phase.standardConcentrationUnits().pow(-stoich.second); } } - - // Incorporate pressure dependence for low-pressure falloff and high- - // pressure chemically-activated reaction limits - rcUnits *= rxn_phase_units.pow(-pressure_dependence); return rcUnits; } @@ -580,10 +577,24 @@ Arrhenius readArrhenius(const Reaction& R, const AnyValue& rate, const Kinetics& kin, const UnitSystem& units, int pressure_dependence=0) { - Units rate_units = rateCoeffUnits(R, kin, pressure_dependence); - Arrhenius arr; - arr.setParameters(rate, units, rate_units); - return arr; + double A, b, Ta; + Units rc_units = R.rate_units; + if (pressure_dependence) { + Units rxn_phase_units = kin.thermo(kin.reactionPhaseIndex()).standardConcentrationUnits(); + rc_units *= rxn_phase_units.pow(-pressure_dependence); + } + if (rate.is()) { + auto& rate_map = rate.as(); + A = units.convert(rate_map["A"], rc_units); + b = rate_map["b"].asDouble(); + Ta = units.convertActivationEnergy(rate_map["Ea"], "K"); + } else { + auto& rate_vec = rate.asVector(3); + A = units.convert(rate_vec[0], rc_units); + b = rate_vec[1].asDouble(); + Ta = units.convertActivationEnergy(rate_vec[2], "K"); + } + return Arrhenius(A, b, Ta); } //! Parse falloff parameters, given a rateCoeff node @@ -695,6 +706,9 @@ void setupReaction(Reaction& R, const XML_Node& rxn_node) R.duplicate = rxn_node.hasAttrib("duplicate"); const std::string& rev = rxn_node["reversible"]; R.reversible = (rev == "true" || rev == "yes"); + + // Prevent invalid conversions of the rate coefficient + R.rate_units = Units(0.0); } void parseReactionEquation(Reaction& R, const AnyValue& equation, @@ -782,6 +796,7 @@ void setupReaction(Reaction& R, const AnyMap& node, const Kinetics& kin) R.allow_negative_orders = node.getBool("negative-orders", false); R.allow_nonreactant_orders = node.getBool("nonreactant-orders", false); + R.rate_units = rateCoeffUnits(R, kin); R.input = node; } @@ -861,6 +876,7 @@ void setupFalloffReaction(FalloffReaction& R, const XML_Node& rxn_node) if (rxn_node["negative_A"] == "yes") { R.allow_negative_pre_exponential_factor = true; } + R.low_rate_units = Units(0.0); readFalloff(R, rc_node); readEfficiencies(R.third_body, rc_node); setupReaction(R, rxn_node); @@ -904,16 +920,20 @@ void setupFalloffReaction(FalloffReaction& R, const AnyMap& node, R.third_body.efficiencies[third_body.substr(2, third_body.size() - 3)] = 1.0; } + R.low_rate_units = R.rate_units; + Units rxn_phase_units = kin.thermo(kin.reactionPhaseIndex()).standardConcentrationUnits(); if (node["type"] == "falloff") { R.low_rate = readArrhenius(R, node["low-P-rate-constant"], kin, node.units(), 1); R.high_rate = readArrhenius(R, node["high-P-rate-constant"], kin, node.units()); + R.low_rate_units *= rxn_phase_units.pow(-1); } else { // type == "chemically-activated" R.low_rate = readArrhenius(R, node["low-P-rate-constant"], kin, node.units()); R.high_rate = readArrhenius(R, node["high-P-rate-constant"], kin, node.units(), -1); + R.rate_units *= rxn_phase_units; } readFalloff(R, node); @@ -943,6 +963,7 @@ void setupChemicallyActivatedReaction(ChemicallyActivatedReaction& R, throw CanteraError("setupChemicallyActivatedReaction", "Did not find " "the correct number of Arrhenius rate expressions"); } + R.low_rate_units = Units(0.0); readFalloff(R, rc_node); readEfficiencies(R.third_body, rc_node); setupReaction(R, rxn_node); @@ -1020,8 +1041,7 @@ void setupChebyshevReaction(ChebyshevReaction&R, const AnyMap& node, } } const UnitSystem& units = node.units(); - Units rcUnits = rateCoeffUnits(R, kin); - coeffs(0, 0) += std::log10(units.convertTo(1.0, rcUnits)); + coeffs(0, 0) += std::log10(units.convertTo(1.0, R.rate_units)); R.rate = ChebyshevRate(units.convert(T_range[0], "K"), units.convert(T_range[1], "K"), units.convert(P_range[0], "Pa"), @@ -1069,6 +1089,7 @@ void setupInterfaceReaction(InterfaceReaction& R, const AnyMap& node, R.rate = readArrhenius(R, node["rate-constant"], kin, node.units()); } else if (node.hasKey("sticking-coefficient")) { R.is_sticking_coefficient = true; + R.rate_units = Units(); // sticking coefficients are dimensionless R.rate = readArrhenius(R, node["sticking-coefficient"], kin, node.units()); R.use_motz_wise_correction = node.getBool("Motz-Wise", kin.thermo().input().getBool("Motz-Wise", false)); diff --git a/src/kinetics/RxnRates.cpp b/src/kinetics/RxnRates.cpp index 216ca1dd2a..cdf9565feb 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -51,9 +51,17 @@ void Arrhenius::setParameters(const AnyValue& rate, } } -void Arrhenius::getParameters(AnyMap& rateNode) const +void Arrhenius::getParameters(AnyMap& rateNode, const Units& rate_units) const { - rateNode["A"] = preExponentialFactor(); + if (rate_units.factor() != 0.0) { + rateNode["A"].setQuantity(preExponentialFactor(), rate_units); + } else { + // @TODO: This branch can be removed after CTI/XML support is removed + // in Cantera 3.0. + rateNode["A"] = preExponentialFactor(); + rateNode["__unconvertible__"] = true; + } + rateNode["b"] = temperatureExponent(); rateNode["Ea"].setQuantity(activationEnergy_R(), "K", true); rateNode.setFlowStyle(); diff --git a/test/data/pdep-test.yaml b/test/data/pdep-test.yaml new file mode 100644 index 0000000000..c50fafc030 --- /dev/null +++ b/test/data/pdep-test.yaml @@ -0,0 +1,304 @@ +description: |- + Generic mechanism header + This line contains a non-unicode character () that should just be ignored. + +generator: ck2yaml +input-files: [pdep-test.inp] +cantera-version: 2.6.0a1 +date: Mon, 08 Mar 2021 23:43:36 -0500 + +units: {length: cm, time: s, quantity: mol, activation-energy: cal/mol} + +phases: +- name: gas + thermo: ideal-gas + elements: [H, C] + species: [H, R1A, R1B, P1, R2, P2A, P2B, R3, P3A, P3B, R4, P4, R5, P5A, + P5B, R6, P6A, P6B, R7, P7A, P7B] + kinetics: gas + state: {T: 300.0, P: 1 atm} + +species: +- name: H + composition: {H: 1} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [2.5, 7.05332819e-13, -1.99591964e-15, 2.30081632e-18, -9.27732332e-22, + 2.54736599e+04, -0.446682853] + - [2.50000001, -2.30842973e-11, 1.61561948e-14, -4.73515235e-18, 4.98197357e-22, + 2.54736599e+04, -0.446682914] + note: 'NOTE: All of this thermo data is bogus' +- name: R1A + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R1B + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P1 + composition: {C: 2, H: 7} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R2 + composition: {C: 2, H: 7} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P2A + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P2B + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R3 + composition: {C: 2, H: 7} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P3A + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P3B + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R4 + composition: {C: 1, H: 3} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P4 + composition: {C: 1, H: 3} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R5 + composition: {C: 2, H: 7} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P5A + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P5B + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R6 + composition: {C: 2, H: 8} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P6A + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P6B + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: R7 + composition: {C: 2, H: 7} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P7A + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] +- name: P7B + composition: {C: 1, H: 4} + thermo: + model: NASA7 + temperature-ranges: [200.0, 1000.0, 3500.0] + data: + - [5.14987613, -0.0136709788, 4.91800599e-05, -4.84743026e-08, 1.66693956e-11, + -1.02466476e+04, -4.64130376] + - [0.074851495, 0.0133909467, -5.73285809e-06, 1.22292535e-09, -1.0181523e-13, + -9468.34459, 18.437318] + +reactions: +- equation: R1A + R1B <=> P1 + H # Reaction 1 + type: pressure-dependent-Arrhenius + rate-constants: + - {P: 0.01 atm, A: 1.2124e+16, b: -0.5779, Ea: 1.08727e+04} + - {P: 1.0 atm, A: 4.9108e+31, b: -4.8507, Ea: 2.47728e+04} + - {P: 10.0 atm, A: 1.2866e+47, b: -9.0246, Ea: 3.97965e+04} + - {P: 100.0 atm, A: 5.9632e+56, b: -11.529, Ea: 5.25996e+04} + note: Single PLOG reaction +- equation: H + R2 <=> P2A + P2B # Reaction 2 + type: pressure-dependent-Arrhenius + rate-constants: + - {P: 0.001316 atm, A: 1.23e+08, b: 1.53, Ea: 4737.0} + - {P: 0.039474 atm, A: 2.72e+09, b: 1.2, Ea: 6834.0} + - {P: 1.0 atm, A: 1.26e+20, b: -1.83, Ea: 1.5003e+04} + - {P: 1.0 atm, A: 1.23e+04, b: 2.68, Ea: 6335.0} + - {P: 10.0 atm, A: 1.68e+16, b: -0.6, Ea: 1.4754e+04} + - {P: 10.0 atm, A: 3.31e+08, b: 1.14, Ea: 8886.0} + - {P: 100.0 atm, A: 1.37e+17, b: -0.79, Ea: 1.7603e+04} + - {P: 100.0 atm, A: 1.28e+06, b: 1.71, Ea: 9774.0} + note: Multiple PLOG expressions at the same pressure +- equation: H + R3 <=> P3A + P3B # Reaction 3 + type: pressure-dependent-Arrhenius + rate-constants: + - {P: 0.001315789 atm, A: 2.44e+10, b: 1.04, Ea: 3.98 kcal/mol} + - {P: 0.039473684 atm, A: 3.89e+10, b: 0.989, Ea: 4.114 kcal/mol} + - {P: 1.0 atm, A: 3.46e+12, b: 0.442, Ea: 5.463 kcal/mol} + - {P: 10.0 atm, A: 1.72e+14, b: -0.01, Ea: 7.134 kcal/mol} + - {P: 100.0 atm, A: -7.41e+30, b: -5.54, Ea: 12.108 kcal/mol} + - {P: 100.0 atm, A: 1.9e+15, b: -0.29, Ea: 8.306 kcal/mol} + note: PLOG with duplicate rates, negative A-factors, and custom energy + units +- equation: H + R4 <=> H + P4 # Reaction 4 + type: pressure-dependent-Arrhenius + rate-constants: + - {P: 10.0 atm, A: 2.889338e-17 cm^3/molec/s, b: 1.98, Ea: 4521.0} + note: Degenerate PLOG with a single rate expression and custom quantity + units +- equation: R5 + H (+M) <=> P5A + P5B (+M) # Reaction 5 + type: Chebyshev + temperature-range: [300.0, 2000.0] + pressure-range: [0.009869232667160128 atm, 98.69232667160128 atm] + data: + - [8.2883, -1.1397, -0.12059, 0.016034] + - [1.9764, 1.0037, 7.2865e-03, -0.030432] + - [0.3177, 0.26889, 0.094806, -7.6385e-03] + - [-0.031285, -0.039412, 0.044375, 0.014458] + note: Bimolecular CHEB +- equation: R6 (+M) <=> P6A + P6B (+M) # Reaction 6 + type: Chebyshev + temperature-range: [290.0, 3000.0] + pressure-range: [0.009869232667160128 atm, 98.69232667160128 atm] + data: + - [-14.428, 0.25997, -0.022432, -2.787e-03] + - [22.063, 0.48809, -0.039643, -5.4811e-03] + - [-0.23294, 0.4019, -0.026073, -5.0486e-03] + - [-0.29366, 0.28568, -9.3373e-03, -4.0102e-03] + - [-0.22621, 0.16919, 4.8581e-03, -2.3803e-03] + - [-0.14322, 0.077111, 0.012708, -6.4154e-04] + note: Unimolecular decomposition CHEB +- equation: R7 + H (+M) <=> P7A + P7B (+M) # Reaction 7 + type: Chebyshev + temperature-range: [300.0, 2000.0] + pressure-range: [0.009869232667160128 atm, 98.69232667160128 atm] + units: {quantity: molec} + data: + - [8.2883, -1.1397, -0.12059, 0.016034] + - [1.9764, 1.0037, 7.2865e-03, -0.030432] + - [0.3177, 0.26889, 0.094806, -7.6385e-03] + - [-0.031285, -0.039412, 0.044375, 0.014458] + note: Bimolecular CHEB with local quantity units diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index ea057a6bf7..e7c9060911 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -107,18 +107,86 @@ TEST(YamlWriter, reactions) } } -TEST(YamlWriter, reactionUnits) +TEST(YamlWriter, reaction_units_from_Yaml) { auto original = newSolution("h2o2.yaml"); YamlWriter writer; writer.addPhase(original); writer.setPrecision(14); UnitSystem outUnits; - std::map defaults = {{"activation-energy", "K"}}; + std::map defaults = { + {"activation-energy", "K"}, + {"quantity", "mol"}, + {"length", "cm"} + }; outUnits.setDefaults(defaults); writer.setUnits(outUnits); - writer.toYamlFile("generated-h2o2-K.yaml"); - auto duplicate = newSolution("generated-h2o2-K.yaml"); + writer.toYamlFile("generated-h2o2-outunits.yaml"); + auto duplicate = newSolution("generated-h2o2-outunits.yaml"); + + auto kin1 = original->kinetics(); + auto kin2 = duplicate->kinetics(); + + ASSERT_EQ(kin1->nReactions(), kin2->nReactions()); + vector_fp kf1(kin1->nReactions()), kf2(kin1->nReactions()); + kin1->getFwdRateConstants(kf1.data()); + kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < kin1->nReactions(); i++) { + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for reaction i = " << i; + } +} + +TEST(YamlWriter, reaction_units_from_Xml) +{ + auto original = newSolution("h2o2.xml"); + YamlWriter writer; + writer.addPhase(original); + writer.setPrecision(14); + UnitSystem outUnits; + std::map defaults = { + {"activation-energy", "K"}, + {"quantity", "mol"}, + {"length", "cm"} + }; + outUnits.setDefaults(defaults); + writer.setUnits(outUnits); + // Should fail because pre-exponential factors from XML can't be converted + EXPECT_THROW(writer.toYamlFile("generated-h2o2-fail.yaml"), CanteraError); + + // Outputting with the default MKS+kmol system still works + writer.setUnits(UnitSystem()); + writer.toYamlFile("generated-h2o2-from-xml.yaml"); + auto duplicate = newSolution("generated-h2o2-from-xml.yaml"); + + auto kin1 = original->kinetics(); + auto kin2 = duplicate->kinetics(); + + ASSERT_EQ(kin1->nReactions(), kin2->nReactions()); + vector_fp kf1(kin1->nReactions()), kf2(kin1->nReactions()); + kin1->getFwdRateConstants(kf1.data()); + kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < kin1->nReactions(); i++) { + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for reaction i = " << i; + } +} + +TEST(YamlWriter, chebyshev_units_from_Yaml) +{ + auto original = newSolution("pdep-test.yaml"); + YamlWriter writer; + writer.addPhase(original); + writer.setPrecision(14); + UnitSystem outUnits; + std::map defaults = { + {"activation-energy", "K"}, + {"quantity", "mol"}, + {"length", "cm"}, + {"pressure", "atm"} + }; + outUnits.setDefaults(defaults); + writer.setUnits(outUnits); + writer.toYamlFile("generated-pdep-test.yaml"); + auto duplicate = newSolution("generated-pdep-test.yaml"); auto kin1 = original->kinetics(); auto kin2 = duplicate->kinetics(); From cbb8568686ba971ebaef7b33a05b20a0ab6741c7 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 9 Mar 2021 20:34:48 -0500 Subject: [PATCH 44/57] Eliminate redundancy in implementation of AnyValue::as --- include/cantera/base/AnyMap.inl.h | 21 +++------------------ 1 file changed, 3 insertions(+), 18 deletions(-) diff --git a/include/cantera/base/AnyMap.inl.h b/include/cantera/base/AnyMap.inl.h index 1346e17d5d..f3ba3ac153 100644 --- a/include/cantera/base/AnyMap.inl.h +++ b/include/cantera/base/AnyMap.inl.h @@ -37,24 +37,9 @@ const T &AnyValue::as() const { template T &AnyValue::as() { - try { - if (typeid(T) == typeid(double) && m_value->type() == typeid(long int)) { - // Implicit conversion of long int to double - *m_value = static_cast(as()); - m_equals = eq_comparer; - } - return boost::any_cast(*m_value); - } catch (boost::bad_any_cast&) { - if (m_value->type() == typeid(void)) { - // Values that have not been set are of type 'void' - throw InputFileError("AnyValue::as", *this, - "Key '{}' not found or contains no value", m_key); - } else { - throw InputFileError("AnyValue::as", *this, - "Key '{}' contains a '{}',\nnot a '{}'", - m_key, demangle(m_value->type()), demangle(typeid(T))); - } - } + // To avoid duplicating the code from the const version, call that version + // and just remove the const specifier from the return value + return const_cast(const_cast(this)->as()); } template From f20c77436dae82f772aa5e2e17ad871889ea9e5d Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 9 Mar 2021 20:44:14 -0500 Subject: [PATCH 45/57] [Input] Handle variable output units for all thermo/species thermo types --- include/cantera/base/AnyMap.inl.h | 10 ++++++ include/cantera/thermo/MetalPhase.h | 2 +- include/cantera/thermo/ShomatePoly.h | 2 +- src/base/AnyMap.cpp | 8 +++++ src/kinetics/Reaction.cpp | 10 +++++- src/thermo/BinarySolutionTabulatedThermo.cpp | 4 +-- src/thermo/ConstCpPoly.cpp | 8 ++--- src/thermo/DebyeHuckel.cpp | 8 ++--- src/thermo/HMWSoln.cpp | 2 +- src/thermo/IdealSolidSolnPhase.cpp | 12 ++++--- src/thermo/LatticePhase.cpp | 13 +++++--- src/thermo/MargulesVPSSTP.cpp | 18 +++++----- src/thermo/MaskellSolidSolnPhase.cpp | 2 +- src/thermo/Mu0Poly.cpp | 9 ++--- src/thermo/Nasa9PolyMultiTempRegion.cpp | 2 +- src/thermo/NasaPoly2.cpp | 2 +- src/thermo/PDSS_ConstVol.cpp | 2 +- src/thermo/PDSS_HKFT.cpp | 35 +++++++++----------- src/thermo/PDSS_SSVol.cpp | 11 +++++- src/thermo/RedlichKisterVPSSTP.cpp | 7 ++-- src/thermo/RedlichKwongMFTP.cpp | 10 ++++-- src/thermo/SpeciesThermoInterpType.cpp | 2 +- src/thermo/StoichSubstance.cpp | 10 +++--- src/thermo/SurfPhase.cpp | 3 +- src/thermo/ThermoPhase.cpp | 6 ++-- test/thermo/thermoToYaml.cpp | 3 ++ 26 files changed, 126 insertions(+), 75 deletions(-) diff --git a/include/cantera/base/AnyMap.inl.h b/include/cantera/base/AnyMap.inl.h index f3ba3ac153..6768c223b6 100644 --- a/include/cantera/base/AnyMap.inl.h +++ b/include/cantera/base/AnyMap.inl.h @@ -20,6 +20,16 @@ const T &AnyValue::as() const { // Implicit conversion of long int to double *m_value = static_cast(as()); m_equals = eq_comparer; + } else if (typeid(T) == typeid(std::vector) + && m_value->type() == typeid(std::vector)) { + // Implicit conversion of vector to vector + auto& asAny = as>(); + vector_fp asDouble(asAny.size()); + for (size_t i = 0; i < asAny.size(); i++) { + asDouble[i] = asAny[i].as(); + } + *m_value = std::move(asDouble); + m_equals = eq_comparer>; } return boost::any_cast(*m_value); } catch (boost::bad_any_cast&) { diff --git a/include/cantera/thermo/MetalPhase.h b/include/cantera/thermo/MetalPhase.h index 3df291e9f9..f1b09593b0 100644 --- a/include/cantera/thermo/MetalPhase.h +++ b/include/cantera/thermo/MetalPhase.h @@ -115,7 +115,7 @@ class MetalPhase : public ThermoPhase virtual void getParameters(AnyMap& phaseNode) const { ThermoPhase::getParameters(phaseNode); - phaseNode["density"] = density(); + phaseNode["density"].setQuantity(density(), "kg/m^3"); } virtual void setParametersFromXML(const XML_Node& eosdata) { diff --git a/include/cantera/thermo/ShomatePoly.h b/include/cantera/thermo/ShomatePoly.h index 2c3e36209f..cf4c2d2b2d 100644 --- a/include/cantera/thermo/ShomatePoly.h +++ b/include/cantera/thermo/ShomatePoly.h @@ -332,7 +332,7 @@ class ShomatePoly2 : public SpeciesThermoInterpType SpeciesThermoInterpType::getParameters(thermo); thermo["model"] = "Shomate"; vector_fp Tranges {m_lowT, m_midT, m_highT}; - thermo["temperature-ranges"] = Tranges; + thermo["temperature-ranges"].setQuantity(Tranges, "K"); thermo["data"] = std::vector(); msp_low.getParameters(thermo); msp_high.getParameters(thermo); diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index d97814a133..ec397b1ccf 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -1133,6 +1133,10 @@ void AnyValue::applyUnits(shared_ptr& units) item.applyUnits(units); } } + } else if (is>()) { + for (auto& v : as>()) { + v.applyUnits(units); + } } else if (is()) { auto& Q = as(); if (Q.converter) { @@ -1150,6 +1154,10 @@ void AnyValue::applyUnits(shared_ptr& units) vector_fp converted(old.size()); scale(old.begin(), old.end(), converted.begin(), factor); *this = std::move(converted); + } else { + throw CanteraError("AnyValue::applyUnits", "Don't know how to " + "convert Quantity with held type '{}' in key '{}'", + Q.value.type_str(), m_key); } } } diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 6317283d6b..0dbf1e832d 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -458,7 +458,15 @@ void ChebyshevReaction::getParameters(AnyMap& reactionNode) const // is known. A lambda function is used here to override the default behavior Units rate_units2 = rate_units; auto converter = [rate_units2](AnyValue& coeffs, const UnitSystem& units) { - coeffs.asVector()[0][0] += std::log10(units.convertFrom(1.0, rate_units2)); + if (rate_units2.factor() != 0.0) { + coeffs.asVector()[0][0] += std::log10(units.convertFrom(1.0, rate_units2)); + } else if (units.getDelta(UnitSystem()).size()) { + //! @todo This special case can be removed after Cantera 3.0 when + //! the XML/CTI formats are removed + throw CanteraError("ChebyshevReaction::getParameters lambda", + "Cannot convert rate constant from CTI/XML input to a " + "non-default unit system"); + } }; AnyValue coeffs; coeffs = std::move(coeffs2d); diff --git a/src/thermo/BinarySolutionTabulatedThermo.cpp b/src/thermo/BinarySolutionTabulatedThermo.cpp index 91bd09d014..25c6b27466 100644 --- a/src/thermo/BinarySolutionTabulatedThermo.cpp +++ b/src/thermo/BinarySolutionTabulatedThermo.cpp @@ -119,8 +119,8 @@ void BinarySolutionTabulatedThermo::getParameters(AnyMap& phaseNode) const phaseNode["tabulated-species"] = speciesName(m_kk_tab); AnyMap tabThermo; tabThermo["mole-fractions"] = m_molefrac_tab; - tabThermo["enthalpy"] = m_enthalpy_tab; - tabThermo["entropy"] = m_entropy_tab; + tabThermo["enthalpy"].setQuantity(m_enthalpy_tab, "J/kmol"); + tabThermo["entropy"].setQuantity(m_entropy_tab, "J/kmol/K"); phaseNode["tabulated-thermo"] = std::move(tabThermo); } diff --git a/src/thermo/ConstCpPoly.cpp b/src/thermo/ConstCpPoly.cpp index 92ed311057..64edbe522c 100644 --- a/src/thermo/ConstCpPoly.cpp +++ b/src/thermo/ConstCpPoly.cpp @@ -86,10 +86,10 @@ void ConstCpPoly::getParameters(AnyMap& thermo) const { thermo["model"] = "constant-cp"; SpeciesThermoInterpType::getParameters(thermo); - thermo["T0"] = m_t0; - thermo["h0"] = m_h0_R * GasConstant; - thermo["s0"] = m_s0_R * GasConstant; - thermo["cp0"] = m_cp0_R * GasConstant; + thermo["T0"].setQuantity(m_t0, "K"); + thermo["h0"].setQuantity(m_h0_R * GasConstant, "J/kmol"); + thermo["s0"].setQuantity(m_s0_R * GasConstant, "J/kmol/K"); + thermo["cp0"].setQuantity(m_cp0_R * GasConstant, "J/kmol/K"); } doublereal ConstCpPoly::reportHf298(doublereal* const h298) const diff --git a/src/thermo/DebyeHuckel.cpp b/src/thermo/DebyeHuckel.cpp index 3b95abc4bd..6437bbd133 100644 --- a/src/thermo/DebyeHuckel.cpp +++ b/src/thermo/DebyeHuckel.cpp @@ -626,11 +626,11 @@ void DebyeHuckel::getParameters(AnyMap& phaseNode) const if (m_form_A_Debye == A_DEBYE_WATER) { activityNode["A_Debye"] = "variable"; } else if (m_A_Debye != A_Debye_default) { - activityNode["A_Debye"] = fmt::format("{} kg^0.5/gmol^0.5", m_A_Debye); + activityNode["A_Debye"].setQuantity(m_A_Debye, "kg^0.5/gmol^0.5"); } if (m_B_Debye != B_Debye_default) { - activityNode["B_Debye"] = fmt::format("{} kg^0.5/gmol^0.5/m", m_B_Debye); + activityNode["B_Debye"].setQuantity(m_B_Debye, "kg^0.5/gmol^0.5/m"); } if (m_maxIionicStrength != maxIionicStrength_default) { activityNode["max-ionic-strength"] = m_maxIionicStrength; @@ -639,7 +639,7 @@ void DebyeHuckel::getParameters(AnyMap& phaseNode) const activityNode["use-Helgeson-fixed-form"] = true; } if (!isnan(m_Aionic_default)) { - activityNode["default-ionic-radius"] = m_Aionic_default; + activityNode["default-ionic-radius"].setQuantity(m_Aionic_default, "m"); } for (double B_dot : m_B_Dot) { if (B_dot != 0.0) { @@ -673,7 +673,7 @@ void DebyeHuckel::getSpeciesParameters(const std::string& name, checkSpeciesIndex(k); AnyMap dhNode; if (m_Aionic[k] != m_Aionic_default) { - dhNode["ionic-radius"] = m_Aionic[k]; + dhNode["ionic-radius"].setQuantity(m_Aionic[k], "m"); } int estDefault = cEST_nonpolarNeutral; diff --git a/src/thermo/HMWSoln.cpp b/src/thermo/HMWSoln.cpp index ac89b12452..0afd547300 100644 --- a/src/thermo/HMWSoln.cpp +++ b/src/thermo/HMWSoln.cpp @@ -827,7 +827,7 @@ void HMWSoln::getParameters(AnyMap& phaseNode) const if (m_form_A_Debye == A_DEBYE_WATER) { activityNode["A_Debye"] = "variable"; } else if (m_A_Debye != A_Debye_default) { - activityNode["A_Debye"] = fmt::format("{} kg^0.5/gmol^0.5", m_A_Debye); + activityNode["A_Debye"].setQuantity(m_A_Debye, "kg^0.5/gmol^0.5"); } if (m_maxIionicStrength != maxIionicStrength_default) { activityNode["max-ionic-strength"] = m_maxIionicStrength; diff --git a/src/thermo/IdealSolidSolnPhase.cpp b/src/thermo/IdealSolidSolnPhase.cpp index a2bf24731a..e2d6aad497 100644 --- a/src/thermo/IdealSolidSolnPhase.cpp +++ b/src/thermo/IdealSolidSolnPhase.cpp @@ -440,14 +440,18 @@ void IdealSolidSolnPhase::getSpeciesParameters(const std::string &name, if (S->input.hasKey("equation-of-state")) { auto& eosIn = S->input["equation-of-state"]; if (eosIn.hasKey("density")) { - eosNode["density"] = molecularWeight(k) / m_speciesMolarVolume[k]; + eosNode["density"].setQuantity( + molecularWeight(k) / m_speciesMolarVolume[k], "kg/m^3"); } else if (eosIn.hasKey("molar-density")) { - eosNode["molar-density"] = 1.0 / m_speciesMolarVolume[k]; + eosNode["molar-density"].setQuantity(1.0 / m_speciesMolarVolume[k], + "kmol/m^3"); } else { - eosNode["molar-volume"] = m_speciesMolarVolume[k]; + eosNode["molar-volume"].setQuantity(m_speciesMolarVolume[k], + "m^3/kmol"); } } else { - eosNode["molar-volume"] = m_speciesMolarVolume[k]; + eosNode["molar-volume"].setQuantity(m_speciesMolarVolume[k], + "m^3/kmol"); } } diff --git a/src/thermo/LatticePhase.cpp b/src/thermo/LatticePhase.cpp index 5f3c57e214..1f03fc3c11 100644 --- a/src/thermo/LatticePhase.cpp +++ b/src/thermo/LatticePhase.cpp @@ -300,7 +300,7 @@ void LatticePhase::initThermo() void LatticePhase::getParameters(AnyMap& phaseNode) const { ThermoPhase::getParameters(phaseNode); - phaseNode["site-density"] = m_site_density; + phaseNode["site-density"].setQuantity(m_site_density, "kmol/m^3"); } void LatticePhase::getSpeciesParameters(const std::string& name, @@ -318,20 +318,23 @@ void LatticePhase::getSpeciesParameters(const std::string& name, if (eosIn.hasKey("density")) { eosOut["model"] = "constant-volume"; - eosOut["density"] = molecularWeight(k) / m_speciesMolarVolume[k]; + eosOut["density"].setQuantity( + molecularWeight(k) / m_speciesMolarVolume[k], "kg/m^3"); } else if (eosIn.hasKey("molar-density")) { eosOut["model"] = "constant-volume"; - eosOut["molar-density"] = 1.0 / m_speciesMolarVolume[k]; + eosOut["molar-density"].setQuantity(1.0 / m_speciesMolarVolume[k], + "kmol/m^3"); } else if (eosIn.hasKey("molar-volume")) { eosOut["model"] = "constant-volume"; - eosOut["molar-volume"] = m_speciesMolarVolume[k]; + eosOut["molar-volume"].setQuantity(m_speciesMolarVolume[k], + "m^3/kmol"); } } else if (S->input.hasKey("molar_volume")) { // Species came from XML auto& eosOut = speciesNode["equation-of-state"].getMapWhere( "model", "constant-volume", true); eosOut["model"] = "constant-volume"; - eosOut["molar-volume"] = m_speciesMolarVolume[k]; + eosOut["molar-volume"].setQuantity(m_speciesMolarVolume[k], "m^3/kmol"); } // Otherwise, species volume is determined by the phase-level site density } diff --git a/src/thermo/MargulesVPSSTP.cpp b/src/thermo/MargulesVPSSTP.cpp index 0b5adf07b6..adbbc35d97 100644 --- a/src/thermo/MargulesVPSSTP.cpp +++ b/src/thermo/MargulesVPSSTP.cpp @@ -204,7 +204,7 @@ void MargulesVPSSTP::initThermo() s = item.convertVector("excess-entropy", "J/kmol/K", 2); } if (item.hasKey("excess-volume-enthalpy")) { - vh = item.convertVector("excess-volume-enthalpy", "m^3/kmol/K", 2); + vh = item.convertVector("excess-volume-enthalpy", "m^3/kmol", 2); } if (item.hasKey("excess-volume-entropy")) { vs = item.convertVector("excess-volume-entropy", "m^3/kmol/K", 2); @@ -225,20 +225,20 @@ void MargulesVPSSTP::getParameters(AnyMap& phaseNode) const interaction["species"] = vector{ speciesName(m_pSpecies_A_ij[n]), speciesName(m_pSpecies_B_ij[n])}; if (m_HE_b_ij[n] != 0 || m_HE_c_ij[n] != 0) { - interaction["excess-enthalpy"] = - vector_fp{m_HE_b_ij[n], m_HE_c_ij[n]}; + interaction["excess-enthalpy"].setQuantity( + {m_HE_b_ij[n], m_HE_c_ij[n]}, "J/kmol"); } if (m_SE_b_ij[n] != 0 || m_SE_c_ij[n] != 0) { - interaction["excess-entropy"] = - vector_fp{m_SE_b_ij[n], m_SE_c_ij[n]}; + interaction["excess-entropy"].setQuantity( + {m_SE_b_ij[n], m_SE_c_ij[n]}, "J/kmol/K"); } if (m_VHE_b_ij[n] != 0 || m_VHE_c_ij[n] != 0) { - interaction["excess-volume-enthalpy"] = - vector_fp{m_VHE_b_ij[n], m_VHE_c_ij[n]}; + interaction["excess-volume-enthalpy"].setQuantity( + {m_VHE_b_ij[n], m_VHE_c_ij[n]}, "m^3/kmol"); } if (m_VSE_b_ij[n] != 0 || m_VSE_c_ij[n] != 0) { - interaction["excess-volume-entropy"] = - vector_fp{m_VSE_b_ij[n], m_VSE_c_ij[n]}; + interaction["excess-volume-entropy"].setQuantity( + {m_VSE_b_ij[n], m_VSE_c_ij[n]}, "m^3/kmol/K"); } interactions.push_back(std::move(interaction)); } diff --git a/src/thermo/MaskellSolidSolnPhase.cpp b/src/thermo/MaskellSolidSolnPhase.cpp index 0e29e8e16b..b754205ca0 100644 --- a/src/thermo/MaskellSolidSolnPhase.cpp +++ b/src/thermo/MaskellSolidSolnPhase.cpp @@ -171,7 +171,7 @@ void MaskellSolidSolnPhase::initThermo() void MaskellSolidSolnPhase::getParameters(AnyMap& phaseNode) const { VPStandardStateTP::getParameters(phaseNode); - phaseNode["excess-enthalpy"] = h_mixing; + phaseNode["excess-enthalpy"].setQuantity(h_mixing, "J/kmol"); phaseNode["product-species"] = speciesName(product_species_index); } diff --git a/src/thermo/Mu0Poly.cpp b/src/thermo/Mu0Poly.cpp index 8fb98bfc68..abac5a62a9 100644 --- a/src/thermo/Mu0Poly.cpp +++ b/src/thermo/Mu0Poly.cpp @@ -161,8 +161,8 @@ void Mu0Poly::getParameters(AnyMap& thermo) const { SpeciesThermoInterpType::getParameters(thermo); thermo["model"] = "piecewise-Gibbs"; - thermo["h0"] = m_H298 * GasConstant; - map data; + thermo["h0"].setQuantity(m_H298 * GasConstant, "J/kmol"); + AnyMap data; bool dimensionless = m_input.getBool("dimensionless", false); if (dimensionless) { thermo["dimensionless"] = true; @@ -171,10 +171,11 @@ void Mu0Poly::getParameters(AnyMap& thermo) const if (dimensionless) { data[fmt::format("{}", m_t0_int[i])] = m_mu0_R_int[i] / m_t0_int[i]; } else { - data[fmt::format("{}", m_t0_int[i])] = m_mu0_R_int[i] * GasConstant; + data[fmt::format("{}", m_t0_int[i])].setQuantity( + m_mu0_R_int[i] * GasConstant, "J/kmol"); } } - thermo["data"] = data; + thermo["data"] = std::move(data); } Mu0Poly* newMu0ThermoFromXML(const XML_Node& Mu0Node) diff --git a/src/thermo/Nasa9PolyMultiTempRegion.cpp b/src/thermo/Nasa9PolyMultiTempRegion.cpp index a209ec46ac..71c89cbac3 100644 --- a/src/thermo/Nasa9PolyMultiTempRegion.cpp +++ b/src/thermo/Nasa9PolyMultiTempRegion.cpp @@ -195,7 +195,7 @@ void Nasa9PolyMultiTempRegion::getParameters(AnyMap& thermo) const SpeciesThermoInterpType::getParameters(thermo); auto T_ranges = m_lowerTempBounds; T_ranges.push_back(m_highT); - thermo["temperature-ranges"] = T_ranges; + thermo["temperature-ranges"].setQuantity(T_ranges, "K"); thermo["data"] = std::vector(); for (const auto& region : m_regionPts) { region->getParameters(thermo); diff --git a/src/thermo/NasaPoly2.cpp b/src/thermo/NasaPoly2.cpp index bfb38c43ac..18102557ed 100644 --- a/src/thermo/NasaPoly2.cpp +++ b/src/thermo/NasaPoly2.cpp @@ -27,7 +27,7 @@ void NasaPoly2::getParameters(AnyMap& thermo) const thermo["model"] = "NASA7"; SpeciesThermoInterpType::getParameters(thermo); vector_fp Tranges {m_lowT, m_midT, m_highT}; - thermo["temperature-ranges"] = Tranges; + thermo["temperature-ranges"].setQuantity(Tranges, "K"); thermo["data"] = std::vector(); mnp_low.getParameters(thermo); mnp_high.getParameters(thermo); diff --git a/src/thermo/PDSS_ConstVol.cpp b/src/thermo/PDSS_ConstVol.cpp index e8edfb1206..43b1fc69b4 100644 --- a/src/thermo/PDSS_ConstVol.cpp +++ b/src/thermo/PDSS_ConstVol.cpp @@ -56,7 +56,7 @@ void PDSS_ConstVol::getParameters(AnyMap &eosNode) const { PDSS::getParameters(eosNode); eosNode["model"] = "constant-volume"; - eosNode["molar-volume"] = m_constMolarVolume; + eosNode["molar-volume"].setQuantity(m_constMolarVolume, "m^3/kmol"); } doublereal PDSS_ConstVol::intEnergy_mole() const diff --git a/src/thermo/PDSS_HKFT.cpp b/src/thermo/PDSS_HKFT.cpp index 5c71422e1a..20299e5cc5 100644 --- a/src/thermo/PDSS_HKFT.cpp +++ b/src/thermo/PDSS_HKFT.cpp @@ -458,25 +458,22 @@ void PDSS_HKFT::getParameters(AnyMap& eosNode) const { PDSS::getParameters(eosNode); eosNode["model"] = "HKFT"; - UnitSystem U; - U.setDefaults({"cal", "gmol", "bar"}); - eosNode["h0"] = U.convertTo(m_deltaH_formation_tr_pr, "J/kmol"); - eosNode["g0"] = U.convertTo(m_deltaG_formation_tr_pr, "J/kmol"); - eosNode["s0"] = U.convertTo(m_Entrop_tr_pr, "J/kmol/K"); - - eosNode["a"] = vector_fp{ - U.convertTo(m_a1, "J/kmol/Pa"), - U.convertTo(m_a2, "J/kmol"), - U.convertTo(m_a3, "J*K/kmol/Pa"), - U.convertTo(m_a4, "J*K/kmol") - }; - - eosNode["c"] = vector_fp{ - U.convertTo(m_c1, "J/kmol/K"), - U.convertTo(m_c2, "J*K/kmol") - }; - - eosNode["omega"] = U.convertTo(m_omega_pr_tr, "J/kmol"); + eosNode["h0"].setQuantity(m_deltaH_formation_tr_pr, "cal/gmol"); + eosNode["g0"].setQuantity(m_deltaG_formation_tr_pr, "cal/gmol"); + eosNode["s0"].setQuantity(m_Entrop_tr_pr, "cal/gmol/K"); + + std::vector a(4), c(2); + a[0].setQuantity(m_a1, "cal/gmol/bar"); + a[1].setQuantity(m_a2, "cal/gmol"); + a[2].setQuantity(m_a3, "cal*K/gmol/bar"); + a[3].setQuantity(m_a4, "cal*K/gmol"); + eosNode["a"] = std::move(a); + + c[0].setQuantity(m_c1, "cal/gmol/K"); + c[1].setQuantity(m_c2, "cal*K/gmol"); + eosNode["c"] = std::move(c); + + eosNode["omega"].setQuantity(m_omega_pr_tr, "cal/gmol"); } doublereal PDSS_HKFT::deltaH() const diff --git a/src/thermo/PDSS_SSVol.cpp b/src/thermo/PDSS_SSVol.cpp index 681c809a84..2ebdcd7fde 100644 --- a/src/thermo/PDSS_SSVol.cpp +++ b/src/thermo/PDSS_SSVol.cpp @@ -68,12 +68,21 @@ void PDSS_SSVol::setDensityPolynomial(double* coeffs) { void PDSS_SSVol::getParameters(AnyMap& eosNode) const { PDSS::getParameters(eosNode); + std::vector data(4); if (volumeModel_ == SSVolume_Model::density_tpoly) { eosNode["model"] = "density-temperature-polynomial"; + data[0].setQuantity(TCoeff_[0], "kg/m^3"); + data[1].setQuantity(TCoeff_[1], "kg/m^3/K"); + data[2].setQuantity(TCoeff_[2], "kg/m^3/K^2"); + data[3].setQuantity(TCoeff_[3], "kg/m^3/K^3"); } else { eosNode["model"] = "molar-volume-temperature-polynomial"; + data[0].setQuantity(TCoeff_[0], "m^3/kmol"); + data[1].setQuantity(TCoeff_[1], "m^3/kmol/K"); + data[2].setQuantity(TCoeff_[2], "m^3/kmol/K^2"); + data[3].setQuantity(TCoeff_[3], "m^3/kmol/K^3"); } - eosNode["data"] = TCoeff_; + eosNode["data"] = std::move(data); } void PDSS_SSVol::initThermo() diff --git a/src/thermo/RedlichKisterVPSSTP.cpp b/src/thermo/RedlichKisterVPSSTP.cpp index b334ac8298..8d0f734a62 100644 --- a/src/thermo/RedlichKisterVPSSTP.cpp +++ b/src/thermo/RedlichKisterVPSSTP.cpp @@ -202,11 +202,12 @@ void RedlichKisterVPSSTP::getParameters(AnyMap& phaseNode) const while (s.size() > 1 && s.back() == 0) { s.pop_back(); } - interaction["excess-enthalpy"] = std::move(h); - interaction["excess-entropy"] = std::move(s); + interaction["excess-enthalpy"].setQuantity(std::move(h), "J/kmol"); + interaction["excess-entropy"].setQuantity(std::move(s), "J/kmol/K"); interactions.push_back(std::move(interaction)); } - phaseNode["interactions"] = std::move(interactions);} + phaseNode["interactions"] = std::move(interactions); +} void RedlichKisterVPSSTP::initLengths() { diff --git a/src/thermo/RedlichKwongMFTP.cpp b/src/thermo/RedlichKwongMFTP.cpp index 5b89262090..5d06410ba9 100644 --- a/src/thermo/RedlichKwongMFTP.cpp +++ b/src/thermo/RedlichKwongMFTP.cpp @@ -673,11 +673,15 @@ void RedlichKwongMFTP::getSpeciesParameters(const std::string& name, size_t counter = k + m_kk * k; if (a_coeff_vec(1, counter) != 0.0) { - eosNode["a"] = vector_fp{a_coeff_vec(0, counter), a_coeff_vec(1, counter)}; + vector coeffs(2); + coeffs[0].setQuantity(a_coeff_vec(0, counter), "Pa*m^6/kmol^2*K^0.5"); + coeffs[1].setQuantity(a_coeff_vec(1, counter), "Pa*m^6/kmol^2/K^0.5"); + eosNode["a"] = std::move(coeffs); } else { - eosNode["a"] = a_coeff_vec(0, counter); + eosNode["a"].setQuantity(a_coeff_vec(0, counter), + "Pa*m^6/kmol^2*K^0.5"); } - eosNode["b"] = b_vec_Curr_[k]; + eosNode["b"].setQuantity(b_vec_Curr_[k], "m^3/kmol"); } diff --git a/src/thermo/SpeciesThermoInterpType.cpp b/src/thermo/SpeciesThermoInterpType.cpp index a051e57c68..01f633123f 100644 --- a/src/thermo/SpeciesThermoInterpType.cpp +++ b/src/thermo/SpeciesThermoInterpType.cpp @@ -55,7 +55,7 @@ void SpeciesThermoInterpType::reportParameters(size_t& index, int& type, void SpeciesThermoInterpType::getParameters(AnyMap& thermo) const { if (m_Pref != OneAtm) { - thermo["reference-pressure"] = m_Pref; + thermo["reference-pressure"].setQuantity(m_Pref, "Pa"); } } diff --git a/src/thermo/StoichSubstance.cpp b/src/thermo/StoichSubstance.cpp index c94a4510bd..6532f975a4 100644 --- a/src/thermo/StoichSubstance.cpp +++ b/src/thermo/StoichSubstance.cpp @@ -168,14 +168,16 @@ void StoichSubstance::getSpeciesParameters(const std::string& name, if (S->input.hasKey("equation-of-state")) { auto& eosIn = S->input["equation-of-state"]; if (eosIn.hasKey("density")) { - eosNode["density"] = density(); + eosNode["density"].setQuantity(density(), "kg/m^3"); } else if (eosIn.hasKey("molar-density")) { - eosNode["molar-density"] = density() / meanMolecularWeight(); + eosNode["molar-density"].setQuantity(density() / meanMolecularWeight(), + "kmol/m^3"); } else { - eosNode["molar-volume"] = meanMolecularWeight() / density(); + eosNode["molar-volume"].setQuantity(meanMolecularWeight() / density(), + "m^3/kmol"); } } else { - eosNode["molar-volume"] = meanMolecularWeight() / density(); + eosNode["molar-volume"].setQuantity(meanMolecularWeight() / density(), "m^3/kmol"); } } diff --git a/src/thermo/SurfPhase.cpp b/src/thermo/SurfPhase.cpp index 8377c5b0f5..abb188602a 100644 --- a/src/thermo/SurfPhase.cpp +++ b/src/thermo/SurfPhase.cpp @@ -346,7 +346,8 @@ void SurfPhase::initThermo() void SurfPhase::getParameters(AnyMap& phaseNode) const { ThermoPhase::getParameters(phaseNode); - phaseNode["site-density"] = m_n0; + phaseNode["site-density"].setQuantity( + m_n0, Units(1.0, 0, -static_cast(m_ndim), 0, 0, 0, 1)); } void SurfPhase::setStateFromXML(const XML_Node& state) diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 15e66081e8..64a729ea6e 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1203,13 +1203,13 @@ void ThermoPhase::getParameters(AnyMap& phaseNode) const AnyMap state; auto stateVars = nativeState(); if (stateVars.count("T")) { - state["T"] = temperature(); + state["T"].setQuantity(temperature(), "K"); } if (stateVars.count("D")) { - state["density"] = density(); + state["density"].setQuantity(density(), "kg/m^3"); } else if (stateVars.count("P")) { - state["P"] = pressure(); + state["P"].setQuantity(pressure(), "Pa"); } if (stateVars.count("Y")) { diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index d701b4dddb..d53cfff155 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -16,11 +16,13 @@ class ThermoToYaml : public testing::Test // added by the overrides of getParameters. thermo->input().clear(); thermo->getParameters(data); + data.applyUnits(); speciesData.resize(thermo->nSpecies()); eosData.resize(thermo->nSpecies()); for (size_t k = 0; k < thermo->nSpecies(); k++) { thermo->getSpeciesParameters(thermo->speciesName(k), speciesData[k]); + speciesData[k].applyUnits(); if (speciesData[k].hasKey("equation-of-state")) { // Get the first EOS node, for convenience eosData[k] = speciesData[k]["equation-of-state"].asVector()[0]; @@ -40,6 +42,7 @@ TEST_F(ThermoToYaml, simpleIdealGas) thermo->setState_TP(1010, 2e5); double rho = thermo->density(); thermo->getParameters(data); + data.applyUnits(); ASSERT_EQ(data["thermo"], "ideal-gas"); ASSERT_EQ(data["state"]["T"], 1010); From aac2c5abb13d1b1114f97fe3ac823edd61dd6a82 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 9 Mar 2021 22:24:47 -0500 Subject: [PATCH 46/57] [Input] Add tests of YamlWriter for surface and edge phases --- test/general/test_serialization.cpp | 125 ++++++++++++++++++++++++++++ 1 file changed, 125 insertions(+) diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index e7c9060911..fc370710b9 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -4,6 +4,7 @@ #include "gtest/gtest.h" #include "cantera/base/YamlWriter.h" #include "cantera/thermo.h" +#include "cantera/thermo/SurfPhase.h" #include "cantera/base/Solution.h" #include "cantera/kinetics.h" #include "cantera/transport/TransportData.h" @@ -229,3 +230,127 @@ TEST(YamlWriter, multipleReactionSections) ASSERT_EQ(kin2->reactionString(3), kin3->reactionString(kin3->nReactions() - 1)); } + +TEST(YamlWriter, Interface) +{ + shared_ptr gas1(newPhase("ptcombust.yaml", "gas")); + shared_ptr surf1(newPhase("ptcombust.yaml", "Pt_surf")); + std::vector phases1{surf1.get(), gas1.get()}; + shared_ptr kin1 = newKinetics(phases1, "ptcombust.yaml", "Pt_surf"); + + double T = 900; + double P = OneAtm; + surf1->setState_TPX(T, P, "PT(S): 0.5, H(S): 0.1, CO(S): 0.4"); + gas1->setState_TPY(T, P, "H2: 0.5, CH4:0.48, OH:0.005, H:0.005"); + + YamlWriter writer; + writer.addPhase(gas1); + writer.addPhase(surf1, kin1); + UnitSystem U{"mm", "molec"}; + U.setDefaultActivationEnergy("K"); + writer.setUnits(U); + writer.toYamlFile("generated-ptcombust.yaml"); + + shared_ptr gas2(newPhase("generated-ptcombust.yaml", "gas")); + shared_ptr surf2(newPhase("generated-ptcombust.yaml", "Pt_surf")); + std::vector phases2{surf2.get(), gas2.get()}; + shared_ptr kin2 = newKinetics(phases2, "generated-ptcombust.yaml", "Pt_surf"); + + auto iface1 = std::dynamic_pointer_cast(surf1); + auto iface2 = std::dynamic_pointer_cast(surf2); + + EXPECT_NEAR(iface1->siteDensity(), iface2->siteDensity(), + 1e-13 * iface2->siteDensity()); + + ASSERT_EQ(kin1->nReactions(), kin2->nReactions()); + vector_fp kf1(kin1->nReactions()), kf2(kin1->nReactions()); + kin1->getFwdRateConstants(kf1.data()); + kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < kin1->nReactions(); i++) { + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for reaction i = " << i; + } + + vector_fp wdot1(kin1->nTotalSpecies()); + vector_fp wdot2(kin2->nTotalSpecies()); + kin1->getNetProductionRates(wdot1.data()); + kin2->getNetProductionRates(wdot2.data()); + for (size_t i = 0; i < kin1->nTotalSpecies(); i++) { + EXPECT_NEAR(wdot1[i], wdot2[i], 1e-13 * fabs(wdot1[i])) << "for species i = " << i; + } +} + +TEST(YamlWriter, sofc) +{ + shared_ptr gas1(newPhase("sofc.yaml", "gas")); + shared_ptr metal1(newPhase("sofc.yaml", "metal")); + shared_ptr ox_bulk1(newPhase("sofc.yaml", "oxide_bulk")); + shared_ptr metal_surf1(newPhase("sofc.yaml", "metal_surface")); + shared_ptr oxide_surf1(newPhase("sofc.yaml", "oxide_surface")); + shared_ptr tpb1(newPhase("sofc.yaml", "tpb")); + + std::vector tpb_phases1{tpb1.get(), metal_surf1.get(), oxide_surf1.get(), metal1.get()}; + std::vector ox_phases1{oxide_surf1.get(), ox_bulk1.get(), gas1.get()}; + + shared_ptr tpb_kin1 = newKinetics(tpb_phases1, "sofc.yaml", "tpb"); + shared_ptr ox_kin1 = newKinetics(ox_phases1, "sofc.yaml", "oxide_surface"); + + YamlWriter writer; + writer.addPhase(tpb1, tpb_kin1); + writer.addPhase(metal_surf1); + writer.addPhase(oxide_surf1, ox_kin1); + writer.addPhase(metal1); + writer.addPhase(gas1); + writer.addPhase(ox_bulk1); + + UnitSystem U; + U.setDefaults({"cm", "atm"}); + U.setDefaultActivationEnergy("eV"); + writer.setUnits(U); + writer.toYamlFile("generated-sofc.yaml"); + + shared_ptr gas2(newPhase("generated-sofc.yaml", "gas")); + shared_ptr metal2(newPhase("generated-sofc.yaml", "metal")); + shared_ptr ox_bulk2(newPhase("generated-sofc.yaml", "oxide_bulk")); + shared_ptr metal_surf2(newPhase("generated-sofc.yaml", "metal_surface")); + shared_ptr oxide_surf2(newPhase("generated-sofc.yaml", "oxide_surface")); + shared_ptr tpb2(newPhase("generated-sofc.yaml", "tpb")); + + std::vector tpb_phases2{tpb2.get(), metal_surf2.get(), oxide_surf2.get(), metal2.get()}; + std::vector ox_phases2{oxide_surf2.get(), ox_bulk2.get(), gas2.get()}; + + shared_ptr tpb_kin2 = newKinetics(tpb_phases2, "generated-sofc.yaml", "tpb"); + shared_ptr ox_kin2 = newKinetics(ox_phases2, "generated-sofc.yaml", "oxide_surface"); + + ASSERT_EQ(tpb_kin1->nReactions(), tpb_kin2->nReactions()); + vector_fp kf1(tpb_kin1->nReactions()), kf2(tpb_kin1->nReactions()); + tpb_kin1->getFwdRateConstants(kf1.data()); + tpb_kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < tpb_kin1->nReactions(); i++) { + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for tpb reaction i = " << i; + } + + vector_fp wdot1(tpb_kin1->nTotalSpecies()); + vector_fp wdot2(tpb_kin2->nTotalSpecies()); + tpb_kin1->getNetProductionRates(wdot1.data()); + tpb_kin2->getNetProductionRates(wdot2.data()); + for (size_t i = 0; i < tpb_kin1->nTotalSpecies(); i++) { + EXPECT_NEAR(wdot1[i], wdot2[i], 1e-13 * fabs(wdot1[i])) << "for species i = " << i; + } + + ASSERT_EQ(ox_kin1->nReactions(), ox_kin2->nReactions()); + kf1.resize(ox_kin1->nReactions()); + kf2.resize(ox_kin1->nReactions()); + ox_kin1->getFwdRateConstants(kf1.data()); + ox_kin2->getFwdRateConstants(kf2.data()); + for (size_t i = 0; i < ox_kin1->nReactions(); i++) { + EXPECT_NEAR(kf1[i], kf2[i], 1e-13 * kf1[i]) << "for ox reaction i = " << i; + } + + wdot1.resize(ox_kin1->nTotalSpecies()); + wdot2.resize(ox_kin2->nTotalSpecies()); + ox_kin1->getNetProductionRates(wdot1.data()); + ox_kin2->getNetProductionRates(wdot2.data()); + for (size_t i = 0; i < ox_kin1->nTotalSpecies(); i++) { + EXPECT_NEAR(wdot1[i], wdot2[i], 1e-13 * fabs(wdot1[i])) << "for ox species i = " << i; + } +} From 4df688693033ba0fb538b93cd7069d493a44ffdd Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Wed, 10 Mar 2021 17:19:51 -0500 Subject: [PATCH 47/57] [Input/Python] Enable generation of YAML files from Python Solutions --- include/cantera/base/YamlWriter.h | 15 +++-- interfaces/cython/cantera/_cantera.pxd | 13 ++++ interfaces/cython/cantera/_cantera.pyx | 1 + interfaces/cython/cantera/base.pyx | 39 +++++++++++ interfaces/cython/cantera/kinetics.pyx | 16 +++++ .../cython/cantera/test/test_composite.py | 41 ++++++++++++ interfaces/cython/cantera/yamlwriter.pyx | 67 +++++++++++++++++++ src/base/YamlWriter.cpp | 6 ++ test/general/test_serialization.cpp | 41 +++++------- 9 files changed, 211 insertions(+), 28 deletions(-) create mode 100644 interfaces/cython/cantera/yamlwriter.pyx diff --git a/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h index 45b8b354ad..0bb023b0ec 100644 --- a/include/cantera/base/YamlWriter.h +++ b/include/cantera/base/YamlWriter.h @@ -31,7 +31,12 @@ class YamlWriter void addPhase(shared_ptr thermo, shared_ptr kin={}, shared_ptr tran={}); + //! Return a YAML string that contains the definitions for the added phases, + //! species, and reactions std::string toYamlString() const; + + //! Write the definitions for the added phases, species and reactions to + //! the specified file. void toYamlFile(const std::string& filename) const; //! For output floating point values, set the maximum number of digits to @@ -47,10 +52,12 @@ class YamlWriter m_skip_user_defined = skip; } - //! Set the units to be used in the output file - void setUnits(const UnitSystem& units) { - m_output_units = units; - } + //! Set the units to be used in the output file. Dimensions not specified + //! will use Cantera's defaults. + //! @param units A map where keys are dimensions (mass, length, time, + //! quantity, pressure, energy, activation-energy) and the values are + //! corresponding units supported by the UnitSystem class. + void setUnits(const std::map& units={}); protected: std::vector> m_phases; diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index ca5761ae94..9da836824d 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -546,6 +546,15 @@ cdef extern from "cantera/transport/TransportData.h" namespace "Cantera": double dispersion_coefficient double quadrupole_polarizability +cdef extern from "cantera/base/YamlWriter.h" namespace "Cantera": + cdef cppclass CxxYamlWriter "Cantera::YamlWriter": + CxxYamlWriter() + void addPhase(shared_ptr[CxxSolution]) except +translate_exception + string toYamlString() except +translate_exception + void toYamlFile(string&) except +translate_exception + void setPrecision(int) + void skipUserDefined(cbool) + void setUnits(stdmap[string, string]&) except +translate_exception cdef extern from "cantera/equil/MultiPhase.h" namespace "Cantera": cdef cppclass CxxMultiPhase "Cantera::MultiPhase": @@ -1106,6 +1115,10 @@ cdef class Transport(_SolutionBase): cdef class DustyGasTransport(Transport): pass +cdef class YamlWriter: + cdef shared_ptr[CxxYamlWriter] _writer + cdef CxxYamlWriter* writer + cdef class Mixture: cdef CxxMultiPhase* mix cdef list _phases diff --git a/interfaces/cython/cantera/_cantera.pyx b/interfaces/cython/cantera/_cantera.pyx index 98ec9fd7e8..40548fb28e 100644 --- a/interfaces/cython/cantera/_cantera.pyx +++ b/interfaces/cython/cantera/_cantera.pyx @@ -20,6 +20,7 @@ include "reaction.pyx" include "kinetics.pyx" include "transport.pyx" +include "yamlwriter.pyx" include "mixture.pyx" include "reactor.pyx" include "onedim.pyx" diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index cc09905354..103c1bf5cb 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -239,6 +239,45 @@ cdef class _SolutionBase: self.transport.getParameters(params) return mergeAnyMap(params, self.thermo.input()) + def write_yaml(self, filename, phases=None, units=None, precision=None, + skip_user_defined=None): + """ + Write the definition for this phase, any additional phases specified, + and their species and reactions to the specified file. + + :param filename: + The name of the output file + :param phases: + Additional ThermoPhase / Solution objects to be included in the + output file + :param units: + A dictionary of the units to be used for each dimension. See + `YamlWriter.output_units`. + :param precision: + For output floating point values, the maximum number of digits to + the right of the decimal point. The default is 15 digits. + :param skip_user_defined: + If `True`, user-defined fields which are not used by Cantera will + be stripped from the output. + """ + Y = YamlWriter() + Y.add_solution(self) + if phases is not None: + if isinstance(phases, _SolutionBase): + # "phases" is just a single phase object + Y.add_solution(phases) + else: + # Assume that "phases" is an iterable + for phase in phases: + Y.add_solution(phase) + if units is not None: + Y.output_units = units + if precision is not None: + Y.precision = precision + if skip_user_defined is not None: + Y.skip_user_defined = skip_user_defined + Y.to_file(filename) + def __getitem__(self, selection): copy = self.__class__(origin=self) if isinstance(selection, slice): diff --git a/interfaces/cython/cantera/kinetics.pyx b/interfaces/cython/cantera/kinetics.pyx index 866b9e5ce5..26373be886 100644 --- a/interfaces/cython/cantera/kinetics.pyx +++ b/interfaces/cython/cantera/kinetics.pyx @@ -473,3 +473,19 @@ cdef class InterfaceKinetics(Kinetics): species in all phases. """ return self.net_production_rates[self._phase_slice(phase)] + + def write_yaml(self, filename, phases=None, units=None, precision=None, + skip_user_defined=None): + """ + See `_SolutionBase.write_yaml`. + """ + if phases is not None: + phases = list(phases) + else: + phases = [] + + for phase in self._phase_indices: + if isinstance(phase, _SolutionBase) and phase is not self: + phases.append(phase) + + super().write_yaml(filename, phases, units, precision, skip_user_defined) diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index e3ac050bd5..fbc34e9092 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -460,6 +460,47 @@ def test_input_data_debye_huckel(self): self.assertNotIn('kinetics', data) self.assertNotIn('transport', data) + def test_yaml_simple(self): + gas = ct.Solution('h2o2.yaml') + gas.write_yaml('h2o2-generated.yaml') + with open('h2o2-generated.yaml', 'r') as infile: + generated = yaml.safe_load(infile) + for key in ('generator', 'date', 'phases', 'species', 'reactions'): + self.assertIn(key, generated) + self.assertEqual(generated['phases'][0]['transport'], 'mixture-averaged') + for i, species in enumerate(generated['species']): + self.assertEqual(species['composition'], gas.species(i).composition) + for i, reaction in enumerate(generated['reactions']): + self.assertEqual(reaction['equation'], gas.reaction_equation(i)) + + def test_yaml_outunits(self): + gas = ct.Solution('h2o2.yaml') + units = {'length': 'cm', 'quantity': 'mol', 'energy': 'cal'} + gas.write_yaml('h2o2-generated.yaml', units=units) + with open('h2o2-generated.yaml') as infile: + generated = yaml.safe_load(infile) + with open(pjoin(self.cantera_data, "h2o2.yaml")) as infile: + original = yaml.safe_load(infile) + self.assertEqual(generated['units'], units) + + for r1, r2 in zip(original['reactions'], generated['reactions']): + if 'rate-constant' in r1: + self.assertNear(r1['rate-constant']['A'], r2['rate-constant']['A']) + self.assertNear(r1['rate-constant']['Ea'], r2['rate-constant']['Ea']) + + def test_yaml_surface(self): + gas = ct.Solution('ptcombust.yaml', 'gas') + surf = ct.Interface('ptcombust.yaml', 'Pt_surf', [gas]) + surf.write_yaml('ptcombust-generated.yaml') + + with open('ptcombust-generated.yaml') as infile: + generated = yaml.safe_load(infile) + for key in ('phases', 'species', 'gas-reactions', 'Pt_surf-reactions'): + self.assertIn(key, generated) + self.assertEqual(len(generated['gas-reactions']), gas.n_reactions) + self.assertEqual(len(generated['Pt_surf-reactions']), surf.n_reactions) + self.assertEqual(len(generated['species']), surf.n_total_species) + class TestSpeciesSerialization(utilities.CanteraTest): def test_species_simple(self): diff --git a/interfaces/cython/cantera/yamlwriter.pyx b/interfaces/cython/cantera/yamlwriter.pyx new file mode 100644 index 0000000000..8d2fda604b --- /dev/null +++ b/interfaces/cython/cantera/yamlwriter.pyx @@ -0,0 +1,67 @@ +# 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. + +cdef class YamlWriter: + """ + A class for generating full YAML input files from multiple Solution objects + """ + def __cinit__(self): + self._writer.reset(new CxxYamlWriter()) + self.writer = self._writer.get() + + def add_solution(self, _SolutionBase soln): + """ Include a phase definition for the specified Solution object """ + self.writer.addPhase(soln._base) + + def to_file(self, filename): + """ + Write the definitions for the added phases, species and reactions to + the specified file. + """ + self.writer.toYamlFile(stringify(filename)) + + def to_string(self): + """ + Return a YAML string that contains the definitions for the added phases, + species, and reactions. + """ + return pystr(self.writer.toYamlString()) + + property precision: + """ + For output floating point values, set the maximum number of digits to + the right of the decimal point. The default is 15 digits. + """ + def __set__(self, int precision): + self.writer.setPrecision(precision) + + property skip_user_defined: + """ + By default user-defined data present in the input is preserved on + output. This method can be used to skip output of user-defined data + fields which are not directly used by Cantera. + """ + def __set__(self, pybool skip): + self.writer.skipUserDefined(skip) + + property output_units: + """ + Set the units to be used in the output file. Dimensions not specified + will use Cantera's defaults. + + :param units: + A map where keys are dimensions (mass, length, time, quantity, + pressure, energy, activation-energy), and the values are + corresponding units such as kg, mm, s, kmol, Pa, cal, and eV. + """ + def __set__(self, units): + cdef stdmap[string, string] cxxunits + for dimension, unit in units.items(): + cxxunits[stringify(dimension)] = stringify(unit) + self.writer.setUnits(cxxunits) + + def __reduce__(self): + raise NotImplementedError('YamlWriter object is not picklable') + + def __copy__(self): + raise NotImplementedError('YamlWriter object is not copyable') diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 53ae8b1e09..77e5f18112 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -208,4 +208,10 @@ void YamlWriter::toYamlFile(const std::string& filename) const out << toYamlString(); } +void YamlWriter::setUnits(const std::map& units) +{ + m_output_units = UnitSystem(); + m_output_units.setDefaults(units); +} + } diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index fc370710b9..0703b967bf 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -114,14 +114,11 @@ TEST(YamlWriter, reaction_units_from_Yaml) YamlWriter writer; writer.addPhase(original); writer.setPrecision(14); - UnitSystem outUnits; - std::map defaults = { + writer.setUnits({ {"activation-energy", "K"}, {"quantity", "mol"}, {"length", "cm"} - }; - outUnits.setDefaults(defaults); - writer.setUnits(outUnits); + }); writer.toYamlFile("generated-h2o2-outunits.yaml"); auto duplicate = newSolution("generated-h2o2-outunits.yaml"); @@ -143,19 +140,16 @@ TEST(YamlWriter, reaction_units_from_Xml) YamlWriter writer; writer.addPhase(original); writer.setPrecision(14); - UnitSystem outUnits; - std::map defaults = { + writer.setUnits({ {"activation-energy", "K"}, {"quantity", "mol"}, {"length", "cm"} - }; - outUnits.setDefaults(defaults); - writer.setUnits(outUnits); + }); // Should fail because pre-exponential factors from XML can't be converted EXPECT_THROW(writer.toYamlFile("generated-h2o2-fail.yaml"), CanteraError); // Outputting with the default MKS+kmol system still works - writer.setUnits(UnitSystem()); + writer.setUnits(); writer.toYamlFile("generated-h2o2-from-xml.yaml"); auto duplicate = newSolution("generated-h2o2-from-xml.yaml"); @@ -177,15 +171,12 @@ TEST(YamlWriter, chebyshev_units_from_Yaml) YamlWriter writer; writer.addPhase(original); writer.setPrecision(14); - UnitSystem outUnits; - std::map defaults = { + writer.setUnits({ {"activation-energy", "K"}, {"quantity", "mol"}, {"length", "cm"}, {"pressure", "atm"} - }; - outUnits.setDefaults(defaults); - writer.setUnits(outUnits); + }); writer.toYamlFile("generated-pdep-test.yaml"); auto duplicate = newSolution("generated-pdep-test.yaml"); @@ -246,9 +237,11 @@ TEST(YamlWriter, Interface) YamlWriter writer; writer.addPhase(gas1); writer.addPhase(surf1, kin1); - UnitSystem U{"mm", "molec"}; - U.setDefaultActivationEnergy("K"); - writer.setUnits(U); + writer.setUnits({ + {"length", "mm"}, + {"quantity", "molec"}, + {"activation-energy", "K"} + }); writer.toYamlFile("generated-ptcombust.yaml"); shared_ptr gas2(newPhase("generated-ptcombust.yaml", "gas")); @@ -301,11 +294,11 @@ TEST(YamlWriter, sofc) writer.addPhase(metal1); writer.addPhase(gas1); writer.addPhase(ox_bulk1); - - UnitSystem U; - U.setDefaults({"cm", "atm"}); - U.setDefaultActivationEnergy("eV"); - writer.setUnits(U); + writer.setUnits({ + {"length", "cm"}, + {"pressure", "atm"}, + {"activation-energy", "eV"} + }); writer.toYamlFile("generated-sofc.yaml"); shared_ptr gas2(newPhase("generated-sofc.yaml", "gas")); From 85fb40154892c2406f5a7450fe423c486a7042dc Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 13 Mar 2021 16:43:12 -0500 Subject: [PATCH 48/57] [Input] Serialize Redlich-Kwong binary interaction parameters --- include/cantera/base/AnyMap.inl.h | 2 ++ include/cantera/thermo/RedlichKwongMFTP.h | 3 +++ src/base/AnyMap.cpp | 18 ++++++++++++++++++ src/thermo/RedlichKwongMFTP.cpp | 18 +++++++++++++++++- 4 files changed, 40 insertions(+), 1 deletion(-) diff --git a/include/cantera/base/AnyMap.inl.h b/include/cantera/base/AnyMap.inl.h index 6768c223b6..6ccfa962d0 100644 --- a/include/cantera/base/AnyMap.inl.h +++ b/include/cantera/base/AnyMap.inl.h @@ -57,6 +57,8 @@ bool AnyValue::is() const { return m_value->type() == typeid(T); } +template<> bool AnyValue::is>() const; + template AnyValue &AnyValue::operator=(const std::vector &value) { *m_value = value; diff --git a/include/cantera/thermo/RedlichKwongMFTP.h b/include/cantera/thermo/RedlichKwongMFTP.h index 5c2f40aa92..5c59459e77 100644 --- a/include/cantera/thermo/RedlichKwongMFTP.h +++ b/include/cantera/thermo/RedlichKwongMFTP.h @@ -326,6 +326,9 @@ class RedlichKwongMFTP : public MixtureFugacityTP Array2D a_coeff_vec; + //! Explicitly-specified binary interaction parameters + std::map>> m_binaryParameters; + //! For each species, true if the a and b coefficients were determined from //! the critical properties database std::vector m_coeffs_from_db; diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index ec397b1ccf..ec20876336 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -763,6 +763,24 @@ void AnyValue::setQuantity(const AnyValue& value, const unitConverter& converter m_equals = eq_comparer; } +template<> +bool AnyValue::is>() const +{ + if (m_value->type() == typeid(vector)) { + return true; + } else if (m_value->type() == typeid(vector)) { + for (const auto& item : as>()) { + if (!(item.is() + || (item.is() && item.as().value.is()))) + { + return false; + } + } + return true; + } else { + return false; + } +} // Specializations for "double" diff --git a/src/thermo/RedlichKwongMFTP.cpp b/src/thermo/RedlichKwongMFTP.cpp index 5d06410ba9..35507bf5d2 100644 --- a/src/thermo/RedlichKwongMFTP.cpp +++ b/src/thermo/RedlichKwongMFTP.cpp @@ -106,6 +106,9 @@ void RedlichKwongMFTP::setBinaryCoeffs(const std::string& species_i, if (a1 != 0.0) { m_formTempParam = 1; // expression is temperature-dependent } + + m_binaryParameters[species_i][species_j] = {a0, a1}; + m_binaryParameters[species_j][species_i] = {a0, a1}; size_t counter1 = ki + m_kk * kj; size_t counter2 = kj + m_kk * ki; a_coeff_vec(0, counter1) = a_coeff_vec(0, counter2) = a0; @@ -682,9 +685,22 @@ void RedlichKwongMFTP::getSpeciesParameters(const std::string& name, "Pa*m^6/kmol^2*K^0.5"); } eosNode["b"].setQuantity(b_vec_Curr_[k], "m^3/kmol"); + if (m_binaryParameters.count(name)) { + AnyMap bin_a; + for (const auto& item : m_binaryParameters.at(name)) { + if (item.second.second == 0) { + bin_a[item.first].setQuantity(item.second.first, "Pa*m^6/kmol^2*K^0.5"); + } else { + vector coeffs(2); + coeffs[0].setQuantity(item.second.first, "Pa*m^6/kmol^2*K^0.5"); + coeffs[1].setQuantity(item.second.second, "Pa*m^6/kmol^2/K^0.5"); + bin_a[item.first] = std::move(coeffs); + } + } + eosNode["binary-a"] = std::move(bin_a); + } } - vector RedlichKwongMFTP::getCoeff(const std::string& iName) { vector_fp spCoeff{NAN, NAN}; From 22f703b8e199a86b169d9e9a5559bd0e8063149b Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sat, 10 Apr 2021 17:41:49 -0400 Subject: [PATCH 49/57] [Input] Set rate_units for reactions whenever possible Setting the rate_units for a Reaction requires a Kinetics object. When created from a YAML file, this is available while the Reaction is being constructed. For Reactions created from CTI/XML or directly in code, this can be done when the Reaction is added to a Kinetics object. Serializing using a non-default unit system is not possible if neither of these conditions are met. --- include/cantera/kinetics/Reaction.h | 16 ++-- src/base/AnyMap.cpp | 7 +- src/kinetics/Kinetics.cpp | 6 ++ src/kinetics/Reaction.cpp | 121 +++++++++++++++------------- src/kinetics/RxnRates.cpp | 5 +- test/general/test_serialization.cpp | 4 - 6 files changed, 86 insertions(+), 73 deletions(-) diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index 58d1f69269..ec66de22de 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -48,6 +48,11 @@ class Reaction //! The type of reaction virtual std::string type() const = 0; // pure virtual function + //! Calculate the units of the rate constant. These are determined by the units + //! of the standard concentration of the reactant species' phases and the phase + //! where the reaction occurs. Sets the value of #rate_units. + virtual void calculateRateCoeffUnits(const Kinetics& kin); + //! Ensure that the rate constant and other parameters for this reaction are //! valid. virtual void validate(); @@ -193,6 +198,7 @@ class ThreeBodyReaction : public ElementaryReaction virtual std::string reactantString() const; virtual std::string productString() const; + virtual void calculateRateCoeffUnits(const Kinetics& kin); virtual void getParameters(AnyMap& reactionNode) const; //! Relative efficiencies of third-body species in enhancing the reaction @@ -217,6 +223,7 @@ class FalloffReaction : public Reaction virtual std::string reactantString() const; virtual std::string productString() const; virtual void validate(); + virtual void calculateRateCoeffUnits(const Kinetics& kin); virtual void getParameters(AnyMap& reactionNode) const; //! The rate constant in the low-pressure limit @@ -255,6 +262,7 @@ class ChemicallyActivatedReaction : public FalloffReaction return "chemically-activated"; } + virtual void calculateRateCoeffUnits(const Kinetics& kin); virtual void getParameters(AnyMap& reactionNode) const; }; @@ -433,14 +441,6 @@ std::vector> getReactions(const AnyValue& items, void parseReactionEquation(Reaction& R, const AnyValue& equation, const Kinetics& kin); -//! The units of the rate constant. These are determined by the units of the -//! standard concentration of the reactant species' phases of the phase -//! where the reaction occurs. -//! -//! @todo Rate units will become available as `rate_units` after serialization -//! is implemented. -Units rateCoeffUnits(const Reaction& R, const Kinetics& kin); - // declarations of setup functions void setupElementaryReaction(ElementaryReaction&, const XML_Node&); //! @internal May be changed without notice in future versions diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index ec20876336..25e8f26ba7 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -1090,15 +1090,14 @@ void AnyValue::applyUnits(shared_ptr& units) if (is()) { AnyMap& m = as(); - //! @todo Remove this check after CTI/XML support is removed in Cantera 3.0. if (m.getBool("__unconvertible__", false)) { AnyMap delta = units->getDelta(UnitSystem()); if (delta.hasKey("length") || delta.hasKey("quantity") || delta.hasKey("time")) { - throw CanteraError("AnyValue::applyUnits", "AnyMap contains" - " values that cannot be converted to non-default unit" - " systems (probably reaction rates read from XML files)"); + throw CanteraError("AnyValue::applyUnits", "AnyMap contains values" + " that cannot be converted to non-default unit systems (probably" + " reaction rates not associated with a Kinetics object)"); } } // Units declaration applicable to this map diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index 3e00f5bdee..ee6f40a95e 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -567,6 +567,12 @@ bool Kinetics::addReaction(shared_ptr r) } } + // For reactions created outside the context of a Kinetics object, the units + // of the rate coefficient can't be determined in advance. Do that here. + if (r->rate_units.factor() == 0) { + r->calculateRateCoeffUnits(*this); + } + checkReactionBalance(*r); size_t irxn = nReactions(); // index of the new reaction diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 0dbf1e832d..cfb78ee758 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -31,6 +31,7 @@ Reaction::Reaction() , duplicate(false) , allow_nonreactant_orders(false) , allow_negative_orders(false) + , rate_units(0.0) , m_valid(true) { } @@ -44,6 +45,7 @@ Reaction::Reaction(const Composition& reactants_, , duplicate(false) , allow_nonreactant_orders(false) , allow_negative_orders(false) + , rate_units(0.0) , m_valid(true) { } @@ -54,6 +56,7 @@ Reaction::Reaction(int type) , duplicate(false) , allow_nonreactant_orders(false) , allow_negative_orders(false) + , rate_units(0.0) , m_valid(true) { warn_deprecated("Reaction::Reaction()", @@ -70,6 +73,7 @@ Reaction::Reaction(int type, const Composition& reactants_, , duplicate(false) , allow_nonreactant_orders(false) , allow_negative_orders(false) + , rate_units(0.0) , m_valid(true) { warn_deprecated("Reaction::Reaction()", @@ -167,6 +171,36 @@ std::string Reaction::equation() const } } +void Reaction::calculateRateCoeffUnits(const Kinetics& kin) +{ + if (!valid()) { + // If a reaction is invalid because of missing species in the Kinetics + // object, determining the units of the rate coefficient is impossible. + return; + } + + // Determine the units of the rate coefficient + Units rxn_phase_units = kin.thermo(kin.reactionPhaseIndex()).standardConcentrationUnits(); + rate_units = rxn_phase_units; + rate_units *= Units(1.0, 0, 0, -1); + for (const auto& order : orders) { + const auto& phase = kin.speciesPhase(order.first); + rate_units *= phase.standardConcentrationUnits().pow(-order.second); + } + for (const auto& stoich : reactants) { + // Order for each reactant is the reactant stoichiometric coefficient, + // unless already overridden by user-specified orders + if (stoich.first == "M" || ba::starts_with(stoich.first, "(+")) { + // calculateRateCoeffUnits may be called before these pseudo-species + // have been stripped from the reactants + continue; + } else if (orders.find(stoich.first) == orders.end()) { + const auto& phase = kin.speciesPhase(stoich.first); + rate_units *= phase.standardConcentrationUnits().pow(-stoich.second); + } + } +} + ElementaryReaction::ElementaryReaction(const Composition& reactants_, const Composition products_, const Arrhenius& rate_) @@ -239,6 +273,13 @@ std::string ThreeBodyReaction::productString() const { return ElementaryReaction::productString() + " + M"; } +void ThreeBodyReaction::calculateRateCoeffUnits(const Kinetics& kin) +{ + ElementaryReaction::calculateRateCoeffUnits(kin); + const ThermoPhase& rxn_phase = kin.thermo(kin.reactionPhaseIndex()); + rate_units *= rxn_phase.standardConcentrationUnits().pow(-1); +} + void ThreeBodyReaction::getParameters(AnyMap& reactionNode) const { ElementaryReaction::getParameters(reactionNode); @@ -254,6 +295,7 @@ FalloffReaction::FalloffReaction() : Reaction() , falloff(new Falloff()) , allow_negative_pre_exponential_factor(false) + , low_rate_units(0.0) { reaction_type = FALLOFF_RXN; } @@ -267,6 +309,7 @@ FalloffReaction::FalloffReaction( , high_rate(high_rate_) , third_body(tbody) , falloff(new Falloff()) + , low_rate_units(0.0) { reaction_type = FALLOFF_RXN; } @@ -306,6 +349,14 @@ void FalloffReaction::validate() { } } +void FalloffReaction::calculateRateCoeffUnits(const Kinetics& kin) +{ + Reaction::calculateRateCoeffUnits(kin); + const ThermoPhase& rxn_phase = kin.thermo(kin.reactionPhaseIndex()); + low_rate_units = rate_units; + low_rate_units *= rxn_phase.standardConcentrationUnits().pow(-1); +} + void FalloffReaction::getParameters(AnyMap& reactionNode) const { Reaction::getParameters(reactionNode); @@ -339,6 +390,14 @@ ChemicallyActivatedReaction::ChemicallyActivatedReaction( reaction_type = CHEMACT_RXN; } +void ChemicallyActivatedReaction::calculateRateCoeffUnits(const Kinetics& kin) +{ + Reaction::calculateRateCoeffUnits(kin); // Skip FalloffReaction + const ThermoPhase& rxn_phase = kin.thermo(kin.reactionPhaseIndex()); + low_rate_units = rate_units; + rate_units *= rxn_phase.standardConcentrationUnits(); +} + void ChemicallyActivatedReaction::getParameters(AnyMap& reactionNode) const { FalloffReaction::getParameters(reactionNode); @@ -408,6 +467,7 @@ void Reaction2::setParameters(const AnyMap& node, const Kinetics& kin) allow_negative_orders = node.getBool("negative-orders", false); allow_nonreactant_orders = node.getBool("nonreactant-orders", false); + calculateRateCoeffUnits(kin); input = node; } @@ -461,10 +521,8 @@ void ChebyshevReaction::getParameters(AnyMap& reactionNode) const if (rate_units2.factor() != 0.0) { coeffs.asVector()[0][0] += std::log10(units.convertFrom(1.0, rate_units2)); } else if (units.getDelta(UnitSystem()).size()) { - //! @todo This special case can be removed after Cantera 3.0 when - //! the XML/CTI formats are removed throw CanteraError("ChebyshevReaction::getParameters lambda", - "Cannot convert rate constant from CTI/XML input to a " + "Cannot convert rate constant with unknown dimensions to a " "non-default unit system"); } }; @@ -550,37 +608,6 @@ Arrhenius readArrhenius(const XML_Node& arrhenius_node) getFloat(arrhenius_node, "E", "actEnergy") / GasConstant); } -Units rateCoeffUnits(const Reaction& R, const Kinetics& kin) -{ - if (!R.valid()) { - // If a reaction is invalid because of missing species in the Kinetics - // object, determining the units of the rate coefficient is impossible. - return Units(); - } - - // Determine the units of the rate coefficient - Units rxn_phase_units = kin.thermo(kin.reactionPhaseIndex()).standardConcentrationUnits(); - Units rcUnits = rxn_phase_units; - rcUnits *= Units(1.0, 0, 0, -1); - for (const auto& order : R.orders) { - const auto& phase = kin.speciesPhase(order.first); - rcUnits *= phase.standardConcentrationUnits().pow(-order.second); - } - for (const auto& stoich : R.reactants) { - // Order for each reactant is the reactant stoichiometric coefficient, - // unless already overridden by user-specified orders - if (stoich.first == "M") { - rcUnits *= rxn_phase_units.pow(-1); - } else if (ba::starts_with(stoich.first, "(+")) { - continue; - } else if (R.orders.find(stoich.first) == R.orders.end()) { - const auto& phase = kin.speciesPhase(stoich.first); - rcUnits *= phase.standardConcentrationUnits().pow(-stoich.second); - } - } - return rcUnits; -} - Arrhenius readArrhenius(const Reaction& R, const AnyValue& rate, const Kinetics& kin, const UnitSystem& units, int pressure_dependence=0) @@ -714,9 +741,6 @@ void setupReaction(Reaction& R, const XML_Node& rxn_node) R.duplicate = rxn_node.hasAttrib("duplicate"); const std::string& rev = rxn_node["reversible"]; R.reversible = (rev == "true" || rev == "yes"); - - // Prevent invalid conversions of the rate coefficient - R.rate_units = Units(0.0); } void parseReactionEquation(Reaction& R, const AnyValue& equation, @@ -804,7 +828,7 @@ void setupReaction(Reaction& R, const AnyMap& node, const Kinetics& kin) R.allow_negative_orders = node.getBool("negative-orders", false); R.allow_nonreactant_orders = node.getBool("nonreactant-orders", false); - R.rate_units = rateCoeffUnits(R, kin); + R.calculateRateCoeffUnits(kin); R.input = node; } @@ -884,7 +908,6 @@ void setupFalloffReaction(FalloffReaction& R, const XML_Node& rxn_node) if (rxn_node["negative_A"] == "yes") { R.allow_negative_pre_exponential_factor = true; } - R.low_rate_units = Units(0.0); readFalloff(R, rc_node); readEfficiencies(R.third_body, rc_node); setupReaction(R, rxn_node); @@ -928,21 +951,10 @@ void setupFalloffReaction(FalloffReaction& R, const AnyMap& node, R.third_body.efficiencies[third_body.substr(2, third_body.size() - 3)] = 1.0; } - R.low_rate_units = R.rate_units; - Units rxn_phase_units = kin.thermo(kin.reactionPhaseIndex()).standardConcentrationUnits(); - if (node["type"] == "falloff") { - R.low_rate = readArrhenius(R, node["low-P-rate-constant"], kin, - node.units(), 1); - R.high_rate = readArrhenius(R, node["high-P-rate-constant"], kin, - node.units()); - R.low_rate_units *= rxn_phase_units.pow(-1); - } else { // type == "chemically-activated" - R.low_rate = readArrhenius(R, node["low-P-rate-constant"], kin, - node.units()); - R.high_rate = readArrhenius(R, node["high-P-rate-constant"], kin, - node.units(), -1); - R.rate_units *= rxn_phase_units; - } + R.low_rate = readArrhenius(R, node["low-P-rate-constant"], kin, + node.units(), 1); + R.high_rate = readArrhenius(R, node["high-P-rate-constant"], kin, + node.units()); readFalloff(R, node); } @@ -971,7 +983,6 @@ void setupChemicallyActivatedReaction(ChemicallyActivatedReaction& R, throw CanteraError("setupChemicallyActivatedReaction", "Did not find " "the correct number of Arrhenius rate expressions"); } - R.low_rate_units = Units(0.0); readFalloff(R, rc_node); readEfficiencies(R.third_body, rc_node); setupReaction(R, rxn_node); diff --git a/src/kinetics/RxnRates.cpp b/src/kinetics/RxnRates.cpp index cdf9565feb..d1fe62efaf 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -56,9 +56,10 @@ void Arrhenius::getParameters(AnyMap& rateNode, const Units& rate_units) const if (rate_units.factor() != 0.0) { rateNode["A"].setQuantity(preExponentialFactor(), rate_units); } else { - // @TODO: This branch can be removed after CTI/XML support is removed - // in Cantera 3.0. rateNode["A"] = preExponentialFactor(); + // This can't be converted to a different unit system because the dimensions of + // the rate constant were not set. Can occur if the reaction was created outside + // the context of a Kinetics object and never added to a Kinetics object. rateNode["__unconvertible__"] = true; } diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp index 0703b967bf..1ed26e9293 100644 --- a/test/general/test_serialization.cpp +++ b/test/general/test_serialization.cpp @@ -145,11 +145,7 @@ TEST(YamlWriter, reaction_units_from_Xml) {"quantity", "mol"}, {"length", "cm"} }); - // Should fail because pre-exponential factors from XML can't be converted - EXPECT_THROW(writer.toYamlFile("generated-h2o2-fail.yaml"), CanteraError); - // Outputting with the default MKS+kmol system still works - writer.setUnits(); writer.toYamlFile("generated-h2o2-from-xml.yaml"); auto duplicate = newSolution("generated-h2o2-from-xml.yaml"); From bc61d7670a96a697e257e282a3373d4f373006a7 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 11 Apr 2021 11:17:11 -0400 Subject: [PATCH 50/57] [Input] Fix key ordering when outputting to Python Fixes an error in the AnyValue assignment operator introduced in dab4b7066. --- interfaces/cython/cantera/test/test_composite.py | 12 ++++++++++++ src/base/AnyMap.cpp | 2 +- 2 files changed, 13 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index fbc34e9092..4fa8dfdcd0 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -1,5 +1,6 @@ from os.path import join as pjoin import os +import sys import numpy as np from collections import OrderedDict @@ -450,6 +451,17 @@ def test_input_data_custom(self): self.assertEqual(data['custom-field']['first'], True) self.assertEqual(data['custom-field']['last'], [100, 200, 300]) + if sys.version_info >= (3,7): + # Check that items are ordered as expected + self.assertEqual( + list(data), + ['name', 'thermo', 'elements', 'species', 'state', 'custom-field'] + ) + self.assertEqual( + list(data['custom-field']), + ['first', 'second', 'last'] + ) + def test_input_data_debye_huckel(self): soln = ct.Solution('thermo-models.yaml', 'debye-huckel-B-dot-ak') data = soln.input_data diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 25e8f26ba7..81376d1ad5 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -614,7 +614,7 @@ AnyValue& AnyValue::operator=(AnyValue const& other) { if (this == &other) { return *this; } - AnyBase::operator=(*this); + AnyBase::operator=(other); m_key = other.m_key; m_value.reset(new boost::any{*other.m_value}); m_equals = other.m_equals; From 660986642e540763089dc9a8ebdc51f8f532d073 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 11 Apr 2021 19:57:40 -0400 Subject: [PATCH 51/57] [Input] Use full AnyMap sorting algorithm in all cases The simpler sorting algorithm used in Python did not account for the additional ordering rules allowed by the AnyMap class. --- include/cantera/base/AnyMap.h | 49 ++++++++ interfaces/cython/cantera/_cantera.pxd | 11 +- interfaces/cython/cantera/utils.pyx | 24 +--- src/base/AnyMap.cpp | 160 ++++++++++++++----------- 4 files changed, 155 insertions(+), 89 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 199f556892..c86ce4f4aa 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -528,6 +528,55 @@ class AnyMap : public AnyBase return Iterator(m_data.end(), m_data.end()); } + class OrderedIterator; + + //! Proxy for iterating over an AnyMap in the defined output ordering. + //! See ordered(). + class OrderedProxy { + public: + OrderedProxy() {} + OrderedProxy(const AnyMap& data); + OrderedIterator begin() const; + OrderedIterator end() const; + + typedef std::vector, + const std::pair*>> OrderVector; + private: + const AnyMap* m_data; + OrderVector m_ordered; + std::unique_ptr> m_units; + }; + + //! Defined to allow the OrderedProxy class to be used with range-based + //! for loops. + class OrderedIterator { + public: + OrderedIterator() {} + OrderedIterator(const OrderedProxy::OrderVector::const_iterator& start, + const OrderedProxy::OrderVector::const_iterator& stop); + + const std::pair& operator*() const { + return *m_iter->second; + } + const std::pair* operator->() const { + return &(*m_iter->second); + } + bool operator!=(const OrderedIterator& right) const { + return m_iter != right.m_iter; + } + OrderedIterator& operator++() { ++m_iter; return *this; } + + private: + OrderedProxy::OrderVector::const_iterator m_iter; + OrderedProxy::OrderVector::const_iterator m_stop; + }; + + // Return a proxy object that allows iteration in an order determined by the + // order of insertion, the location in an input file, and rules specified by + // the addOrderingRules() method. + OrderedProxy ordered() const { return OrderedProxy(*this); } + //! Returns the number of elements in this map size_t size() const { return m_data.size(); diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 9da836824d..9ccc00b33a 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -58,9 +58,19 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": Iterator& operator++() cbool operator!=(Iterator&) + cppclass OrderedIterator: + pair[string, CxxAnyValue]& operator*() + OrderedIterator& operator++() + cbool operator!=(OrderedIterator&) + + cppclass OrderedProxy: + OrderedIterator begin() + OrderedIterator end() + CxxAnyMap() Iterator begin() Iterator end() + OrderedProxy ordered() except +translate_exception CxxAnyValue& operator[](string) except +translate_exception cbool hasKey(string) string keys_str() @@ -74,7 +84,6 @@ cdef extern from "cantera/base/AnyMap.h" namespace "Cantera": string type_str() cbool isType "is" [T]() cbool isScalar() - pair[int, int] order() CxxAnyMap AnyMapFromYamlFile "Cantera::AnyMap::fromYamlFile" (string) except +translate_exception CxxAnyMap AnyMapFromYamlString "Cantera::AnyMap::fromYamlString" (string) except +translate_exception diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 1360273d9b..78845e0867 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -116,14 +116,9 @@ cdef anyvalueToPython(string name, CxxAnyValue& v): cdef anymapToPython(CxxAnyMap& m): - py_items = [] m.applyUnits() - for item in m: - py_items.append((item.second.order(), - pystr(item.first), - anyvalueToPython(item.first, item.second))) - py_items.sort() - return {key: value for (_, key, value) in py_items} + return {pystr(item.first): anyvalueToPython(item.first, item.second) + for item in m.ordered()} cdef mergeAnyMap(CxxAnyMap& primary, CxxAnyMap& extra): @@ -134,16 +129,9 @@ cdef mergeAnyMap(CxxAnyMap& primary, CxxAnyMap& extra): Used to combine generated data representing the current state of the object (primary) with user-supplied fields (extra) not directly used by Cantera. """ - py_items = [] - for item in primary: - py_items.append((item.second.order(), - pystr(item.first), - anyvalueToPython(item.first, item.second))) + out = {pystr(item.first): anyvalueToPython(item.first, item.second) + for item in primary.ordered()} for item in extra: if not primary.hasKey(item.first): - py_items.append((item.second.order(), - pystr(item.first), - anyvalueToPython(item.first, item.second))) - - py_items.sort() - return {key: value for (_, key, value) in py_items} + out[pystr(item.first)] = anyvalueToPython(item.first, item.second) + return out diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 81376d1ad5..0a39ed4a05 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -249,78 +249,14 @@ struct convert { YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) { - // Initial sort based on the order in which items are added - vector, std::string, const AnyValue*>> ordered; - // Units always come first - AnyValue units; - if (rhs.hasKey("__units__") && rhs["__units__"].as().size()) { - units = rhs["__units__"]; - units.setFlowStyle(); - ordered.emplace_back(std::pair{-2, 0}, std::string("units"), &units); - } - - int head = 0; // sort key of the first programmatically-added item - int tail = 0; // sort key of the last programmatically-added item - for (const auto& item : rhs) { - const auto& order = item.second.order(); - if (order.first == -1) { // Item is not from an input file - head = std::min(head, order.second); - tail = std::max(tail, order.second); - } - ordered.emplace_back(order, item.first, &item.second); - } - std::sort(ordered.begin(), ordered.end()); - - // Adjust sort keys for items that should moved to the beginning or end of - // the list - if (rhs.hasKey("__type__")) { - bool order_changed = false; - const auto& itemType = rhs["__type__"].asString(); - std::unique_lock lock(yaml_field_order_mutex); - if (AnyMap::s_headFields.count(itemType)) { - for (const auto& key : AnyMap::s_headFields[itemType]) { - for (auto& item : ordered) { - if (std::get<0>(item).first >= 0) { - // This and following items come from an input file and - // should not be re-ordered - break; - } - if (std::get<1>(item) == key) { - std::get<0>(item).second = --head; - order_changed = true; - } - } - } - } - if (AnyMap::s_tailFields.count(itemType)) { - for (const auto& key : AnyMap::s_tailFields[itemType]) { - for (auto& item : ordered) { - if (std::get<0>(item).first >= 0) { - // This and following items come from an input file and - // should not be re-ordered - break; - } - if (std::get<1>(item) == key) { - std::get<0>(item).second = ++tail; - order_changed = true; - } - } - } - } - - if (order_changed) { - std::sort(ordered.begin(), ordered.end()); - } - } - bool flow = rhs.getBool("__flow__", false); if (flow) { out << YAML::Flow; out << YAML::BeginMap; size_t width = 15; - for (const auto& item : ordered) { - const auto& name = std::get<1>(item); - const auto& value = *std::get<2>(item); + for (const auto& item : rhs.ordered()) { + const auto& name = item.first; + const auto& value = item.second; string valueStr; bool foundType = true; if (value.is()) { @@ -354,9 +290,9 @@ YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) } } else { out << YAML::BeginMap; - for (const auto& item : ordered) { - out << std::get<1>(item); - out << *std::get<2>(item); + for (const auto& item : rhs.ordered()) { + out << item.first; + out << item.second; } } out << YAML::EndMap; @@ -1526,6 +1462,90 @@ AnyMap::Iterator& AnyMap::Iterator::operator++() return *this; } + +AnyMap::OrderedProxy::OrderedProxy(const AnyMap& data) + : m_data(&data) +{ + // Units always come first + if (m_data->hasKey("__units__") && m_data->at("__units__").as().size()) { + m_units.reset(new std::pair{"units", m_data->at("__units__")}); + m_units->second.setFlowStyle(); + m_ordered.emplace_back(std::pair{-2, 0}, m_units.get()); + } + + int head = 0; // sort key of the first programmatically-added item + int tail = 0; // sort key of the last programmatically-added item + for (auto& item : *m_data) { + const auto& order = item.second.order(); + if (order.first == -1) { // Item is not from an input file + head = std::min(head, order.second); + tail = std::max(tail, order.second); + } + m_ordered.emplace_back(order, &item); + } + std::sort(m_ordered.begin(), m_ordered.end()); + + // Adjust sort keys for items that should moved to the beginning or end of + // the list + if (m_data->hasKey("__type__")) { + bool order_changed = false; + const auto& itemType = m_data->at("__type__").asString(); + std::unique_lock lock(yaml_field_order_mutex); + if (AnyMap::s_headFields.count(itemType)) { + for (const auto& key : AnyMap::s_headFields[itemType]) { + for (auto& item : m_ordered) { + if (item.first.first >= 0) { + // This and following items come from an input file and + // should not be re-ordered + break; + } + if (item.second->first == key) { + item.first.second = --head; + order_changed = true; + } + } + } + } + if (AnyMap::s_tailFields.count(itemType)) { + for (const auto& key : AnyMap::s_tailFields[itemType]) { + for (auto& item : m_ordered) { + if (item.first.first >= 0) { + // This and following items come from an input file and + // should not be re-ordered + break; + } + if (item.second->first == key) { + item.first.second = ++tail; + order_changed = true; + } + } + } + } + + if (order_changed) { + std::sort(m_ordered.begin(), m_ordered.end()); + } + } +} + +AnyMap::OrderedIterator AnyMap::OrderedProxy::begin() const +{ + return OrderedIterator(m_ordered.begin(), m_ordered.end()); +} + +AnyMap::OrderedIterator AnyMap::OrderedProxy::end() const +{ + return OrderedIterator(m_ordered.end(), m_ordered.end()); +} + +AnyMap::OrderedIterator::OrderedIterator( + const AnyMap::OrderedProxy::OrderVector::const_iterator& start, + const AnyMap::OrderedProxy::OrderVector::const_iterator& stop) +{ + m_iter = start; + m_stop = stop; +} + bool AnyMap::operator==(const AnyMap& other) const { // First, make sure that 'other' has all of the non-hidden keys that are in From 8574c1c56bb6dcf358d185c2c2af16980d035153 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 12 Apr 2021 17:01:32 -0400 Subject: [PATCH 52/57] [Input] Refactor to reduce Python manipulations of AnyMap Eliminates the 'mergeAnyMap' function, and introduces a 'parameters' method for classes to return an AnyMap which can optionally contain the user-provided input data, rather than needing to create the AnyMap in advance and add the user-created fields separately. --- include/cantera/base/AnyMap.h | 4 ++ include/cantera/base/Solution.h | 3 + include/cantera/kinetics/Kinetics.h | 4 +- include/cantera/kinetics/Reaction.h | 18 ++++-- include/cantera/thermo/Species.h | 3 +- .../cantera/thermo/SpeciesThermoInterpType.h | 17 ++++-- include/cantera/thermo/ThermoPhase.h | 15 +++-- include/cantera/transport/TransportBase.h | 4 +- include/cantera/transport/TransportData.h | 15 ++++- interfaces/cython/cantera/_cantera.pxd | 17 ++---- interfaces/cython/cantera/base.pyx | 9 +-- interfaces/cython/cantera/reaction.pyx | 4 +- interfaces/cython/cantera/speciesthermo.pyx | 4 +- interfaces/cython/cantera/thermo.pyx | 7 +-- interfaces/cython/cantera/transport.pyx | 4 +- interfaces/cython/cantera/utils.pyx | 16 ----- src/base/AnyMap.cpp | 9 +++ src/base/Solution.cpp | 15 +++++ src/base/YamlWriter.cpp | 60 +------------------ src/kinetics/Kinetics.cpp | 9 ++- src/kinetics/Reaction.cpp | 34 +++++++---- src/thermo/Species.cpp | 47 +++++++-------- src/thermo/SpeciesThermoInterpType.cpp | 12 +++- src/thermo/ThermoPhase.cpp | 10 ++++ src/transport/TransportBase.cpp | 6 +- src/transport/TransportData.cpp | 10 ++++ test/kinetics/kineticsFromYaml.cpp | 3 +- test/thermo/thermoParameterizations.cpp | 15 ++--- test/thermo/thermoToYaml.cpp | 4 +- 29 files changed, 195 insertions(+), 183 deletions(-) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index c86ce4f4aa..61cc22c947 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -437,6 +437,10 @@ class AnyMap : public AnyBase //! Erase all items in the mapping void clear(); + //! Add items from `other` to this AnyMap. If keys in `other` also exist in + //! this AnyMap, the `keepExisting` option determines which item is used. + void update(const AnyMap& other, bool keepExisting=true); + //! Return a string listing the keys in this AnyMap, e.g. for use in error //! messages std::string keys_str() const; diff --git a/include/cantera/base/Solution.h b/include/cantera/base/Solution.h index 155c2d674c..cf4ae4b524 100644 --- a/include/cantera/base/Solution.h +++ b/include/cantera/base/Solution.h @@ -14,6 +14,7 @@ namespace Cantera class ThermoPhase; class Kinetics; class Transport; +class AnyMap; //! A container class holding managers for all pieces defining a phase class Solution : public std::enable_shared_from_this @@ -61,6 +62,8 @@ class Solution : public std::enable_shared_from_this return m_transport; } + AnyMap parameters(bool withInput=false) const; + protected: shared_ptr m_thermo; //!< ThermoPhase manager shared_ptr m_kinetics; //!< Kinetics manager diff --git a/include/cantera/kinetics/Kinetics.h b/include/cantera/kinetics/Kinetics.h index 39838ec903..a355f8a687 100644 --- a/include/cantera/kinetics/Kinetics.h +++ b/include/cantera/kinetics/Kinetics.h @@ -704,10 +704,10 @@ class Kinetics */ virtual void init() {} - //! Store the parameters for a phase definition which are needed to + //! Return the parameters for a phase definition which are needed to //! reconstruct an identical object using the newKinetics function. This //! excludes the reaction definitions, which are handled separately. - virtual void getParameters(AnyMap& phaseNode); + AnyMap parameters(); /** * Resize arrays with sizes that depend on the total number of species. diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index ec66de22de..9598c3e1ff 100644 --- a/include/cantera/kinetics/Reaction.h +++ b/include/cantera/kinetics/Reaction.h @@ -57,6 +57,14 @@ class Reaction //! valid. virtual void validate(); + //! Return the parameters such that an identical Reaction could be reconstructed + //! using the newReaction() function. Behavior specific to derived classes is + //! handled by the getParameters() method. + //! @param withInput If true, include additional input data fields associated + //! with the object, such as user-defined fields from a YAML input file, as + //! contained in the #input attribute. + AnyMap parameters(bool withInput=true) const; + //! Get validity flag of reaction bool valid() const { return m_valid; @@ -67,11 +75,6 @@ class Reaction m_valid = valid; } - //! Store the parameters of a Reaction needed to reconstruct an identical - //! object using the newReaction(AnyMap&, Kinetics&) function. Does not - //! include user-defined fields available in the #input map. - virtual void getParameters(AnyMap& reactionNode) const; - //! Type of the reaction. The valid types are listed in the file, //! reaction_defs.h, with constants ending in `RXN`. /*! @@ -117,6 +120,11 @@ class Reaction Units rate_units; protected: + //! Store the parameters of a Reaction needed to reconstruct an identical + //! object using the newReaction(AnyMap&, Kinetics&) function. Does not + //! include user-defined fields available in the #input map. + virtual void getParameters(AnyMap& reactionNode) const; + //! Flag indicating whether reaction is set up correctly bool m_valid; }; diff --git a/include/cantera/thermo/Species.h b/include/cantera/thermo/Species.h index c2120a4c5b..201c52f107 100644 --- a/include/cantera/thermo/Species.h +++ b/include/cantera/thermo/Species.h @@ -15,6 +15,7 @@ namespace Cantera class SpeciesThermoInterpType; class TransportData; class XML_Node; +class ThermoPhase; //! Contains data about a single chemical species /*! @@ -35,7 +36,7 @@ class Species Species& operator=(const Species& other) = delete; ~Species(); - void getParameters(AnyMap& speciesNode, bool withInput=true) const; + AnyMap parameters(const ThermoPhase* phase=0, bool withInput=true) const; //! The name of the species std::string name; diff --git a/include/cantera/thermo/SpeciesThermoInterpType.h b/include/cantera/thermo/SpeciesThermoInterpType.h index 60baab1dc9..1b4a1ea8df 100644 --- a/include/cantera/thermo/SpeciesThermoInterpType.h +++ b/include/cantera/thermo/SpeciesThermoInterpType.h @@ -227,10 +227,14 @@ class SpeciesThermoInterpType doublereal& refPressure, doublereal* const coeffs) const; - //! Store the parameters of the species thermo object such that an identical - //! species thermo object could be reconstructed using the - //! newSpeciesThermo() function. - virtual void getParameters(AnyMap& thermo) const; + //! Return the parameters of the species thermo object such that an + //! identical species thermo object could be reconstructed using the + //! newSpeciesThermo() function. Behavior specific to derived classes is + //! handled by the getParameters() method. + //! @param withInput If true, include additional input data fields associated + //! with the object, such as user-defined fields from a YAML input file, as + //! returned by the input() method. + AnyMap parameters(bool withInput=true) const; //! Report the 298 K Heat of Formation of the standard state of one species //! (J kmol-1) @@ -273,6 +277,11 @@ class SpeciesThermoInterpType AnyMap& input(); protected: + //! Store the parameters of the species thermo object such that an identical + //! species thermo object could be reconstructed using the + //! newSpeciesThermo() function. + virtual void getParameters(AnyMap& thermo) const; + //! lowest valid temperature doublereal m_lowT; //! Highest valid temperature diff --git a/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index 2c5171d110..1f79d49609 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -1709,10 +1709,12 @@ class ThermoPhase : public Phase virtual void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); - //! Store the parameters of a ThermoPhase object such that an identical - //! one could be reconstructed using the newPhase(AnyMap&) function. This - //! does not include user-defined fields available in input(). - virtual void getParameters(AnyMap& phaseNode) const; + //! Returns the parameters of a ThermoPhase object such that an identical + //! one could be reconstructed using the newPhase(AnyMap&) function. + //! @param withInput If true, include additional input data fields associated + //! with the phase description, such as user-defined fields from a YAML input + //! file, as returned by the input() method. + AnyMap parameters(bool withInput=true) const; //! Get phase-specific parameters of a Species object such that an //! identical one could be reconstructed and added to this phase. @@ -1872,6 +1874,11 @@ class ThermoPhase : public Phase //@} protected: + //! Store the parameters of a ThermoPhase object such that an identical + //! one could be reconstructed using the newPhase(AnyMap&) function. This + //! does not include user-defined fields available in input(). + virtual void getParameters(AnyMap& phaseNode) const; + //! Fills `names` and `data` with the column names and species thermo //! properties to be included in the output of the reportCSV method. virtual void getCsvReportData(std::vector& names, diff --git a/include/cantera/transport/TransportBase.h b/include/cantera/transport/TransportBase.h index ace86afb3b..5733fe6e5b 100644 --- a/include/cantera/transport/TransportBase.h +++ b/include/cantera/transport/TransportBase.h @@ -602,11 +602,11 @@ class Transport */ virtual void setParameters(const int type, const int k, const doublereal* const p); - //! Store the parameters for a phase definition which are needed to + //! Return the parameters for a phase definition which are needed to //! reconstruct an identical object using the newTransport function. This //! excludes the individual species transport properties, which are handled //! separately. - virtual void getParameters(AnyMap& phaseNode); + AnyMap parameters() const; //! Sets the velocity basis /*! diff --git a/include/cantera/transport/TransportData.h b/include/cantera/transport/TransportData.h index 715de3a28c..4cf099089c 100644 --- a/include/cantera/transport/TransportData.h +++ b/include/cantera/transport/TransportData.h @@ -24,12 +24,21 @@ class TransportData virtual void validate(const Species& species) {} - //! Store the parameters needed to reconstruct a TransportData object. Does - //! not include user-defined fields available in #input. - virtual void getParameters(AnyMap& transportNode) const; + //! Return the parameters such that an identical species transport object + //! could be reconstructed using the newTransportData() function. Behavior + //! specific to derived classes is handled by the getParameters() method. + //! @param withInput If true, include additional input data fields associated + //! with the object, such as user-defined fields from a YAML input file, as + //! stored in the #input attribute. + AnyMap parameters(bool withInput) const; //! Input data used for specific models AnyMap input; + +protected: + //! Store the parameters needed to reconstruct a TransportData object. Does + //! not include user-defined fields available in #input. + virtual void getParameters(AnyMap& transportNode) const; }; //! Transport data for a single gas-phase species which can be used in diff --git a/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 9ccc00b33a..cb9dce619f 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -125,8 +125,7 @@ cdef extern from "cantera/thermo/SpeciesThermoInterpType.h": double refPressure() void reportParameters(size_t&, int&, double&, double&, double&, double* const) except +translate_exception int nCoeffs() except +translate_exception - void getParameters(CxxAnyMap&) except +translate_exception - CxxAnyMap& input() + CxxAnyMap parameters(cbool) except +translate_exception cdef extern from "cantera/thermo/SpeciesThermoFactory.h": cdef CxxSpeciesThermo* CxxNewSpeciesThermo "Cantera::newSpeciesThermoInterpType"\ @@ -145,8 +144,7 @@ cdef extern from "cantera/thermo/Species.h" namespace "Cantera": Composition composition double charge double size - void getParameters(CxxAnyMap&) except +translate_exception - CxxAnyMap input + CxxAnyMap parameters(CxxThermoPhase*) except +translate_exception cdef shared_ptr[CxxSpecies] CxxNewSpecies "newSpecies" (XML_Node&) cdef vector[shared_ptr[CxxSpecies]] CxxGetSpecies "getSpecies" (XML_Node&) @@ -162,6 +160,7 @@ cdef extern from "cantera/base/Solution.h" namespace "Cantera": void setThermo(shared_ptr[CxxThermoPhase]) void setKinetics(shared_ptr[CxxKinetics]) void setTransport(shared_ptr[CxxTransport]) + CxxAnyMap parameters(cbool) except +translate_exception cdef shared_ptr[CxxSolution] CxxNewSolution "Cantera::Solution::create" () @@ -178,8 +177,6 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera": # miscellaneous string type() string phaseOfMatter() except +translate_exception - CxxAnyMap& input() - void getParameters(CxxAnyMap&) except +translate_exception void getSpeciesParameters(string, CxxAnyMap&) except +translate_exception string report(cbool, double) except +translate_exception cbool hasPhaseTransition() @@ -369,7 +366,7 @@ cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera": string equation() string type() void validate() except +translate_exception - void getParameters(CxxAnyMap&) except +translate_exception + CxxAnyMap parameters(cbool) except +translate_exception int reaction_type Composition reactants Composition products @@ -379,7 +376,6 @@ cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera": cbool duplicate cbool allow_nonreactant_orders cbool allow_negative_orders - CxxAnyMap input cdef cppclass CxxReaction2 "Cantera::Reaction2" (CxxReaction): CxxReaction2() @@ -491,7 +487,6 @@ cdef extern from "cantera/kinetics/Kinetics.h" namespace "Cantera": void addReaction(shared_ptr[CxxReaction]) except +translate_exception void modifyReaction(int, shared_ptr[CxxReaction]) except +translate_exception void invalidateCache() except +translate_exception - void getParameters(CxxAnyMap&) except +translate_exception shared_ptr[CxxReaction] reaction(size_t) except +translate_exception cbool isReversible(int) except +translate_exception @@ -517,7 +512,6 @@ cdef extern from "cantera/transport/TransportBase.h" namespace "Cantera": cdef cppclass CxxTransport "Cantera::Transport": CxxTransport(CxxThermoPhase*) string transportType() - void getParameters(CxxAnyMap&) except +translate_exception double viscosity() except +translate_exception double thermalConductivity() except +translate_exception double electricalConductivity() except +translate_exception @@ -537,8 +531,7 @@ cdef extern from "cantera/transport/DustyGasTransport.h" namespace "Cantera": cdef extern from "cantera/transport/TransportData.h" namespace "Cantera": cdef cppclass CxxTransportData "Cantera::TransportData": CxxTransportData() - CxxAnyMap input - void getParameters(CxxAnyMap&) except +translate_exception + CxxAnyMap parameters(cbool) except +translate_exception cdef cppclass CxxGasTransportData "Cantera::GasTransportData" (CxxTransportData): CxxGasTransportData() diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 103c1bf5cb..1b4094ebef 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -230,14 +230,7 @@ cdef class _SolutionBase: definition. """ def __get__(self): - cdef CxxAnyMap params - if self.thermo: - self.thermo.getParameters(params) - if self.kinetics: - self.kinetics.getParameters(params) - if self.transport: - self.transport.getParameters(params) - return mergeAnyMap(params, self.thermo.input()) + return anymapToPython(self.base.parameters(True)) def write_yaml(self, filename, phases=None, units=None, precision=None, skip_user_defined=None): diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index 6b351f44e2..928cf9dba9 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -327,9 +327,7 @@ cdef class Reaction: definition. """ def __get__(self): - cdef CxxAnyMap params - self.reaction.getParameters(params) - return mergeAnyMap(params, self.reaction.input) + return anymapToPython(self.reaction.parameters(True)) def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self.equation) diff --git a/interfaces/cython/cantera/speciesthermo.pyx b/interfaces/cython/cantera/speciesthermo.pyx index f3c72ecb5e..a44024272d 100644 --- a/interfaces/cython/cantera/speciesthermo.pyx +++ b/interfaces/cython/cantera/speciesthermo.pyx @@ -88,9 +88,7 @@ cdef class SpeciesThermo: property input_data: def __get__(self): - cdef CxxAnyMap params - self.spthermo.getParameters(params) - return mergeAnyMap(params, self.spthermo.input()) + return anymapToPython(self.spthermo.parameters(True)) def cp(self, T): """ diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 688888a0e2..0abf68f899 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -264,11 +264,8 @@ cdef class Species: property input_data: def __get__(self): - cdef CxxAnyMap params - self.species.getParameters(params) - if self._phase: - self._phase.thermo.getSpeciesParameters(self.species.name, params) - return mergeAnyMap(params, self.species.input) + cdef CxxThermoPhase* phase = self._phase.thermo if self._phase else NULL + return anymapToPython(self.species.parameters(phase)) def __repr__(self): return ''.format(self.name) diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index e3f06f164d..1f7f16ed4e 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -58,9 +58,7 @@ cdef class GasTransportData: property input_data: def __get__(self): - cdef CxxAnyMap params - self.data.getParameters(params) - return mergeAnyMap(params, self.data.input) + return anymapToPython(self.data.parameters(True)) property geometry: """ diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 78845e0867..7978dba110 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -119,19 +119,3 @@ cdef anymapToPython(CxxAnyMap& m): m.applyUnits() return {pystr(item.first): anyvalueToPython(item.first, item.second) for item in m.ordered()} - - -cdef mergeAnyMap(CxxAnyMap& primary, CxxAnyMap& extra): - """ - Combine two AnyMaps into a single Python dict. Items from the second map - are included only if there is no corresponding key in the first map. - - Used to combine generated data representing the current state of the object - (primary) with user-supplied fields (extra) not directly used by Cantera. - """ - out = {pystr(item.first): anyvalueToPython(item.first, item.second) - for item in primary.ordered()} - for item in extra: - if not primary.hasKey(item.first): - out[pystr(item.first)] = anyvalueToPython(item.first, item.second) - return out diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 0a39ed4a05..0e5c464a1c 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -1356,6 +1356,15 @@ void AnyMap::clear() m_data.clear(); } +void AnyMap::update(const AnyMap& other, bool keepExisting) +{ + for (const auto& item : other) { + if (!keepExisting || !hasKey(item.first)) { + (*this)[item.first] = item.second; + } + } +} + std::string AnyMap::keys_str() const { fmt::memory_buffer b; diff --git a/src/base/Solution.cpp b/src/base/Solution.cpp index 7366fccadf..989034b3b3 100644 --- a/src/base/Solution.cpp +++ b/src/base/Solution.cpp @@ -58,6 +58,21 @@ void Solution::setTransport(shared_ptr transport) { } } +AnyMap Solution::parameters(bool withInput) const +{ + AnyMap out = m_thermo->parameters(false); + if (m_kinetics) { + out.update(m_kinetics->parameters()); + } + if (m_transport) { + out.update(m_transport->parameters()); + } + if (withInput) { + out.update(m_thermo->input()); + } + return out; +} + shared_ptr newSolution(const std::string& infile, const std::string& name, const std::string& transport, diff --git a/src/base/YamlWriter.cpp b/src/base/YamlWriter.cpp index 77e5f18112..ad4ddf6ba6 100644 --- a/src/base/YamlWriter.cpp +++ b/src/base/YamlWriter.cpp @@ -54,27 +54,8 @@ std::string YamlWriter::toYamlString() const std::vector phaseDefs(m_phases.size()); size_t nspecies_total = 0; for (size_t i = 0; i < m_phases.size(); i++) { - m_phases[i]->thermo()->getParameters(phaseDefs[i]); + phaseDefs[i] = m_phases[i]->parameters(!m_skip_user_defined); nspecies_total += m_phases[i]->thermo()->nSpecies(); - const auto& kin = m_phases[i]->kinetics(); - if (kin) { - kin->getParameters(phaseDefs[i]); - if (phaseDefs[i].hasKey("kinetics") && kin->nReactions() == 0) { - phaseDefs[i]["reactions"] = "none"; - } - } - const auto& tran = m_phases[i]->transport(); - if (tran) { - tran->getParameters(phaseDefs[i]); - } - - if (!m_skip_user_defined) { - for (const auto& item : m_phases[i]->thermo()->input()) { - if (!phaseDefs[i].hasKey(item.first)) { - phaseDefs[i][item.first] = item.second; - } - } - } } output["phases"] = phaseDefs; @@ -86,31 +67,7 @@ std::string YamlWriter::toYamlString() const const auto thermo = phase->thermo(); for (const auto& name : thermo->speciesNames()) { const auto& species = thermo->species(name); - AnyMap speciesDef; - species->getParameters(speciesDef, !m_skip_user_defined); - - thermo->getSpeciesParameters(name, speciesDef); - if (!m_skip_user_defined - && species->input.hasKey("equation-of-state")) { - auto& eosIn = species->input["equation-of-state"].asVector(); - for (const auto& eos : eosIn) { - auto& out = speciesDef["equation-of-state"].getMapWhere( - "model", eos["model"].asString(), true); - for (const auto& item : eos) { - if (!out.hasKey(item.first)) { - out[item.first] = item.second; - } - } - } - } - - if (!m_skip_user_defined) { - for (const auto& item : species->input) { - if (!speciesDef.hasKey(item.first)) { - speciesDef[item.first] = item.second; - } - } - } + AnyMap speciesDef = species->parameters(thermo.get(), !m_skip_user_defined); if (speciesDefIndex.count(name) == 0) { speciesDefs.emplace_back(speciesDef); @@ -135,18 +92,7 @@ std::string YamlWriter::toYamlString() const } std::vector reactions; for (size_t i = 0; i < kin->nReactions(); i++) { - const auto reaction = kin->reaction(i); - AnyMap reactionDef; - reaction->getParameters(reactionDef); - if (!m_skip_user_defined) { - for (const auto& item : reaction->input) { - if (!reactionDef.hasKey(item.first)) { - reactionDef[item.first] = item.second; - } - } - } - - reactions.push_back(std::move(reactionDef)); + reactions.push_back(kin->reaction(i)->parameters(!m_skip_user_defined)); } allReactions[phase->name()] = std::move(reactions); } diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index ee6f40a95e..02874970c0 100644 --- a/src/kinetics/Kinetics.cpp +++ b/src/kinetics/Kinetics.cpp @@ -494,12 +494,17 @@ void Kinetics::addPhase(ThermoPhase& thermo) resizeSpecies(); } -void Kinetics::getParameters(AnyMap& phaseNode) +AnyMap Kinetics::parameters() { + AnyMap out; string name = KineticsFactory::factory()->canonicalize(kineticsType()); if (name != "none") { - phaseNode["kinetics"] = name; + out["kinetics"] = name; + if (nReactions() == 0) { + out["reactions"] = "none"; + } } + return out; } void Kinetics::resizeSpecies() diff --git a/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index cfb78ee758..10e699b5a6 100644 --- a/src/kinetics/Reaction.cpp +++ b/src/kinetics/Reaction.cpp @@ -102,6 +102,28 @@ void Reaction::validate() } } +AnyMap Reaction::parameters(bool withInput) const +{ + AnyMap out; + getParameters(out); + if (withInput) { + out.update(input); + } + + static bool reg = AnyMap::addOrderingRules("Reaction", + {{"head", "type"}, + {"head", "equation"}, + {"tail", "duplicate"}, + {"tail", "orders"}, + {"tail", "negative-orders"}, + {"tail", "nonreactant-orders"} + }); + if (reg) { + out["__type__"] = "Reaction"; + } + return out; +} + void Reaction::getParameters(AnyMap& reactionNode) const { reactionNode["equation"] = equation(); @@ -118,18 +140,6 @@ void Reaction::getParameters(AnyMap& reactionNode) const if (allow_nonreactant_orders) { reactionNode["nonreactant-orders"] = true; } - - static bool reg = AnyMap::addOrderingRules("Reaction", - {{"head", "type"}, - {"head", "equation"}, - {"tail", "duplicate"}, - {"tail", "orders"}, - {"tail", "negative-orders"}, - {"tail", "nonreactant-orders"} - }); - if (reg) { - reactionNode["__type__"] = "Reaction"; - } } std::string Reaction::reactantString() const diff --git a/src/thermo/Species.cpp b/src/thermo/Species.cpp index 6e6359bae0..f8e012daf0 100644 --- a/src/thermo/Species.cpp +++ b/src/thermo/Species.cpp @@ -4,10 +4,12 @@ #include "cantera/thermo/Species.h" #include "cantera/thermo/SpeciesThermoInterpType.h" #include "cantera/thermo/SpeciesThermoFactory.h" +#include "cantera/thermo/ThermoPhase.h" #include "cantera/transport/TransportData.h" #include "cantera/base/stringUtils.h" #include "cantera/base/ctexceptions.h" #include "cantera/base/ctml.h" +#include "cantera/base/global.h" #include #include #include @@ -33,8 +35,9 @@ Species::~Species() { } -void Species::getParameters(AnyMap& speciesNode, bool withInput) const +AnyMap Species::parameters(const ThermoPhase* phase, bool withInput) const { + AnyMap speciesNode; speciesNode["name"] = name; speciesNode["composition"] = composition; speciesNode["composition"].setFlowStyle(); @@ -45,34 +48,30 @@ void Species::getParameters(AnyMap& speciesNode, bool withInput) const if (size != 1) { speciesNode["size"] = size; } - - AnyMap thermoNode; - if (thermo && thermo->reportType() != 0) { - thermo->getParameters(thermoNode); - } - if (thermo && withInput) { - for (const auto& item : thermo->input()) { - if (!thermoNode.hasKey(item.first)) { - thermoNode[item.first] = item.second; - } + if (thermo) { + AnyMap thermoNode = thermo->parameters(withInput); + if (thermoNode.size()) { + speciesNode["thermo"] = std::move(thermoNode); } } - if (thermoNode.size()) { - speciesNode["thermo"] = std::move(thermoNode); - } - if (transport) { - AnyMap transportNode; - transport->getParameters(transportNode); - if (withInput) { - for (const auto& item : transport->input) { - if (!transportNode.hasKey(item.first)) { - transportNode[item.first] = item.second; - } - } + speciesNode["transport"] = transport->parameters(withInput); + } + if (phase) { + phase->getSpeciesParameters(name, speciesNode); + } + if (withInput && input.hasKey("equation-of-state")) { + auto& eosIn = input["equation-of-state"].asVector(); + for (const auto& eos : eosIn) { + auto& out = speciesNode["equation-of-state"].getMapWhere( + "model", eos["model"].asString(), true); + out.update(eos); } - speciesNode["transport"] = std::move(transportNode); } + if (withInput) { + speciesNode.update(input); + } + return speciesNode; } shared_ptr newSpecies(const XML_Node& species_node) diff --git a/src/thermo/SpeciesThermoInterpType.cpp b/src/thermo/SpeciesThermoInterpType.cpp index 01f633123f..53625e3a13 100644 --- a/src/thermo/SpeciesThermoInterpType.cpp +++ b/src/thermo/SpeciesThermoInterpType.cpp @@ -52,9 +52,19 @@ void SpeciesThermoInterpType::reportParameters(size_t& index, int& type, throw NotImplementedError("SpeciesThermoInterpType::reportParameters"); } +AnyMap SpeciesThermoInterpType::parameters(bool withInput) const +{ + AnyMap out; + getParameters(out); + if (withInput) { + out.update(m_input); + } + return out; +} + void SpeciesThermoInterpType::getParameters(AnyMap& thermo) const { - if (m_Pref != OneAtm) { + if (m_Pref != OneAtm && reportType() != 0) { thermo["reference-pressure"].setQuantity(m_Pref, "Pa"); } } diff --git a/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 64a729ea6e..0c70202b03 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1189,6 +1189,16 @@ void ThermoPhase::setParameters(const AnyMap& phaseNode, const AnyMap& rootNode) m_input = phaseNode; } +AnyMap ThermoPhase::parameters(bool withInput) const +{ + AnyMap out; + getParameters(out); + if (withInput) { + out.update(m_input); + } + return out; +} + void ThermoPhase::getParameters(AnyMap& phaseNode) const { phaseNode["name"] = name(); diff --git a/src/transport/TransportBase.cpp b/src/transport/TransportBase.cpp index 5af328b9d7..0030ce9afd 100644 --- a/src/transport/TransportBase.cpp +++ b/src/transport/TransportBase.cpp @@ -53,12 +53,14 @@ void Transport::setParameters(const int type, const int k, throw NotImplementedError("Transport::setParameters"); } -void Transport::getParameters(AnyMap& phaseNode) +AnyMap Transport::parameters() const { + AnyMap out; string name = TransportFactory::factory()->canonicalize(transportType()); if (name != "") { - phaseNode["transport"] = name; + out["transport"] = name; } + return out; } void Transport::setThermo(ThermoPhase& thermo) diff --git a/src/transport/TransportData.cpp b/src/transport/TransportData.cpp index 03886c723a..523719ed84 100644 --- a/src/transport/TransportData.cpp +++ b/src/transport/TransportData.cpp @@ -14,6 +14,16 @@ namespace Cantera { +AnyMap TransportData::parameters(bool withInput) const +{ + AnyMap out; + getParameters(out); + if (withInput) { + out.update(input); + } + return out; +} + void TransportData::getParameters(AnyMap &transportNode) const { } diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index 3641ce21ea..7db21dde4f 100644 --- a/test/kinetics/kineticsFromYaml.cpp +++ b/test/kinetics/kineticsFromYaml.cpp @@ -329,8 +329,7 @@ class ReactionToYaml : public testing::Test void duplicateReaction(size_t i) { auto kin = soln->kinetics(); iOld = i; - AnyMap rdata1; - kin->reaction(iOld)->getParameters(rdata1); + AnyMap rdata1 = kin->reaction(iOld)->parameters(); AnyMap rdata2 = AnyMap::fromYamlString(rdata1.toYamlString()); duplicate = newReaction(rdata2, *kin); kin->addReaction(duplicate); diff --git a/test/thermo/thermoParameterizations.cpp b/test/thermo/thermoParameterizations.cpp index 68ac34b8b0..b4a5be7e91 100644 --- a/test/thermo/thermoParameterizations.cpp +++ b/test/thermo/thermoParameterizations.cpp @@ -245,8 +245,7 @@ TEST(SpeciesThermo, Mu0PolyFromYaml) { TEST(SpeciesThermo, NasaPoly2ToYaml) { shared_ptr soln = newSolution("../data/simplephases.cti", "nasa1"); auto original = soln->thermo()->species("H2O")->thermo; - AnyMap h2o_data1; - original->getParameters(h2o_data1); + AnyMap h2o_data1 = original->parameters(); AnyMap h2o_data2 = AnyMap::fromYamlString(h2o_data1.toYamlString()); auto duplicate = newSpeciesThermo(h2o_data2); double cp1, cp2, h1, h2, s1, s2; @@ -263,8 +262,7 @@ TEST(SpeciesThermo, NasaPoly2ToYaml) { TEST(SpeciesThermo, ShomatePolyToYaml) { shared_ptr soln = newSolution("../data/simplephases.cti", "shomate1"); auto original = soln->thermo()->species("CO2")->thermo; - AnyMap co2_data1; - original->getParameters(co2_data1); + AnyMap co2_data1 = original->parameters(); AnyMap co2_data2 = AnyMap::fromYamlString(co2_data1.toYamlString()); auto duplicate = newSpeciesThermo(co2_data2); double cp1, cp2, h1, h2, s1, s2; @@ -281,8 +279,7 @@ TEST(SpeciesThermo, ShomatePolyToYaml) { TEST(SpeciesThermo, ConstCpToYaml) { shared_ptr soln = newSolution("../data/simplephases.cti", "simple1"); auto original = soln->thermo()->species("H2O")->thermo; - AnyMap h2o_data1; - original->getParameters(h2o_data1); + AnyMap h2o_data1 = original->parameters(); AnyMap h2o_data2 = AnyMap::fromYamlString(h2o_data1.toYamlString()); auto duplicate = newSpeciesThermo(h2o_data2); double cp1, cp2, h1, h2, s1, s2; @@ -300,8 +297,7 @@ TEST(SpeciesThermo, PiecewiseGibbsToYaml) { shared_ptr soln = newSolution("../data/thermo-models.yaml", "debye-huckel-beta_ij"); auto original = soln->thermo()->species("OH-")->thermo; - AnyMap oh_data; - original->getParameters(oh_data); + AnyMap oh_data = original->parameters(); auto duplicate = newSpeciesThermo(AnyMap::fromYamlString(oh_data.toYamlString())); double cp1, cp2, h1, h2, s1, s2; for (double T : {274, 300, 330, 340}) { @@ -317,8 +313,7 @@ TEST(SpeciesThermo, PiecewiseGibbsToYaml) { TEST(SpeciesThermo, Nasa9PolyToYaml) { shared_ptr soln = newSolution("airNASA9.cti"); auto original = soln->thermo()->species("N2+")->thermo; - AnyMap n2p_data1; - original->getParameters(n2p_data1); + AnyMap n2p_data1 = original->parameters(); AnyMap n2p_data2 = AnyMap::fromYamlString(n2p_data1.toYamlString()); auto duplicate = newSpeciesThermo(n2p_data2); double cp1, cp2, h1, h2, s1, s2; diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index d53cfff155..fde4a64cd2 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -15,7 +15,7 @@ class ThermoToYaml : public testing::Test // to check for here, clear it so that the only parameters are those // added by the overrides of getParameters. thermo->input().clear(); - thermo->getParameters(data); + data = thermo->parameters(); data.applyUnits(); speciesData.resize(thermo->nSpecies()); @@ -41,7 +41,7 @@ TEST_F(ThermoToYaml, simpleIdealGas) setup("ideal-gas.yaml", "simple"); thermo->setState_TP(1010, 2e5); double rho = thermo->density(); - thermo->getParameters(data); + data = thermo->parameters(); data.applyUnits(); ASSERT_EQ(data["thermo"], "ideal-gas"); From ece7c0dde32812ad1690604eef415dacd295ae68 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 12 Apr 2021 18:00:34 -0400 Subject: [PATCH 53/57] [Python/Input] Make function names more Pythonic --- interfaces/cython/cantera/base.pyx | 2 +- interfaces/cython/cantera/reaction.pyx | 2 +- interfaces/cython/cantera/speciesthermo.pyx | 2 +- interfaces/cython/cantera/thermo.pyx | 2 +- interfaces/cython/cantera/transport.pyx | 2 +- interfaces/cython/cantera/utils.pyx | 12 ++++++------ 6 files changed, 11 insertions(+), 11 deletions(-) diff --git a/interfaces/cython/cantera/base.pyx b/interfaces/cython/cantera/base.pyx index 1b4094ebef..f3a60801d6 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -230,7 +230,7 @@ cdef class _SolutionBase: definition. """ def __get__(self): - return anymapToPython(self.base.parameters(True)) + return anymap_to_dict(self.base.parameters(True)) def write_yaml(self, filename, phases=None, units=None, precision=None, skip_user_defined=None): diff --git a/interfaces/cython/cantera/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index 928cf9dba9..eb0135f1c6 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -327,7 +327,7 @@ cdef class Reaction: definition. """ def __get__(self): - return anymapToPython(self.reaction.parameters(True)) + return anymap_to_dict(self.reaction.parameters(True)) def __repr__(self): return '<{}: {}>'.format(self.__class__.__name__, self.equation) diff --git a/interfaces/cython/cantera/speciesthermo.pyx b/interfaces/cython/cantera/speciesthermo.pyx index a44024272d..b62a4dc99e 100644 --- a/interfaces/cython/cantera/speciesthermo.pyx +++ b/interfaces/cython/cantera/speciesthermo.pyx @@ -88,7 +88,7 @@ cdef class SpeciesThermo: property input_data: def __get__(self): - return anymapToPython(self.spthermo.parameters(True)) + return anymap_to_dict(self.spthermo.parameters(True)) def cp(self, T): """ diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 0abf68f899..680d239caa 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -265,7 +265,7 @@ cdef class Species: property input_data: def __get__(self): cdef CxxThermoPhase* phase = self._phase.thermo if self._phase else NULL - return anymapToPython(self.species.parameters(phase)) + return anymap_to_dict(self.species.parameters(phase)) def __repr__(self): return ''.format(self.name) diff --git a/interfaces/cython/cantera/transport.pyx b/interfaces/cython/cantera/transport.pyx index 1f7f16ed4e..105f83818b 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -58,7 +58,7 @@ cdef class GasTransportData: property input_data: def __get__(self): - return anymapToPython(self.data.parameters(True)) + return anymap_to_dict(self.data.parameters(True)) property geometry: """ diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 7978dba110..965b815a65 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -73,7 +73,7 @@ class CanteraError(RuntimeError): cdef public PyObject* pyCanteraError = CanteraError -cdef anyvalueToPython(string name, CxxAnyValue& v): +cdef anyvalue_to_python(string name, CxxAnyValue& v): cdef CxxAnyMap a cdef CxxAnyValue b if v.isScalar(): @@ -86,9 +86,9 @@ cdef anyvalueToPython(string name, CxxAnyValue& v): elif v.isType[cbool](): return v.asType[cbool]() elif v.isType[CxxAnyMap](): - return anymapToPython(v.asType[CxxAnyMap]()) + return anymap_to_dict(v.asType[CxxAnyMap]()) elif v.isType[vector[CxxAnyMap]](): - return [anymapToPython(a) for a in v.asType[vector[CxxAnyMap]]()] + return [anymap_to_dict(a) for a in v.asType[vector[CxxAnyMap]]()] elif v.isType[vector[double]](): return v.asType[vector[double]]() elif v.isType[vector[string]](): @@ -98,7 +98,7 @@ cdef anyvalueToPython(string name, CxxAnyValue& v): elif v.isType[vector[cbool]](): return v.asType[vector[cbool]]() elif v.isType[vector[CxxAnyValue]](): - return [anyvalueToPython(name, b) + return [anyvalue_to_python(name, b) for b in v.asType[vector[CxxAnyValue]]()] elif v.isType[vector[vector[double]]](): return v.asType[vector[vector[double]]]() @@ -115,7 +115,7 @@ cdef anyvalueToPython(string name, CxxAnyValue& v): pystr(name), v.type_str())) -cdef anymapToPython(CxxAnyMap& m): +cdef anymap_to_dict(CxxAnyMap& m): m.applyUnits() - return {pystr(item.first): anyvalueToPython(item.first, item.second) + return {pystr(item.first): anyvalue_to_python(item.first, item.second) for item in m.ordered()} From daf1f423fdbe8ad3f15d00ff5e9aff59a6f7cf5e Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 12 Apr 2021 18:17:11 -0400 Subject: [PATCH 54/57] [Input/Examples] Update Python examples to demonstrate YAML output --- .../cython/cantera/examples/kinetics/extract_submechanism.py | 4 +++- .../cython/cantera/examples/kinetics/mechanism_reduction.py | 5 ++++- 2 files changed, 7 insertions(+), 2 deletions(-) diff --git a/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py b/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py index 19a7c9b0d8..05c06e8152 100644 --- a/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py +++ b/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py @@ -7,7 +7,7 @@ mechanism and the submechanism, which demonstrates that the submechanism contains all of the important species and reactions. -Requires: cantera >= 2.5.0, matplotlib >= 2.0 +Requires: cantera >= 2.6.0, matplotlib >= 2.0 """ from timeit import default_timer @@ -57,6 +57,8 @@ gas2 = ct.Solution(thermo='ideal-gas', kinetics='gas', species=species, reactions=reactions) +# Save the resulting mechanism for later use +gas2.write_yaml("gri30-CO-H2-submech.yaml") def solve_flame(gas): gas.TPX = 373, 0.05*ct.one_atm, 'H2:0.4, CO:0.6, O2:1, N2:3.76' diff --git a/interfaces/cython/cantera/examples/kinetics/mechanism_reduction.py b/interfaces/cython/cantera/examples/kinetics/mechanism_reduction.py index e3ef4d0ec8..547f2af184 100644 --- a/interfaces/cython/cantera/examples/kinetics/mechanism_reduction.py +++ b/interfaces/cython/cantera/examples/kinetics/mechanism_reduction.py @@ -12,7 +12,7 @@ to see whether the reduced mechanisms with a certain number of species are able to adequately simulate the ignition delay problem. -Requires: cantera >= 2.5.0, matplotlib >= 2.0 +Requires: cantera >= 2.6.0, matplotlib >= 2.0 """ import cantera as ct @@ -65,6 +65,9 @@ gas2 = ct.Solution(thermo='IdealGas', kinetics='GasKinetics', species=species, reactions=reactions) + # save the reduced mechanism for later use + gas2.write_yaml("gri30-reduced-{}-reaction.yaml".format(N)) + # Re-run the ignition problem with the reduced mechanism gas2.TPX = initial_state r = ct.IdealGasConstPressureReactor(gas2) From 2e0061edd16c15133142a0b25480b55e05f97c80 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 12 Apr 2021 18:57:42 -0400 Subject: [PATCH 55/57] [Input/Test] Add tests of solution properties for round-tripped phases --- .../cython/cantera/test/test_composite.py | 31 +++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index 4fa8dfdcd0..ba7970d685 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -474,6 +474,9 @@ def test_input_data_debye_huckel(self): def test_yaml_simple(self): gas = ct.Solution('h2o2.yaml') + gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0' + gas.equilibrate('HP') + gas.TP = 1500, ct.one_atm gas.write_yaml('h2o2-generated.yaml') with open('h2o2-generated.yaml', 'r') as infile: generated = yaml.safe_load(infile) @@ -485,8 +488,19 @@ def test_yaml_simple(self): for i, reaction in enumerate(generated['reactions']): self.assertEqual(reaction['equation'], gas.reaction_equation(i)) + gas2 = ct.Solution("h2o2-generated.yaml") + self.assertArrayNear(gas.concentrations, gas2.concentrations) + self.assertArrayNear(gas.partial_molar_enthalpies, + gas2.partial_molar_enthalpies) + self.assertArrayNear(gas.forward_rate_constants, + gas2.forward_rate_constants) + self.assertArrayNear(gas.mix_diff_coeffs, gas2.mix_diff_coeffs) + def test_yaml_outunits(self): gas = ct.Solution('h2o2.yaml') + gas.TPX = 500, ct.one_atm, 'H2: 1.0, O2: 1.0' + gas.equilibrate('HP') + gas.TP = 1500, ct.one_atm units = {'length': 'cm', 'quantity': 'mol', 'energy': 'cal'} gas.write_yaml('h2o2-generated.yaml', units=units) with open('h2o2-generated.yaml') as infile: @@ -500,9 +514,19 @@ def test_yaml_outunits(self): self.assertNear(r1['rate-constant']['A'], r2['rate-constant']['A']) self.assertNear(r1['rate-constant']['Ea'], r2['rate-constant']['Ea']) + gas2 = ct.Solution("h2o2-generated.yaml") + self.assertArrayNear(gas.concentrations, gas2.concentrations) + self.assertArrayNear(gas.partial_molar_enthalpies, + gas2.partial_molar_enthalpies) + self.assertArrayNear(gas.forward_rate_constants, + gas2.forward_rate_constants) + self.assertArrayNear(gas.mix_diff_coeffs, gas2.mix_diff_coeffs) + def test_yaml_surface(self): gas = ct.Solution('ptcombust.yaml', 'gas') surf = ct.Interface('ptcombust.yaml', 'Pt_surf', [gas]) + gas.TPY = 900, ct.one_atm, np.ones(gas.n_species) + surf.coverages = np.ones(surf.n_species) surf.write_yaml('ptcombust-generated.yaml') with open('ptcombust-generated.yaml') as infile: @@ -513,6 +537,13 @@ def test_yaml_surface(self): self.assertEqual(len(generated['Pt_surf-reactions']), surf.n_reactions) self.assertEqual(len(generated['species']), surf.n_total_species) + gas2 = ct.Solution('ptcombust-generated.yaml', 'gas') + surf2 = ct.Solution('ptcombust-generated.yaml', 'Pt_surf', [gas2]) + self.assertArrayNear(surf.concentrations, surf2.concentrations) + self.assertArrayNear(surf.partial_molar_enthalpies, + surf2.partial_molar_enthalpies) + self.assertArrayNear(surf.forward_rate_constants, + surf2.forward_rate_constants) class TestSpeciesSerialization(utilities.CanteraTest): def test_species_simple(self): From 96808a9cb75061cc0234008b3b0ceca5329ff412 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 12 Apr 2021 22:50:34 -0400 Subject: [PATCH 56/57] [Input] Fix AnyMap handling of 2D string / boolean arrays --- src/base/AnyMap.cpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index 0e5c464a1c..fabbdb3d5d 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -458,9 +458,9 @@ struct convert { target = node.as>>(); } else if (subtypes == (Type::Integer | Type::Double) || subtypes == Type::Double) { target = node.as>>(); - } else if (types == Type::String) { + } else if (subtypes == Type::String) { target = node.as>>(); - } else if (types == Type::Bool) { + } else if (subtypes == Type::Bool) { target = node.as>>(); } else { target = node.as>(); From c8aea00ae9619eddb6399ff5bcca254255f27616 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Tue, 13 Apr 2021 18:47:29 -0400 Subject: [PATCH 57/57] [Input/Test] Improve test coverage of YAML serialization --- .../cython/cantera/test/test_composite.py | 25 +++++++++++ test/general/test_containers.cpp | 44 +++++++++++++++++++ test/general/test_units.cpp | 27 +++++++++++- test/kinetics/kineticsFromYaml.cpp | 35 +++++++++++++++ test/thermo/thermoToYaml.cpp | 6 +++ 5 files changed, 136 insertions(+), 1 deletion(-) diff --git a/interfaces/cython/cantera/test/test_composite.py b/interfaces/cython/cantera/test/test_composite.py index ba7970d685..489a5620a1 100644 --- a/interfaces/cython/cantera/test/test_composite.py +++ b/interfaces/cython/cantera/test/test_composite.py @@ -545,6 +545,31 @@ def test_yaml_surface(self): self.assertArrayNear(surf.forward_rate_constants, surf2.forward_rate_constants) + def test_yaml_eos(self): + ice = ct.Solution('water.yaml', 'ice') + ice.TP = 270, 2 * ct.one_atm + ice.write_yaml('ice-generated.yaml', units={'length': 'mm', 'mass': 'g'}) + + ice2 = ct.Solution('ice-generated.yaml') + self.assertNear(ice.density, ice2.density) + self.assertNear(ice.entropy_mole, ice2.entropy_mole) + + def test_yaml_inconsistent_species(self): + gas = ct.Solution('h2o2.yaml') + gas2 = ct.Solution('h2o2.yaml') + gas2.name = 'modified' + # modify the NASA coefficients for one species + h2 = gas2.species('H2') + nasa_coeffs = h2.thermo.coeffs + nasa_coeffs[1] += 0.1 + nasa_coeffs[8] += 0.1 + h2.thermo = ct.NasaPoly2(h2.thermo.min_temp, h2.thermo.max_temp, + h2.thermo.reference_pressure, nasa_coeffs) + gas2.modify_species(gas2.species_index('H2'), h2) + with self.assertRaisesRegex(ct.CanteraError, "different definitions"): + gas.write_yaml('h2o2-error.yaml', phases=gas2) + + class TestSpeciesSerialization(utilities.CanteraTest): def test_species_simple(self): gas = ct.Solution('h2o2.yaml') diff --git a/test/general/test_containers.cpp b/test/general/test_containers.cpp index 88c754597d..791daad12d 100644 --- a/test/general/test_containers.cpp +++ b/test/general/test_containers.cpp @@ -385,6 +385,50 @@ TEST(AnyMap, dumpYamlString) generated["species"].getMapWhere("name", "OH")["thermo"]["data"].asVector()); } +TEST(AnyMap, YamlFlowStyle) +{ + AnyMap original; + original["x"] = 3; + original["y"] = true; + original["z"] = AnyMap::fromYamlString("{zero: 1, half: 2}"); + original.setFlowStyle(); + std::string serialized = original.toYamlString(); + // The serialized version should contain two lines, and end with a newline. + EXPECT_EQ(std::count(serialized.begin(), serialized.end(), '\n'), 2); + AnyMap generated = AnyMap::fromYamlString(serialized); + for (const auto& item : original) { + EXPECT_TRUE(generated.hasKey(item.first)); + } +} + +TEST(AnyMap, nestedVectorsToYaml) +{ + std::vector words{"foo", "bar", "baz", "qux", "foobar"}; + std::vector> strings; + std::vector> booleans; + std::vector> integers; + for (size_t i = 0; i < 3; i++) { + strings.emplace_back(); + booleans.emplace_back(); + integers.emplace_back(); + for (size_t j = 0; j < 4; j++) { + strings.back().push_back(words[(i + 3 * j) % words.size()]); + booleans.back().push_back(i == j); + integers.back().push_back(6*i + j); + } + } + AnyMap original; + original["strings"] = strings; + original["booleans"] = booleans; + original["integers"] = integers; + std::string serialized = original.toYamlString(); + AnyMap generated = AnyMap::fromYamlString(serialized); + + EXPECT_EQ(generated["strings"].asVector>(), strings); + EXPECT_EQ(generated["booleans"].asVector>(), booleans); + EXPECT_EQ(generated["integers"].asVector>(), integers); +} + TEST(AnyMap, definedKeyOrdering) { AnyMap m = AnyMap::fromYamlString("{zero: 1, half: 2}"); diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp index cb3779cf51..4759dfca17 100644 --- a/test/general/test_units.cpp +++ b/test/general/test_units.cpp @@ -54,10 +54,11 @@ TEST(Units, with_defaults1) { } TEST(Units, with_defaults2) { - UnitSystem U({"dyn/cm^2"}); + UnitSystem U({"dyn/cm^2", "K"}); EXPECT_DOUBLE_EQ(U.convertTo(1.0, "Pa"), 0.1); EXPECT_DOUBLE_EQ(U.convertFrom(1.0, "Pa"), 10); EXPECT_DOUBLE_EQ(U.convertTo(1.0, "N/m^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convertTo(300.0, "K"), 300.0); } TEST(Units, with_defaults_map) { @@ -139,6 +140,12 @@ TEST(Units, activation_energies6) { EXPECT_DOUBLE_EQ(U.convertActivationEnergyTo(1, "eV"), 1.0); } +TEST(Units, activation_energies_bad) { + UnitSystem U; + EXPECT_THROW(U.convertActivationEnergyTo(1000, "kg"), CanteraError); + EXPECT_THROW(U.convertActivationEnergyFrom(1000, "K^2"), CanteraError); +} + TEST(Units, from_anymap) { AnyMap m = AnyMap::fromYamlString( "{p: 12 bar, v: 10, A: 1 cm^2, V: 1," @@ -180,6 +187,24 @@ TEST(Units, to_anymap) { EXPECT_DOUBLE_EQ(m["density"].asVector()[1], 20.0 * 1e-6); } +TEST(Units, anymap_quantities) { + AnyMap m; + std::vector values(2); + values[0].setQuantity(8, "kg/m^3"); + values[1].setQuantity(12, "mg/cl"); + m["a"] = values; + values.emplace_back("hello"); + m["b"] = values; + m.applyUnits(); + EXPECT_TRUE(m["a"].is()); + m.applyUnits(); + EXPECT_TRUE(m["a"].is()); + auto converted = m["a"].asVector(); + EXPECT_DOUBLE_EQ(converted[0], 8.0); + EXPECT_DOUBLE_EQ(converted[1], 1.2); + EXPECT_FALSE(m["b"].is()); +} + TEST(Units, to_anymap_nested) { UnitSystem U1{"g", "cm", "mol"}; UnitSystem U2{"mg", "km"}; diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index 7db21dde4f..c6a89bbf3a 100644 --- a/test/kinetics/kineticsFromYaml.cpp +++ b/test/kinetics/kineticsFromYaml.cpp @@ -6,6 +6,7 @@ #include "cantera/kinetics/KineticsFactory.h" #include "cantera/kinetics/ReactionFactory.h" #include "cantera/thermo/ThermoFactory.h" +#include "cantera/base/Array.h" using namespace Cantera; @@ -388,6 +389,17 @@ TEST_F(ReactionToYaml, TroeFalloff) compareReactions(); } +TEST_F(ReactionToYaml, SriFalloff) +{ + soln = newSolution("sri-falloff.xml"); + soln->thermo()->setState_TPY(1000, 2e5, "R1A: 0.1, R1B:0.2, H: 0.2, R2:0.5"); + duplicateReaction(0); + EXPECT_TRUE(std::dynamic_pointer_cast(duplicate)); + compareReactions(); + duplicateReaction(1); + compareReactions(); +} + TEST_F(ReactionToYaml, chemicallyActivated) { soln = newSolution("chemically-activated-reaction.xml"); @@ -449,3 +461,26 @@ TEST_F(ReactionToYaml, electrochemical) compareReactions(); compareReactions(); } + +TEST_F(ReactionToYaml, unconvertible1) +{ + ElementaryReaction R({{"H2", 1}, {"OH", 1}}, + {{"H2O", 1}, {"H", 1}}, + Arrhenius(1e5, -1.0, 12.5)); + AnyMap params = R.parameters(); + UnitSystem U{"g", "cm", "mol"}; + params.setUnits(U); + EXPECT_THROW(params.applyUnits(), CanteraError); +} + +TEST_F(ReactionToYaml, unconvertible2) +{ + Array2D coeffs(2, 2, 1.0); + ChebyshevReaction R({{"H2", 1}, {"OH", 1}}, + {{"H2O", 1}, {"H", 1}}, + ChebyshevRate(273, 3000, 1e2, 1e7, coeffs)); + UnitSystem U{"g", "cm", "mol"}; + AnyMap params = R.parameters(); + params.setUnits(U); + EXPECT_THROW(params.applyUnits(), CanteraError); +} diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp index fde4a64cd2..ebe348566b 100644 --- a/test/thermo/thermoToYaml.cpp +++ b/test/thermo/thermoToYaml.cpp @@ -411,6 +411,12 @@ TEST_F(ThermoYamlRoundTrip, IdealMolalSolution) compareThermo(308, 1.1e5, "H2O(l): 0.95, H2S(aq): 0.01, CO2(aq): 0.04"); } +TEST_F(ThermoYamlRoundTrip, IdealSolutionVpss) +{ + roundtrip("thermo-models.yaml", "IdealSolnGas-liquid"); + compareThermo(320, 1.5e5, "Li(l):1.0"); +} + TEST_F(ThermoYamlRoundTrip, IonsFromNeutral) { roundtrip("thermo-models.yaml", "ions-from-neutral-molecule",