From 00f6b88fa446dec93fb5dccc024e382074233a50 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Mon, 22 Jan 2018 18:49:01 -0500 Subject: [PATCH] [Input] Implement construction of AnyMap from YAML string/file --- include/cantera/base/AnyMap.h | 9 ++ src/base/AnyMap.cpp | 228 +++++++++++++++++++++++++++++++ test/general/test_containers.cpp | 31 +++++ 3 files changed, 268 insertions(+) diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index ffb67daa3..c31365889 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -175,6 +175,15 @@ class AnyMap public: AnyMap() {}; + //! Create an AnyMap from a YAML file. + /*! + * Searches the Cantera include path for the file. + */ + static AnyMap fromYamlFile(const std::string& name); + + //! Create an AnyMap from a string containing a YAML document + static AnyMap fromYamlString(const std::string& yaml); + AnyValue& operator[](const std::string& key); const AnyValue& at(const std::string& key) const; diff --git a/src/base/AnyMap.cpp b/src/base/AnyMap.cpp index eeaff2b6a..907c0f6df 100644 --- a/src/base/AnyMap.cpp +++ b/src/base/AnyMap.cpp @@ -4,6 +4,224 @@ // at http://www.cantera.org/license.txt for license and copyright information. #include "cantera/base/AnyMap.h" +#include "cantera/base/yaml.h" +#include "cantera/base/stringUtils.h" + +#include + +namespace ba = boost::algorithm; + +namespace { // helper functions + +bool isFloat(const std::string& val) +{ + // This function duplicates the logic of fpValueCheck, but doesn't throw + // exceptions if the string isn't a float + std::string str = ba::trim_copy(val); + if (str.empty()) { + return false; + } + int numDot = 0; + int numExp = 0; + int istart = 0; + char ch = str[0]; + if (ch == '+' || ch == '-') { + istart = 1; + if (str.size() == 1) { + return false; + } + } + for (size_t i = istart; i < str.size(); i++) { + ch = str[i]; + if (isdigit(ch)) { + } else if (ch == '.') { + numDot++; + if (numDot > 1) { + return false; + } + if (numExp > 0) { + return false; + } + } else if (ch == 'e' || ch == 'E') { + numExp++; + if (numExp > 1 || i == str.size() - 1) { + return false; + } + ch = str[i+1]; + if (ch == '+' || ch == '-') { + if (i + 1 == str.size() - 1) { + return false; + } + i++; + } + } else { + return false; + } + } + return true; +} + +bool isInt(const std::string& val) +{ + std::string str = ba::trim_copy(val); + if (str.empty()) { + return false; + } + int istart = 0; + char ch = str[0]; + if (ch == '+' || ch == '-') { + istart = 1; + if (str.size() == 1) { + return false; + } + } + for (size_t i = istart; i < str.size(); i++) { + if (!isdigit(str[i])) { + return false; + } + } + return true; +} + +bool isBool(const std::string& val) { + std::string str = ba::trim_copy(val); + return (val == "true" || val == "True" || val == "false" || val == "False"); +} + +enum class Type : char { + Unknown = 0, + Integer = 1, + Double = 2, + String = 4, + Bool = 8, + Map = 16, + Sequence = 32 +}; + +Type operator|(Type lhs, Type rhs) +{ + return Type(static_cast(lhs) | static_cast(rhs)); +} + +Type elementTypes(const YAML::Node& node) +{ + // See what kinds of elements we have: + Type types = Type::Unknown; + for (const auto& el : node) { + if (el.IsMap()) { + types = types | Type::Map; + } else if (el.IsSequence()) { + types = types | Type::Sequence; + } else if (el.IsScalar()) { + std::string nodestr = el.as(); + if (isInt(nodestr)) { + types = types | Type::Integer; + } else if (isFloat(nodestr)) { + types = types | Type::Double; + } else if (isBool(nodestr)) { + types = types | Type::Bool; + } else { + types = types | Type::String; + } + } + } + return types; +} + +} // end anonymous namespace + +namespace YAML { // YAML converters + +using namespace Cantera; + +template<> +struct convert { + static Node encode(const Cantera::AnyMap& rhs) { + throw NotImplementedError("AnyMap::encode"); + } + static bool decode(const Node& node, Cantera::AnyMap& target) { + if (!node.IsMap()) { + std::string text = YAML::Dump(node); + if (text.size() > 300) { + text.resize(300); + } + throw CanteraError("YAML::convert", + "YAML node is not a map. Node begins with:\n'''\n{}\n'''", text); + } + for (const auto& child : node) { + std::string key = child.first.as(); + if (child.second.IsMap()) { + target[key] = child.second.as(); + } else { + target[key] = child.second.as(); + target[key].setKey(key); + } + } + return true; + } +}; + +template<> +struct convert { + static Node encode(const Cantera::AnyValue& rhs) { + throw NotImplementedError("AnyValue::encode"); + } + + static bool decode(const Node& node, Cantera::AnyValue& target) { + if (node.IsScalar()) { + // Scalar nodes are int, doubles, or strings + std::string nodestr = node.as(); + if (isInt(nodestr)) { + target = intValue(nodestr); + } else if (isFloat(nodestr)) { + target = fpValue(nodestr); + } else if (isBool(nodestr)) { + target = node.as(); + } else { + target = nodestr; + } + return true; + } else if (node.IsSequence()) { + // Convert sequences of the same element type to vectors of that type + Type types = elementTypes(node); + if (types == Type::Integer) { + target = node.as>(); + } else if (types == (Type::Integer | Type::Double) || types == Type::Double) { + target = node.as(); + } else if (types == Type::String) { + target = node.as>(); + } else if (types == Type::Bool) { + target = node.as>(); + } else if (types == Type::Map) { + target = node.as>(); + } else if (types == Type::Sequence) { + // Create nested vectors when data types are compatible + Type subtypes = Type::Unknown; + for (const auto& el : node) { + subtypes = subtypes | elementTypes(el); + } + if (subtypes == Type::Integer) { + target = node.as>>(); + } else if (subtypes == (Type::Integer | Type::Double) || subtypes == Type::Double) { + target = node.as>>(); + } else if (types == Type::String) { + target = node.as>>(); + } else if (types == Type::Bool) { + target = node.as>>(); + } else { + target = node.as>(); + } + } else { + // If types are different, create a vector of generic values + target = node.as>(); + } + return true; + } + return false; + } +}; + +} namespace Cantera { @@ -192,4 +410,14 @@ bool AnyMap::hasKey(const std::string& key) const } } +AnyMap AnyMap::fromYamlString(const std::string& yaml) { + YAML::Node node = YAML::Load(yaml); + return node.as(); +} + +AnyMap AnyMap::fromYamlFile(const std::string& name) { + YAML::Node node = YAML::LoadFile(findInputFile(name)); + return node.as(); +} + } diff --git a/test/general/test_containers.cpp b/test/general/test_containers.cpp index f285dfcfb..934586bbb 100644 --- a/test/general/test_containers.cpp +++ b/test/general/test_containers.cpp @@ -88,3 +88,34 @@ TEST(AnyMap, vector) // Should have modified the copy in the map EXPECT_EQ(m["nested/item"].asVector().size(), (size_t) 4); } + +TEST(AnyMap, loadYaml) +{ + AnyMap m = AnyMap::fromYamlString( + "name: NO2\n" + "composition: {N: 1, O: 2}\n" + "thermo:\n" + " model: NASA7\n" + " reference-pressure: 1 atm\n" + " temperature-ranges: [200, 1000, 6000]\n" + " data:\n" + " - [3.944031200E+00, -1.585429000E-03, 1.665781200E-05, -2.047542600E-08,\n" + " 7.835056400E-12, 2.896617900E+03, 6.311991700E+00]\n" + " - [4.884754200E+00, 2.172395600E-03, -8.280690600E-07, 1.574751000E-10,\n" + " -1.051089500E-14, 2.316498300E+03, -1.174169500E-01]\n" + "transport: # this is a comment\n" + " model: gas\n" + " geometry: nonlinear\n" + " flag: true\n"); + + EXPECT_EQ(m["name"].asString(), "NO2"); + EXPECT_EQ(m["composition/N"].asInt(), 1); + EXPECT_EQ(m["thermo/reference-pressure"].asString(), "1 atm"); + EXPECT_EQ(m["thermo/temperature-ranges"].asVector()[0], 200); + EXPECT_EQ(m["transport/geometry"].asString(), "nonlinear"); + EXPECT_TRUE(m["transport/flag"].asBool()); + auto coeffs = m["thermo/data"].asVector(); + EXPECT_EQ(coeffs.size(), (size_t) 2); + EXPECT_EQ(coeffs[0].size(), (size_t) 7); + EXPECT_DOUBLE_EQ(coeffs[1][2], -8.280690600E-07); +}