From b8cb2c30f87cbfbbb0b69647a4dc1d576e405447 Mon Sep 17 00:00:00 2001 From: Ray Speth Date: Sun, 18 Nov 2018 23:02:02 -0500 Subject: [PATCH] Add 'Units' class for doing conversions with dimensionality checks Introduces a new, more natural notation for writing unit strings, for use in YAML input files. Unlike 'toSI', conversions are checked for dimensional consistency. --- include/cantera/base/Units.h | 95 ++++++++++++++++++ src/base/Units.cpp | 190 +++++++++++++++++++++++++++++++++++ src/base/units.h | 2 +- test/general/test_units.cpp | 32 ++++++ 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 include/cantera/base/Units.h create mode 100644 src/base/Units.cpp create mode 100644 test/general/test_units.cpp diff --git a/include/cantera/base/Units.h b/include/cantera/base/Units.h new file mode 100644 index 000000000..d573f83e0 --- /dev/null +++ b/include/cantera/base/Units.h @@ -0,0 +1,95 @@ +/** + * @file Units.h + * Header for unit conversion utilities, which are used to translate + * user input from input files (See \ref inputfiles and + * class \link Cantera::Units Units\endlink). + */ + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://www.cantera.org/license.txt for license and copyright information. + +#ifndef CT_UNITS_H +#define CT_UNITS_H + +#include "cantera/base/ct_defs.h" + +namespace Cantera +{ + +//! A representation of the units associated with a dimensional quantity. +/*! + * Used for converting quantities between unit systems and checking for + * dimensional consistency. Units objects are mainly used within UnitSystem + * class to convert values from a user-specified Unit system to Cantera's + * base units (SI + kmol). + */ +class Units +{ +public: + //! Create a Units object with the specified dimensions. + explicit Units(double factor=1.0, double mass=0, double length=0, + double time=0, double temperature=0, double current=0, + double quantity=0); + + //! Create an object with the specified dimensions + explicit Units(const std::string& name); + + //! Returns `true` if the specified Units are dimensionally consistent + bool convertible(const Units& other) const; + + //! Return the factor for converting from this unit to Cantera's base + //! units. + double factor() const { return m_factor; } + + //! Multiply two Units objects, combining their conversion factors and + //! dimensions + Units& operator*=(const Units& other); + + //! Provide a string representation of these Units + std::string str() const; + +private: + //! Scale the unit by the factor `k` + void scale(double k) { m_factor *= k; } + + //! Raise these Units to a power, changing both the conversion factor and + //! the dimensions of these Units. + Units pow(double expoonent) const; + + double m_factor; //!< conversion factor to Cantera base units + double m_mass_dim; + double m_length_dim; + double m_time_dim; + double m_temperature_dim; + double m_current_dim; + double m_quantity_dim; + + friend class UnitSystem; +}; + + +//! Unit conversion utility +/*! + * String representations of units can be written using multiplication, + * division, and exponentiation. Spaces are ignored. Positive, negative, and + * decimal exponents are permitted. Examples: + * + * kg*m/s^2 + * J/kmol + * m*s^-2 + * J/kg/K + * + * @ingroup inputfiles + */ +class UnitSystem +{ +public: + //! Convert `value` from the units of `src` to the units of `dest`. + double convert(double value, const std::string& src, + const std::string& dest) const; + double convert(double value, const Units& src, const Units& dest) const; +}; + +} + +#endif diff --git a/src/base/Units.cpp b/src/base/Units.cpp new file mode 100644 index 000000000..010b3cea2 --- /dev/null +++ b/src/base/Units.cpp @@ -0,0 +1,190 @@ +//! @file Units.cpp + +// This file is part of Cantera. See License.txt in the top-level directory or +// at https://www.cantera.org/license.txt for license and copyright information. + +#include "cantera/base/Units.h" +#include "cantera/base/ctexceptions.h" +#include "cantera/base/global.h" +#include "cantera/base/stringUtils.h" +#include "cantera/base/AnyMap.h" + +namespace { +using namespace Cantera; + +const std::map knownUnits{ + {"", Units(1.0)}, + {"1", Units(1.0)}, + + // Mass [M] + {"kg", Units(1.0, 1, 0, 0)}, + {"g", Units(1e-3, 1, 0, 0)}, + + // Length [L] + {"m", Units(1.0, 0, 1, 0)}, + {"micron", Units(1e-6, 0, 1, 0)}, + {"angstrom", Units(1e-10, 0, 1, 0)}, + {"Å", Units(1e-10, 0, 1, 0)}, + + // Time [T] + {"s", Units(1.0, 0, 0, 1)}, + {"min", Units(60, 0, 0, 1)}, + {"hr", Units(3600, 0, 0, 1)}, + + // Temperature [K] + {"K", Units(1.0, 0, 0, 0, 1)}, + {"C", Units(1.0, 0, 0, 0, 1)}, + + // Current [A] + {"A", Units(1.0, 0, 0, 0, 0, 1)}, + + // Quantity [Q] + {"mol", Units(1e-3, 0, 0, 0, 0, 0, 1)}, + {"gmol", Units(1e-3, 0, 0, 0, 0, 0, 1)}, + {"mole", Units(1e-3, 0, 0, 0, 0, 0, 1)}, + {"kmol", Units(1.0, 0, 0, 0, 0, 0, 1)}, + {"kgmol", Units(1.0, 0, 0, 0, 0, 0, 1)}, + {"molec", Units(1.0/Avogadro, 0, 0, 0, 0, 0, 1)}, + + // Energy [M*L^2/T^2] + {"J", Units(1.0, 1, 2, -2)}, + {"cal", Units(4.184, 1, 2, -2)}, + {"erg", Units(1e-7, 1, 2, -2)}, + {"eV", Units(ElectronCharge, 1, 2, -2)}, + + // Pressure [M/L/T^2] + {"Pa", Units(1.0, 1, -1, -2)}, + {"atm", Units(OneAtm, 1, -1, -2)}, + {"bar", Units(1.0e5, 1, -1, -2)}, + + // Volume [L^3] + {"m^3", Units(1.0, 0, 3, 0)}, + {"liter", Units(0.001, 0, 3, 0)}, + {"L", Units(0.001, 0, 3, 0)}, + {"l", Units(0.001, 0, 3, 0)}, + {"cc", Units(1.0e-6, 0, 3, 0)}, + + // Other electrical units + {"ohm", Units(1.0, 1, 2, -3, 0, -2)}, // kg*m^2/s^3/A^2 + {"V", Units(1.0, 1, 2, -3, 0, -1)}, // kg*m^2/s^3/A + {"coulomb", Units(1.0, 0, 0, 1, 0, 1)}, // A*s +}; + +} + +namespace Cantera +{ + +Units::Units(double factor, double mass, double length, double time, + double temperature, double current, double quantity) + : m_factor(factor) + , m_mass_dim(mass) + , m_length_dim(length) + , m_time_dim(time) + , m_temperature_dim(temperature) + , m_current_dim(current) + , m_quantity_dim(quantity) +{ +} + +Units::Units(const std::string& name) + : m_factor(1.0) + , m_mass_dim(0) + , m_length_dim(0) + , m_time_dim(0) + , m_temperature_dim(0) + , m_current_dim(0) + , m_quantity_dim(0) +{ + size_t start = 0; + while (true) { + // Split into groups of the form 'unit^exponent' + size_t stop = name.find_first_of("*/", start); + size_t carat = name.find('^', start); + if (carat > stop) { + // No carat in this group + carat = npos; + } + std::string unit = trimCopy( + name.substr(start, std::min(carat, stop) - start)); + + double exponent = 1.0; + if (carat != npos) { + exponent = fpValueCheck(name.substr(carat+1, stop-carat-1)); + } + if (start != 0 && name[start-1] == '/') { + // This unit is in the denominator + exponent = -exponent; + } + + if (knownUnits.find(unit) != knownUnits.end()) { + // Incorporate the unit defined by the current group + *this *= knownUnits.at(unit).pow(exponent); + } else { + throw CanteraError("Units::Units(string)", + "Unknown unit '{}' in unit string '{}'", unit, name); + } + + start = stop+1; + if (stop == npos) { + break; + } + } +} + +bool Units::convertible(const Units& other) const +{ + return (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); +} + +Units& Units::operator*=(const Units& other) +{ + 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; + return *this; +} + +Units Units::pow(double exponent) const { + return Units(std::pow(m_factor, exponent), + m_mass_dim * exponent, + m_length_dim * exponent, + m_time_dim * exponent, + m_temperature_dim * exponent, + m_current_dim * exponent, + m_quantity_dim * exponent); +} + +std::string Units::str() const { + return fmt::format("Units({} kg^{} * m^{} * s^{} * K^{} * A^{} * kmol^{})", + m_factor, m_mass_dim, m_length_dim, m_time_dim, + m_temperature_dim, m_current_dim, m_quantity_dim); +} + + +double UnitSystem::convert(double value, const std::string& src, + const std::string& dest) const +{ + return convert(value, Units(src), Units(dest)); +} + +double UnitSystem::convert(double value, const Units& src, + const Units& dest) const +{ + if (!src.convertible(dest)) { + throw CanteraError("UnitSystem::convert", + "Incompatible units:\n {} and\n {}", src.str(), dest.str()); + } + return value * src.factor() / dest.factor(); +} + +} diff --git a/src/base/units.h b/src/base/units.h index 85f40b3f9..a29186432 100644 --- a/src/base/units.h +++ b/src/base/units.h @@ -8,7 +8,7 @@ */ // This file is part of Cantera. See License.txt in the top-level directory or -// at http://www.cantera.org/license.txt for license and copyright information. +// at https://www.cantera.org/license.txt for license and copyright information. #ifndef CT_UNITS_H #define CT_UNITS_H diff --git a/test/general/test_units.cpp b/test/general/test_units.cpp new file mode 100644 index 000000000..1853a0b0f --- /dev/null +++ b/test/general/test_units.cpp @@ -0,0 +1,32 @@ +#include "gtest/gtest.h" +#include "cantera/base/Units.h" + +using namespace Cantera; + +TEST(Units, convert_to_base_units) { + UnitSystem U; + EXPECT_DOUBLE_EQ(U.convert(1.0, "Pa", "kg/m/s^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, "J", "kg*m^2/s^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, "ohm", "kg*m^2/s^3/A^2"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, "V", "kg*m^2/A*s^-3"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, "coulomb", "A*s"), 1.0); +} + +TEST(Units, notation) { + UnitSystem U; + EXPECT_DOUBLE_EQ(U.convert(2.0, "m^2", "m*m"), 2.0); + EXPECT_DOUBLE_EQ(U.convert(3.0, "", "kg/kg"), 3.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, "1/m^2", "m^-2"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(4.0, "s^3", "s^5/s^2"), 4.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, "kg * m/s ^2", "s^-2*kg*m"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(1.0, " kg*m / s^ 2", "s ^-2 * kg*m"), 1.0); +} + +TEST(Units, basic_conversions) { + UnitSystem U; + EXPECT_DOUBLE_EQ(U.convert(100, "cm", "m"), 1.0); + EXPECT_DOUBLE_EQ(U.convert(2, "kmol", "mol"), 2000); + EXPECT_DOUBLE_EQ(U.convert(1000, "cal", "J"), 4184); + EXPECT_DOUBLE_EQ(U.convert(2, "m^3", "l"), 2000); + EXPECT_DOUBLE_EQ(U.convert(1, "atm", "Pa"), 101325); +}