From 7694a5a962706084451d53dac4d5e2b05ec426b6 Mon Sep 17 00:00:00 2001 From: BenPinet Date: Wed, 21 Oct 2026 15:30:54 +0200 Subject: [PATCH 1/6] feat(ExternalTimeSeriesHelpers): store time series apart from the BRep Time series computed outside of a BRep (e.g. by a simulator) are stored per unique vertex and per Block polyhedron, saved/loaded in a native og_ets file and written on the Block meshes with apply_to. Co-Authored-By: Claude Opus 5.5 --- .../helpers/external_time_series_helpers.hpp | 118 ++++++++ .../io/external_time_series_helpers_input.hpp | 46 ++++ .../external_time_series_helpers_output.hpp | 48 ++++ src/geode/model/CMakeLists.txt | 6 + .../helpers/external_time_series_helpers.cpp | 255 ++++++++++++++++++ .../io/external_time_series_helpers_input.cpp | 72 +++++ .../external_time_series_helpers_output.cpp | 88 ++++++ tests/model/CMakeLists.txt | 7 + .../test-external-time-series-helpers.cpp | 211 +++++++++++++++ 9 files changed, 851 insertions(+) create mode 100644 include/geode/model/helpers/external_time_series_helpers.hpp create mode 100644 include/geode/model/representation/io/external_time_series_helpers_input.hpp create mode 100644 include/geode/model/representation/io/external_time_series_helpers_output.hpp create mode 100644 src/geode/model/helpers/external_time_series_helpers.cpp create mode 100644 src/geode/model/representation/io/external_time_series_helpers_input.cpp create mode 100644 src/geode/model/representation/io/external_time_series_helpers_output.cpp create mode 100644 tests/model/test-external-time-series-helpers.cpp diff --git a/include/geode/model/helpers/external_time_series_helpers.hpp b/include/geode/model/helpers/external_time_series_helpers.hpp new file mode 100644 index 000000000..5396918b3 --- /dev/null +++ b/include/geode/model/helpers/external_time_series_helpers.hpp @@ -0,0 +1,118 @@ +/* + * 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 + +namespace bitsery +{ + class Access; +} // namespace bitsery + +namespace geode +{ + class AttributeManager; + class BRep; + struct uuid; +} // namespace geode + +namespace geode +{ + /*! + * Time series computed outside of a BRep (e.g. by a simulator), stored + * apart from the model and written on it on demand. + * Vertex values are stored per unique vertex of the model and polyhedron + * values per polyhedron of each Block mesh. + * Time series are expected to be created as time step attributes (see + * AttributeManager::create_time_step_attribute), but any transferable + * attribute is stored and applied the same way. + */ + class opengeode_model_api ExternalTimeSeriesHelpers + { + OPENGEODE_DISABLE_COPY( ExternalTimeSeriesHelpers ); + + public: + ExternalTimeSeriesHelpers(); + /*! + * Create empty attribute managers sized to the model unique vertices + * and to the polyhedra of its Blocks. + */ + explicit ExternalTimeSeriesHelpers( const BRep& model ); + ExternalTimeSeriesHelpers( ExternalTimeSeriesHelpers&& other ) noexcept; + ExternalTimeSeriesHelpers& operator=( + ExternalTimeSeriesHelpers&& other ) noexcept; + ~ExternalTimeSeriesHelpers(); + + [[nodiscard]] static std::string_view native_extension_static() + { + return "og_ets"; + } + + [[nodiscard]] std::string_view native_extension() const + { + return native_extension_static(); + } + + /*! + * Attributes on the unique vertices of the model + */ + [[nodiscard]] AttributeManager& unique_vertex_attribute_manager(); + + [[nodiscard]] const AttributeManager& + unique_vertex_attribute_manager() const; + + /*! + * Attributes on the polyhedra of a Block mesh + * @exception OpenGeodeException if the Block is unknown + */ + [[nodiscard]] AttributeManager& block_polyhedron_attribute_manager( + const uuid& block_id ); + + [[nodiscard]] const AttributeManager& + block_polyhedron_attribute_manager( const uuid& block_id ) const; + + /*! + * Write the transferable attributes on the Block meshes of the model: + * unique vertex attributes on each Block vertex, polyhedron attributes + * on the matching Block polyhedra. Non-transferable attributes are + * skipped. Attribute ids and properties (time steps included) are + * kept. + * @exception OpenGeodeException if the model does not have the same + * unique vertices or Block polyhedra as the one used for creation + */ + void apply_to( BRep& model ) const; + + private: + friend class bitsery::Access; + template < typename Archive > + void serialize( Archive& archive ); + + private: + IMPLEMENTATION_MEMBER( impl_ ); + }; +} // namespace geode diff --git a/include/geode/model/representation/io/external_time_series_helpers_input.hpp b/include/geode/model/representation/io/external_time_series_helpers_input.hpp new file mode 100644 index 000000000..47f66c041 --- /dev/null +++ b/include/geode/model/representation/io/external_time_series_helpers_input.hpp @@ -0,0 +1,46 @@ +/* + * 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 + +namespace geode +{ + class ExternalTimeSeriesHelpers; +} // namespace geode + +namespace geode +{ + /*! + * Load an ExternalTimeSeriesHelpers from a native file + * (ExternalTimeSeriesHelpers::native_extension_static()). + * @param[in] filename Path to the file to load. + * @exception OpenGeodeException if the file has another extension or + * cannot be read + */ + [[nodiscard]] ExternalTimeSeriesHelpers opengeode_model_api + load_external_time_series_helpers( std::string_view filename ); +} // namespace geode diff --git a/include/geode/model/representation/io/external_time_series_helpers_output.hpp b/include/geode/model/representation/io/external_time_series_helpers_output.hpp new file mode 100644 index 000000000..5c778d222 --- /dev/null +++ b/include/geode/model/representation/io/external_time_series_helpers_output.hpp @@ -0,0 +1,48 @@ +/* + * 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 + +namespace geode +{ + class ExternalTimeSeriesHelpers; +} // namespace geode + +namespace geode +{ + /*! + * Save an ExternalTimeSeriesHelpers in a native file + * (ExternalTimeSeriesHelpers::native_extension_static()). + * @param[in] helpers ExternalTimeSeriesHelpers to save. + * @param[in] filename Path to the file where save the + * ExternalTimeSeriesHelpers. + * @exception OpenGeodeException if the file has another extension or + * cannot be written + */ + void opengeode_model_api save_external_time_series_helpers( + const ExternalTimeSeriesHelpers& helpers, std::string_view filename ); +} // namespace geode diff --git a/src/geode/model/CMakeLists.txt b/src/geode/model/CMakeLists.txt index a3d017bc4..bff448fc3 100644 --- a/src/geode/model/CMakeLists.txt +++ b/src/geode/model/CMakeLists.txt @@ -34,6 +34,7 @@ add_geode_library( "helpers/convert_model_meshes.cpp" "helpers/convert_to_mesh.cpp" "helpers/create_coordinate_system.cpp" + "helpers/external_time_series_helpers.cpp" "helpers/model_component_filter.cpp" "helpers/model_concatener.cpp" "helpers/model_coordinate_reference_system.cpp" @@ -97,6 +98,8 @@ add_geode_library( "representation/core/section.cpp" "representation/io/brep_input.cpp" "representation/io/brep_time_series_input.cpp" + "representation/io/external_time_series_helpers_input.cpp" + "representation/io/external_time_series_helpers_output.cpp" "representation/io/section_input.cpp" "representation/io/brep_output.cpp" "representation/io/section_output.cpp" @@ -117,6 +120,7 @@ add_geode_library( "helpers/convert_model_meshes.hpp" "helpers/convert_to_mesh.hpp" "helpers/create_coordinate_system.hpp" + "helpers/external_time_series_helpers.hpp" "helpers/model_component_filter.hpp" "helpers/model_concatener.hpp" "helpers/model_coordinate_reference_system.hpp" @@ -173,6 +177,8 @@ add_geode_library( "representation/core/mapping.hpp" "representation/io/brep_input.hpp" "representation/io/brep_time_series_input.hpp" + "representation/io/external_time_series_helpers_input.hpp" + "representation/io/external_time_series_helpers_output.hpp" "representation/io/section_input.hpp" "representation/io/brep_output.hpp" "representation/io/section_output.hpp" diff --git a/src/geode/model/helpers/external_time_series_helpers.cpp b/src/geode/model/helpers/external_time_series_helpers.cpp new file mode 100644 index 000000000..802cbb878 --- /dev/null +++ b/src/geode/model/helpers/external_time_series_helpers.cpp @@ -0,0 +1,255 @@ +/* + * 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. + * + */ + +#include + +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include + +namespace geode +{ + class ExternalTimeSeriesHelpers::Impl + { + friend class bitsery::Access; + + public: + Impl() = default; + + explicit Impl( const BRep& model ) + { + unique_vertex_attributes_.resize( model.nb_unique_vertices() ); + for( const auto& block : model.blocks() ) + { + block_polyhedron_attributes_[block.id()].resize( + block.mesh().nb_polyhedra() ); + } + } + + [[nodiscard]] AttributeManager& unique_vertex_attribute_manager() + { + return unique_vertex_attributes_; + } + + [[nodiscard]] const AttributeManager& + unique_vertex_attribute_manager() const + { + return unique_vertex_attributes_; + } + + [[nodiscard]] AttributeManager& block_polyhedron_attribute_manager( + const uuid& block_id ) + { + const auto it = block_polyhedron_attributes_.find( block_id ); + check_block( it != block_polyhedron_attributes_.end(), block_id ); + return it->second; + } + + [[nodiscard]] const AttributeManager& + block_polyhedron_attribute_manager( const uuid& block_id ) const + { + const auto it = block_polyhedron_attributes_.find( block_id ); + check_block( it != block_polyhedron_attributes_.end(), block_id ); + return it->second; + } + + void apply_to( const BRep& model ) const + { + check_model( model ); + for( const auto& block : model.blocks() ) + { + apply_vertices( model, block ); + } + for( const auto& [block_id, polyhedron_attributes] : + block_polyhedron_attributes_ ) + { + apply_polyhedra( + model.block( block_id ), polyhedron_attributes ); + } + } + + private: + template < typename Archive > + void serialize( Archive& archive ) + { + archive.ext( *this, + Growable< Archive, Impl >{ + { []( Archive& local_archive, Impl& impl ) { + local_archive.object( impl.unique_vertex_attributes_ ); + local_archive.ext( impl.block_polyhedron_attributes_, + bitsery::ext::StdMap{ + impl.block_polyhedron_attributes_.max_size() }, + []( Archive& map_archive, uuid& block_id, + AttributeManager& polyhedron_attributes ) { + map_archive.object( block_id ); + map_archive.object( polyhedron_attributes ); + } ); + } } } ); + } + + static void check_block( bool is_known, const uuid& block_id ) + { + OpenGeodeModelException::check_exception( is_known, nullptr, + OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers] Unknown Block ", + block_id.string() ); + } + + void check_model( const BRep& model ) const + { + OpenGeodeModelException::check_exception( + unique_vertex_attributes_.nb_elements() + == model.nb_unique_vertices(), + nullptr, OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] Model has ", + model.nb_unique_vertices(), " unique vertices instead of ", + unique_vertex_attributes_.nb_elements() ); + for( const auto& [block_id, polyhedron_attributes] : + block_polyhedron_attributes_ ) + { + check_block_polyhedra( model, block_id, polyhedron_attributes ); + } + } + + static void check_block_polyhedra( const BRep& model, + const uuid& block_id, + const AttributeManager& polyhedron_attributes ) + { + OpenGeodeModelException::check_exception( + model.has_block( block_id ), nullptr, + OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] Model has no Block ", + block_id.string() ); + const auto nb_polyhedra = + model.block( block_id ).mesh().nb_polyhedra(); + OpenGeodeModelException::check_exception( + nb_polyhedra == polyhedron_attributes.nb_elements(), nullptr, + OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] Block ", + block_id.string(), " has ", nb_polyhedra, + " polyhedra instead of ", polyhedron_attributes.nb_elements() ); + } + + void apply_vertices( const BRep& model, const Block3D& block ) const + { + const auto& mesh = block.mesh(); + GenericMapping< index_t > unique2block; + unique2block.reserve( mesh.nb_vertices() ); + ComponentMeshVertex block_vertex{ block.component_id(), 0 }; + for( const auto vertex : Range{ mesh.nb_vertices() } ) + { + block_vertex.vertex = vertex; + const auto unique_vertex = model.unique_vertex( block_vertex ); + if( unique_vertex != NO_ID ) + { + unique2block.map( unique_vertex, vertex ); + } + } + mesh.vertex_attribute_manager().import( + unique_vertex_attributes_, unique2block ); + } + + static void apply_polyhedra( const Block3D& block, + const AttributeManager& polyhedron_attributes ) + { + block.mesh().polyhedron_attribute_manager().copy( + polyhedron_attributes ); + } + + private: + AttributeManager unique_vertex_attributes_; + absl::flat_hash_map< uuid, AttributeManager > + block_polyhedron_attributes_; + }; + + ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers() = default; + + ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers( const BRep& model ) + : impl_{ model } + { + } + + ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers( + ExternalTimeSeriesHelpers&& ) noexcept = default; + + ExternalTimeSeriesHelpers& ExternalTimeSeriesHelpers::operator=( + ExternalTimeSeriesHelpers&& ) noexcept = default; + + ExternalTimeSeriesHelpers::~ExternalTimeSeriesHelpers() = default; + + AttributeManager& + ExternalTimeSeriesHelpers::unique_vertex_attribute_manager() + { + return impl_->unique_vertex_attribute_manager(); + } + + const AttributeManager& + ExternalTimeSeriesHelpers::unique_vertex_attribute_manager() const + { + return impl_->unique_vertex_attribute_manager(); + } + + AttributeManager& + ExternalTimeSeriesHelpers::block_polyhedron_attribute_manager( + const uuid& block_id ) + { + return impl_->block_polyhedron_attribute_manager( block_id ); + } + + const AttributeManager& + ExternalTimeSeriesHelpers::block_polyhedron_attribute_manager( + const uuid& block_id ) const + { + return impl_->block_polyhedron_attribute_manager( block_id ); + } + + void ExternalTimeSeriesHelpers::apply_to( BRep& model ) const + { + impl_->apply_to( model ); + } + + template < typename Archive > + void ExternalTimeSeriesHelpers::serialize( Archive& archive ) + { + archive.ext( *this, Growable< Archive, ExternalTimeSeriesHelpers >{ + { []( Archive& local_archive, + ExternalTimeSeriesHelpers& helpers ) { + local_archive.object( helpers.impl_ ); + } } } ); + } + + SERIALIZE_BITSERY_ARCHIVE( opengeode_model_api, ExternalTimeSeriesHelpers ); +} // namespace geode diff --git a/src/geode/model/representation/io/external_time_series_helpers_input.cpp b/src/geode/model/representation/io/external_time_series_helpers_input.cpp new file mode 100644 index 000000000..4c98cc35d --- /dev/null +++ b/src/geode/model/representation/io/external_time_series_helpers_input.cpp @@ -0,0 +1,72 @@ +/* + * 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. + * + */ + +#include + +#include + +#include +#include +#include +#include + +#include + +namespace geode +{ + ExternalTimeSeriesHelpers load_external_time_series_helpers( + std::string_view filename ) + { + const Timer timer; + OpenGeodeModelException::check_exception( + extension_from_filename( filename ) + == ExternalTimeSeriesHelpers::native_extension_static(), + nullptr, OpenGeodeException::TYPE::data, + "[load_external_time_series_helpers] Wrong extension for file ", + filename, ", only ", + ExternalTimeSeriesHelpers::native_extension_static(), + " is supported" ); + std::ifstream file{ to_string( filename ), std::ifstream::binary }; + OpenGeodeModelException::check_exception( !file.fail(), nullptr, + OpenGeodeException::TYPE::data, + "[load_external_time_series_helpers] Failed to open file: ", + filename ); + TContext context{}; + BitseryExtensions::register_deserialize_pcontext( + std::get< 0 >( context ) ); + Deserializer archive{ context, file }; + ExternalTimeSeriesHelpers helpers; + archive.object( helpers ); + const auto& adapter = archive.adapter(); + OpenGeodeModelException::check_exception( + adapter.error() == bitsery::ReaderError::NoError + && adapter.isCompletedSuccessfully() + && std::get< 1 >( context ).isValid(), + nullptr, OpenGeodeException::TYPE::internal, + "[load_external_time_series_helpers] Error while reading file: ", + filename ); + Logger::info( "ExternalTimeSeriesHelpers loaded from ", filename, + " in ", timer.duration() ); + return helpers; + } +} // namespace geode diff --git a/src/geode/model/representation/io/external_time_series_helpers_output.cpp b/src/geode/model/representation/io/external_time_series_helpers_output.cpp new file mode 100644 index 000000000..c181ea218 --- /dev/null +++ b/src/geode/model/representation/io/external_time_series_helpers_output.cpp @@ -0,0 +1,88 @@ +/* + * 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. + * + */ + +#include + +#include +#include + +#include +#include +#include +#include + +#include + +namespace +{ + void write_external_time_series_helpers( + const geode::ExternalTimeSeriesHelpers& helpers, + std::string_view filename ) + { + std::ofstream file{ geode::to_string( filename ), + std::ofstream::binary }; + geode::OpenGeodeModelException::check_exception( !file.fail(), nullptr, + geode::OpenGeodeException::TYPE::data, + "[save_external_time_series_helpers] Failed to open file: ", + filename ); + geode::TContext context{}; + geode::BitseryExtensions::register_serialize_pcontext( + std::get< 0 >( context ) ); + geode::Serializer archive{ context, file }; + archive.object( helpers ); + archive.adapter().flush(); + geode::OpenGeodeModelException::check_exception( + std::get< 1 >( context ).isValid(), nullptr, + geode::OpenGeodeException::TYPE::internal, + "[save_external_time_series_helpers] Error while writing file: ", + filename ); + } +} // namespace + +namespace geode +{ + void save_external_time_series_helpers( + const ExternalTimeSeriesHelpers& helpers, std::string_view filename ) + { + const Timer timer; + OpenGeodeModelException::check_exception( + extension_from_filename( filename ) + == ExternalTimeSeriesHelpers::native_extension_static(), + nullptr, OpenGeodeException::TYPE::data, + "[save_external_time_series_helpers] Wrong extension for file ", + filename, ", only ", + ExternalTimeSeriesHelpers::native_extension_static(), + " is supported" ); + try + { + write_external_time_series_helpers( helpers, filename ); + } + catch( ... ) + { + std::filesystem::remove( to_string( filename ) ); + throw; + } + Logger::info( "ExternalTimeSeriesHelpers saved in ", filename, " in ", + timer.duration() ); + } +} // namespace geode diff --git a/tests/model/CMakeLists.txt b/tests/model/CMakeLists.txt index b07483ff9..047f6e601 100644 --- a/tests/model/CMakeLists.txt +++ b/tests/model/CMakeLists.txt @@ -73,6 +73,13 @@ add_geode_test( ${PROJECT_NAME}::model ESSENTIAL ) +add_geode_test( + SOURCE "test-external-time-series-helpers.cpp" + DEPENDENCIES + ${PROJECT_NAME}::basic + ${PROJECT_NAME}::mesh + ${PROJECT_NAME}::model +) add_geode_test( SOURCE "test-model-component-filter.cpp" DEPENDENCIES diff --git a/tests/model/test-external-time-series-helpers.cpp b/tests/model/test-external-time-series-helpers.cpp new file mode 100644 index 000000000..3ee3a558f --- /dev/null +++ b/tests/model/test-external-time-series-helpers.cpp @@ -0,0 +1,211 @@ +/* + * 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. + * + */ + +#include + +#include + +#include +#include +#include +#include +#include +#include +#include + +#include + +#include +#include +#include +#include +#include +#include + +#include + +namespace +{ + constexpr auto PRESSURE = "pressure"; + constexpr auto VELOCITY = "velocity"; + constexpr std::array< double, 2 > TIMES{ 0., 10. }; + + double pressure( geode::index_t unique_vertex, double time ) + { + return unique_vertex + time; + } + + std::array< double, 3 > velocity( geode::index_t polyhedron ) + { + return { static_cast< double >( polyhedron ), 1., 2. }; + } + + geode::ExternalTimeSeriesHelpers create_helpers( + const geode::BRep& model, const geode::uuid& block_id ) + { + geode::ExternalTimeSeriesHelpers helpers{ model }; + auto& vertices = helpers.unique_vertex_attribute_manager(); + for( const auto time : TIMES ) + { + const auto attribute_id = + vertices.create_time_step_attribute< geode::VariableAttribute, + double >( PRESSURE, time, + geode::AttributeValues< double >{}, + geode::AttributeProperties{} ); + auto attribute = + vertices.find_attribute< geode::VariableAttribute, double >( + attribute_id ); + for( const auto unique_vertex : + geode::Range{ model.nb_unique_vertices() } ) + { + attribute->set_value( + unique_vertex, pressure( unique_vertex, time ) ); + } + } + auto& polyhedra = + helpers.block_polyhedron_attribute_manager( block_id ); + const auto attribute_id = + polyhedra.create_time_step_attribute< geode::VariableAttribute, + std::array< double, 3 > >( VELOCITY, TIMES[1], + geode::AttributeValues< std::array< double, 3 > >{}, + geode::AttributeProperties{} ); + auto attribute = polyhedra.find_attribute< geode::VariableAttribute, + std::array< double, 3 > >( attribute_id ); + for( const auto polyhedron : + geode::Range{ model.block( block_id ).mesh().nb_polyhedra() } ) + { + attribute->set_value( polyhedron, velocity( polyhedron ) ); + } + return helpers; + } + + void check_pressures( const geode::BRep& model ) + { + for( const auto& block : model.blocks() ) + { + const auto& mesh = block.mesh(); + const geode::AttributeTimeSeries< double > series{ + mesh.vertex_attribute_manager(), PRESSURE + }; + geode::OpenGeodeModelException::test( + series.nb_time_steps() == TIMES.size(), + "[Test] Wrong number of pressure time steps on Block ", + block.id().string() ); + for( const auto step : geode::Range{ series.nb_time_steps() } ) + { + geode::OpenGeodeModelException::test( + series.time( step ) == TIMES[step], + "[Test] Wrong pressure time" ); + for( const auto vertex : geode::Range{ mesh.nb_vertices() } ) + { + const auto unique_vertex = + model.unique_vertex( { block.component_id(), vertex } ); + geode::OpenGeodeModelException::test( + series.value( step, vertex ) + == pressure( unique_vertex, TIMES[step] ), + "[Test] Wrong pressure on vertex ", vertex, + " of Block ", block.id().string() ); + } + } + } + } + + void check_velocities( + const geode::BRep& model, const geode::uuid& block_id ) + { + const auto& mesh = model.block( block_id ).mesh(); + const geode::AttributeTimeSeries< std::array< double, 3 > > series{ + mesh.polyhedron_attribute_manager(), VELOCITY + }; + geode::OpenGeodeModelException::test( + series.nb_time_steps() == 1 && series.time( 0 ) == TIMES[1], + "[Test] Wrong velocity time steps" ); + for( const auto polyhedron : geode::Range{ mesh.nb_polyhedra() } ) + { + geode::OpenGeodeModelException::test( + series.value( 0, polyhedron ) == velocity( polyhedron ), + "[Test] Wrong velocity on polyhedron ", polyhedron ); + } + } + + void check_other_model( const geode::ExternalTimeSeriesHelpers& helpers, + const geode::BRep& model ) + { + auto other = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "test_mesh3.og_brep" ) ); + geode::OpenGeodeModelException::test( + other.nb_unique_vertices() != model.nb_unique_vertices(), + "[Test] Models should have different unique vertices" ); + bool thrown{ false }; + try + { + helpers.apply_to( other ); + } + catch( const geode::OpenGeodeException& ) + { + thrown = true; + } + geode::OpenGeodeModelException::test( + thrown, "[Test] Applying on another model should throw" ); + } + + void check_wrong_extension( + const geode::ExternalTimeSeriesHelpers& helpers ) + { + bool thrown{ false }; + try + { + geode::save_external_time_series_helpers( + helpers, "test_external_time_series.txt" ); + } + catch( const geode::OpenGeodeException& ) + { + thrown = true; + } + geode::OpenGeodeModelException::test( + thrown, "[Test] Saving with another extension should throw" ); + } +} // namespace + +void test() +{ + geode::OpenGeodeModelLibrary::initialize(); + auto model = + geode::load_brep( absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); + geode::OpenGeodeModelException::test( + model.nb_blocks() > 0, "[Test] Model has no Block" ); + const auto block_id = ( *model.blocks().begin() ).id(); + const auto filename = absl::StrCat( "test_external_time_series.", + geode::ExternalTimeSeriesHelpers::native_extension_static() ); + geode::save_external_time_series_helpers( + create_helpers( model, block_id ), filename ); + const auto helpers = geode::load_external_time_series_helpers( filename ); + + helpers.apply_to( model ); + check_pressures( model ); + check_velocities( model, block_id ); + check_other_model( helpers, model ); + check_wrong_extension( helpers ); +} + +OPENGEODE_TEST( "external-time-series-helpers" ) From b059fb6a10fdfbea7904e7237bccfa02071496a7 Mon Sep 17 00:00:00 2001 From: BenPinet Date: Wed, 21 Oct 2026 15:30:54 +0200 Subject: [PATCH 2/6] feat(BRepTimeSeriesInput): load native og_ets time series Register an og_ets reader in BRepTimeSeriesInputFactory: it loads the ExternalTimeSeriesHelpers and applies them on the BRep, so load_brep_time_series handles the native format. Co-Authored-By: Claude Opus 5.5 --- .../helpers/external_time_series_helpers.hpp | 1 + .../io/geode/geode_brep_time_series_input.hpp | 57 +++++++++++++++++++ src/geode/model/CMakeLists.txt | 2 + src/geode/model/common.cpp | 9 +++ .../io/geode/geode_brep_time_series_input.cpp | 34 +++++++++++ .../test-external-time-series-helpers.cpp | 7 +++ 6 files changed, 110 insertions(+) create mode 100644 include/geode/model/representation/io/geode/geode_brep_time_series_input.hpp create mode 100644 src/geode/model/representation/io/geode/geode_brep_time_series_input.cpp diff --git a/include/geode/model/helpers/external_time_series_helpers.hpp b/include/geode/model/helpers/external_time_series_helpers.hpp index 5396918b3..655355065 100644 --- a/include/geode/model/helpers/external_time_series_helpers.hpp +++ b/include/geode/model/helpers/external_time_series_helpers.hpp @@ -51,6 +51,7 @@ namespace geode * Time series are expected to be created as time step attributes (see * AttributeManager::create_time_step_attribute), but any transferable * attribute is stored and applied the same way. + * The native file can be loaded onto a BRep with load_brep_time_series. */ class opengeode_model_api ExternalTimeSeriesHelpers { diff --git a/include/geode/model/representation/io/geode/geode_brep_time_series_input.hpp b/include/geode/model/representation/io/geode/geode_brep_time_series_input.hpp new file mode 100644 index 000000000..851f95cb8 --- /dev/null +++ b/include/geode/model/representation/io/geode/geode_brep_time_series_input.hpp @@ -0,0 +1,57 @@ +/* + * 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 + +namespace geode +{ + class opengeode_model_api OpenGeodeBRepTimeSeriesInput final + : public BRepTimeSeriesInput + { + public: + explicit OpenGeodeBRepTimeSeriesInput( std::string_view filename ) + : BRepTimeSeriesInput{ filename } + { + } + + [[nodiscard]] static std::string_view extension() + { + return ExternalTimeSeriesHelpers::native_extension_static(); + } + + [[nodiscard]] AdditionalFiles additional_files() const final + { + return {}; + } + + [[nodiscard]] Percentage is_loadable() const final + { + return Percentage{ 1 }; + } + + void read( BRep& brep ) final; + }; +} // namespace geode diff --git a/src/geode/model/CMakeLists.txt b/src/geode/model/CMakeLists.txt index bff448fc3..6f4efa8dc 100644 --- a/src/geode/model/CMakeLists.txt +++ b/src/geode/model/CMakeLists.txt @@ -105,6 +105,7 @@ add_geode_library( "representation/io/section_output.cpp" "representation/io/geode/geode_brep_input.cpp" "representation/io/geode/geode_brep_output.cpp" + "representation/io/geode/geode_brep_time_series_input.cpp" "representation/io/geode/geode_section_input.cpp" "representation/io/geode/geode_section_output.cpp" PUBLIC_HEADERS @@ -185,6 +186,7 @@ add_geode_library( "representation/io/geode/geode_brep_input.hpp" "representation/io/geode/geode_section_input.hpp" "representation/io/geode/geode_brep_output.hpp" + "representation/io/geode/geode_brep_time_series_input.hpp" "representation/io/geode/geode_section_output.hpp" ADVANCED_HEADERS "helpers/detail/build_model_boundaries.hpp" diff --git a/src/geode/model/common.cpp b/src/geode/model/common.cpp index 064559a49..db81f7122 100644 --- a/src/geode/model/common.cpp +++ b/src/geode/model/common.cpp @@ -28,6 +28,7 @@ #include #include #include +#include #include #include @@ -46,6 +47,13 @@ namespace geode::OpenGeodeSectionOutput::extension().data() ); } + void register_brep_time_series_input() + { + geode::BRepTimeSeriesInputFactory::register_creator< + geode::OpenGeodeBRepTimeSeriesInput >( + geode::OpenGeodeBRepTimeSeriesInput::extension().data() ); + } + void register_brep_output() { geode::BRepOutputFactory::register_creator< @@ -68,6 +76,7 @@ namespace geode OpenGeodeMeshLibrary::initialize(); register_brep_input(); register_section_input(); + register_brep_time_series_input(); register_brep_output(); register_section_output(); BitseryExtensions::register_functions( diff --git a/src/geode/model/representation/io/geode/geode_brep_time_series_input.cpp b/src/geode/model/representation/io/geode/geode_brep_time_series_input.cpp new file mode 100644 index 000000000..43077ac67 --- /dev/null +++ b/src/geode/model/representation/io/geode/geode_brep_time_series_input.cpp @@ -0,0 +1,34 @@ +/* + * 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. + * + */ + +#include + +#include + +namespace geode +{ + void OpenGeodeBRepTimeSeriesInput::read( BRep& brep ) + { + load_external_time_series_helpers( filename() ).apply_to( brep ); + } +} // namespace geode diff --git a/tests/model/test-external-time-series-helpers.cpp b/tests/model/test-external-time-series-helpers.cpp index 3ee3a558f..7294e674f 100644 --- a/tests/model/test-external-time-series-helpers.cpp +++ b/tests/model/test-external-time-series-helpers.cpp @@ -39,6 +39,7 @@ #include #include #include +#include #include #include @@ -204,6 +205,12 @@ void test() helpers.apply_to( model ); check_pressures( model ); check_velocities( model, block_id ); + + auto reloaded_model = + geode::load_brep( absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); + geode::load_brep_time_series( reloaded_model, filename ); + check_pressures( reloaded_model ); + check_velocities( reloaded_model, block_id ); check_other_model( helpers, model ); check_wrong_extension( helpers ); } From ebb42cf15abcf0d1ecf6419e96bf215f4e2bc5ad Mon Sep 17 00:00:00 2001 From: BenPinet Date: Wed, 21 Oct 2026 15:30:54 +0200 Subject: [PATCH 3/6] feat(ExternalTimeSeriesHelpers): replace Block steps when applying A Block time step with the same name and time as an applied one is deleted first, so that applying helpers built with other attribute ids (e.g. a reloaded simulation) does not duplicate the step. Co-Authored-By: Claude Opus 5.5 --- .../helpers/external_time_series_helpers.hpp | 3 +- .../helpers/external_time_series_helpers.cpp | 47 +++++++++++++++++-- .../test-external-time-series-helpers.cpp | 3 ++ 3 files changed, 48 insertions(+), 5 deletions(-) diff --git a/include/geode/model/helpers/external_time_series_helpers.hpp b/include/geode/model/helpers/external_time_series_helpers.hpp index 655355065..0ee84c0c9 100644 --- a/include/geode/model/helpers/external_time_series_helpers.hpp +++ b/include/geode/model/helpers/external_time_series_helpers.hpp @@ -102,7 +102,8 @@ namespace geode * unique vertex attributes on each Block vertex, polyhedron attributes * on the matching Block polyhedra. Non-transferable attributes are * skipped. Attribute ids and properties (time steps included) are - * kept. + * kept. A Block time step with the same name and time as an applied + * one is replaced. * @exception OpenGeodeException if the model does not have the same * unique vertices or Block polyhedra as the one used for creation */ diff --git a/src/geode/model/helpers/external_time_series_helpers.cpp b/src/geode/model/helpers/external_time_series_helpers.cpp index 802cbb878..339a2eae6 100644 --- a/src/geode/model/helpers/external_time_series_helpers.cpp +++ b/src/geode/model/helpers/external_time_series_helpers.cpp @@ -40,6 +40,40 @@ #include #include +namespace +{ + void delete_replaced_steps( const geode::AttributeManager& from, + geode::AttributeManager& to, + std::string_view name ) + { + for( const auto& step : from.time_steps( name ) ) + { + const auto attribute = + from.find_generic_attribute( step.attribute_id ); + if( !attribute->properties().transferable ) + { + continue; + } + for( const auto& existing_step : to.time_steps( name ) ) + { + if( existing_step.time == step.time ) + { + to.delete_attribute( existing_step.attribute_id ); + } + } + } + } + + void delete_replaced_steps( + const geode::AttributeManager& from, geode::AttributeManager& to ) + { + for( const auto& name : from.time_series_names() ) + { + delete_replaced_steps( from, to, name ); + } + } +} // namespace + namespace geode { class ExternalTimeSeriesHelpers::Impl @@ -178,15 +212,20 @@ namespace geode unique2block.map( unique_vertex, vertex ); } } - mesh.vertex_attribute_manager().import( - unique_vertex_attributes_, unique2block ); + auto& vertex_attributes = mesh.vertex_attribute_manager(); + delete_replaced_steps( + unique_vertex_attributes_, vertex_attributes ); + vertex_attributes.import( unique_vertex_attributes_, unique2block ); } static void apply_polyhedra( const Block3D& block, const AttributeManager& polyhedron_attributes ) { - block.mesh().polyhedron_attribute_manager().copy( - polyhedron_attributes ); + auto& block_polyhedron_attributes = + block.mesh().polyhedron_attribute_manager(); + delete_replaced_steps( + polyhedron_attributes, block_polyhedron_attributes ); + block_polyhedron_attributes.copy( polyhedron_attributes ); } private: diff --git a/tests/model/test-external-time-series-helpers.cpp b/tests/model/test-external-time-series-helpers.cpp index 7294e674f..bd479518b 100644 --- a/tests/model/test-external-time-series-helpers.cpp +++ b/tests/model/test-external-time-series-helpers.cpp @@ -205,6 +205,9 @@ void test() helpers.apply_to( model ); check_pressures( model ); check_velocities( model, block_id ); + create_helpers( model, block_id ).apply_to( model ); + check_pressures( model ); + check_velocities( model, block_id ); auto reloaded_model = geode::load_brep( absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); From f97912fe592c9ecbd56d8d599ef09e15a8c0c2d2 Mon Sep 17 00:00:00 2001 From: BenPinet Date: Thu, 22 Oct 2026 16:17:29 +0200 Subject: [PATCH 4/6] feat(ExternalTimeSeriesHelpers): match time series geometrically Store values on two point sets (simulation unique vertices and cell barycenters) instead of attribute managers sized to a given BRep, and match them with the Block vertices and polyhedra when applying, with a tolerance relative to the coordinates magnitude. Co-Authored-By: Claude Opus 5.5 --- .../helpers/external_time_series_helpers.hpp | 55 ++- .../helpers/external_time_series_helpers.cpp | 367 +++++++++++------- .../test-external-time-series-helpers.cpp | 193 +++++++-- 3 files changed, 401 insertions(+), 214 deletions(-) diff --git a/include/geode/model/helpers/external_time_series_helpers.hpp b/include/geode/model/helpers/external_time_series_helpers.hpp index 0ee84c0c9..27a2f8a13 100644 --- a/include/geode/model/helpers/external_time_series_helpers.hpp +++ b/include/geode/model/helpers/external_time_series_helpers.hpp @@ -23,6 +23,7 @@ #pragma once +#include #include #include @@ -36,9 +37,9 @@ namespace bitsery namespace geode { - class AttributeManager; + FORWARD_DECLARATION_DIMENSION_CLASS( PointSet ); + ALIAS_3D( PointSet ); class BRep; - struct uuid; } // namespace geode namespace geode @@ -46,8 +47,10 @@ namespace geode /*! * Time series computed outside of a BRep (e.g. by a simulator), stored * apart from the model and written on it on demand. - * Vertex values are stored per unique vertex of the model and polyhedron - * values per polyhedron of each Block mesh. + * Values are stored as vertex attributes of two point sets: the + * simulation unique vertices (one point per colocated position) and the + * simulation cell barycenters. They are matched geometrically with the + * Block vertices and polyhedra of a BRep when applied. * Time series are expected to be created as time step attributes (see * AttributeManager::create_time_step_attribute), but any transferable * attribute is stored and applied the same way. @@ -59,11 +62,6 @@ namespace geode public: ExternalTimeSeriesHelpers(); - /*! - * Create empty attribute managers sized to the model unique vertices - * and to the polyhedra of its Blocks. - */ - explicit ExternalTimeSeriesHelpers( const BRep& model ); ExternalTimeSeriesHelpers( ExternalTimeSeriesHelpers&& other ) noexcept; ExternalTimeSeriesHelpers& operator=( ExternalTimeSeriesHelpers&& other ) noexcept; @@ -80,32 +78,33 @@ namespace geode } /*! - * Attributes on the unique vertices of the model + * Set the simulation unique vertices, carrying the vertex values as + * vertex attributes + * @exception OpenGeodeException if the point set is null */ - [[nodiscard]] AttributeManager& unique_vertex_attribute_manager(); - - [[nodiscard]] const AttributeManager& - unique_vertex_attribute_manager() const; + void set_unique_vertices( std::unique_ptr< PointSet3D > vertices ); /*! - * Attributes on the polyhedra of a Block mesh - * @exception OpenGeodeException if the Block is unknown + * Set the simulation cell barycenters, carrying the cell values as + * vertex attributes + * @exception OpenGeodeException if the point set is null */ - [[nodiscard]] AttributeManager& block_polyhedron_attribute_manager( - const uuid& block_id ); - - [[nodiscard]] const AttributeManager& - block_polyhedron_attribute_manager( const uuid& block_id ) const; + void set_cell_barycenters( std::unique_ptr< PointSet3D > barycenters ); /*! * Write the transferable attributes on the Block meshes of the model: - * unique vertex attributes on each Block vertex, polyhedron attributes - * on the matching Block polyhedra. Non-transferable attributes are - * skipped. Attribute ids and properties (time steps included) are - * kept. A Block time step with the same name and time as an applied - * one is replaced. - * @exception OpenGeodeException if the model does not have the same - * unique vertices or Block polyhedra as the one used for creation + * each unique vertex on the closest model unique vertex (on all its + * Block vertices), each cell barycenter on the Block polyhedron with + * the closest barycenter. Block elements matched with no point keep + * no value, Blocks matched with no point are left untouched. + * Attribute ids and properties (time steps included) are kept. A + * Block time step with the same name and time as an applied one is + * replaced. + * The matching tolerance is GLOBAL_EPSILON relative to the largest + * absolute coordinate of the points (when larger than 1), to accept + * points rounded by the simulator (e.g. stored in single precision). + * @exception OpenGeodeException if a point is farther than the + * tolerance from its closest Block element */ void apply_to( BRep& model ) const; diff --git a/src/geode/model/helpers/external_time_series_helpers.cpp b/src/geode/model/helpers/external_time_series_helpers.cpp index 339a2eae6..818ef02f7 100644 --- a/src/geode/model/helpers/external_time_series_helpers.cpp +++ b/src/geode/model/helpers/external_time_series_helpers.cpp @@ -23,9 +23,11 @@ #include -#include +#include +#include +#include -#include +#include #include #include @@ -34,6 +36,12 @@ #include #include +#include +#include +#include +#include + +#include #include #include @@ -42,8 +50,18 @@ namespace { + // Point set vertex matched with each element of each Block, NO_ID if none + using BlocksMatches = + absl::flat_hash_map< geode::uuid, std::vector< geode::index_t > >; + + struct BlockPolyhedron + { + geode::uuid block_id; + geode::index_t polyhedron; + }; + void delete_replaced_steps( const geode::AttributeManager& from, - geode::AttributeManager& to, + geode::AttributeManager& target, std::string_view name ) { for( const auto& step : from.time_steps( name ) ) @@ -54,193 +72,262 @@ namespace { continue; } - for( const auto& existing_step : to.time_steps( name ) ) + for( const auto& existing_step : target.time_steps( name ) ) { if( existing_step.time == step.time ) { - to.delete_attribute( existing_step.attribute_id ); + target.delete_attribute( existing_step.attribute_id ); } } } } void delete_replaced_steps( - const geode::AttributeManager& from, geode::AttributeManager& to ) + const geode::AttributeManager& from, geode::AttributeManager& target ) { for( const auto& name : from.time_series_names() ) { - delete_replaced_steps( from, to, name ); + delete_replaced_steps( from, target, name ); } } -} // namespace -namespace geode -{ - class ExternalTimeSeriesHelpers::Impl + // Coordinates are rounded relatively to their magnitude + double matching_tolerance( const geode::PointSet3D& point_set ) { - friend class bitsery::Access; - - public: - Impl() = default; - - explicit Impl( const BRep& model ) + const auto box = point_set.bounding_box(); + double largest_coordinate{ 1 }; + for( const auto& point : { box.min(), box.max() } ) { - unique_vertex_attributes_.resize( model.nb_unique_vertices() ); - for( const auto& block : model.blocks() ) + for( const auto axis : geode::LRange{ 3 } ) { - block_polyhedron_attributes_[block.id()].resize( - block.mesh().nb_polyhedra() ); + largest_coordinate = std::max( + largest_coordinate, std::fabs( point.value( axis ) ) ); } } + return geode::GLOBAL_EPSILON * largest_coordinate; + } - [[nodiscard]] AttributeManager& unique_vertex_attribute_manager() + // Each point is matched with the closest element, elements without + // point (e.g. outside of the simulated domain) are not matched. + // Returns the point matched with each element, NO_ID if none. + std::vector< geode::index_t > match_points( + const geode::PointSet3D& point_set, + std::string_view point_set_name, + std::vector< geode::Point3D > element_points ) + { + std::vector< geode::index_t > matches( + element_points.size(), geode::NO_ID ); + if( point_set.nb_vertices() == 0 ) { - return unique_vertex_attributes_; + return matches; } - - [[nodiscard]] const AttributeManager& - unique_vertex_attribute_manager() const + geode::OpenGeodeModelException::check_exception( + !element_points.empty(), nullptr, + geode::OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] No Block element to match " + "the ", + point_set_name, " with" ); + const auto tolerance = matching_tolerance( point_set ); + const geode::NNSearch3D search{ std::move( element_points ) }; + for( const auto vertex : geode::Range{ point_set.nb_vertices() } ) { - return unique_vertex_attributes_; + const auto& point = point_set.point( vertex ); + const auto element = search.closest_neighbor( point ); + const auto distance = + geode::point_point_distance( point, search.point( element ) ); + geode::OpenGeodeModelException::check_exception( + distance <= tolerance, nullptr, + geode::OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] Point ", vertex, + " of the ", point_set_name, " [", point.string(), + "] is at distance ", distance, + " from the closest Block element, more than the tolerance ", + tolerance ); + matches[element] = vertex; } + return matches; + } - [[nodiscard]] AttributeManager& block_polyhedron_attribute_manager( - const uuid& block_id ) + // Matching is done on the model unique vertices so that the colocated + // vertices of several Blocks get the same values + std::vector< geode::index_t > match_unique_vertices( + const geode::BRep& model, const geode::PointSet3D& unique_vertices ) + { + std::vector< geode::Point3D > points; + std::vector< geode::index_t > model_unique_vertices; + std::vector< bool > visited( model.nb_unique_vertices(), false ); + for( const auto& block : model.blocks() ) { - const auto it = block_polyhedron_attributes_.find( block_id ); - check_block( it != block_polyhedron_attributes_.end(), block_id ); - return it->second; + const auto& mesh = block.mesh(); + for( const auto vertex : geode::Range{ mesh.nb_vertices() } ) + { + const auto unique_vertex = + model.unique_vertex( { block.component_id(), vertex } ); + if( unique_vertex == geode::NO_ID || visited[unique_vertex] ) + { + continue; + } + visited[unique_vertex] = true; + points.push_back( mesh.point( vertex ) ); + model_unique_vertices.push_back( unique_vertex ); + } } - - [[nodiscard]] const AttributeManager& - block_polyhedron_attribute_manager( const uuid& block_id ) const + const auto point_matches = match_points( + unique_vertices, "unique vertices", std::move( points ) ); + std::vector< geode::index_t > matches( + model.nb_unique_vertices(), geode::NO_ID ); + for( const auto index : geode::Indices{ model_unique_vertices } ) { - const auto it = block_polyhedron_attributes_.find( block_id ); - check_block( it != block_polyhedron_attributes_.end(), block_id ); - return it->second; + matches[model_unique_vertices[index]] = point_matches[index]; } + return matches; + } - void apply_to( const BRep& model ) const + BlocksMatches match_block_vertices( + const geode::BRep& model, const geode::PointSet3D& unique_vertices ) + { + const auto unique_matches = + match_unique_vertices( model, unique_vertices ); + BlocksMatches matches; + for( const auto& block : model.blocks() ) { - check_model( model ); - for( const auto& block : model.blocks() ) + const auto& mesh = block.mesh(); + auto& block_matches = matches[block.id()]; + block_matches.resize( mesh.nb_vertices(), geode::NO_ID ); + for( const auto vertex : geode::Range{ mesh.nb_vertices() } ) { - apply_vertices( model, block ); + const auto unique_vertex = + model.unique_vertex( { block.component_id(), vertex } ); + if( unique_vertex != geode::NO_ID ) + { + block_matches[vertex] = unique_matches[unique_vertex]; + } } - for( const auto& [block_id, polyhedron_attributes] : - block_polyhedron_attributes_ ) + } + return matches; + } + + BlocksMatches match_block_polyhedra( + const geode::BRep& model, const geode::PointSet3D& cell_barycenters ) + { + BlocksMatches matches; + std::vector< geode::Point3D > barycenters; + std::vector< BlockPolyhedron > polyhedra; + for( const auto& block : model.blocks() ) + { + const auto& mesh = block.mesh(); + matches[block.id()].resize( mesh.nb_polyhedra(), geode::NO_ID ); + for( const auto polyhedron : geode::Range{ mesh.nb_polyhedra() } ) { - apply_polyhedra( - model.block( block_id ), polyhedron_attributes ); + barycenters.push_back( + mesh.polyhedron_barycenter( polyhedron ) ); + polyhedra.push_back( { block.id(), polyhedron } ); } } + const auto point_matches = match_points( + cell_barycenters, "cell barycenters", std::move( barycenters ) ); + for( const auto index : geode::Indices{ polyhedra } ) + { + const auto& block_polyhedron = polyhedra[index]; + matches[block_polyhedron.block_id][block_polyhedron.polyhedron] = + point_matches[index]; + } + return matches; + } - private: - template < typename Archive > - void serialize( Archive& archive ) + void apply_matches( const geode::AttributeManager& from, + absl::Span< const geode::index_t > matches, + geode::AttributeManager& target ) + { + geode::GenericMapping< geode::index_t > point2element; + for( const auto element : geode::Indices{ matches } ) { - archive.ext( *this, - Growable< Archive, Impl >{ - { []( Archive& local_archive, Impl& impl ) { - local_archive.object( impl.unique_vertex_attributes_ ); - local_archive.ext( impl.block_polyhedron_attributes_, - bitsery::ext::StdMap{ - impl.block_polyhedron_attributes_.max_size() }, - []( Archive& map_archive, uuid& block_id, - AttributeManager& polyhedron_attributes ) { - map_archive.object( block_id ); - map_archive.object( polyhedron_attributes ); - } ); - } } } ); + if( matches[element] != geode::NO_ID ) + { + point2element.map( matches[element], element ); + } + } + if( point2element.size_in() == 0 ) + { + return; } + delete_replaced_steps( from, target ); + target.import( from, point2element ); + } +} // namespace - static void check_block( bool is_known, const uuid& block_id ) +namespace geode +{ + class ExternalTimeSeriesHelpers::Impl + { + friend class bitsery::Access; + + public: + Impl() + : unique_vertices_{ PointSet3D::create() }, + cell_barycenters_{ PointSet3D::create() } { - OpenGeodeModelException::check_exception( is_known, nullptr, - OpenGeodeException::TYPE::data, - "[ExternalTimeSeriesHelpers] Unknown Block ", - block_id.string() ); } - void check_model( const BRep& model ) const + void set_unique_vertices( std::unique_ptr< PointSet3D > vertices ) { - OpenGeodeModelException::check_exception( - unique_vertex_attributes_.nb_elements() - == model.nb_unique_vertices(), - nullptr, OpenGeodeException::TYPE::data, - "[ExternalTimeSeriesHelpers::apply_to] Model has ", - model.nb_unique_vertices(), " unique vertices instead of ", - unique_vertex_attributes_.nb_elements() ); - for( const auto& [block_id, polyhedron_attributes] : - block_polyhedron_attributes_ ) - { - check_block_polyhedra( model, block_id, polyhedron_attributes ); - } + check_point_set( vertices.get(), "unique vertices" ); + unique_vertices_ = std::move( vertices ); } - static void check_block_polyhedra( const BRep& model, - const uuid& block_id, - const AttributeManager& polyhedron_attributes ) + void set_cell_barycenters( std::unique_ptr< PointSet3D > barycenters ) { - OpenGeodeModelException::check_exception( - model.has_block( block_id ), nullptr, - OpenGeodeException::TYPE::data, - "[ExternalTimeSeriesHelpers::apply_to] Model has no Block ", - block_id.string() ); - const auto nb_polyhedra = - model.block( block_id ).mesh().nb_polyhedra(); - OpenGeodeModelException::check_exception( - nb_polyhedra == polyhedron_attributes.nb_elements(), nullptr, - OpenGeodeException::TYPE::data, - "[ExternalTimeSeriesHelpers::apply_to] Block ", - block_id.string(), " has ", nb_polyhedra, - " polyhedra instead of ", polyhedron_attributes.nb_elements() ); + check_point_set( barycenters.get(), "cell barycenters" ); + cell_barycenters_ = std::move( barycenters ); } - void apply_vertices( const BRep& model, const Block3D& block ) const + void apply_to( const BRep& model ) const { - const auto& mesh = block.mesh(); - GenericMapping< index_t > unique2block; - unique2block.reserve( mesh.nb_vertices() ); - ComponentMeshVertex block_vertex{ block.component_id(), 0 }; - for( const auto vertex : Range{ mesh.nb_vertices() } ) + const auto vertex_matches = + match_block_vertices( model, *unique_vertices_ ); + const auto polyhedron_matches = + match_block_polyhedra( model, *cell_barycenters_ ); + for( const auto& block : model.blocks() ) { - block_vertex.vertex = vertex; - const auto unique_vertex = model.unique_vertex( block_vertex ); - if( unique_vertex != NO_ID ) - { - unique2block.map( unique_vertex, vertex ); - } + const auto& mesh = block.mesh(); + apply_matches( unique_vertices_->vertex_attribute_manager(), + vertex_matches.at( block.id() ), + mesh.vertex_attribute_manager() ); + apply_matches( cell_barycenters_->vertex_attribute_manager(), + polyhedron_matches.at( block.id() ), + mesh.polyhedron_attribute_manager() ); } - auto& vertex_attributes = mesh.vertex_attribute_manager(); - delete_replaced_steps( - unique_vertex_attributes_, vertex_attributes ); - vertex_attributes.import( unique_vertex_attributes_, unique2block ); } - static void apply_polyhedra( const Block3D& block, - const AttributeManager& polyhedron_attributes ) + private: + template < typename Archive > + void serialize( Archive& archive ) { - auto& block_polyhedron_attributes = - block.mesh().polyhedron_attribute_manager(); - delete_replaced_steps( - polyhedron_attributes, block_polyhedron_attributes ); - block_polyhedron_attributes.copy( polyhedron_attributes ); + archive.ext( + *this, Growable< Archive, Impl >{ + { []( Archive& local_archive, Impl& impl ) { + local_archive.ext( impl.unique_vertices_, + bitsery::ext::StdSmartPtr{} ); + local_archive.ext( impl.cell_barycenters_, + bitsery::ext::StdSmartPtr{} ); + } } } ); + } + + static void check_point_set( + const PointSet3D* point_set, std::string_view name ) + { + OpenGeodeModelException::check_exception( point_set != nullptr, + nullptr, OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers] Null ", name, " point set" ); } private: - AttributeManager unique_vertex_attributes_; - absl::flat_hash_map< uuid, AttributeManager > - block_polyhedron_attributes_; + std::unique_ptr< PointSet3D > unique_vertices_; + std::unique_ptr< PointSet3D > cell_barycenters_; }; ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers() = default; - ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers( const BRep& model ) - : impl_{ model } - { - } - ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers( ExternalTimeSeriesHelpers&& ) noexcept = default; @@ -249,30 +336,16 @@ namespace geode ExternalTimeSeriesHelpers::~ExternalTimeSeriesHelpers() = default; - AttributeManager& - ExternalTimeSeriesHelpers::unique_vertex_attribute_manager() - { - return impl_->unique_vertex_attribute_manager(); - } - - const AttributeManager& - ExternalTimeSeriesHelpers::unique_vertex_attribute_manager() const - { - return impl_->unique_vertex_attribute_manager(); - } - - AttributeManager& - ExternalTimeSeriesHelpers::block_polyhedron_attribute_manager( - const uuid& block_id ) + void ExternalTimeSeriesHelpers::set_unique_vertices( + std::unique_ptr< PointSet3D > vertices ) { - return impl_->block_polyhedron_attribute_manager( block_id ); + impl_->set_unique_vertices( std::move( vertices ) ); } - const AttributeManager& - ExternalTimeSeriesHelpers::block_polyhedron_attribute_manager( - const uuid& block_id ) const + void ExternalTimeSeriesHelpers::set_cell_barycenters( + std::unique_ptr< PointSet3D > barycenters ) { - return impl_->block_polyhedron_attribute_manager( block_id ); + impl_->set_cell_barycenters( std::move( barycenters ) ); } void ExternalTimeSeriesHelpers::apply_to( BRep& model ) const diff --git a/tests/model/test-external-time-series-helpers.cpp b/tests/model/test-external-time-series-helpers.cpp index bd479518b..84ba1716f 100644 --- a/tests/model/test-external-time-series-helpers.cpp +++ b/tests/model/test-external-time-series-helpers.cpp @@ -22,6 +22,9 @@ */ #include +#include +#include +#include #include @@ -33,10 +36,16 @@ #include #include +#include +#include + +#include +#include #include #include #include +#include #include #include #include @@ -50,6 +59,10 @@ namespace constexpr auto PRESSURE = "pressure"; constexpr auto VELOCITY = "velocity"; constexpr std::array< double, 2 > TIMES{ 0., 10. }; + // test_mesh3 coordinates go up to 10: the matching tolerance is + // 10 * GLOBAL_EPSILON + constexpr double ROUNDING_TRANSLATION{ geode::GLOBAL_EPSILON }; + constexpr double WRONG_TRANSLATION{ 100 * geode::GLOBAL_EPSILON }; double pressure( geode::index_t unique_vertex, double time ) { @@ -61,45 +74,92 @@ namespace return { static_cast< double >( polyhedron ), 1., 2. }; } - geode::ExternalTimeSeriesHelpers create_helpers( - const geode::BRep& model, const geode::uuid& block_id ) + template < typename T > + std::shared_ptr< geode::VariableAttribute< T > > create_time_step( + geode::AttributeManager& manager, std::string_view name, double time ) + { + const auto attribute_id = + manager.create_time_step_attribute< geode::VariableAttribute, T >( + name, time, geode::AttributeValues< T >{}, + geode::AttributeProperties{} ); + return manager.find_attribute< geode::VariableAttribute, T >( + attribute_id ); + } + + // One point per model unique vertex + std::unique_ptr< geode::PointSet3D > create_unique_vertices( + const geode::BRep& model ) { - geode::ExternalTimeSeriesHelpers helpers{ model }; - auto& vertices = helpers.unique_vertex_attribute_manager(); + auto vertices = geode::PointSet3D::create(); + auto builder = geode::PointSetBuilder3D::create( *vertices ); + std::vector< geode::index_t > unique_vertices; + std::vector< bool > visited( model.nb_unique_vertices(), false ); + for( const auto& block : model.blocks() ) + { + const auto& mesh = block.mesh(); + for( const auto vertex : geode::Range{ mesh.nb_vertices() } ) + { + const auto unique_vertex = + model.unique_vertex( { block.component_id(), vertex } ); + if( visited[unique_vertex] ) + { + continue; + } + visited[unique_vertex] = true; + builder->create_point( mesh.point( vertex ) ); + unique_vertices.push_back( unique_vertex ); + } + } for( const auto time : TIMES ) { - const auto attribute_id = - vertices.create_time_step_attribute< geode::VariableAttribute, - double >( PRESSURE, time, - geode::AttributeValues< double >{}, - geode::AttributeProperties{} ); - auto attribute = - vertices.find_attribute< geode::VariableAttribute, double >( - attribute_id ); - for( const auto unique_vertex : - geode::Range{ model.nb_unique_vertices() } ) + auto attribute = create_time_step< double >( + vertices->vertex_attribute_manager(), PRESSURE, time ); + for( const auto point : geode::Indices{ unique_vertices } ) { attribute->set_value( - unique_vertex, pressure( unique_vertex, time ) ); + point, pressure( unique_vertices[point], time ) ); } } - auto& polyhedra = - helpers.block_polyhedron_attribute_manager( block_id ); - const auto attribute_id = - polyhedra.create_time_step_attribute< geode::VariableAttribute, - std::array< double, 3 > >( VELOCITY, TIMES[1], - geode::AttributeValues< std::array< double, 3 > >{}, - geode::AttributeProperties{} ); - auto attribute = polyhedra.find_attribute< geode::VariableAttribute, - std::array< double, 3 > >( attribute_id ); - for( const auto polyhedron : - geode::Range{ model.block( block_id ).mesh().nb_polyhedra() } ) + return vertices; + } + + // Barycenters of a single Block: the other Blocks get no velocity + std::unique_ptr< geode::PointSet3D > create_cell_barycenters( + const geode::BRep& model, + const geode::uuid& block_id, + const geode::Vector3D& translation ) + { + auto barycenters = geode::PointSet3D::create(); + auto builder = geode::PointSetBuilder3D::create( *barycenters ); + const auto& mesh = model.block( block_id ).mesh(); + auto attribute = create_time_step< std::array< double, 3 > >( + barycenters->vertex_attribute_manager(), VELOCITY, TIMES[1] ); + for( const auto polyhedron : geode::Range{ mesh.nb_polyhedra() } ) { - attribute->set_value( polyhedron, velocity( polyhedron ) ); + const auto point = builder->create_point( + mesh.polyhedron_barycenter( polyhedron ) + translation ); + attribute->set_value( point, velocity( polyhedron ) ); } + return barycenters; + } + + geode::ExternalTimeSeriesHelpers create_helpers( const geode::BRep& model, + const geode::uuid& block_id, + const geode::Vector3D& translation ) + { + geode::ExternalTimeSeriesHelpers helpers; + helpers.set_unique_vertices( create_unique_vertices( model ) ); + helpers.set_cell_barycenters( + create_cell_barycenters( model, block_id, translation ) ); return helpers; } + geode::ExternalTimeSeriesHelpers create_helpers( + const geode::BRep& model, const geode::uuid& block_id ) + { + return create_helpers( model, block_id, geode::Vector3D{} ); + } + void check_pressures( const geode::BRep& model ) { for( const auto& block : model.blocks() ) @@ -111,7 +171,7 @@ namespace geode::OpenGeodeModelException::test( series.nb_time_steps() == TIMES.size(), "[Test] Wrong number of pressure time steps on Block ", - block.id().string() ); + block.id().string(), ": ", series.nb_time_steps() ); for( const auto step : geode::Range{ series.nb_time_steps() } ) { geode::OpenGeodeModelException::test( @@ -147,16 +207,26 @@ namespace series.value( 0, polyhedron ) == velocity( polyhedron ), "[Test] Wrong velocity on polyhedron ", polyhedron ); } + for( const auto& block : model.blocks() ) + { + if( block.id() == block_id ) + { + continue; + } + geode::OpenGeodeModelException::test( + block.mesh() + .polyhedron_attribute_manager() + .time_steps( VELOCITY ) + .empty(), + "[Test] Block ", block.id().string(), + " should have no velocity" ); + } } - void check_other_model( const geode::ExternalTimeSeriesHelpers& helpers, - const geode::BRep& model ) + void check_other_model( const geode::ExternalTimeSeriesHelpers& helpers ) { auto other = geode::load_brep( - absl::StrCat( geode::DATA_PATH, "test_mesh3.og_brep" ) ); - geode::OpenGeodeModelException::test( - other.nb_unique_vertices() != model.nb_unique_vertices(), - "[Test] Models should have different unique vertices" ); + absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); bool thrown{ false }; try { @@ -170,6 +240,49 @@ namespace thrown, "[Test] Applying on another model should throw" ); } + // Translated barycenters are matched only with a large enough tolerance + // Barycenters translated below the tolerance are matched, beyond it + // they are rejected + void check_tolerance( const geode::uuid& block_id ) + { + auto model = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "test_mesh3.og_brep" ) ); + create_helpers( + model, block_id, geode::Vector3D{ { ROUNDING_TRANSLATION, 0, 0 } } ) + .apply_to( model ); + check_velocities( model, block_id ); + const auto wrong_helpers = create_helpers( + model, block_id, geode::Vector3D{ { WRONG_TRANSLATION, 0, 0 } } ); + bool thrown{ false }; + try + { + wrong_helpers.apply_to( model ); + } + catch( const geode::OpenGeodeException& ) + { + thrown = true; + } + geode::OpenGeodeModelException::test( thrown, + "[Test] Applying barycenters translated beyond the tolerance " + "should throw" ); + } + + void check_null_point_set() + { + geode::ExternalTimeSeriesHelpers helpers; + bool thrown{ false }; + try + { + helpers.set_cell_barycenters( nullptr ); + } + catch( const geode::OpenGeodeException& ) + { + thrown = true; + } + geode::OpenGeodeModelException::test( + thrown, "[Test] Setting a null point set should throw" ); + } + void check_wrong_extension( const geode::ExternalTimeSeriesHelpers& helpers ) { @@ -191,8 +304,8 @@ namespace void test() { geode::OpenGeodeModelLibrary::initialize(); - auto model = - geode::load_brep( absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); + auto model = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "test_mesh3.og_brep" ) ); geode::OpenGeodeModelException::test( model.nb_blocks() > 0, "[Test] Model has no Block" ); const auto block_id = ( *model.blocks().begin() ).id(); @@ -209,12 +322,14 @@ void test() check_pressures( model ); check_velocities( model, block_id ); - auto reloaded_model = - geode::load_brep( absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); + auto reloaded_model = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "test_mesh3.og_brep" ) ); geode::load_brep_time_series( reloaded_model, filename ); check_pressures( reloaded_model ); check_velocities( reloaded_model, block_id ); - check_other_model( helpers, model ); + check_other_model( helpers ); + check_tolerance( block_id ); + check_null_point_set(); check_wrong_extension( helpers ); } From 358a966b77cdde005086fbf8dbc4682d58b5ac4a Mon Sep 17 00:00:00 2001 From: BenPinet Date: Fri, 23 Oct 2026 12:02:18 +0200 Subject: [PATCH 5/6] feat(ExternalTimeSeriesHelpers): reject ambiguous matches Throw when several points are matched with the same Block element or when a point is matched with colocated Block elements, instead of silently keeping the last point. Import one-to-one matches through an old2new array and build a GenericMapping only when a point fills several elements of a Block. Co-Authored-By: Claude Opus 5.5 --- .../helpers/external_time_series_helpers.hpp | 4 +- .../helpers/external_time_series_helpers.cpp | 60 +++++++++++++++++-- .../test-external-time-series-helpers.cpp | 25 ++++++++ 3 files changed, 82 insertions(+), 7 deletions(-) diff --git a/include/geode/model/helpers/external_time_series_helpers.hpp b/include/geode/model/helpers/external_time_series_helpers.hpp index 27a2f8a13..ecee8b309 100644 --- a/include/geode/model/helpers/external_time_series_helpers.hpp +++ b/include/geode/model/helpers/external_time_series_helpers.hpp @@ -104,7 +104,9 @@ namespace geode * absolute coordinate of the points (when larger than 1), to accept * points rounded by the simulator (e.g. stored in single precision). * @exception OpenGeodeException if a point is farther than the - * tolerance from its closest Block element + * tolerance from its closest Block element, if several points are + * matched with the same Block element, or if a point is matched with + * colocated Block elements */ void apply_to( BRep& model ) const; diff --git a/src/geode/model/helpers/external_time_series_helpers.cpp b/src/geode/model/helpers/external_time_series_helpers.cpp index 818ef02f7..5be004a2e 100644 --- a/src/geode/model/helpers/external_time_series_helpers.cpp +++ b/src/geode/model/helpers/external_time_series_helpers.cpp @@ -109,6 +109,8 @@ namespace // Each point is matched with the closest element, elements without // point (e.g. outside of the simulated domain) are not matched. + // Matching is rejected when ambiguous: several points matched with the + // same element, or a point matched with colocated elements. // Returns the point matched with each element, NO_ID if none. std::vector< geode::index_t > match_points( const geode::PointSet3D& point_set, @@ -132,7 +134,8 @@ namespace for( const auto vertex : geode::Range{ point_set.nb_vertices() } ) { const auto& point = point_set.point( vertex ); - const auto element = search.closest_neighbor( point ); + const auto closest = search.neighbors( point, 2 ); + const auto element = closest.front(); const auto distance = geode::point_point_distance( point, search.point( element ) ); geode::OpenGeodeModelException::check_exception( @@ -143,6 +146,22 @@ namespace "] is at distance ", distance, " from the closest Block element, more than the tolerance ", tolerance ); + geode::OpenGeodeModelException::check_exception( + closest.size() < 2 + || geode::point_point_distance( + search.point( element ), search.point( closest[1] ) ) + > geode::GLOBAL_EPSILON, + nullptr, geode::OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] Point ", vertex, + " of the ", point_set_name, " [", point.string(), + "] is matched with colocated Block elements" ); + geode::OpenGeodeModelException::check_exception( + matches[element] == geode::NO_ID, nullptr, + geode::OpenGeodeException::TYPE::data, + "[ExternalTimeSeriesHelpers::apply_to] Points ", + matches[element], " and ", vertex, " of the ", point_set_name, + " are matched with the same Block element [", + search.point( element ).string(), "]" ); matches[element] = vertex; } return matches; @@ -235,24 +254,53 @@ namespace return matches; } + // A point fills several elements of a Block only when its unique vertex + // has several vertices in the Block mesh (e.g. mesh cut along an + // internal Surface): a GenericMapping is then needed + geode::GenericMapping< geode::index_t > point_to_elements( + absl::Span< const geode::index_t > matches ) + { + geode::GenericMapping< geode::index_t > point2elements; + for( const auto element : geode::Indices{ matches } ) + { + if( matches[element] != geode::NO_ID ) + { + point2elements.map( matches[element], element ); + } + } + return point2elements; + } + void apply_matches( const geode::AttributeManager& from, absl::Span< const geode::index_t > matches, geode::AttributeManager& target ) { - geode::GenericMapping< geode::index_t > point2element; + std::vector< geode::index_t > point2element( + from.nb_elements(), geode::NO_ID ); + bool matched{ false }; + bool one_to_one{ true }; for( const auto element : geode::Indices{ matches } ) { - if( matches[element] != geode::NO_ID ) + const auto point = matches[element]; + if( point == geode::NO_ID ) { - point2element.map( matches[element], element ); + continue; } + matched = true; + one_to_one = one_to_one && point2element[point] == geode::NO_ID; + point2element[point] = element; } - if( point2element.size_in() == 0 ) + if( !matched ) { return; } delete_replaced_steps( from, target ); - target.import( from, point2element ); + if( one_to_one ) + { + target.import( from, point2element ); + return; + } + target.import( from, point_to_elements( matches ) ); } } // namespace diff --git a/tests/model/test-external-time-series-helpers.cpp b/tests/model/test-external-time-series-helpers.cpp index 84ba1716f..e4a0c305a 100644 --- a/tests/model/test-external-time-series-helpers.cpp +++ b/tests/model/test-external-time-series-helpers.cpp @@ -267,6 +267,30 @@ namespace "should throw" ); } + // Values of colocated points cannot be told apart + void check_colocated_points( const geode::uuid& block_id ) + { + auto model = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "test_mesh3.og_brep" ) ); + auto barycenters = + create_cell_barycenters( model, block_id, geode::Vector3D{} ); + geode::PointSetBuilder3D::create( *barycenters ) + ->create_point( barycenters->point( 0 ) ); + geode::ExternalTimeSeriesHelpers helpers; + helpers.set_cell_barycenters( std::move( barycenters ) ); + bool thrown{ false }; + try + { + helpers.apply_to( model ); + } + catch( const geode::OpenGeodeException& ) + { + thrown = true; + } + geode::OpenGeodeModelException::test( + thrown, "[Test] Applying colocated barycenters should throw" ); + } + void check_null_point_set() { geode::ExternalTimeSeriesHelpers helpers; @@ -329,6 +353,7 @@ void test() check_velocities( reloaded_model, block_id ); check_other_model( helpers ); check_tolerance( block_id ); + check_colocated_points( block_id ); check_null_point_set(); check_wrong_extension( helpers ); } From fa8c7257c095c29d373e1658de7847741aae22e6 Mon Sep 17 00:00:00 2001 From: BenPinet <126688250+BenPinet@users.noreply.github.com> Date: Fri, 23 Oct 2026 10:03:35 +0000 Subject: [PATCH 6/6] Apply prepare changes --- .releaserc | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.releaserc b/.releaserc index bf47fdf5a..30a901b9b 100644 --- a/.releaserc +++ b/.releaserc @@ -6,6 +6,6 @@ plugins: [ '@semantic-release/commit-analyzer', '@semantic-release/release-notes-generator', - '@semantic-release/github' + ['@semantic-release/github', { successCommentCondition: false, failCommentCondition: false }] ] }