diff --git a/include/cantera/base/AnyMap.h b/include/cantera/base/AnyMap.h index 8dcdde0d0d3..61cc22c947c 100644 --- a/include/cantera/base/AnyMap.h +++ b/include/cantera/base/AnyMap.h @@ -18,6 +18,13 @@ namespace boost class any; } +namespace YAML +{ +class Emitter; +Emitter& operator<<(Emitter& out, const Cantera::AnyMap& rhs); +Emitter& operator<<(Emitter& out, const Cantera::AnyValue& rhs); +} + namespace Cantera { @@ -38,10 +45,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 @@ -129,6 +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 @@ -217,8 +253,14 @@ class AnyValue : public AnyBase //! Returns `true` when getMapWhere() would succeed bool hasMapWhere(const std::string& key, const std::string& value) const; - //! @see AnyMap::applyUnits - void applyUnits(const UnitSystem& units); + //! Return values used to determine the sort order when outputting to YAML + std::pair order() const; + + //! @see AnyMap::applyUnits(const UnitSystem&) + void applyUnits(shared_ptr& units); + + //! @see AnyMap::setFlowStyle + void setFlowStyle(bool flow=true); private: std::string demangle(const std::type_info& type) const; @@ -254,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 @@ -355,7 +399,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. /*! @@ -369,10 +413,17 @@ 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; + //! 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; @@ -386,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; @@ -447,6 +502,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); @@ -476,6 +532,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(); @@ -485,7 +590,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`. @@ -496,28 +601,82 @@ 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); + + //! 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); + + //! 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; //! 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 //! 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/include/cantera/base/AnyMap.inl.h b/include/cantera/base/AnyMap.inl.h index 1346e17d5d9..6ccfa962d01 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&) { @@ -37,24 +47,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 @@ -62,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/base/Solution.h b/include/cantera/base/Solution.h index 155c2d674c3..cf4ae4b5247 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/base/Units.h b/include/cantera/base/Units.h index 4e212015828..2591d487547 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; } @@ -140,13 +142,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 +172,14 @@ 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; + double convertActivationEnergyTo(double value, const Units& 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 @@ -182,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; @@ -207,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/include/cantera/base/YamlWriter.h b/include/cantera/base/YamlWriter.h new file mode 100644 index 00000000000..0bb023b0ecb --- /dev/null +++ b/include/cantera/base/YamlWriter.h @@ -0,0 +1,78 @@ +//! @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" +#include "cantera/base/Units.h" + +namespace Cantera +{ + +class Solution; +class ThermoPhase; +class Kinetics; +class Transport; + +//! 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); + + //! 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={}); + + //! 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 + //! the right of the decimal point. The default is 15 digits. + void setPrecision(long int n) { + 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; + } + + //! 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; + + //! @see setPrecision() + long int m_float_precision; + + //! @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; +}; + +} + +#endif diff --git a/include/cantera/kinetics/Falloff.h b/include/cantera/kinetics/Falloff.h index 0adefb56c48..f31dc754c68 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/Kinetics.h b/include/cantera/kinetics/Kinetics.h index f448243f937..a355f8a687c 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() {} + //! 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. + AnyMap parameters(); + /** * Resize arrays with sizes that depend on the total number of species. * Automatically called before adding each Reaction and Phase. diff --git a/include/cantera/kinetics/Reaction.h b/include/cantera/kinetics/Reaction.h index f19b3cf69d7..9598c3e1ff9 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 { @@ -46,10 +48,23 @@ 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(); + //! 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; @@ -99,7 +114,17 @@ 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: + //! 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; }; @@ -139,6 +164,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 +206,8 @@ 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 //! rate. @@ -203,6 +231,8 @@ 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 Arrhenius low_rate; @@ -218,6 +248,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 @@ -235,6 +269,9 @@ class ChemicallyActivatedReaction : public FalloffReaction virtual std::string type() const { return "chemically-activated"; } + + virtual void calculateRateCoeffUnits(const Kinetics& kin); + virtual void getParameters(AnyMap& reactionNode) const; }; //! A pressure-dependent reaction parameterized by logarithmically interpolating @@ -251,6 +288,8 @@ class PlogReaction : public Reaction } virtual void validate(); + virtual void getParameters(AnyMap& reactionNode) const; + Plog rate; }; @@ -262,6 +301,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"; @@ -334,6 +374,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"; @@ -367,6 +408,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; @@ -407,15 +449,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, - int pressure_dependence=0); - // declarations of setup functions void setupElementaryReaction(ElementaryReaction&, const XML_Node&); //! @internal May be changed without notice in future versions diff --git a/include/cantera/kinetics/RxnRates.h b/include/cantera/kinetics/RxnRates.h index 7205eb62268..e2a90951e94 100644 --- a/include/cantera/kinetics/RxnRates.h +++ b/include/cantera/kinetics/RxnRates.h @@ -18,6 +18,8 @@ class Array2D; class AnyValue; class UnitSystem; class Units; +class AnyMap; +class Units; //! Arrhenius reaction rate type depends only on temperature /** @@ -45,6 +47,8 @@ class Arrhenius void setParameters(const AnyValue& rate, const UnitSystem& units, const Units& rate_units); + void getParameters(AnyMap& rateNode, const Units& rate_units) const; + //! Update concentration-dependent parts of the rate coefficient. /*! * For this class, there are no concentration-dependent parts, so this diff --git a/include/cantera/thermo/BinarySolutionTabulatedThermo.h b/include/cantera/thermo/BinarySolutionTabulatedThermo.h index d1f05b7eda2..1a242a9ee26 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/ConstCpPoly.h b/include/cantera/thermo/ConstCpPoly.h index 4ba4b353ba3..df24d4a949f 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/DebyeHuckel.h b/include/cantera/thermo/DebyeHuckel.h index d9a81c9673a..6617b6a0e0a 100644 --- a/include/cantera/thermo/DebyeHuckel.h +++ b/include/cantera/thermo/DebyeHuckel.h @@ -778,6 +778,9 @@ 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 @@ -966,6 +969,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 4b9a509af3b..a1c57ea9a52 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 03c3457b4e7..7900f6074fe 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 2a991402df6..d40aa902fc5 100644 --- a/include/cantera/thermo/IdealSolidSolnPhase.h +++ b/include/cantera/thermo/IdealSolidSolnPhase.h @@ -558,6 +558,9 @@ 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/include/cantera/thermo/IdealSolnGasVPSS.h b/include/cantera/thermo/IdealSolnGasVPSS.h index 3deae06b0c5..840fbbb3821 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 1526b110b26..df8750ca09b 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 725d7726166..29c6aa9b319 100644 --- a/include/cantera/thermo/LatticePhase.h +++ b/include/cantera/thermo/LatticePhase.h @@ -618,6 +618,9 @@ class LatticePhase : public ThermoPhase void setSiteDensity(double sitedens); 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 9cae772b812..c91846480cf 100644 --- a/include/cantera/thermo/LatticeSolidPhase.h +++ b/include/cantera/thermo/LatticeSolidPhase.h @@ -434,6 +434,9 @@ 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 getSpeciesParameters(const std::string& name, + AnyMap& speciesNode) const; virtual void setParametersFromXML(const XML_Node& eosdata); diff --git a/include/cantera/thermo/MargulesVPSSTP.h b/include/cantera/thermo/MargulesVPSSTP.h index def54ea821c..353bb72589b 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 18eda59eb93..cdfc0bd8175 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 b59ea911897..f1b09593b0a 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"].setQuantity(density(), "kg/m^3"); + } + virtual void setParametersFromXML(const XML_Node& eosdata) { eosdata._require("model","Metal"); doublereal rho = getFloat(eosdata, "density", "density"); diff --git a/include/cantera/thermo/Mu0Poly.h b/include/cantera/thermo/Mu0Poly.h index 42fbf651943..057a75c79c0 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 99c0d9cf94b..4012b3e6d8b 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 2bd3f36c466..e8afee49563 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 75ed2000473..c0d892cf085 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 d8dbf5bf897..25eb6c64f98 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/PDSS.h b/include/cantera/thermo/PDSS.h index 52fdf3c1b2a..d2087625003 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_ConstVol.h b/include/cantera/thermo/PDSS_ConstVol.h index ab322983f0a..9c48bfd068c 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_HKFT.h b/include/cantera/thermo/PDSS_HKFT.h index 42a81a90afe..9959d30c81a 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/include/cantera/thermo/PDSS_IdealGas.h b/include/cantera/thermo/PDSS_IdealGas.h index 46de3bdbf63..3e44d366d6c 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_IonsFromNeutral.h b/include/cantera/thermo/PDSS_IonsFromNeutral.h index 34ae662082d..fe242003c05 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/PDSS_SSVol.h b/include/cantera/thermo/PDSS_SSVol.h index d38bb56adb6..a044c441aac 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/include/cantera/thermo/PDSS_Water.h b/include/cantera/thermo/PDSS_Water.h index 60b1e816532..f6a8c2482fa 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/include/cantera/thermo/PureFluidPhase.h b/include/cantera/thermo/PureFluidPhase.h index a350ca544e1..7071666b6f3 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 c0a49fe2894..6e8d4a4aecd 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/RedlichKwongMFTP.h b/include/cantera/thermo/RedlichKwongMFTP.h index d6317e33d9d..5c59459e773 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,13 @@ 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; + int NSolns_; doublereal Vroot_[3]; diff --git a/include/cantera/thermo/ShomatePoly.h b/include/cantera/thermo/ShomatePoly.h index c904118fc72..cf4c2d2b2d6 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"].setQuantity(Tranges, "K"); + 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/Species.h b/include/cantera/thermo/Species.h index a113ba51305..201c52f1076 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,6 +36,8 @@ class Species Species& operator=(const Species& other) = delete; ~Species(); + 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 12303701248..1b4a1ea8dfb 100644 --- a/include/cantera/thermo/SpeciesThermoInterpType.h +++ b/include/cantera/thermo/SpeciesThermoInterpType.h @@ -14,6 +14,7 @@ #include "cantera/base/ct_defs.h" #include "cantera/base/ctexceptions.h" +#include "cantera/base/AnyMap.h" namespace Cantera { @@ -226,6 +227,15 @@ class SpeciesThermoInterpType doublereal& refPressure, doublereal* const coeffs) 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) /*! @@ -262,13 +272,24 @@ class SpeciesThermoInterpType throw NotImplementedError("SpeciesThermoInterpType::resetHf298"); } + //! Access input data associated with the species thermo definition + const AnyMap& input() const; + 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 doublereal m_highT; //! Reference state pressure doublereal m_Pref; + + AnyMap m_input; }; } diff --git a/include/cantera/thermo/StoichSubstance.h b/include/cantera/thermo/StoichSubstance.h index 06f4ad71450..f0b53ff9121 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/include/cantera/thermo/SurfPhase.h b/include/cantera/thermo/SurfPhase.h index 185b3e3a868..a514e51604c 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/include/cantera/thermo/ThermoPhase.h b/include/cantera/thermo/ThermoPhase.h index 639c5ed9601..1f79d496095 100644 --- a/include/cantera/thermo/ThermoPhase.h +++ b/include/cantera/thermo/ThermoPhase.h @@ -1709,6 +1709,22 @@ class ThermoPhase : public Phase virtual void setParameters(const AnyMap& phaseNode, const AnyMap& rootNode=AnyMap()); + //! 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. + /*! + * @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(); @@ -1858,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/thermo/VPStandardStateTP.h b/include/cantera/thermo/VPStandardStateTP.h index cec8e4882ac..1038ca14d44 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/include/cantera/transport/TransportBase.h b/include/cantera/transport/TransportBase.h index 86dfc9e356b..5733fe6e5b5 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); + //! 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. + AnyMap parameters() const; + //! Sets the velocity basis /*! * What the transport object does with this parameter is up to the diff --git a/include/cantera/transport/TransportData.h b/include/cantera/transport/TransportData.h index a005571dfa5..4cf099089c3 100644 --- a/include/cantera/transport/TransportData.h +++ b/include/cantera/transport/TransportData.h @@ -24,8 +24,21 @@ class TransportData virtual void validate(const Species& species) {} + //! 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 @@ -57,6 +70,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/interfaces/cython/cantera/_cantera.pxd b/interfaces/cython/cantera/_cantera.pxd index 4b267fa4bc3..cb9dce619fd 100644 --- a/interfaces/cython/cantera/_cantera.pxd +++ b/interfaces/cython/cantera/_cantera.pxd @@ -53,14 +53,37 @@ 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&) + + 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() + void applyUnits() 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 + string type_str() + cbool isType "is" [T]() + cbool isScalar() CxxAnyMap AnyMapFromYamlFile "Cantera::AnyMap::fromYamlFile" (string) except +translate_exception CxxAnyMap AnyMapFromYamlString "Cantera::AnyMap::fromYamlString" (string) except +translate_exception @@ -102,6 +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 + CxxAnyMap parameters(cbool) except +translate_exception cdef extern from "cantera/thermo/SpeciesThermoFactory.h": cdef CxxSpeciesThermo* CxxNewSpeciesThermo "Cantera::newSpeciesThermoInterpType"\ @@ -120,6 +144,7 @@ cdef extern from "cantera/thermo/Species.h" namespace "Cantera": Composition composition double charge double size + CxxAnyMap parameters(CxxThermoPhase*) except +translate_exception cdef shared_ptr[CxxSpecies] CxxNewSpecies "newSpecies" (XML_Node&) cdef vector[shared_ptr[CxxSpecies]] CxxGetSpecies "getSpecies" (XML_Node&) @@ -135,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" () @@ -151,6 +177,7 @@ cdef extern from "cantera/thermo/ThermoPhase.h" namespace "Cantera": # miscellaneous string type() string phaseOfMatter() except +translate_exception + void getSpeciesParameters(string, CxxAnyMap&) except +translate_exception string report(cbool, double) except +translate_exception cbool hasPhaseTransition() cbool isPure() @@ -339,6 +366,8 @@ cdef extern from "cantera/kinetics/Reaction.h" namespace "Cantera": string equation() string type() void validate() except +translate_exception + CxxAnyMap parameters(cbool) except +translate_exception + int reaction_type Composition reactants Composition products Composition orders @@ -502,6 +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 parameters(cbool) except +translate_exception cdef cppclass CxxGasTransportData "Cantera::GasTransportData" (CxxTransportData): CxxGasTransportData() @@ -518,6 +548,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": @@ -993,12 +1032,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 @@ -1023,6 +1056,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) @@ -1077,6 +1117,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 98ec9fd7e83..40548fb28e6 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 c92b008a0c4..f3a60801d63 100644 --- a/interfaces/cython/cantera/base.pyx +++ b/interfaces/cython/cantera/base.pyx @@ -223,6 +223,54 @@ cdef class _SolutionBase: for reaction in reactions: 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): + return anymap_to_dict(self.base.parameters(True)) + + 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/examples/kinetics/extract_submechanism.py b/interfaces/cython/cantera/examples/kinetics/extract_submechanism.py index 19a7c9b0d81..05c06e81520 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 e3ef4d0ec8f..547f2af1841 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) diff --git a/interfaces/cython/cantera/kinetics.pyx b/interfaces/cython/cantera/kinetics.pyx index 866b9e5ce5e..26373be886d 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/reaction.pyx b/interfaces/cython/cantera/reaction.pyx index ee2370c784d..eb0135f1c68 100644 --- a/interfaces/cython/cantera/reaction.pyx +++ b/interfaces/cython/cantera/reaction.pyx @@ -320,6 +320,15 @@ 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): + 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 dee15c72f24..b62a4dc99ea 100644 --- a/interfaces/cython/cantera/speciesthermo.pyx +++ b/interfaces/cython/cantera/speciesthermo.pyx @@ -80,12 +80,16 @@ 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): + return anymap_to_dict(self.spthermo.parameters(True)) + 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 06e1742d73f..489a5620a1d 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 @@ -422,3 +423,170 @@ 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]) + + 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 + 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) + + 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) + 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)) + + 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: + 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']) + + 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: + 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) + + 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) + + 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') + 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/test/test_kinetics.py b/interfaces/cython/cantera/test/test_kinetics.py index 7a21bce410b..b3c6b7ecf1b 100644 --- a/interfaces/cython/cantera/test/test_kinetics.py +++ b/interfaces/cython/cantera/test/test_kinetics.py @@ -842,6 +842,25 @@ 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.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) + 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) diff --git a/interfaces/cython/cantera/thermo.pyx b/interfaces/cython/cantera/thermo.pyx index 0daad5d049c..680d239caa8 100644 --- a/interfaces/cython/cantera/thermo.pyx +++ b/interfaces/cython/cantera/thermo.pyx @@ -262,6 +262,11 @@ cdef class Species: def __set__(self, GasTransportData tran): self.species.transport = tran._data + property input_data: + def __get__(self): + cdef CxxThermoPhase* phase = self._phase.thermo if self._phase else NULL + return anymap_to_dict(self.species.parameters(phase)) + def __repr__(self): return ''.format(self.name) @@ -548,6 +553,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 +578,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 b9b0bb24e81..105f83818b3 100644 --- a/interfaces/cython/cantera/transport.pyx +++ b/interfaces/cython/cantera/transport.pyx @@ -56,6 +56,10 @@ cdef class GasTransportData: dipole, polarizability, rotational_relaxation, acentric_factor, dispersion_coefficient, quadrupole_polarizability) + property input_data: + def __get__(self): + return anymap_to_dict(self.data.parameters(True)) + property geometry: """ Get/Set the string specifying the molecular geometry. One of `atom`, diff --git a/interfaces/cython/cantera/utils.pyx b/interfaces/cython/cantera/utils.pyx index 963112d0865..965b815a65f 100644 --- a/interfaces/cython/cantera/utils.pyx +++ b/interfaces/cython/cantera/utils.pyx @@ -72,3 +72,50 @@ class CanteraError(RuntimeError): pass cdef public PyObject* pyCanteraError = CanteraError + +cdef anyvalue_to_python(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 anymap_to_dict(v.asType[CxxAnyMap]()) + elif v.isType[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]](): + 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 [anyvalue_to_python(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 of held type '{}'".format( + pystr(name), v.type_str())) + + +cdef anymap_to_dict(CxxAnyMap& m): + m.applyUnits() + return {pystr(item.first): anyvalue_to_python(item.first, item.second) + for item in m.ordered()} diff --git a/interfaces/cython/cantera/yamlwriter.pyx b/interfaces/cython/cantera/yamlwriter.pyx new file mode 100644 index 00000000000..8d2fda604b5 --- /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/AnyMap.cpp b/src/base/AnyMap.cpp index 241528c302e..fabbdb3d5d8 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 @@ -18,10 +19,14 @@ #include namespace ba = boost::algorithm; +using std::vector; +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) { @@ -138,6 +143,67 @@ Type elementTypes(const YAML::Node& node) return types; } +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 (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 + 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; + } +} + +struct Quantity +{ + AnyValue value; + Units units; + bool isActivationEnergy; + AnyValue::unitConverter converter; + + bool operator==(const Quantity& other) const { + return value == other.value && units == other.units + && isActivationEnergy == other.isActivationEnergy; + } +}; + Cantera::AnyValue Empty; } // end anonymous namespace @@ -151,6 +217,7 @@ struct convert { static Node encode(const Cantera::AnyMap& rhs) { throw NotImplementedError("AnyMap::encode"); } + static bool decode(const Node& node, Cantera::AnyMap& target) { target.setLoc(node.Mark().line, node.Mark().column); if (node.IsSequence()) { @@ -168,22 +235,177 @@ 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; } }; +YAML::Emitter& operator<<(YAML::Emitter& out, const AnyMap& rhs) +{ + bool flow = rhs.getBool("__flow__", false); + if (flow) { + out << YAML::Flow; + out << YAML::BeginMap; + size_t width = 15; + for (const auto& item : rhs.ordered()) { + const auto& name = item.first; + const auto& value = item.second; + string valueStr; + bool foundType = true; + 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 + name.size() + valueStr.size() + 4 > 79) { + out << YAML::Newline; + width = 15; + } + 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 << name; + out << value; + width = 99; // Force newline after this item as well + } + } + } else { + out << YAML::BeginMap; + for (const auto& item : rhs.ordered()) { + out << item.first; + out << item.second; + } + } + out << YAML::EndMap; + return out; +} + +void emitFlowVector(YAML::Emitter& out, const vector& v, long int 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("operator<<(YAML::Emitter&, AnyValue&)", + "Don't know how to encode value of type '{}' with key '{}'", + rhs.type_str(), rhs.m_key); + } + } 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 '{}' with key '{}'", + rhs.type_str(), rhs.m_key); + } + return out; +} + + template<> struct convert { static Node encode(const Cantera::AnyValue& rhs) { - throw NotImplementedError("AnyValue::encode"); + throw NotImplementedError(""); } static bool decode(const Node& node, Cantera::AnyValue& target) { @@ -236,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>(); @@ -261,8 +483,7 @@ struct convert { } -namespace Cantera -{ +namespace Cantera { std::map AnyValue::s_typenames = { {typeid(double).name(), "double"}, @@ -274,11 +495,14 @@ 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() : m_line(-1) - , m_column(-1) + , m_column(0) {} void AnyBase::setLoc(int line, int column) @@ -326,7 +550,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; @@ -450,6 +674,50 @@ 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(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; + *m_value = Quantity{v, Units(units), false}; + 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; +} + +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" AnyValue::AnyValue(double value) @@ -669,7 +937,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); } } @@ -719,7 +988,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); } } @@ -746,26 +1016,62 @@ bool AnyValue::hasMapWhere(const std::string& key, const std::string& value) con } } -void AnyValue::applyUnits(const UnitSystem& units) +std::pair AnyValue::order() const +{ + return {m_line, m_column}; +} + +void AnyValue::applyUnits(shared_ptr& units) { if (is()) { + AnyMap& m = as(); + + 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 not associated with a Kinetics object)"); + } + } // 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) { // 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()); @@ -780,8 +1086,38 @@ void AnyValue::applyUnits(const UnitSystem& units) item.applyUnits(units); } } + } else if (is>()) { + for (auto& v : as>()) { + v.applyUnits(units); + } + } else if (is()) { + auto& Q = as(); + 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 { + *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); + } else { + throw CanteraError("AnyValue::applyUnits", "Don't know how to " + "convert Quantity with held type '{}' in key '{}'", + Q.value.type_str(), m_key); + } } +} +void AnyValue::setFlowStyle(bool flow) +{ + as().setFlowStyle(); } std::string AnyValue::demangle(const std::type_info& type) const @@ -940,21 +1276,30 @@ std::vector& AnyValue::asVector(size_t nMin, size_t nMax) // Methods of class AnyMap +AnyMap::AnyMap() + : m_units(new UnitSystem()) +{ +} + 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; 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); } + + // 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 { // Return an already-existing item @@ -972,6 +1317,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 { @@ -997,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; @@ -1103,6 +1471,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 @@ -1126,18 +1578,60 @@ 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_units.setDefaults(at("units").asMap()); + m_data["__units__"] = std::move(m_data["units"]); m_data.erase("units"); } + if (hasKey("__units__")) { + 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); } } +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; +} + +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 { @@ -1150,7 +1644,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; } @@ -1192,7 +1686,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; @@ -1213,6 +1707,15 @@ AnyMap AnyMap::fromYamlFile(const std::string& name, return cache_item.first; } +std::string AnyMap::toYamlString() const +{ + YAML::Emitter out; + const_cast(this)->applyUnits(); + out << *this; + out << YAML::Newline; + return out.c_str(); +} + AnyMap::Iterator begin(const AnyValue& v) { return v.as().begin(); } diff --git a/src/base/Solution.cpp b/src/base/Solution.cpp index 7366fccadf1..989034b3b3b 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/Units.cpp b/src/base/Units.cpp index dfb97f7fdfe..ef21472c11e 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; @@ -225,6 +226,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) @@ -244,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 @@ -274,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 { @@ -306,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"))) { @@ -335,12 +362,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 +378,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 +422,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,19 +477,40 @@ 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"))) { - 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::convertActivationEnergy", - "'{}' is not a unit of activation energy", dest); + throw CanteraError("UnitSystem::convertActivationEnergyTo", + "'{}' is not a unit of activation energy", dest.str()); + } +} + +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); } } @@ -456,11 +520,44 @@ double UnitSystem::convertActivationEnergy(const AnyValue& v, 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); } } +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/base/YamlWriter.cpp b/src/base/YamlWriter.cpp new file mode 100644 index 00000000000..ad4ddf6ba65 --- /dev/null +++ b/src/base/YamlWriter.cpp @@ -0,0 +1,163 @@ +// 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 "cantera/kinetics/Kinetics.h" +#include "cantera/kinetics/Reaction.h" +#include "cantera/transport/TransportBase.h" + +#include +#include + +namespace Cantera { + +YamlWriter::YamlWriter() + : m_float_precision(15) + , m_skip_user_defined(false) +{ +} + +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); +} + +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; + 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++) { + phaseDefs[i] = m_phases[i]->parameters(!m_skip_user_defined); + 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) { + const auto thermo = phase->thermo(); + for (const auto& name : thermo->speciesNames()) { + const auto& species = thermo->species(name); + AnyMap speciesDef = species->parameters(thermo.get(), !m_skip_user_defined); + + 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; + + // 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++) { + reactions.push_back(kin->reaction(i)->parameters(!m_skip_user_defined)); + } + 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}; + } + } + } + + output.setMetadata("precision", AnyValue(m_float_precision)); + output.setUnits(m_output_units); + return output.toYamlString(); +} + +void YamlWriter::toYamlFile(const std::string& filename) const +{ + std::ofstream out(filename); + out << toYamlString(); +} + +void YamlWriter::setUnits(const std::map& units) +{ + m_output_units = UnitSystem(); + m_output_units.setDefaults(units); +} + +} diff --git a/src/kinetics/Falloff.cpp b/src/kinetics/Falloff.cpp index 3958e59acff..c94496f5081 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,19 @@ void Troe::getParameters(double* params) const { params[3] = m_t2; } +void Troe::getParameters(AnyMap& reactionNode) const +{ + AnyMap params; + params["A"] = m_a; + params["T3"].setQuantity(1.0 / m_rt3, "K"); + params["T1"].setQuantity(1.0 / m_rt1, "K"); + if (std::abs(m_t2) > SmallNumber) { + params["T2"].setQuantity(m_t2, "K"); + } + params.setFlowStyle(); + reactionNode["Troe"] = std::move(params); +} + void SRI::init(const vector_fp& c) { if (c.size() != 3 && c.size() != 5) { @@ -137,4 +151,18 @@ void SRI::getParameters(double* params) const params[4] = m_e; } +void SRI::getParameters(AnyMap& reactionNode) const +{ + AnyMap params; + params["A"] = m_a; + 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; + } + params.setFlowStyle(); + reactionNode["SRI"] = std::move(params); +} + } diff --git a/src/kinetics/Kinetics.cpp b/src/kinetics/Kinetics.cpp index 9f883583a51..02874970c04 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,19 @@ void Kinetics::addPhase(ThermoPhase& thermo) resizeSpecies(); } +AnyMap Kinetics::parameters() +{ + AnyMap out; + string name = KineticsFactory::factory()->canonicalize(kineticsType()); + if (name != "none") { + out["kinetics"] = name; + if (nReactions() == 0) { + out["reactions"] = "none"; + } + } + return out; +} + void Kinetics::resizeSpecies() { m_kk = 0; @@ -558,6 +572,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/KineticsFactory.cpp b/src/kinetics/KineticsFactory.cpp index e45b887fff2..6c96d6f3563 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/src/kinetics/Reaction.cpp b/src/kinetics/Reaction.cpp index 23b5047ba70..10e699b5a6a 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()", @@ -98,6 +102,46 @@ 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(); + + 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; + } +} + std::string Reaction::reactantString() const { std::ostringstream result; @@ -137,6 +181,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_) @@ -165,6 +239,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, rate_units); + reactionNode["rate-constant"] = std::move(rateNode); +} + ThirdBody::ThirdBody(double default_eff) : default_efficiency(default_eff) { @@ -198,10 +283,29 @@ 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); + 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; + } +} + FalloffReaction::FalloffReaction() : Reaction() , falloff(new Falloff()) , allow_negative_pre_exponential_factor(false) + , low_rate_units(0.0) { reaction_type = FALLOFF_RXN; } @@ -215,6 +319,7 @@ FalloffReaction::FalloffReaction( , high_rate(high_rate_) , third_body(tbody) , falloff(new Falloff()) + , low_rate_units(0.0) { reaction_type = FALLOFF_RXN; } @@ -254,6 +359,33 @@ 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); + reactionNode["type"] = "falloff"; + AnyMap lowRateNode; + low_rate.getParameters(lowRateNode, low_rate_units); + reactionNode["low-P-rate-constant"] = std::move(lowRateNode); + AnyMap highRateNode; + high_rate.getParameters(highRateNode, rate_units); + reactionNode["high-P-rate-constant"] = std::move(highRateNode); + 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; + } +} + ChemicallyActivatedReaction::ChemicallyActivatedReaction() { reaction_type = CHEMACT_RXN; @@ -268,6 +400,20 @@ 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); + reactionNode["type"] = "chemically-activated"; +} + PlogReaction::PlogReaction() : Reaction() { @@ -282,6 +428,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; + rateNode["P"].setQuantity(r.first, "Pa"); + r.second.getParameters(rateNode, rate_units); + rateList.push_back(std::move(rateNode)); + } + reactionNode["rate-constants"] = std::move(rateList); +} + ChebyshevReaction::ChebyshevReaction() : Reaction() { @@ -317,6 +477,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; } @@ -329,7 +490,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))); } @@ -344,14 +504,43 @@ 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); } +void ChebyshevReaction::getParameters(AnyMap& reactionNode) const +{ + Reaction::getParameters(reactionNode); + reactionNode["type"] = "Chebyshev"; + 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(); + 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]; + } + } + // 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) { + if (rate_units2.factor() != 0.0) { + coeffs.asVector()[0][0] += std::log10(units.convertFrom(1.0, rate_units2)); + } else if (units.getDelta(UnitSystem()).size()) { + throw CanteraError("ChebyshevReaction::getParameters lambda", + "Cannot convert rate constant with unknown dimensions to a " + "non-default unit system"); + } + }; + AnyValue coeffs; + coeffs = std::move(coeffs2d); + reactionNode["data"].setQuantity(coeffs, converter); +} + InterfaceReaction::InterfaceReaction() : is_sticking_coefficient(false) , use_motz_wise_correction(false) @@ -370,6 +559,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"].setQuantity(d.second.E, "K", true); + deps[d.first] = std::move(dep); + } + reactionNode["coverage-dependencies"] = std::move(deps); + } +} + ElectrochemicalReaction::ElectrochemicalReaction() : beta(0.5) , exchange_current_density_formulation(false) @@ -385,6 +600,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"), @@ -392,52 +618,28 @@ Arrhenius readArrhenius(const XML_Node& arrhenius_node) getFloat(arrhenius_node, "E", "actEnergy") / GasConstant); } -Units rateCoeffUnits(const Reaction& R, const Kinetics& kin, - int pressure_dependence) -{ - 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 - 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 (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; -} - 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 @@ -636,6 +838,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.calculateRateCoeffUnits(kin); R.input = node; } @@ -758,17 +961,10 @@ void setupFalloffReaction(FalloffReaction& R, const AnyMap& node, R.third_body.efficiencies[third_body.substr(2, third_body.size() - 3)] = 1.0; } - 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()); - } 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.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); } @@ -874,8 +1070,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, R.rate_units)); R.rate = ChebyshevRate(units.convert(T_range[0], "K"), units.convert(T_range[1], "K"), units.convert(P_range[0], "Pa"), @@ -923,6 +1118,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 0b722881587..d1fe62efaf8 100644 --- a/src/kinetics/RxnRates.cpp +++ b/src/kinetics/RxnRates.cpp @@ -51,6 +51,23 @@ void Arrhenius::setParameters(const AnyValue& rate, } } +void Arrhenius::getParameters(AnyMap& rateNode, const Units& rate_units) const +{ + if (rate_units.factor() != 0.0) { + rateNode["A"].setQuantity(preExponentialFactor(), rate_units); + } else { + 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; + } + + rateNode["b"] = temperatureExponent(); + rateNode["Ea"].setQuantity(activationEnergy_R(), "K", true); + rateNode.setFlowStyle(); +} + SurfaceArrhenius::SurfaceArrhenius() : m_b(0.0) , m_E(0.0) diff --git a/src/thermo/BinarySolutionTabulatedThermo.cpp b/src/thermo/BinarySolutionTabulatedThermo.cpp index 1312a1dfdde..25c6b274668 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"].setQuantity(m_enthalpy_tab, "J/kmol"); + tabThermo["entropy"].setQuantity(m_entropy_tab, "J/kmol/K"); + 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/ConstCpPoly.cpp b/src/thermo/ConstCpPoly.cpp index d398de59cb7..64edbe522c6 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 +{ + thermo["model"] = "constant-cp"; + SpeciesThermoInterpType::getParameters(thermo); + 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 { double temp = 298.15; diff --git a/src/thermo/DebyeHuckel.cpp b/src/thermo/DebyeHuckel.cpp index b3a610ac0e7..6437bbd133e 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,129 @@ 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"].setQuantity(m_A_Debye, "kg^0.5/gmol^0.5"); + } + + if (m_B_Debye != B_Debye_default) { + 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; + } + if (m_useHelgesonFixedForm) { + activityNode["use-Helgeson-fixed-form"] = true; + } + if (!isnan(m_Aionic_default)) { + activityNode["default-ionic-radius"].setQuantity(m_Aionic_default, "m"); + } + 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); +} + +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"].setQuantity(m_Aionic[k], "m"); + } + + 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/src/thermo/HMWSoln.cpp b/src/thermo/HMWSoln.cpp index a3bb53b57ef..0afd547300c 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"].setQuantity(m_A_Debye, "kg^0.5/gmol^0.5"); + } + 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 d6016322894..e1e50482f4f 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 79ccb7fc68b..e2d6aad4978 100644 --- a/src/thermo/IdealSolidSolnPhase.cpp +++ b/src/thermo/IdealSolidSolnPhase.cpp @@ -418,6 +418,43 @@ 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::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"].setQuantity( + molecularWeight(k) / m_speciesMolarVolume[k], "kg/m^3"); + } else if (eosIn.hasKey("molar-density")) { + eosNode["molar-density"].setQuantity(1.0 / m_speciesMolarVolume[k], + "kmol/m^3"); + } else { + eosNode["molar-volume"].setQuantity(m_speciesMolarVolume[k], + "m^3/kmol"); + } + } else { + eosNode["molar-volume"].setQuantity(m_speciesMolarVolume[k], + "m^3/kmol"); + } +} + 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 f73f21e5e63..293e9f56365 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 93220c2370c..05740d00daf 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 ee81249ecad..1f03fc3c114 100644 --- a/src/thermo/LatticePhase.cpp +++ b/src/thermo/LatticePhase.cpp @@ -297,6 +297,49 @@ void LatticePhase::initThermo() } } +void LatticePhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + phaseNode["site-density"].setQuantity(m_site_density, "kmol/m^3"); +} + +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"].setQuantity( + molecularWeight(k) / m_speciesMolarVolume[k], "kg/m^3"); + } else if (eosIn.hasKey("molar-density")) { + eosOut["model"] = "constant-volume"; + 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"].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"].setQuantity(m_speciesMolarVolume[k], "m^3/kmol"); + } + // 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 9166e822baa..20d9c0170d8 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,39 @@ 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; +} + +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/src/thermo/MargulesVPSSTP.cpp b/src/thermo/MargulesVPSSTP.cpp index b86ed83d08c..adbbc35d974 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); @@ -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"].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"].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"].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"].setQuantity( + {m_VSE_b_ij[n], m_VSE_c_ij[n]}, "m^3/kmol/K"); + } + 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 32403feddc4..b754205ca08 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"].setQuantity(h_mixing, "J/kmol"); + phaseNode["product-species"] = speciesName(product_species_index); +} void MaskellSolidSolnPhase::initThermoXML(XML_Node& phaseNode, const std::string& id_) { diff --git a/src/thermo/Mu0Poly.cpp b/src/thermo/Mu0Poly.cpp index 6f3b97bacbb..abac5a62a94 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,27 @@ void Mu0Poly::reportParameters(size_t& n, int& type, } } +void Mu0Poly::getParameters(AnyMap& thermo) const +{ + SpeciesThermoInterpType::getParameters(thermo); + thermo["model"] = "piecewise-Gibbs"; + thermo["h0"].setQuantity(m_H298 * GasConstant, "J/kmol"); + AnyMap data; + bool dimensionless = m_input.getBool("dimensionless", false); + if (dimensionless) { + thermo["dimensionless"] = true; + } + for (size_t i = 0; i < m_numIntervals+1; i++) { + 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])].setQuantity( + m_mu0_R_int[i] * GasConstant, "J/kmol"); + } + } + thermo["data"] = std::move(data); +} + Mu0Poly* newMu0ThermoFromXML(const XML_Node& Mu0Node) { bool dimensionlessMu0Values = false; diff --git a/src/thermo/Nasa9Poly1.cpp b/src/thermo/Nasa9Poly1.cpp index e89df8e3328..01c9d9090ba 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 9f46954ebb2..71c89cbac3e 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 +{ + thermo["model"] = "NASA9"; + SpeciesThermoInterpType::getParameters(thermo); + auto T_ranges = m_lowerTempBounds; + T_ranges.push_back(m_highT); + 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 a3312c89be5..18102557ed9 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 +{ + thermo["model"] = "NASA7"; + SpeciesThermoInterpType::getParameters(thermo); + vector_fp Tranges {m_lowT, m_midT, m_highT}; + thermo["temperature-ranges"].setQuantity(Tranges, "K"); + 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/PDSS_ConstVol.cpp b/src/thermo/PDSS_ConstVol.cpp index 25f4b3495e9..43b1fc69b47 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"].setQuantity(m_constMolarVolume, "m^3/kmol"); +} + doublereal PDSS_ConstVol::intEnergy_mole() const { doublereal pV = (m_pres * m_Vss); diff --git a/src/thermo/PDSS_HKFT.cpp b/src/thermo/PDSS_HKFT.cpp index 3a8d04cf0db..20299e5cc5a 100644 --- a/src/thermo/PDSS_HKFT.cpp +++ b/src/thermo/PDSS_HKFT.cpp @@ -454,6 +454,28 @@ void PDSS_HKFT::setParametersFromXML(const XML_Node& speciesNode) } } +void PDSS_HKFT::getParameters(AnyMap& eosNode) const +{ + PDSS::getParameters(eosNode); + eosNode["model"] = "HKFT"; + 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 { doublereal pbar = m_pres * 1.0E-5; diff --git a/src/thermo/PDSS_IdealGas.cpp b/src/thermo/PDSS_IdealGas.cpp index 9933f8bc81c..a9f4602a89d 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_IonsFromNeutral.cpp b/src/thermo/PDSS_IonsFromNeutral.cpp index fa424c9fd35..34124face03 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/PDSS_SSVol.cpp b/src/thermo/PDSS_SSVol.cpp index 9297cd38e9f..2ebdcd7fde6 100644 --- a/src/thermo/PDSS_SSVol.cpp +++ b/src/thermo/PDSS_SSVol.cpp @@ -65,6 +65,26 @@ void PDSS_SSVol::setDensityPolynomial(double* coeffs) { volumeModel_ = SSVolume_Model::density_tpoly; } +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"] = std::move(data); +} + void PDSS_SSVol::initThermo() { PDSS::initThermo(); diff --git a/src/thermo/PDSS_Water.cpp b/src/thermo/PDSS_Water.cpp index 2ee58ee10f7..a685bd440e6 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/src/thermo/PureFluidPhase.cpp b/src/thermo/PureFluidPhase.cpp index a12412de15c..24e99d9d5c2 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 de093c7d0a1..8d0f734a628 100644 --- a/src/thermo/RedlichKisterVPSSTP.cpp +++ b/src/thermo/RedlichKisterVPSSTP.cpp @@ -186,6 +186,29 @@ 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"].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); +} + void RedlichKisterVPSSTP::initLengths() { dlnActCoeffdlnN_.resize(m_kk, m_kk); diff --git a/src/thermo/RedlichKwongMFTP.cpp b/src/thermo/RedlichKwongMFTP.cpp index 50ca1998557..35507bf5d23 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; @@ -520,6 +523,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 +595,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 +622,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 +652,55 @@ 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) { + 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"].setQuantity(a_coeff_vec(0, counter), + "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}; @@ -759,6 +808,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/src/thermo/Species.cpp b/src/thermo/Species.cpp index eb43f519033..f8e012daf0d 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,6 +35,45 @@ Species::~Species() { } +AnyMap Species::parameters(const ThermoPhase* phase, bool withInput) const +{ + AnyMap speciesNode; + speciesNode["name"] = name; + speciesNode["composition"] = composition; + speciesNode["composition"].setFlowStyle(); + + if (charge != 0) { + speciesNode["charge"] = charge; + } + if (size != 1) { + speciesNode["size"] = size; + } + if (thermo) { + AnyMap thermoNode = thermo->parameters(withInput); + if (thermoNode.size()) { + speciesNode["thermo"] = std::move(thermoNode); + } + } + if (transport) { + 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); + } + } + if (withInput) { + speciesNode.update(input); + } + return speciesNode; +} + shared_ptr newSpecies(const XML_Node& species_node) { std::string name = species_node["name"]; @@ -110,14 +151,15 @@ 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()); + 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/src/thermo/SpeciesThermoFactory.cpp b/src/thermo/SpeciesThermoFactory.cpp index 48ec0b9cab2..23620cec6a0 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) @@ -386,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/src/thermo/SpeciesThermoInterpType.cpp b/src/thermo/SpeciesThermoInterpType.cpp index 7ef37423d63..53625e3a13d 100644 --- a/src/thermo/SpeciesThermoInterpType.cpp +++ b/src/thermo/SpeciesThermoInterpType.cpp @@ -52,6 +52,23 @@ 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 && reportType() != 0) { + thermo["reference-pressure"].setQuantity(m_Pref, "Pa"); + } +} + doublereal SpeciesThermoInterpType::reportHf298(doublereal* const h298) const { throw NotImplementedError("SpeciesThermoInterpType::reportHf298"); @@ -63,4 +80,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/src/thermo/StoichSubstance.cpp b/src/thermo/StoichSubstance.cpp index 35a48caeec9..6532f975a4f 100644 --- a/src/thermo/StoichSubstance.cpp +++ b/src/thermo/StoichSubstance.cpp @@ -156,6 +156,31 @@ 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"].setQuantity(density(), "kg/m^3"); + } else if (eosIn.hasKey("molar-density")) { + eosNode["molar-density"].setQuantity(density() / meanMolecularWeight(), + "kmol/m^3"); + } else { + eosNode["molar-volume"].setQuantity(meanMolecularWeight() / density(), + "m^3/kmol"); + } + } else { + eosNode["molar-volume"].setQuantity(meanMolecularWeight() / density(), "m^3/kmol"); + } +} + void StoichSubstance::initThermoXML(XML_Node& phaseNode, const std::string& id_) { // Find the Thermo XML node diff --git a/src/thermo/SurfPhase.cpp b/src/thermo/SurfPhase.cpp index 8bbb30ee502..abb188602a0 100644 --- a/src/thermo/SurfPhase.cpp +++ b/src/thermo/SurfPhase.cpp @@ -343,6 +343,13 @@ void SurfPhase::initThermo() } } +void SurfPhase::getParameters(AnyMap& phaseNode) const +{ + ThermoPhase::getParameters(phaseNode); + 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) { double t; diff --git a/src/thermo/ThermoFactory.cpp b/src/thermo/ThermoFactory.cpp index f1a4c52a985..364aae9ee76 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/src/thermo/ThermoPhase.cpp b/src/thermo/ThermoPhase.cpp index 614b7ec789f..0c70202b031 100644 --- a/src/thermo/ThermoPhase.cpp +++ b/src/thermo/ThermoPhase.cpp @@ -1189,6 +1189,69 @@ 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(); + 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(); + + AnyMap state; + auto stateVars = nativeState(); + if (stateVars.count("T")) { + state["T"].setQuantity(temperature(), "K"); + } + + if (stateVars.count("D")) { + state["density"].setQuantity(density(), "kg/m^3"); + } else if (stateVars.count("P")) { + state["P"].setQuantity(pressure(), "Pa"); + } + + 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(); + } + + phaseNode["state"] = std::move(state); + + static bool reg = AnyMap::addOrderingRules("Phase", {{"tail", "state"}}); + if (reg) { + phaseNode["__type__"] = "Phase"; + } +} + const AnyMap& ThermoPhase::input() const { return m_input; diff --git a/src/thermo/VPStandardStateTP.cpp b/src/thermo/VPStandardStateTP.cpp index f0f05a77bac..a277b860c31 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/src/transport/TransportBase.cpp b/src/transport/TransportBase.cpp index ee9ca950c70..0030ce9afd1 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,16 @@ void Transport::setParameters(const int type, const int k, throw NotImplementedError("Transport::setParameters"); } +AnyMap Transport::parameters() const +{ + AnyMap out; + string name = TransportFactory::factory()->canonicalize(transportType()); + if (name != "") { + out["transport"] = name; + } + return out; +} + void Transport::setThermo(ThermoPhase& thermo) { if (!ready()) { diff --git a/src/transport/TransportData.cpp b/src/transport/TransportData.cpp index a614134e237..523719ed84b 100644 --- a/src/transport/TransportData.cpp +++ b/src/transport/TransportData.cpp @@ -13,6 +13,21 @@ 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 +{ +} + GasTransportData::GasTransportData() : diameter(0.0) , well_depth(0.0) @@ -127,6 +142,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; diff --git a/src/transport/TransportFactory.cpp b/src/transport/TransportFactory.cpp index 7d8f70e618f..73f964f656b 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(); }); diff --git a/test/data/ideal-gas.yaml b/test/data/ideal-gas.yaml index ee83a98d688..448c69d68db 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}} @@ -95,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, @@ -102,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/data/pdep-test.yaml b/test/data/pdep-test.yaml new file mode 100644 index 00000000000..c50fafc0303 --- /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/data/thermo-models.yaml b/test/data/thermo-models.yaml index 7c1f998ceb1..2fad3a70a1d 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/general/test_containers.cpp b/test/general/test_containers.cpp index 24695b026cc..791daad12d3 100644 --- a/test/general/test_containers.cpp +++ b/test/general/test_containers.cpp @@ -372,3 +372,85 @@ 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()); +} + +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}"); + 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"]); +} diff --git a/test/general/test_serialization.cpp b/test/general/test_serialization.cpp new file mode 100644 index 00000000000..1ed26e9293f --- /dev/null +++ b/test/general/test_serialization.cpp @@ -0,0 +1,345 @@ +// 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/thermo/SurfPhase.h" +#include "cantera/base/Solution.h" +#include "cantera/kinetics.h" +#include "cantera/transport/TransportData.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, 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"); + + 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) +{ + 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); +} + +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"); + + 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_Yaml) +{ + auto original = newSolution("h2o2.yaml"); + YamlWriter writer; + writer.addPhase(original); + writer.setPrecision(14); + writer.setUnits({ + {"activation-energy", "K"}, + {"quantity", "mol"}, + {"length", "cm"} + }); + 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); + writer.setUnits({ + {"activation-energy", "K"}, + {"quantity", "mol"}, + {"length", "cm"} + }); + + 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); + writer.setUnits({ + {"activation-energy", "K"}, + {"quantity", "mol"}, + {"length", "cm"}, + {"pressure", "atm"} + }); + writer.toYamlFile("generated-pdep-test.yaml"); + auto duplicate = newSolution("generated-pdep-test.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"); + 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)); +} + +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); + writer.setUnits({ + {"length", "mm"}, + {"quantity", "molec"}, + {"activation-energy", "K"} + }); + 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); + writer.setUnits({ + {"length", "cm"}, + {"pressure", "atm"}, + {"activation-energy", "eV"} + }); + 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; + } +} diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp index 951b060c266..4759dfca17d 100644 --- a/test/general/test_units.cpp +++ b/test/general/test_units.cpp @@ -42,20 +42,23 @@ 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); + 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) { @@ -65,14 +68,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 +99,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 +126,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 +136,14 @@ 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, activation_energies_bad) { + UnitSystem U; + EXPECT_THROW(U.convertActivationEnergyTo(1000, "kg"), CanteraError); + EXPECT_THROW(U.convertActivationEnergyFrom(1000, "K^2"), CanteraError); } TEST(Units, from_anymap) { @@ -140,7 +151,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); @@ -148,6 +160,12 @@ 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.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); } TEST(Units, from_anymap_default) { @@ -158,6 +176,70 @@ 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.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, 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"}; + 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" @@ -172,10 +254,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); diff --git a/test/kinetics/kineticsFromYaml.cpp b/test/kinetics/kineticsFromYaml.cpp index 5ce804a7d71..c6a89bbf3a5 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; @@ -322,3 +323,164 @@ 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)->parameters(); + 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, 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"); + 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(); +} + +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(); +} + +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/thermoParameterizations.cpp b/test/thermo/thermoParameterizations.cpp index 5049ad31284..b4a5be7e91f 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,88 @@ 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->parameters(); + 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->parameters(); + 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->parameters(); + 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->parameters(); + 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) << T; + EXPECT_DOUBLE_EQ(h1, h2) << T; + EXPECT_DOUBLE_EQ(s1, s2) << T; + } + 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->parameters(); + 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()); +} diff --git a/test/thermo/thermoToYaml.cpp b/test/thermo/thermoToYaml.cpp new file mode 100644 index 00000000000..ebe348566b3 --- /dev/null +++ b/test/thermo/thermoToYaml.cpp @@ -0,0 +1,463 @@ +#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; + +class ThermoToYaml : public testing::Test +{ +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(); + data = thermo->parameters(); + 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]; + } + } + } + + shared_ptr thermo; + AnyMap data; + std::vector speciesData; + std::vector eosData; +}; + +TEST_F(ThermoToYaml, simpleIdealGas) +{ + setup("ideal-gas.yaml", "simple"); + thermo->setState_TP(1010, 2e5); + double rho = thermo->density(); + data = thermo->parameters(); + data.applyUnits(); + + ASSERT_EQ(data["thermo"], "ideal-gas"); + 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"); + + EXPECT_DOUBLE_EQ(eosData[0]["molar-volume"].asDouble(), 1.5); + EXPECT_DOUBLE_EQ(eosData[2]["molar-volume"].asDouble(), 0.1); +} + +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, 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"); + 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) +{ + 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, 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"); + 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"); + 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) +{ + 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")); + + 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) +{ + 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 + } + } + 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) +{ + 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, 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"); + 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); + + 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, 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", + {"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()); +}