From 2f0816341dd44821f46b8b7402fb07b65beec5fa Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 10:09:17 +0200 Subject: [PATCH 01/16] (WIP) add min/max limit to Wrapper.hpp Will not compile due to std::optional instantiation (used by m_minValue and m_maxVavlue) for abstract types like PartitionBase --- src/coreComponents/dataRepository/Wrapper.hpp | 70 ++++++++++++++++++- 1 file changed, 69 insertions(+), 1 deletion(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 4f23afa084e..de4be8625bf 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -37,8 +37,8 @@ #include "WrapperBase.hpp" // System includes -#include #include +#include #include namespace geos @@ -204,6 +204,8 @@ class Wrapper final : public WrapperBase m_ownsData = castedSource.m_ownsData; m_default = castedSource.m_default; m_dimLabels = castedSource.m_dimLabels; + m_minValue = castedSource.m_minValue; + m_maxValue = castedSource.m_maxValue; } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -718,6 +720,66 @@ class Wrapper final : public WrapperBase return ss.str(); } + /** + * @brief Set a minimum bound for this attribute's value. + * @param minValue the minimum allowed value (inclusive) + * @return pointer to Wrapper + */ + template< typename U=T > + std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > + setMinValue( T const & minValue ) + { + m_minValue = minValue; + return *this; + } + + /** + * @brief Set a maximum bound for this attribute's value. + * @param maxValue the maximum allowed value (inclusive) + * @return pointer to Wrapper + */ + template< typename U=T > + std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > + setMaxValue( T const & maxValue ) + { + m_maxValue = maxValue; + return *this; + } + + /** + * @brief Set both bounds for this attribute's value. + * @param minValue the minimum allowed value (inclusive) + * @param maxValue the maximum allowed value (inclusive) + * @return pointer to Wrapper + */ + template< typename U=T > + std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > + setLimits( T const & minValue, + T const & maxValue ) + { + m_minValue = minValue; + m_maxValue = maxValue; + return *this; + } + + /** + * @brief Accessor for the minimum bound of this attribute's value. + * @return optional containing the typed minimum value, empty if not set + */ + std::optional< T > const & getMinValue() const + { + return m_minValue; + } + + /** + * @brief Accessor for the maximum bound of this attribute's value. + * @return optional containing the typed maximum value, empty if not set + */ + std::optional< T > const & getMaxValue() const + { + return m_maxValue; + } + virtual bool processInputFile( xmlWrapper::xmlNode const & targetNode, xmlWrapper::xmlNodePos const & nodePos ) override { @@ -1100,6 +1162,12 @@ class Wrapper final : public WrapperBase /// stores dimension labels (used mainly for plotting) for multidimensional arrays, empty member otherwise wrapperHelpers::ArrayDimLabels< T > m_dimLabels; + + /// + std::optional< T > m_minValue; + + /// + std::optional< T > m_maxValue; }; } From 45713eada24d47037a45e0ec54dd94c58e6a8e62 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 10:47:35 +0200 Subject: [PATCH 02/16] move min/max optionals in a struct instanciated only when T is limitable --- .../dataRepository/AttributeLimits.hpp | 52 +++++++++++++++++++ .../dataRepository/CMakeLists.txt | 1 + src/coreComponents/dataRepository/Wrapper.hpp | 34 ++++++------ 3 files changed, 71 insertions(+), 16 deletions(-) create mode 100644 src/coreComponents/dataRepository/AttributeLimits.hpp diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp new file mode 100644 index 00000000000..3aa5b1c0587 --- /dev/null +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -0,0 +1,52 @@ +/* + * ------------------------------------------------------------------------------------------------------------ + * 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 +#include + +namespace geos +{ + +namespace dataRepository +{ + +/** + * @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 arithmetic types. Preventing instantiation non-limitable types, especially + * abstract types that can't be instantiated with std::optional< absT >. + */ +template< typename T, bool = std::is_arithmetic< T >::value > +struct Limits +{}; + +template< typename T > +struct Limits< T, true > +{ + std::optional< T > minValue; + std::optional< T > maxValue; +}; + +} /* 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 de4be8625bf..4b01ecc3ab4 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -21,6 +21,7 @@ #define GEOS_DATAREPOSITORY_WRAPPER_HPP_ // Source inclues +#include "dataRepository/AttributeLimits.hpp" #include "wrapperHelpers.hpp" #include "KeyNames.hpp" #include "LvArray/src/limits.hpp" @@ -38,7 +39,6 @@ // System includes #include -#include #include namespace geos @@ -204,8 +204,7 @@ class Wrapper final : public WrapperBase m_ownsData = castedSource.m_ownsData; m_default = castedSource.m_default; m_dimLabels = castedSource.m_dimLabels; - m_minValue = castedSource.m_minValue; - m_maxValue = castedSource.m_maxValue; + m_limits = castedSource.m_limits; } /////////////////////////////////////////////////////////////////////////////////////////////////// @@ -729,7 +728,7 @@ class Wrapper final : public WrapperBase std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > setMinValue( T const & minValue ) { - m_minValue = minValue; + m_limits.minValue = minValue; return *this; } @@ -742,7 +741,7 @@ class Wrapper final : public WrapperBase std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > setMaxValue( T const & maxValue ) { - m_maxValue = maxValue; + m_limits.maxValue = maxValue; return *this; } @@ -757,27 +756,33 @@ class Wrapper final : public WrapperBase setLimits( T const & minValue, T const & maxValue ) { - m_minValue = minValue; - m_maxValue = maxValue; + m_limits.minValue = minValue; + m_limits.maxValue = maxValue; 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 */ - std::optional< T > const & getMinValue() const + template< typename U=T > + std::enable_if_t< std::is_arithmetic< U >::value, std::optional< T > const & > + getMinValue() const { - return m_minValue; + return m_limits.minValue; } /** * @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 */ - std::optional< T > const & getMaxValue() const + template< typename U=T > + std::enable_if_t< std::is_arithmetic< U >::value, std::optional< T > const & > + getMaxValue() const { - return m_maxValue; + return m_limits.maxValue; } virtual bool processInputFile( xmlWrapper::xmlNode const & targetNode, @@ -1163,11 +1168,8 @@ class Wrapper final : public WrapperBase /// stores dimension labels (used mainly for plotting) for multidimensional arrays, empty member otherwise wrapperHelpers::ArrayDimLabels< T > m_dimLabels; - /// - std::optional< T > m_minValue; - - /// - std::optional< T > m_maxValue; + /// stores the (optional) min/max bounds for the wrapped value. + Limits< T > m_limits; }; } From c07a75abcb0df726efef3f7d0e76763078d6bd37 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 11:26:28 +0200 Subject: [PATCH 03/16] create is_limitable trait --- .../dataRepository/AttributeLimits.hpp | 19 ++++++++++++++++--- src/coreComponents/dataRepository/Wrapper.hpp | 10 +++++----- 2 files changed, 21 insertions(+), 8 deletions(-) diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp index 3aa5b1c0587..8cc1dd6a61d 100644 --- a/src/coreComponents/dataRepository/AttributeLimits.hpp +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -26,15 +26,28 @@ namespace geos namespace dataRepository { +/** + * @struct is_limitable + * @tparam T type to check + * @brief Trait determining whether attribute limits can be applied to type @p T + * + * Limits apply to scalar numeric types (integer, real32, real64, etc.) + */ +template< typename T > +struct is_limitable +{ + static constexpr bool value = std::is_arithmetic< T >::value; +}; + /** * @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 arithmetic types. Preventing instantiation non-limitable types, especially - * abstract types that can't be instantiated with std::optional< absT >. + * for limitable types. Preventing instantiation non-limitable types, especially + * abstract types that can't be instantiated with std::optional< absT >. */ -template< typename T, bool = std::is_arithmetic< T >::value > +template< typename T, bool = is_limitable< T >::value > struct Limits {}; diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 4b01ecc3ab4..2d63595511d 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -725,7 +725,7 @@ class Wrapper final : public WrapperBase * @return pointer to Wrapper */ template< typename U=T > - std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > + std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > setMinValue( T const & minValue ) { m_limits.minValue = minValue; @@ -738,7 +738,7 @@ class Wrapper final : public WrapperBase * @return pointer to Wrapper */ template< typename U=T > - std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > + std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > setMaxValue( T const & maxValue ) { m_limits.maxValue = maxValue; @@ -752,7 +752,7 @@ class Wrapper final : public WrapperBase * @return pointer to Wrapper */ template< typename U=T > - std::enable_if_t< std::is_arithmetic< U >::value, Wrapper< T > & > + std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > setLimits( T const & minValue, T const & maxValue ) { @@ -767,7 +767,7 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< std::is_arithmetic< U >::value, std::optional< T > const & > + std::enable_if_t< is_limitable< U >::value, std::optional< T > const & > getMinValue() const { return m_limits.minValue; @@ -779,7 +779,7 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< std::is_arithmetic< U >::value, std::optional< T > const & > + std::enable_if_t< is_limitable< U >::value, std::optional< T > const & > getMaxValue() const { return m_limits.maxValue; From 4c493186beb0a7ae4c99bfb8c8ada943fc5678e3 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 11:44:37 +0200 Subject: [PATCH 04/16] add limits validation for input values --- src/coreComponents/dataRepository/Wrapper.hpp | 39 +++++++++++++++++++ 1 file changed, 39 insertions(+) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 2d63595511d..5deaf5a4389 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -21,6 +21,8 @@ #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" @@ -816,6 +818,11 @@ class Wrapper final : public WrapperBase targetNode, getDefaultValueStruct() ); } + + if( m_successfulReadFromInput ) + { + validateLimits(); + } } catch( std::exception const & ex ) { @@ -1153,6 +1160,38 @@ class Wrapper final : public WrapperBase return this->packByIndexImpl< false >( dummy, packList, withMetadata, onDevice, events ); } + template< typename U=T > + std::enable_if_t< is_limitable< U >::value, void > + validateLimits() + { + if( !m_limits.minValue.has_value() && !m_limits.maxValue.has_value() ) + { + return; + } + + T const & value = reference(); + bool const belowMin = m_limits.minValue.has_value() && ( value < *m_limits.minValue ); + bool const aboveMax = m_limits.maxValue.has_value() && ( value > *m_limits.maxValue ); + if( !belowMin && !aboveMax ) + { + return; + } + + // TODO: show the allowed range in the message + string const msg = GEOS_FMT( "Attribute '{}' has value '{}' outside of the allowed range.", + getName(), value ); + + // TODO: set different output strategies (log, warn, error) + GEOS_LOG_RANK_0( msg ); + } + + template< typename U=T > + std::enable_if_t< !is_limitable< U >::value, void > + validateLimits() + { + /* no-op */ + } + /// 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; From 19d2ae890b5b1fda79f7241d841ed4d00f30a76c Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 16:00:58 +0200 Subject: [PATCH 05/16] add limits modes to limits --- .../dataRepository/AttributeLimits.hpp | 22 +++++++++++++++++++ .../dataRepository/WrapperBase.cpp | 2 ++ .../dataRepository/WrapperBase.hpp | 13 +++++++++++ 3 files changed, 37 insertions(+) diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp index 8cc1dd6a61d..0cfef2ba425 100644 --- a/src/coreComponents/dataRepository/AttributeLimits.hpp +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -17,6 +17,8 @@ #ifndef GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ #define GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ +#include "common/DataTypes.hpp" +#include "common/format/EnumStrings.hpp" #include #include @@ -26,6 +28,26 @@ 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 is_limitable * @tparam T type to check 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; From fd14de46ff3b456ad2f2b68a93184ccf76db9873 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 16:01:49 +0200 Subject: [PATCH 06/16] implement limits modes in validateLimits() --- src/coreComponents/dataRepository/Wrapper.hpp | 23 +++++++++++++++---- 1 file changed, 19 insertions(+), 4 deletions(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 5deaf5a4389..dbfd06f9178 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -756,10 +756,12 @@ class Wrapper final : public WrapperBase template< typename U=T > std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > setLimits( T const & minValue, - T const & maxValue ) + T const & maxValue, + LimitsMode mode = LimitsMode::Warning ) { m_limits.minValue = minValue; m_limits.maxValue = maxValue; + m_limitsMode = mode; return *this; } @@ -1164,7 +1166,8 @@ class Wrapper final : public WrapperBase std::enable_if_t< is_limitable< U >::value, void > validateLimits() { - if( !m_limits.minValue.has_value() && !m_limits.maxValue.has_value() ) + if( (!m_limits.minValue.has_value() && !m_limits.maxValue.has_value()) || + m_limitsMode == LimitsMode::Indicative ) { return; } @@ -1181,8 +1184,20 @@ class Wrapper final : public WrapperBase string const msg = GEOS_FMT( "Attribute '{}' has value '{}' outside of the allowed range.", getName(), value ); - // TODO: set different output strategies (log, warn, error) - GEOS_LOG_RANK_0( msg ); + 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; + } } template< typename U=T > From 32f158602308e66dc2222a834e170a159bb417f9 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 16:35:03 +0200 Subject: [PATCH 07/16] remove setMinValue() and setMaxValue() setLimits() has the same capabilities and should be the only one kept. --- src/coreComponents/dataRepository/Wrapper.hpp | 47 +++++++++---------- 1 file changed, 21 insertions(+), 26 deletions(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index dbfd06f9178..f2e7d249028 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -41,6 +41,7 @@ // System includes #include +#include #include namespace geos @@ -722,46 +723,40 @@ class Wrapper final : public WrapperBase } /** - * @brief Set a minimum bound for this attribute's value. + * @brief Set both bounds for this attribute's value. * @param minValue the minimum allowed value (inclusive) - * @return pointer to Wrapper - */ - template< typename U=T > - std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > - setMinValue( T const & minValue ) - { - m_limits.minValue = minValue; - return *this; - } - - /** - * @brief Set a maximum bound for this attribute's value. * @param maxValue the maximum allowed value (inclusive) + * @param mode the enforcement mode * @return pointer to Wrapper + * + * @note @p minValue and @p maxValue are std::optional(s). + * Set them to std::nullopt to disable a limit. + * + * @code + * registerWrapper( viewKeysStruct::fooString(), &m_foo ) + * .setLimits( 0.0, std::nullopt ) // sets a minimum value of 0.0 and no maximum value + * @endcode */ template< typename U=T > std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > - setMaxValue( T const & maxValue ) + setLimits( std::optional< T > minValue, + std::optional< T > maxValue, + LimitsMode mode = LimitsMode::Warning ) { + m_limits.minValue = minValue; m_limits.maxValue = maxValue; + m_limitsMode = mode; return *this; } - /** - * @brief Set both bounds for this attribute's value. - * @param minValue the minimum allowed value (inclusive) - * @param maxValue the maximum allowed value (inclusive) - * @return pointer to Wrapper - */ template< typename U=T > - std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > - setLimits( T const & minValue, - T const & maxValue, + std::enable_if_t< !is_limitable< U >::value, Wrapper< T > & > + setLimits( std::optional< T >, + std::optional< T >, LimitsMode mode = LimitsMode::Warning ) { - m_limits.minValue = minValue; - m_limits.maxValue = maxValue; - m_limitsMode = mode; + static_assert( is_limitable< U >::value, + "setLimits is only supported on scalar arithmetic types." ); return *this; } From 796bb16be8c28afaa92d81e8abd6911a28a005c2 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 16:41:48 +0200 Subject: [PATCH 08/16] add convenience alias is_limitable_v --- .../dataRepository/AttributeLimits.hpp | 8 +++++++- src/coreComponents/dataRepository/Wrapper.hpp | 14 +++++++------- 2 files changed, 14 insertions(+), 8 deletions(-) diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp index 0cfef2ba425..c3324eb2459 100644 --- a/src/coreComponents/dataRepository/AttributeLimits.hpp +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -61,6 +61,12 @@ struct is_limitable static constexpr bool value = std::is_arithmetic< 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 Limits * @brief Storage for the optional min/max bounds of a wrapped value. @@ -69,7 +75,7 @@ struct is_limitable * for limitable types. Preventing instantiation non-limitable types, especially * abstract types that can't be instantiated with std::optional< absT >. */ -template< typename T, bool = is_limitable< T >::value > +template< typename T, bool = is_limitable_v< T > > struct Limits {}; diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index f2e7d249028..73b72265056 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -738,7 +738,7 @@ class Wrapper final : public WrapperBase * @endcode */ template< typename U=T > - std::enable_if_t< is_limitable< U >::value, Wrapper< T > & > + std::enable_if_t< is_limitable_v< U >, Wrapper< T > & > setLimits( std::optional< T > minValue, std::optional< T > maxValue, LimitsMode mode = LimitsMode::Warning ) @@ -750,12 +750,12 @@ class Wrapper final : public WrapperBase } template< typename U=T > - std::enable_if_t< !is_limitable< U >::value, Wrapper< T > & > + std::enable_if_t< !is_limitable_v< U >, Wrapper< T > & > setLimits( std::optional< T >, std::optional< T >, LimitsMode mode = LimitsMode::Warning ) { - static_assert( is_limitable< U >::value, + static_assert( is_limitable_v< U >, "setLimits is only supported on scalar arithmetic types." ); return *this; } @@ -766,7 +766,7 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< is_limitable< U >::value, std::optional< T > const & > + std::enable_if_t< is_limitable_v< U >, std::optional< T > const & > getMinValue() const { return m_limits.minValue; @@ -778,7 +778,7 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< is_limitable< U >::value, std::optional< T > const & > + std::enable_if_t< is_limitable_v< U >, std::optional< T > const & > getMaxValue() const { return m_limits.maxValue; @@ -1158,7 +1158,7 @@ class Wrapper final : public WrapperBase } template< typename U=T > - std::enable_if_t< is_limitable< U >::value, void > + std::enable_if_t< is_limitable_v< U >, void > validateLimits() { if( (!m_limits.minValue.has_value() && !m_limits.maxValue.has_value()) || @@ -1196,7 +1196,7 @@ class Wrapper final : public WrapperBase } template< typename U=T > - std::enable_if_t< !is_limitable< U >::value, void > + std::enable_if_t< !is_limitable_v< U >, void > validateLimits() { /* no-op */ From 14bda7b78c7a21f50293ca484483fdde894b1f36 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 13 May 2026 16:51:58 +0200 Subject: [PATCH 09/16] modify outside range message --- src/coreComponents/dataRepository/Wrapper.hpp | 13 ++++++------- 1 file changed, 6 insertions(+), 7 deletions(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 73b72265056..4f5aca3a1e8 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -732,10 +732,10 @@ class Wrapper final : public WrapperBase * @note @p minValue and @p maxValue are std::optional(s). * Set them to std::nullopt to disable a limit. * - * @code + * @code * registerWrapper( viewKeysStruct::fooString(), &m_foo ) * .setLimits( 0.0, std::nullopt ) // sets a minimum value of 0.0 and no maximum value - * @endcode + * @endcode */ template< typename U=T > std::enable_if_t< is_limitable_v< U >, Wrapper< T > & > @@ -1162,7 +1162,7 @@ class Wrapper final : public WrapperBase validateLimits() { if( (!m_limits.minValue.has_value() && !m_limits.maxValue.has_value()) || - m_limitsMode == LimitsMode::Indicative ) + m_limitsMode == LimitsMode::Indicative ) { return; } @@ -1175,9 +1175,8 @@ class Wrapper final : public WrapperBase return; } - // TODO: show the allowed range in the message - string const msg = GEOS_FMT( "Attribute '{}' has value '{}' outside of the allowed range.", - getName(), value ); + string const msg = GEOS_FMT( "Value {} for attribute '{}' is outside the allowed range.", + value, getName() ); switch( m_limitsMode ) { @@ -1188,7 +1187,7 @@ class Wrapper final : public WrapperBase case LimitsMode::Error: GEOS_THROW( msg, InputError ); break; - + default: GEOS_LOG_RANK_0( "Unimplemented LimitsMode" ); break; From 581b6d8d29db200b2b656e7503a1f59e9645d331 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Wed, 20 May 2026 10:17:59 +0200 Subject: [PATCH 10/16] add inclusive-exclusive properties using a Bound type --- .../dataRepository/AttributeLimits.hpp | 83 ++++++++++++++++++- src/coreComponents/dataRepository/Wrapper.hpp | 35 ++++---- 2 files changed, 99 insertions(+), 19 deletions(-) diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp index c3324eb2459..fa60c71599b 100644 --- a/src/coreComponents/dataRepository/AttributeLimits.hpp +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -67,12 +67,58 @@ struct is_limitable 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 non-limitable types, especially + * 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 > > @@ -82,10 +128,41 @@ struct Limits template< typename T > struct Limits< T, true > { - std::optional< T > minValue; - std::optional< T > maxValue; + std::optional< Bound< T > > min; + std::optional< Bound< 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 */ diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 4f5aca3a1e8..81d8b2e54be 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -724,35 +724,38 @@ class Wrapper final : public WrapperBase /** * @brief Set both bounds for this attribute's value. - * @param minValue the minimum allowed value (inclusive) - * @param maxValue the maximum allowed value (inclusive) + * @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 minValue and @p maxValue are std::optional(s). + * @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< T > minValue, - std::optional< T > maxValue, + setLimits( std::optional< Bound< T > > min, + std::optional< Bound< T > > max, LimitsMode mode = LimitsMode::Warning ) { - m_limits.minValue = minValue; - m_limits.maxValue = maxValue; + 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 >, Wrapper< T > & > - setLimits( std::optional< T >, - std::optional< T >, + setLimits( std::optional< Bound< T > >, + std::optional< Bound< T > >, LimitsMode mode = LimitsMode::Warning ) { static_assert( is_limitable_v< U >, @@ -766,10 +769,10 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< is_limitable_v< U >, std::optional< T > const & > + std::enable_if_t< is_limitable_v< U >, std::optional< Bound< T > > const & > getMinValue() const { - return m_limits.minValue; + return m_limits.min; } /** @@ -778,10 +781,10 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< is_limitable_v< U >, std::optional< T > const & > + std::enable_if_t< is_limitable_v< U >, std::optional< Bound< T > > const & > getMaxValue() const { - return m_limits.maxValue; + return m_limits.max; } virtual bool processInputFile( xmlWrapper::xmlNode const & targetNode, @@ -1161,15 +1164,15 @@ class Wrapper final : public WrapperBase std::enable_if_t< is_limitable_v< U >, void > validateLimits() { - if( (!m_limits.minValue.has_value() && !m_limits.maxValue.has_value()) || + if( (!m_limits.min.has_value() && !m_limits.max.has_value()) || m_limitsMode == LimitsMode::Indicative ) { return; } T const & value = reference(); - bool const belowMin = m_limits.minValue.has_value() && ( value < *m_limits.minValue ); - bool const aboveMax = m_limits.maxValue.has_value() && ( value > *m_limits.maxValue ); + 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; From 7436f3c1eece9c34b1d9a2be8e455454dcc9d32d Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 22 May 2026 15:50:33 +0200 Subject: [PATCH 11/16] support arrays of numbers --- .../dataRepository/AttributeLimits.hpp | 42 +++++++++++++-- src/coreComponents/dataRepository/Wrapper.hpp | 54 +++++++++++++------ 2 files changed, 75 insertions(+), 21 deletions(-) diff --git a/src/coreComponents/dataRepository/AttributeLimits.hpp b/src/coreComponents/dataRepository/AttributeLimits.hpp index fa60c71599b..fdc2a114e1e 100644 --- a/src/coreComponents/dataRepository/AttributeLimits.hpp +++ b/src/coreComponents/dataRepository/AttributeLimits.hpp @@ -17,6 +17,7 @@ #ifndef GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ #define GEOS_DATAREPOSITORY_ATTRIBUTELIMITS_HPP_ +#include "codingUtilities/traits.hpp" #include "common/DataTypes.hpp" #include "common/format/EnumStrings.hpp" #include @@ -48,17 +49,50 @@ ENUM_STRINGS( LimitsMode, "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 scalar numeric types (integer, real32, real64, etc.) + * 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< T >::value; + static constexpr bool value = std::is_arithmetic< limit_value_type_t< T > >::value; }; /** @@ -128,8 +162,8 @@ struct Limits template< typename T > struct Limits< T, true > { - std::optional< Bound< T > > min; - std::optional< Bound< T > > max; + std::optional< Bound< limit_value_type_t< T > > > min; + std::optional< Bound< limit_value_type_t< T > > > max; }; diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 81d8b2e54be..573ffa5acf3 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -742,8 +742,8 @@ class Wrapper final : public WrapperBase */ template< typename U=T > std::enable_if_t< is_limitable_v< U >, Wrapper< T > & > - setLimits( std::optional< Bound< T > > min, - std::optional< Bound< T > > max, + setLimits( std::optional< Bound< limit_value_type_t< T > > > min, + std::optional< Bound< limit_value_type_t< T > > > max, LimitsMode mode = LimitsMode::Warning ) { m_limits.min = min; @@ -753,9 +753,9 @@ class Wrapper final : public WrapperBase } template< typename U=T > - std::enable_if_t< !is_limitable_v< U >, Wrapper< T > & > - setLimits( std::optional< Bound< T > >, - std::optional< Bound< 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::Warning ) { static_assert( is_limitable_v< U >, @@ -769,7 +769,7 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< is_limitable_v< U >, std::optional< Bound< T > > const & > + std::enable_if_t< is_limitable_v< U >, std::optional< Bound< limit_value_type_t< T > > > const & > getMinValue() const { return m_limits.min; @@ -781,7 +781,7 @@ class Wrapper final : public WrapperBase * @note Only available when T is a limitable type */ template< typename U=T > - std::enable_if_t< is_limitable_v< U >, std::optional< Bound< T > > const & > + std::enable_if_t< is_limitable_v< U >, std::optional< Bound< limit_value_type_t< T > > > const & > getMaxValue() const { return m_limits.max; @@ -1160,17 +1160,9 @@ class Wrapper final : public WrapperBase return this->packByIndexImpl< false >( dummy, packList, withMetadata, onDevice, events ); } - template< typename U=T > - std::enable_if_t< is_limitable_v< U >, void > - validateLimits() + template< typename V > + void validateLimitValue( V const & value ) const { - if( (!m_limits.min.has_value() && !m_limits.max.has_value()) || - m_limitsMode == LimitsMode::Indicative ) - { - return; - } - - T const & value = reference(); 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 ) @@ -1197,6 +1189,34 @@ class Wrapper final : public WrapperBase } } + 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() From a456acae4030c84871a42e5cc77508c9e278970d Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 22 May 2026 16:08:09 +0200 Subject: [PATCH 12/16] set validateLimit() to public --- src/coreComponents/dataRepository/Wrapper.hpp | 71 ++++++++++--------- 1 file changed, 36 insertions(+), 35 deletions(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 573ffa5acf3..5b12d14ca77 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -787,6 +787,42 @@ class Wrapper final : public WrapperBase 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 { @@ -1189,41 +1225,6 @@ class Wrapper final : public WrapperBase } } - 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 */ - } - /// 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; From 80a95418ce9ef5b9ae0c5a1bf7540ca3eb672fae Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 22 May 2026 16:15:28 +0200 Subject: [PATCH 13/16] add tests --- .../dataRepository/unitTests/testWrapper.cpp | 188 ++++++++++++++++++ 1 file changed, 188 insertions(+) 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() ); +} From 65779e4d20dc6a48e35b32bb8a4a02f6d0a8b249 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 22 May 2026 16:58:49 +0200 Subject: [PATCH 14/16] add getDataContext() to the limit validation message --- src/coreComponents/dataRepository/Wrapper.hpp | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 5b12d14ca77..75f178d47b2 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -1207,7 +1207,7 @@ class Wrapper final : public WrapperBase } string const msg = GEOS_FMT( "Value {} for attribute '{}' is outside the allowed range.", - value, getName() ); + value, getDataContext() ); switch( m_limitsMode ) { From 54053c7915548304a7f171503b4d98767b1fe929 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 22 May 2026 17:34:25 +0200 Subject: [PATCH 15/16] add range to the limit validation message This part may be refactored, to build the range string somewhere else --- src/coreComponents/dataRepository/Wrapper.hpp | 10 ++++++++-- 1 file changed, 8 insertions(+), 2 deletions(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 75f178d47b2..2b006f3ca0b 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -1206,8 +1206,14 @@ class Wrapper final : public WrapperBase return; } - string const msg = GEOS_FMT( "Value {} for attribute '{}' is outside the allowed range.", - value, getDataContext() ); + 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 ) { From 7d08bd2d6fd68ef81b728296b884b2b85552b746 Mon Sep 17 00:00:00 2001 From: kdrienCG Date: Fri, 22 May 2026 17:35:31 +0200 Subject: [PATCH 16/16] set default mode to Error --- src/coreComponents/dataRepository/Wrapper.hpp | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/coreComponents/dataRepository/Wrapper.hpp b/src/coreComponents/dataRepository/Wrapper.hpp index 2b006f3ca0b..cef73f75543 100644 --- a/src/coreComponents/dataRepository/Wrapper.hpp +++ b/src/coreComponents/dataRepository/Wrapper.hpp @@ -744,7 +744,7 @@ class Wrapper final : public WrapperBase 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::Warning ) + LimitsMode mode = LimitsMode::Error ) { m_limits.min = min; m_limits.max = max; @@ -756,7 +756,7 @@ class Wrapper final : public WrapperBase 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::Warning ) + LimitsMode mode = LimitsMode::Error ) { static_assert( is_limitable_v< U >, "setLimits is only supported on scalar arithmetic types." );