diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp new file mode 100644 index 00000000000..fdc2a114e1e --- /dev/null +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -0,0 +1,204 @@ +/* + * ------------------------------------------------------------------------------------------------------------ + * SPDX-License-Identifier: LGPL-2.1-only + * + * Copyright (c) 2016-2024 Lawrence Livermore National Security LLC + * Copyright (c) 2018-2024 TotalEnergies + * Copyright (c) 2018-2024 The Board of Trustees of the Leland Stanford Junior University + * Copyright (c) 2023-2024 Chevron + * Copyright (c) 2019- GEOS/GEOSX Contributors + * All rights reserved + * + * See top level LICENSE, COPYRIGHT, CONTRIBUTORS, NOTICE, and ACKNOWLEDGEMENTS files for details. + * ------------------------------------------------------------------------------------------------------------ + */ + + +#ifndef GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ +#define GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ + +#include "codingUtilities/traits.hpp" +#include "common/DataTypes.hpp" +#include "common/format/EnumStrings.hpp" +#include +#include + +namespace geos +{ + +namespace dataRepository +{ + +/** + * @enum LimitsMode + * @brief Enforcement mode associated with the limits of an attribute + * + * - Indicative: the limits are documentation only, no runtime check is performed. + * - Warning: a value outside the limits emits a runtime warning. + * - Error: a value outside the limits throws. + */ +enum class LimitsMode : integer +{ + Indicative, + Warning, + Error +}; + +ENUM_STRINGS( LimitsMode, + "Indicative", + "Warning", + "Error" ); + +/** + * @struct LimitValueType + * @brief Structure giving the underlying value type that limits apply to. + * + * For a scalar T is T itself. For an array T is the type of the values in + * the array (Array::value_type). + */ +template< typename T, bool = traits::is_array_type< T > > +struct LimitValueType +{}; + +template< typename T > +struct LimitValueType< T, true > +{ + using type = typename T::value_type; +}; + +template< typename T > +struct LimitValueType< T, false > +{ + using type = T; +}; + +/** + * @brief Alias resolving to the type that limits apply to + * + * For a scalar value it is the scalar type itself. + * For an array it is the type of the values in the array. + */ +template< typename T > +using limit_value_type_t = typename LimitValueType< T >::type; + +/** + * @struct is_limitable + * @tparam T type to check + * @brief Trait determining whether attribute limits can be applied to type @p T + * + * Limits apply to numeric types (integer, real32, real64, etc.) including arrays + * of numeric types (array1d< integer >, array2d< real64 >, etc.) + */ +template< typename T > +struct is_limitable +{ + static constexpr bool value = std::is_arithmetic< limit_value_type_t< T > >::value; +}; + +/** + * @brief Convenience variable template alias for is_limitable< T >::value + */ +template< typename T > +inline constexpr bool is_limitable_v = is_limitable< T >::value; + +/** + * @struct Bound + * @brief Structure containing informations about an attribute limit. + */ +template< typename T > +struct Bound +{ + T value; + bool isInclusive = true; + + /** + * @brief Bound constructor to write a limit without the "Bound{ ... }" syntax + * @param value The limit value to set + * @param isInclusive Wether the limit should be inclusive or not + * + * @code + * .setLimits( 0.0, 1.0 ) // where setLimits takes `Bound` parameters, those parameters can + * // be written only with the value. The isInclusive property will + * // default to true. + * @endcode + */ + Bound( T v, bool inclusive = true ) + : value( v ), isInclusive( inclusive ) + {} +}; + +/** + * @brief Creates an inclusive limit of @p value + * @param value The inclusive limit value to set + */ +template< typename T > +Bound< T > inclusive( T value ) +{ + return Bound< T >{ value, /*isInclusive*/ true }; +} + +/** + * @brief Creates an exclusive limit of @p value + * @param value The inclusive limit value to set + */ +template< typename T > +Bound< T > exclusive( T value ) +{ + return Bound< T >{ value, /*isInclusive*/ false }; +} + +/** + * @struct Limits + * @brief Storage for the optional min/max bounds of a wrapped value. + * + * Specialized so that the members (std::optional< T >) are only instanciated + * for limitable types. Preventing instantiation of non-limitable types, especially + * abstract types that can't be instantiated with std::optional< absT >. + */ +template< typename T, bool = is_limitable_v< T > > +struct Limits +{}; + +template< typename T > +struct Limits< T, true > +{ + std::optional< Bound< limit_value_type_t< T > > > min; + std::optional< Bound< limit_value_type_t< T > > > max; +}; + + +// Helper methods + +/** + * @brief Compare the given value with the min limit, taking account for the inclusive xor exclusive + * property of the limit. + * @param value The value to compare to the limit + * @param minLimit The min limit containing the inclusive + * @return True if the value is below the min limit, false otherwise + */ +template< typename T > +static bool isValueBelowMin( T const & value, Bound< T > const & minLimit ) +{ + return minLimit.isInclusive ? ( value < minLimit.value ) + : ( value <= minLimit.value ); +} + +/** + * @brief Compare the given value with the max limit, taking account for the inclusive xor exclusive + * property of the limit. + * @param value The value to compare to the limit + * @param maxLimit The max limit containing the inclusive + * @return True if the value is above the max limit, false otherwise + */ +template< typename T > +static bool isValueAboveMax( T const & value, Bound< T > const & maxLimit ) +{ + return maxLimit.isInclusive ? ( value > maxLimit.value ) + : ( value >= maxLimit.value ); +} + +} /* namespace dataRepository */ + +} /* namespace geos */ + +#endif /* GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ */ diff --git a/src/coreComponents/dataRepository/CMakeLists.txt b/src/coreComponents/dataRepository/CMakeLists.txt index 8d0fc0e09ce..f75d8e70ca7 100644 --- a/src/coreComponents/dataRepository/CMakeLists.txt +++ b/src/coreComponents/dataRepository/CMakeLists.txt @@ -22,6 +22,7 @@ Also contains a wrapper to process entries from an xml file into data types. # Specify all headers # set( dataRepository_headers + AttributeLimits.hpp BufferOps.hpp BufferOpsDevice.hpp BufferOps_inline.hpp diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 4f23afa084e..cef73f75543 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -21,6 +21,9 @@ #define GEOS_DATAREPOSITORY_WRAPPER_HPP_ // Source inclues +#include "common/format/Format.hpp" +#include "common/logger/Logger.hpp" +#include "dataRepository/AttributeLimits.hpp" #include "wrapperHelpers.hpp" #include "KeyNames.hpp" #include "LvArray/src/limits.hpp" @@ -37,8 +40,8 @@ #include "WrapperBase.hpp" // System includes -#include #include +#include #include namespace geos @@ -204,6 +207,7 @@ class Wrapper final : public WrapperBase m_ownsData = castedSource.m_ownsData; m_default = castedSource.m_default; m_dimLabels = castedSource.m_dimLabels; + m_limits = castedSource.m_limits; } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -718,6 +722,107 @@ class Wrapper final : public WrapperBase return ss.str(); } + /** + * @brief Set both bounds for this attribute's value. + * @param min the minimum allowed value (inclusive) + * @param max the maximum allowed value (inclusive) + * @param mode the enforcement mode + * @return pointer to Wrapper + * + * @note @p min and @p max are std::optional(s). + * Set them to std::nullopt to disable a limit. + * + * @code + * registerWrapper( viewKeysStruct::fooString(), &m_foo ) + * .setLimits( 0.0, 1.0 ) // sets a minimum value of 0.0 and a maximum value of 1.0 + * .setLimits( 0.0, std::nullopt ) // sets a minimum value of 0.0 and no maximum value + * .setLimits( inclusive( 0.0 ), std::nullopt ) // sets an inclusive (default) minimum value + * .setLimits( exclusive( 0.0 ), std::nullopt ) // sets an exclusive maximum value + * @endcode + */ + template< typename U=T > + std::enable_if_t< is_limitable_v< U >, Wrapper< T > & > + setLimits( std::optional< Bound< limit_value_type_t< T > > > min, + std::optional< Bound< limit_value_type_t< T > > > max, + LimitsMode mode = LimitsMode::Error ) + { + m_limits.min = min; + m_limits.max = max; + m_limitsMode = mode; + return *this; + } + + template< typename U=T > + std::enable_if_t< !is_limitable_v< U > && !traits::is_array_type< U >, Wrapper< T > & > + setLimits( std::optional< Bound< limit_value_type_t< T > > >, + std::optional< Bound< limit_value_type_t< T > > >, + LimitsMode mode = LimitsMode::Error ) + { + static_assert( is_limitable_v< U >, + "setLimits is only supported on scalar arithmetic types." ); + return *this; + } + + /** + * @brief Accessor for the minimum bound of this attribute's value. + * @return optional containing the typed minimum value, empty if not set + * @note Only available when T is a limitable type + */ + template< typename U=T > + std::enable_if_t< is_limitable_v< U >, std::optional< Bound< limit_value_type_t< T > > > const & > + getMinValue() const + { + return m_limits.min; + } + + /** + * @brief Accessor for the maximum bound of this attribute's value. + * @return optional containing the typed maximum value, empty if not set + * @note Only available when T is a limitable type + */ + template< typename U=T > + std::enable_if_t< is_limitable_v< U >, std::optional< Bound< limit_value_type_t< T > > > const & > + getMaxValue() const + { + return m_limits.max; + } + + template< typename U=T > + std::enable_if_t< is_limitable_v< U > && !traits::is_array_type< U >, void > + validateLimits() + { + if( (!m_limits.min.has_value() && !m_limits.max.has_value()) || + m_limitsMode == LimitsMode::Indicative ) + { + return; + } + validateLimitValue( reference() ); + } + + template< typename U=T > + std::enable_if_t< is_limitable_v< U > && traits::is_array_type< U >, void > + validateLimits() + { + if( (!m_limits.min.has_value() && !m_limits.max.has_value()) || + m_limitsMode == LimitsMode::Indicative ) + { + return; + } + auto const values = m_data->toViewConst(); + for( limit_value_type_t< T > value : values ) + { + validateLimitValue( value ); + } + } + + template< typename U=T > + std::enable_if_t< !is_limitable_v< U >, void > + validateLimits() + { + /* no-op */ + } + + virtual bool processInputFile( xmlWrapper::xmlNode const & targetNode, xmlWrapper::xmlNodePos const & nodePos ) override { @@ -749,6 +854,11 @@ class Wrapper final : public WrapperBase targetNode, getDefaultValueStruct() ); } + + if( m_successfulReadFromInput ) + { + validateLimits(); + } } catch( std::exception const & ex ) { @@ -1086,6 +1196,41 @@ class Wrapper final : public WrapperBase return this->packByIndexImpl< false >( dummy, packList, withMetadata, onDevice, events ); } + template< typename V > + void validateLimitValue( V const & value ) const + { + bool const belowMin = m_limits.min.has_value() ? isValueBelowMin( value, *m_limits.min ) : false; + bool const aboveMax = m_limits.max.has_value() ? isValueAboveMax( value, *m_limits.max ) : false; + if( !belowMin && !aboveMax ) + { + return; + } + + string const lowerRange = m_limits.min.has_value() + ? GEOS_FMT( "{}{}", m_limits.min->isInclusive ? "[" : "(", m_limits.min->value ) + : string( "(-inf" ); + string const upperRange = m_limits.max.has_value() + ? GEOS_FMT( "{}{}", m_limits.max->value, m_limits.max->isInclusive ? "]" : ")" ) + : string( "+inf)" ); + string const msg = GEOS_FMT( "Value {} for attribute '{}' is outside the allowed range {}, {}.", + value, getDataContext(), lowerRange, upperRange ); + + switch( m_limitsMode ) + { + case LimitsMode::Warning: + GEOS_WARNING( msg ); + break; + + case LimitsMode::Error: + GEOS_THROW( msg, InputError ); + break; + + default: + GEOS_LOG_RANK_0( "Unimplemented LimitsMode" ); + break; + } + } + /// flag to indicate whether or not this wrapper is responsible for allocation/deallocation of the object at the /// address of m_data bool m_ownsData; @@ -1100,6 +1245,9 @@ class Wrapper final : public WrapperBase /// stores dimension labels (used mainly for plotting) for multidimensional arrays, empty member otherwise wrapperHelpers::ArrayDimLabels< T > m_dimLabels; + + /// stores the (optional) min/max bounds for the wrapped value. + Limits< T > m_limits; }; } diff --git a/src/coreComponents/dataRepository/WrapperBase.cpp b/src/coreComponents/dataRepository/WrapperBase.cpp index 69d8575b422..684679b459f 100644 --- a/src/coreComponents/dataRepository/WrapperBase.cpp +++ b/src/coreComponents/dataRepository/WrapperBase.cpp @@ -39,6 +39,7 @@ WrapperBase::WrapperBase( string const & name, m_inputFlag( InputFlags::INVALID ), m_successfulReadFromInput( false ), m_description(), + m_limitsMode( LimitsMode::Indicative ), m_rtTypeName( rtTypeName ), m_registeringObjects(), m_conduitNode( parent.getConduitNode()[ name ] ), @@ -61,6 +62,7 @@ void WrapperBase::copyWrapperAttributes( WrapperBase const & source ) m_plotLevel = source.m_plotLevel; m_inputFlag = source.m_inputFlag; m_description = source.m_description; + m_limitsMode = source.m_limitsMode; m_rtTypeName = source.m_rtTypeName; } diff --git a/src/coreComponents/dataRepository/WrapperBase.hpp b/src/coreComponents/dataRepository/WrapperBase.hpp index 8a278649ae2..5ebfc57471b 100644 --- a/src/coreComponents/dataRepository/WrapperBase.hpp +++ b/src/coreComponents/dataRepository/WrapperBase.hpp @@ -21,6 +21,7 @@ #include "common/DataTypes.hpp" #include "common/GEOS_RAJA_Interface.hpp" #include "common/Span.hpp" +#include "dataRepository/AttributeLimits.hpp" #include "InputFlags.hpp" #include "xmlWrapper.hpp" #include "RestartFlags.hpp" @@ -529,6 +530,15 @@ class WrapperBase return m_description; } + /** + * @brief Get the enforcement mode of the (optional) attribute limits + * @return the LimitsMode of the wrapper + */ + LimitsMode getLimitsMode() const + { + return m_limitsMode; + } + /** * @brief Get the list of names of groups that registered this wrapper. * @return vector of object names @@ -699,6 +709,9 @@ class WrapperBase /// A string description of the wrapped object string m_description; + /// Enforcement mode of the (optional) attribute limits + LimitsMode m_limitsMode; + /// A string regex to validate the input values string to parse for the wrapped object string m_rtTypeName; diff --git a/src/coreComponents/dataRepository/unitTests/testWrapper.cpp b/src/coreComponents/dataRepository/unitTests/testWrapper.cpp index d04fd2b5440..0dcd64c8018 100644 --- a/src/coreComponents/dataRepository/unitTests/testWrapper.cpp +++ b/src/coreComponents/dataRepository/unitTests/testWrapper.cpp @@ -150,3 +150,191 @@ TYPED_TEST( WrapperSetGet, Description ) this->testDescription( "First description." ); this->testDescription( "Second description." ); } + +class WrapperLimits : public ::testing::Test +{ +protected: + WrapperLimits(): + m_node(), + m_group( "root", m_node ) + {} + + template< typename T > + Wrapper< T > & makeWrapper( string const & name ) + { + return m_group.template registerWrapper< T >( name ); + } + + conduit::Node m_node; + Group m_group; +}; + +TEST_F( WrapperLimits, IsLimitableTrait ) +{ + static_assert( is_limitable_v< integer >, "integer must be limitable" ); + static_assert( is_limitable_v< real64 >, "real64 must be limitable" ); + static_assert( is_limitable_v< array1d< integer > >, "array1d< integer > must be limitable" ); + static_assert( is_limitable_v< array2d< real64 > >, "array2d< real64 > must be limitable" ); + static_assert( is_limitable_v< array3d< integer > >, "array3d< integer > must be limitable" ); + + static_assert( std::is_same< limit_value_type_t< real64 >, real64 >::value, "" ); + static_assert( std::is_same< limit_value_type_t< array1d< real64 > >, real64 >::value, "" ); + static_assert( std::is_same< limit_value_type_t< array2d< integer > >, integer >::value, "" ); +} + +TEST_F( WrapperLimits, ScalarSetGet ) +{ + auto & w = makeWrapper< real64 >( "scalar" ); + w.setLimits( inclusive( 0.0 ), exclusive( 1.0 ), LimitsMode::Error ); + + ASSERT_TRUE( w.getMinValue().has_value() ); + ASSERT_TRUE( w.getMaxValue().has_value() ); + EXPECT_DOUBLE_EQ( w.getMinValue()->value, 0.0 ); + EXPECT_TRUE( w.getMinValue()->isInclusive ); + EXPECT_DOUBLE_EQ( w.getMaxValue()->value, 1.0 ); + EXPECT_FALSE( w.getMaxValue()->isInclusive ); + EXPECT_EQ( w.getLimitsMode(), LimitsMode::Error ); +} + +TEST_F( WrapperLimits, Array1dSetGet ) +{ + auto & w = makeWrapper< array1d< real64 > >( "array1d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + ASSERT_TRUE( w.getMinValue().has_value() ); + ASSERT_TRUE( w.getMaxValue().has_value() ); + EXPECT_DOUBLE_EQ( w.getMinValue()->value, 0.0 ); + EXPECT_DOUBLE_EQ( w.getMaxValue()->value, 1.0 ); + EXPECT_EQ( w.getLimitsMode(), LimitsMode::Error ); +} + +TEST_F( WrapperLimits, ScalarValidateInRange ) +{ + auto & w = makeWrapper< real64 >( "scalar" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + w.reference() = 0.5; + EXPECT_NO_THROW( w.validateLimits() ); +} + +TEST_F( WrapperLimits, ScalarValidateBelowMinThrows ) +{ + auto & w = makeWrapper< real64 >( "scalar" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + w.reference() = -0.1; + EXPECT_THROW( w.validateLimits(), InputError ); +} + +TEST_F( WrapperLimits, ScalarValidateAboveMaxThrows ) +{ + auto & w = makeWrapper< real64 >( "scalar" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + w.reference() = 1.1; + EXPECT_THROW( w.validateLimits(), InputError ); +} + +TEST_F( WrapperLimits, ScalarValidateInclusiveBoundary ) +{ + auto & w = makeWrapper< real64 >( "scalar" ); + w.setLimits( inclusive( 0.0 ), inclusive( 1.0 ), LimitsMode::Error ); + + w.reference() = 0.0; + EXPECT_NO_THROW( w.validateLimits() ); + + w.reference() = 1.0; + EXPECT_NO_THROW( w.validateLimits() ); +} + +TEST_F( WrapperLimits, ScalarValidateExclusiveBoundary ) +{ + auto & w = makeWrapper< real64 >( "scalar" ); + w.setLimits( exclusive( 0.0 ), exclusive( 1.0 ), LimitsMode::Error ); + + w.reference() = 0.0; + EXPECT_THROW( w.validateLimits(), InputError ); + + w.reference() = 0.5; + EXPECT_NO_THROW( w.validateLimits() ); + + w.reference() = 1.0; + EXPECT_THROW( w.validateLimits(), InputError ); +} + +TEST_F( WrapperLimits, Array1dValidateAllInRange ) +{ + auto & w = makeWrapper< array1d< real64 > >( "array1d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + array1d< real64 > & data = w.reference(); + data.resize( 4 ); + data[ 0 ] = 0.0; + data[ 1 ] = 0.25; + data[ 2 ] = 0.75; + data[ 3 ] = 1.0; + + EXPECT_NO_THROW( w.validateLimits() ); +} + +TEST_F( WrapperLimits, Array1dValidateOutOfRange ) +{ + auto & w = makeWrapper< array1d< real64 > >( "array1d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + array1d< real64 > & data = w.reference(); + data.resize( 4 ); + data[ 0 ] = 0.5; + data[ 1 ] = 0.5; + data[ 2 ] = 42.0; + data[ 3 ] = 0.5; + + EXPECT_THROW( w.validateLimits(), InputError ); +} + +TEST_F( WrapperLimits, Array1dValidateEmpty ) +{ + auto & w = makeWrapper< array1d< real64 > >( "array1d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + EXPECT_NO_THROW( w.validateLimits() ); +} + +TEST_F( WrapperLimits, Array2dValidateAllInRange ) +{ + auto & w = makeWrapper< array2d< real64 > >( "array2d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + array2d< real64 > & data = w.reference(); + data.resize( 2, 3 ); + data( 0, 0 ) = 0.1; + data( 0, 1 ) = 0.2; + data( 0, 2 ) = 0.4; + data( 1, 0 ) = 0.6; + data( 1, 1 ) = 0.8; + data( 1, 2 ) = 0.9; + + EXPECT_NO_THROW( w.validateLimits() ); +} + +TEST_F( WrapperLimits, Array2dValidateOutOfRange ) +{ + auto & w = makeWrapper< array2d< real64 > >( "array2d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + array2d< real64 > & data = w.reference(); + data.resize( 2, 3 ); + data( 0, 0 ) = 0.5; + data( 0, 1 ) = 0.5; + data( 0, 2 ) = 0.5; + data( 1, 0 ) = 4000.0; + data( 1, 1 ) = 0.5; + data( 1, 2 ) = 0.5; + + EXPECT_THROW( w.validateLimits(), InputError ); +} + +TEST_F( WrapperLimits, Array2dValidateEmpty ) +{ + auto & w = makeWrapper< array2d< real64 > >( "array2d" ); + w.setLimits( 0.0, 1.0, LimitsMode::Error ); + + EXPECT_NO_THROW( w.validateLimits() ); +}