From 5a04a6bb9785fc43a2139e0111db56ae678b818f Mon Sep 17 00:00:00 2001 From: BenPinet Date: Thu, 1 Oct 2026 14:07:28 +0200 Subject: [PATCH 1/3] feat(TemporalAttributes): add possibility to time attributes --- bindings/python/src/basic/attribute.cpp | 3 +- .../python/src/basic/attribute_manager.cpp | 50 +++++- .../python/tests/basic/test-py-attribute.py | 32 ++++ include/geode/basic/attribute_manager.hpp | 57 ++++++ include/geode/basic/attribute_time_series.hpp | 104 +++++++++++ include/geode/basic/attribute_utils.hpp | 24 +++ src/geode/basic/CMakeLists.txt | 1 + src/geode/basic/attribute_manager.cpp | 98 +++++++++++ tests/basic/test-attribute.cpp | 163 ++++++++++++++++++ 9 files changed, 530 insertions(+), 2 deletions(-) create mode 100644 include/geode/basic/attribute_time_series.hpp diff --git a/bindings/python/src/basic/attribute.cpp b/bindings/python/src/basic/attribute.cpp index 6a8273057..60ca44ceb 100644 --- a/bindings/python/src/basic/attribute.cpp +++ b/bindings/python/src/basic/attribute.cpp @@ -83,7 +83,8 @@ namespace geode .def_readwrite( "assignable", &AttributeProperties::assignable ) .def_readwrite( "interpolable", &AttributeProperties::interpolable ) .def_readwrite( - "transferable", &AttributeProperties::transferable ); + "transferable", &AttributeProperties::transferable ) + .def_readwrite( "time", &AttributeProperties::time ); python_attribute_values_class< bool >( module, "Bool" ); python_attribute_values_class< int >( module, "Int" ); diff --git a/bindings/python/src/basic/attribute_manager.cpp b/bindings/python/src/basic/attribute_manager.cpp index 7b011f2bb..fc1bd5c49 100644 --- a/bindings/python/src/basic/attribute_manager.cpp +++ b/bindings/python/src/basic/attribute_manager.cpp @@ -24,6 +24,7 @@ #include "../common.hpp" #include +#include #include #include #include @@ -96,10 +97,46 @@ namespace geode static_cast< std::shared_ptr< SparseAttribute< type > > ( AttributeManager::* )( const geode::uuid& ) >( &AttributeManager::find_attribute< SparseAttribute, type > ) ); + const auto create_time_step_constant_suffix = + absl::StrCat( "create_time_step_attribute_constant_", suffix ); + manager.def( create_time_step_constant_suffix.c_str(), + &AttributeManager::create_time_step_attribute< ConstantAttribute, + type > ); + const auto create_time_step_variable_suffix = + absl::StrCat( "create_time_step_attribute_variable_", suffix ); + manager.def( create_time_step_variable_suffix.c_str(), + &AttributeManager::create_time_step_attribute< VariableAttribute, + type > ); + const auto create_time_step_sparse_suffix = + absl::StrCat( "create_time_step_attribute_sparse_", suffix ); + manager.def( create_time_step_sparse_suffix.c_str(), + &AttributeManager::create_time_step_attribute< SparseAttribute, + type > ); + } + + template < typename type > + void python_attribute_time_series_class( + pybind11::module& module, const std::string& typestr ) + { + const auto name = absl::StrCat( "AttributeTimeSeries", typestr ); + pybind11::class_< AttributeTimeSeries< type > >( module, name.c_str() ) + .def( + pybind11::init< const AttributeManager&, std::string_view >() ) + .def( "nb_time_steps", &AttributeTimeSeries< type >::nb_time_steps ) + .def( "time", &AttributeTimeSeries< type >::time ) + .def( "step_attribute", + &AttributeTimeSeries< type >::step_attribute, + pybind11::return_value_policy::reference_internal ) + .def( "value", &AttributeTimeSeries< type >::value ) + .def( "element_values", + &AttributeTimeSeries< type >::element_values ); } void define_attribute_manager( pybind11::module& module ) { + pybind11::class_< AttributeTimeStep >( module, "AttributeTimeStep" ) + .def_readonly( "time", &AttributeTimeStep::time ) + .def_readonly( "attribute_id", &AttributeTimeStep::attribute_id ); pybind11::class_< AttributeManager > manager( module, "AttributeManager" ); manager.def( pybind11::init<>() ) @@ -119,7 +156,9 @@ namespace geode &AttributeManager::set_attribute_properties ) .def( "delete_elements", &AttributeManager::delete_elements ) .def( "attribute_ids_matching_name", - &AttributeManager::attribute_ids_matching_name ); + &AttributeManager::attribute_ids_matching_name ) + .def( "time_steps", &AttributeManager::time_steps ) + .def( "time_series_names", &AttributeManager::time_series_names ); python_attribute_class< bool >( manager, "bool" ); python_attribute_class< int >( manager, "int" ); python_attribute_class< unsigned int >( manager, "uint" ); @@ -129,5 +168,14 @@ namespace geode manager, "arraydouble2" ); python_attribute_class< std::array< double, 3 > >( manager, "arraydouble3" ); + python_attribute_time_series_class< bool >( module, "Bool" ); + python_attribute_time_series_class< int >( module, "Int" ); + python_attribute_time_series_class< unsigned int >( module, "UInt" ); + python_attribute_time_series_class< float >( module, "Float" ); + python_attribute_time_series_class< double >( module, "Double" ); + python_attribute_time_series_class< std::array< double, 2 > >( + module, "ArrayDouble2" ); + python_attribute_time_series_class< std::array< double, 3 > >( + module, "ArrayDouble3" ); } } // namespace geode diff --git a/bindings/python/tests/basic/test-py-attribute.py b/bindings/python/tests/basic/test-py-attribute.py index 3fba29304..87ebe3f64 100644 --- a/bindings/python/tests/basic/test-py-attribute.py +++ b/bindings/python/tests/basic/test-py-attribute.py @@ -156,6 +156,37 @@ def test_sparse_attribute_after_element_deletion(manager, double_attribute_id): raise ValueError("Element 7 of sparse attribute should be 12 ") +def test_time_series(): + manager = basic.AttributeManager() + manager.resize(4) + values = basic.AttributeValuesDouble() + values.default_value = 0 + values.no_value = 0 + for time in [10.0, 0.0, 5.0]: + properties = basic.AttributeProperties() + if properties.time is not None: + raise ValueError("[Test] Default properties should have no time") + attribute_id = manager.create_time_step_attribute_variable_double( + "pressure", time, values, properties + ) + attribute = manager.find_attribute_variable_double(attribute_id) + attribute.set_value(2, 100 * time) + if attribute.properties().time != time: + raise ValueError("[Test] Step should hold its time") + if manager.time_series_names() != ["pressure"]: + raise ValueError("[Test] Wrong time series names") + steps = manager.time_steps("pressure") + if [step.time for step in steps] != [0.0, 5.0, 10.0]: + raise ValueError("[Test] Steps should be sorted by time") + series = basic.AttributeTimeSeriesDouble(manager, "pressure") + if series.nb_time_steps() != 3 or series.time(1) != 5.0: + raise ValueError("[Test] Wrong time series steps") + if series.element_values(2) != [0.0, 500.0, 1000.0]: + raise ValueError("[Test] Wrong element values") + if series.value(1, 2) != 500.0 or series.step_attribute(1).value(2) != 500.0: + raise ValueError("[Test] Wrong step value") + + if __name__ == '__main__': manager = basic.AttributeManager() manager.resize(10) @@ -177,3 +208,4 @@ def test_sparse_attribute_after_element_deletion(manager, double_attribute_id): raise ValueError("[Test] Manager should have 10 elements") manager.clear() test_number_of_attributes(manager, 0) + test_time_series() diff --git a/include/geode/basic/attribute_manager.hpp b/include/geode/basic/attribute_manager.hpp index f460e6dda..a2441ec23 100644 --- a/include/geode/basic/attribute_manager.hpp +++ b/include/geode/basic/attribute_manager.hpp @@ -39,6 +39,16 @@ namespace geode { + /*! + * One step of a time series: the attribute holding the values at the + * given time. + */ + struct AttributeTimeStep + { + double time; + uuid attribute_id; + }; + /*! * This class manages all its associated Attributes. * Each Attribute is registered and can be retrieved by a given name. @@ -146,6 +156,41 @@ namespace geode return attribute_id; } + /*! + * Create one step of a time series. + * A time series is the set of attributes sharing a name and having a + * time in their AttributeProperties. + * @param[in] attribute_name The name of the series. + * @param[in] time The time of this step, stored in properties.time. + * @exception OpenGeodeException if time is not finite, if an + * attribute with this name has no time, a different type or the same + * time. + */ + template < template < typename > class Attribute, typename T > + [[nodiscard]] geode::uuid create_time_step_attribute( + std::string_view attribute_name, + double time, + AttributeValues< T > default_values, + AttributeProperties properties ) + { + check_new_time_step( attribute_name, time, typeid( T ).name() ); + properties.time = time; + return create_attribute< Attribute, T >( attribute_name, + std::move( default_values ), std::move( properties ) ); + } + + /*! + * Get the steps of the time series with the given name, sorted by + * time. Empty if no attribute with this name has a time. + */ + [[nodiscard]] std::vector< AttributeTimeStep > time_steps( + std::string_view attribute_name ) const; + + /*! + * Get the distinct names of the attributes having a time. + */ + [[nodiscard]] std::vector< std::string > time_series_names() const; + /*! * Resize all the attributes to the given size * @param[in] size The new attribute size @@ -223,6 +268,11 @@ namespace geode [[nodiscard]] std::string_view attribute_type( const geode::uuid& ) const; + /*! + * Replace all the properties of the attribute. + * @warning The time is replaced too: passing properties with an empty + * time removes the attribute from its time series. + */ void set_attribute_properties( geode::uuid attribute_id, const AttributeProperties& new_properties ); @@ -295,6 +345,13 @@ namespace geode void register_attribute( std::shared_ptr< AttributeBase > attribute, const geode::uuid& ); + /*! + * Check that a new step can be added to the time series. + */ + void check_new_time_step( std::string_view attribute_name, + double time, + std::string_view type ) const; + private: IMPLEMENTATION_MEMBER( impl_ ); }; diff --git a/include/geode/basic/attribute_time_series.hpp b/include/geode/basic/attribute_time_series.hpp new file mode 100644 index 000000000..8f2c071b3 --- /dev/null +++ b/include/geode/basic/attribute_time_series.hpp @@ -0,0 +1,104 @@ +/* + * Copyright (c) 2019 - 2026 Geode-solutions + * + * Permission is hereby granted, free of charge, to any person obtaining a copy + * of this software and associated documentation files (the "Software"), to deal + * in the Software without restriction, including without limitation the rights + * to use, copy, modify, merge, publish, distribute, sublicense, and/or sell + * copies of the Software, and to permit persons to whom the Software is + * furnished to do so, subject to the following conditions: + * + * The above copyright notice and this permission notice shall be included in + * all copies or substantial portions of the Software. + * + * THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR + * IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, + * FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE + * AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER + * LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, + * OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE + * SOFTWARE. + * + */ + +#pragma once + +#include +#include +#include + +#include +#include +#include + +namespace geode +{ + /*! + * Read-only access to the steps of a time series stored in an + * AttributeManager (see AttributeManager::create_time_step_attribute). + * This is a snapshot taken at construction: steps created later are not + * seen, and steps deleted later stay readable. + */ + template < typename T > + class AttributeTimeSeries + { + public: + AttributeTimeSeries( + const AttributeManager& manager, std::string_view name ) + { + const auto steps = manager.time_steps( name ); + times_.reserve( steps.size() ); + attributes_.reserve( steps.size() ); + for( const auto& step : steps ) + { + OpenGeodeBasicException::check_exception( + times_.empty() || times_.back() < step.time, nullptr, + OpenGeodeException::TYPE::data, + "[AttributeTimeSeries] Time series '", name, + "' has several steps at time ", step.time ); + times_.push_back( step.time ); + attributes_.push_back( manager.find_read_only_attribute< T >( + step.attribute_id ) ); + } + } + + [[nodiscard]] index_t nb_time_steps() const + { + return static_cast< index_t >( times_.size() ); + } + + [[nodiscard]] double time( index_t step ) const + { + return times_.at( step ); + } + + [[nodiscard]] const ReadOnlyAttribute< T >& step_attribute( + index_t step ) const + { + return *attributes_.at( step ); + } + + [[nodiscard]] const T& value( index_t step, index_t element ) const + { + return attributes_.at( step )->value( element ); + } + + /*! + * Get the values of one element at every step, in time order. + */ + [[nodiscard]] std::vector< T > element_values( index_t element ) const + { + std::vector< T > values; + values.reserve( attributes_.size() ); + for( const auto& attribute : attributes_ ) + { + values.push_back( attribute->value( element ) ); + } + return values; + } + + private: + std::vector< double > times_; + std::vector< std::shared_ptr< ReadOnlyAttribute< T > > > attributes_; + }; +} // namespace geode diff --git a/include/geode/basic/attribute_utils.hpp b/include/geode/basic/attribute_utils.hpp index 14d56e452..b08f213c2 100644 --- a/include/geode/basic/attribute_utils.hpp +++ b/include/geode/basic/attribute_utils.hpp @@ -23,8 +23,12 @@ #pragma once +#include + #include +#include + #include #include #include @@ -44,6 +48,14 @@ namespace geode struct AttributeProperties { AttributeProperties() = default; + AttributeProperties( bool is_assignable, + bool is_interpolable, + bool is_transferable = true ) + : assignable( is_assignable ), + interpolable( is_interpolable ), + transferable( is_transferable ) + { + } template < typename Archive > void serialize( Archive& serializer ) @@ -59,12 +71,24 @@ namespace geode archive.value1b( properties.assignable ); archive.value1b( properties.interpolable ); archive.value1b( properties.transferable ); + }, + []( Archive& archive, + AttributeProperties& properties ) { + archive.value1b( properties.assignable ); + archive.value1b( properties.interpolable ); + archive.value1b( properties.transferable ); + archive.ext( properties.time, + bitsery::ext::StdOptional{}, + []( Archive& archive2, double& value ) { + archive2.value8b( value ); + } ); } } } ); } bool assignable{ false }; bool interpolable{ false }; bool transferable{ true }; + std::optional< double > time; }; template < typename AttributeType > diff --git a/src/geode/basic/CMakeLists.txt b/src/geode/basic/CMakeLists.txt index 530c02228..048cb1ea5 100644 --- a/src/geode/basic/CMakeLists.txt +++ b/src/geode/basic/CMakeLists.txt @@ -53,6 +53,7 @@ add_geode_library( "algorithm.hpp" "assert.hpp" "attribute_manager.hpp" + "attribute_time_series.hpp" "attribute_utils.hpp" "attribute.hpp" "bitsery_archive.hpp" diff --git a/src/geode/basic/attribute_manager.cpp b/src/geode/basic/attribute_manager.cpp index 01f522105..5d6df836e 100644 --- a/src/geode/basic/attribute_manager.cpp +++ b/src/geode/basic/attribute_manager.cpp @@ -24,8 +24,10 @@ #include #include +#include #include +#include #include #include @@ -285,6 +287,84 @@ namespace geode return ids; } + void check_new_time_step( + std::string_view name, double time, std::string_view type ) const + { + absl::ReaderMutexLock lock{ mutex_ }; + OpenGeodeBasicException::check_exception( std::isfinite( time ), + nullptr, OpenGeodeException::TYPE::data, + "[AttributeManager::create_time_step_attribute] Time of " + "attribute '", + name, "' should be finite, not ", time ); + for( const auto &[attribute_id, attribute] : attributes_ ) + { + if( attribute->name() != name ) + { + continue; + } + const auto &step_time = attribute->properties().time; + OpenGeodeBasicException::check_exception( step_time.has_value(), + nullptr, OpenGeodeException::TYPE::data, + "[AttributeManager::create_time_step_attribute] Attribute " + "'", + name, "' already exists without time (id: ", + attribute_id.string(), ")." ); + OpenGeodeBasicException::check_exception( + attribute->type() == type, nullptr, + OpenGeodeException::TYPE::data, + "[AttributeManager::create_time_step_attribute] Time " + "series '", + name, "' already holds type ", attribute->type(), + ", cannot add a step of type ", type, "." ); + OpenGeodeBasicException::check_exception( + step_time.value() != time, nullptr, + OpenGeodeException::TYPE::data, + "[AttributeManager::create_time_step_attribute] Time " + "series '", + name, "' already has a step at time ", time, "." ); + } + } + + std::vector< AttributeTimeStep > time_steps( + std::string_view name ) const + { + absl::ReaderMutexLock lock{ mutex_ }; + std::vector< AttributeTimeStep > steps; + for( const auto &[attribute_id, attribute] : attributes_ ) + { + const auto &time = attribute->properties().time; + if( time && attribute->name() == name ) + { + steps.push_back( { time.value(), attribute_id } ); + } + } + absl::c_sort( steps, []( const AttributeTimeStep &lhs, + const AttributeTimeStep &rhs ) { + return lhs.time < rhs.time; + } ); + return steps; + } + + std::vector< std::string > time_series_names() const + { + absl::ReaderMutexLock lock{ mutex_ }; + std::vector< std::string > names; + absl::flat_hash_set< std::string_view > found_names; + for( const auto &[attribute_id, attribute] : attributes_ ) + { + const auto &name = attribute->name(); + if( !attribute->properties().time || !name ) + { + continue; + } + if( found_names.emplace( name.value() ).second ) + { + names.emplace_back( name.value() ); + } + } + return names; + } + void copy( const AttributeManager::Impl &attribute_manager_from, const AttributeBase::AttributeKey &key ) { @@ -617,6 +697,24 @@ namespace geode return impl_->attribute_ids_matching_name( name ); } + void AttributeManager::check_new_time_step( std::string_view attribute_name, + double time, + std::string_view type ) const + { + impl_->check_new_time_step( attribute_name, time, type ); + } + + std::vector< AttributeTimeStep > AttributeManager::time_steps( + std::string_view attribute_name ) const + { + return impl_->time_steps( attribute_name ); + } + + std::vector< std::string > AttributeManager::time_series_names() const + { + return impl_->time_series_names(); + } + void AttributeManager::import( const AttributeManager &attribute_manager, const GenericMapping< index_t > &old2new_mapping ) { diff --git a/tests/basic/test-attribute.cpp b/tests/basic/test-attribute.cpp index 8be35bc0e..c04c6eac8 100644 --- a/tests/basic/test-attribute.cpp +++ b/tests/basic/test-attribute.cpp @@ -22,11 +22,13 @@ */ #include +#include #include #include #include +#include #include #include #include @@ -844,6 +846,166 @@ void test_permutation( geode::AttributeManager& manager, double_attribute->value( 7 ) ); } +double pressure_value( double time, double element_tag ) +{ + return 100 * time + element_tag; +} + +void check_pressure_series( const geode::AttributeManager& manager, + const std::vector< double >& expected_times ) +{ + const geode::AttributeTimeSeries< double > pressure{ manager, "pressure" }; + geode::OpenGeodeBasicException::test( + pressure.nb_time_steps() == expected_times.size(), + "Wrong number of pressure steps: ", pressure.nb_time_steps() ); + for( const auto step : geode::Indices{ expected_times } ) + { + geode::OpenGeodeBasicException::test( + pressure.time( step ) == expected_times[step], + "Wrong time for pressure step ", step ); + } + const auto tag = manager.find_read_only_attribute< double >( + manager.attribute_ids_matching_name( "tag" ).value().front() ); + for( const auto element : geode::Range{ manager.nb_elements() } ) + { + const auto values = pressure.element_values( element ); + for( const auto step : geode::Indices{ values } ) + { + const auto expected = + pressure_value( pressure.time( step ), tag->value( element ) ); + geode::OpenGeodeBasicException::test( + values[step] == expected + && pressure.value( step, element ) == expected, + "Wrong pressure for element ", element, " at step ", step, ": ", + values[step], " instead of ", expected ); + } + } +} + +geode::AttributeManager create_time_series_manager() +{ + geode::AttributeManager manager; + manager.resize( 10 ); + auto tag = manager.find_attribute< geode::VariableAttribute, double >( + manager.create_attribute< geode::VariableAttribute, double >( + "tag", { -1, -1 }, {} ) ); + for( const auto element : geode::Range{ manager.nb_elements() } ) + { + tag->set_value( element, element ); + } + for( const auto time : { 0., 10., 5. } ) + { + auto step = manager.find_attribute< geode::VariableAttribute, double >( + manager + .create_time_step_attribute< geode::VariableAttribute, double >( + "pressure", time, { pressure_value( time, -1 ), 0 }, {} ) ); + for( const auto element : geode::Range{ manager.nb_elements() } ) + { + step->set_value( element, pressure_value( time, element ) ); + } + } + for( const auto time : { 1., 2. } ) + { + auto step = manager.find_attribute< geode::VariableAttribute, + std::array< double, 3 > >( + manager.create_time_step_attribute< geode::VariableAttribute, + std::array< double, 3 > >( + "velocity", time, { { 0, 0, 0 }, { 0, 0, 0 } }, {} ) ); + step->set_value( 3, { time, 0, 0 } ); + } + return manager; +} + +void test_time_series_creation( geode::AttributeManager& manager ) +{ + const auto steps = manager.time_steps( "pressure" ); + geode::OpenGeodeBasicException::test( + steps.size() == 3 && steps[0].time == 0 && steps[1].time == 5 + && steps[2].time == 10, + "Pressure steps should be sorted by time" ); + geode::OpenGeodeBasicException::test( + manager.time_steps( "tag" ).empty(), "Tag should have no time step" ); + const geode::AttributeTimeSeries< double > pressure{ manager, "pressure" }; + geode::OpenGeodeBasicException::test( + pressure.step_attribute( 2 ).properties().time == 10, + "Wrong step attribute" ); + const geode::AttributeTimeSeries< std::array< double, 3 > > velocity{ + manager, "velocity" + }; + const auto velocity_values = velocity.element_values( 3 ); + geode::OpenGeodeBasicException::test( velocity_values.size() == 2 + && velocity_values[0][0] == 1 + && velocity_values[1][0] == 2, + "Wrong velocity_values" ); + check_pressure_series( manager, { 0, 5, 10 } ); +} + +void test_time_series_element_edition( geode::AttributeManager& manager ) +{ + std::vector< geode::index_t > permutation{ 2, 1, 4, 6, 7, 8, 5, 9, 3, 0 }; + manager.permute_elements( permutation ); + check_pressure_series( manager, { 0, 5, 10 } ); + std::vector< bool > to_delete( manager.nb_elements(), false ); + to_delete[1] = true; + to_delete[6] = true; + manager.delete_elements( to_delete ); + check_pressure_series( manager, { 0, 5, 10 } ); + manager.resize( manager.nb_elements() + 2 ); + check_pressure_series( manager, { 0, 5, 10 } ); +} + +void test_time_series_transfer( const geode::AttributeManager& manager ) +{ + geode::AttributeManager copied; + copied.copy( manager ); + check_pressure_series( copied, { 0, 5, 10 } ); + + geode::AttributeManager imported; + imported.resize( 3 ); + geode::GenericMapping< geode::index_t > old2new_mapping; + old2new_mapping.map( 4, 0 ); + old2new_mapping.map( 2, 1 ); + old2new_mapping.map( 7, 2 ); + imported.import( manager, old2new_mapping ); + check_pressure_series( imported, { 0, 5, 10 } ); + geode::OpenGeodeBasicException::test( + imported.time_steps( "velocity" ).size() == 2, + "Velocity steps should be imported" ); +} + +void test_time_series_serialization( const geode::AttributeManager& manager ) +{ + std::stringstream stream; + geode::TContext context{}; + geode::register_basic_serialize_pcontext( std::get< 0 >( context ) ); + geode::Serializer archive{ context, stream }; + archive.object( manager ); + archive.adapter().flush(); + + geode::AttributeManager reloaded; + geode::TContext reload_context{}; + geode::register_basic_deserialize_pcontext( + std::get< 0 >( reload_context ) ); + geode::Deserializer unarchive{ reload_context, stream }; + unarchive.object( reloaded ); + geode::OpenGeodeBasicException::test( + unarchive.adapter().error() == bitsery::ReaderError::NoError, + "Error while reading time series" ); + check_pressure_series( reloaded, { 0, 5, 10 } ); + geode::OpenGeodeBasicException::test( + reloaded.time_steps( "velocity" ).size() == 2, + "Velocity steps should be reloaded" ); +} + +void test_time_series() +{ + auto manager = create_time_series_manager(); + test_time_series_creation( manager ); + test_time_series_element_edition( manager ); + test_time_series_transfer( manager ); + test_time_series_serialization( manager ); +} + void test() { geode::AttributeManager manager; @@ -895,6 +1057,7 @@ void test() test_double_array_attribute( manager ); manager.clear(); test_number_of_attributes( manager, 0 ); + test_time_series(); } OPENGEODE_TEST( "attribute" ) From 426fcf07c3ebc267b5913e32defecc499e036ced Mon Sep 17 00:00:00 2001 From: BenPinet <126688250+BenPinet@users.noreply.github.com> Date: Thu, 1 Oct 2026 12:34:37 +0000 Subject: [PATCH 2/3] Apply prepare changes --- bindings/python/src/basic/attribute.cpp | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/bindings/python/src/basic/attribute.cpp b/bindings/python/src/basic/attribute.cpp index 60ca44ceb..e6c8a64e8 100644 --- a/bindings/python/src/basic/attribute.cpp +++ b/bindings/python/src/basic/attribute.cpp @@ -82,8 +82,7 @@ namespace geode .def( pybind11::init<>() ) .def_readwrite( "assignable", &AttributeProperties::assignable ) .def_readwrite( "interpolable", &AttributeProperties::interpolable ) - .def_readwrite( - "transferable", &AttributeProperties::transferable ) + .def_readwrite( "transferable", &AttributeProperties::transferable ) .def_readwrite( "time", &AttributeProperties::time ); python_attribute_values_class< bool >( module, "Bool" ); From c2ad9fdf00b1ece6d8c068928c932bb31b47fd97 Mon Sep 17 00:00:00 2001 From: BenPinet Date: Thu, 1 Oct 2026 15:08:06 +0200 Subject: [PATCH 3/3] fix + comments --- include/geode/basic/attribute_time_series.hpp | 12 +++++++----- include/geode/basic/attribute_utils.hpp | 10 +--------- src/geode/basic/attribute_manager.cpp | 4 +++- 3 files changed, 11 insertions(+), 15 deletions(-) diff --git a/include/geode/basic/attribute_time_series.hpp b/include/geode/basic/attribute_time_series.hpp index 8f2c071b3..b72ecbbb1 100644 --- a/include/geode/basic/attribute_time_series.hpp +++ b/include/geode/basic/attribute_time_series.hpp @@ -27,6 +27,8 @@ #include #include +#include + #include #include #include @@ -86,13 +88,13 @@ namespace geode /*! * Get the values of one element at every step, in time order. */ - [[nodiscard]] std::vector< T > element_values( index_t element ) const + [[nodiscard]] absl::FixedArray< T > element_values( + index_t element ) const { - std::vector< T > values; - values.reserve( attributes_.size() ); - for( const auto& attribute : attributes_ ) + absl::FixedArray< T > values( attributes_.size() ); + for( const auto step : Indices{ attributes_ } ) { - values.push_back( attribute->value( element ) ); + values[step] = attributes_[step]->value( element ); } return values; } diff --git a/include/geode/basic/attribute_utils.hpp b/include/geode/basic/attribute_utils.hpp index b08f213c2..5bcd3a79e 100644 --- a/include/geode/basic/attribute_utils.hpp +++ b/include/geode/basic/attribute_utils.hpp @@ -48,14 +48,6 @@ namespace geode struct AttributeProperties { AttributeProperties() = default; - AttributeProperties( bool is_assignable, - bool is_interpolable, - bool is_transferable = true ) - : assignable( is_assignable ), - interpolable( is_interpolable ), - transferable( is_transferable ) - { - } template < typename Archive > void serialize( Archive& serializer ) @@ -88,7 +80,7 @@ namespace geode bool assignable{ false }; bool interpolable{ false }; bool transferable{ true }; - std::optional< double > time; + std::optional< double > time{}; }; template < typename AttributeType > diff --git a/src/geode/basic/attribute_manager.cpp b/src/geode/basic/attribute_manager.cpp index 5d6df836e..12ff1b7dd 100644 --- a/src/geode/basic/attribute_manager.cpp +++ b/src/geode/basic/attribute_manager.cpp @@ -296,8 +296,10 @@ namespace geode "[AttributeManager::create_time_step_attribute] Time of " "attribute '", name, "' should be finite, not ", time ); - for( const auto &[attribute_id, attribute] : attributes_ ) + for( const auto &id_attribute : attributes_ ) { + const auto &attribute_id = id_attribute.first; + const auto &attribute = id_attribute.second; if( attribute->name() != name ) { continue;