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 }] ] } 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..ecee8b309 --- /dev/null +++ b/include/geode/model/helpers/external_time_series_helpers.hpp @@ -0,0 +1,121 @@ +/* + * 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 + +namespace bitsery +{ + class Access; +} // namespace bitsery + +namespace geode +{ + FORWARD_DECLARATION_DIMENSION_CLASS( PointSet ); + ALIAS_3D( PointSet ); + class BRep; +} // 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. + * 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. + * The native file can be loaded onto a BRep with load_brep_time_series. + */ + class opengeode_model_api ExternalTimeSeriesHelpers + { + OPENGEODE_DISABLE_COPY( ExternalTimeSeriesHelpers ); + + public: + ExternalTimeSeriesHelpers(); + 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(); + } + + /*! + * Set the simulation unique vertices, carrying the vertex values as + * vertex attributes + * @exception OpenGeodeException if the point set is null + */ + void set_unique_vertices( std::unique_ptr< PointSet3D > vertices ); + + /*! + * Set the simulation cell barycenters, carrying the cell values as + * vertex attributes + * @exception OpenGeodeException if the point set is null + */ + void set_cell_barycenters( std::unique_ptr< PointSet3D > barycenters ); + + /*! + * Write the transferable attributes on the Block meshes of the model: + * 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, 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; + + 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/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 a3d017bc4..6f4efa8dc 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,11 +98,14 @@ 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" "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 @@ -117,6 +121,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,12 +178,15 @@ 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" "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/helpers/external_time_series_helpers.cpp b/src/geode/model/helpers/external_time_series_helpers.cpp new file mode 100644 index 000000000..5be004a2e --- /dev/null +++ b/src/geode/model/helpers/external_time_series_helpers.cpp @@ -0,0 +1,415 @@ +/* + * 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 + +#include +#include +#include + +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& target, + 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 : target.time_steps( name ) ) + { + if( existing_step.time == step.time ) + { + target.delete_attribute( existing_step.attribute_id ); + } + } + } + } + + void delete_replaced_steps( + const geode::AttributeManager& from, geode::AttributeManager& target ) + { + for( const auto& name : from.time_series_names() ) + { + delete_replaced_steps( from, target, name ); + } + } + + // Coordinates are rounded relatively to their magnitude + double matching_tolerance( const geode::PointSet3D& point_set ) + { + const auto box = point_set.bounding_box(); + double largest_coordinate{ 1 }; + for( const auto& point : { box.min(), box.max() } ) + { + for( const auto axis : geode::LRange{ 3 } ) + { + largest_coordinate = std::max( + largest_coordinate, std::fabs( point.value( axis ) ) ); + } + } + return geode::GLOBAL_EPSILON * largest_coordinate; + } + + // 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, + 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 matches; + } + 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() } ) + { + const auto& point = point_set.point( vertex ); + 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( + 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 ); + 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; + } + + // 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& 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 ); + } + } + 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 } ) + { + matches[model_unique_vertices[index]] = point_matches[index]; + } + return matches; + } + + 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() ) + { + 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() } ) + { + const auto unique_vertex = + model.unique_vertex( { block.component_id(), vertex } ); + if( unique_vertex != geode::NO_ID ) + { + block_matches[vertex] = unique_matches[unique_vertex]; + } + } + } + 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() } ) + { + 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; + } + + // 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 ) + { + 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 } ) + { + const auto point = matches[element]; + if( point == geode::NO_ID ) + { + continue; + } + matched = true; + one_to_one = one_to_one && point2element[point] == geode::NO_ID; + point2element[point] = element; + } + if( !matched ) + { + return; + } + delete_replaced_steps( from, target ); + if( one_to_one ) + { + target.import( from, point2element ); + return; + } + target.import( from, point_to_elements( matches ) ); + } +} // namespace + +namespace geode +{ + class ExternalTimeSeriesHelpers::Impl + { + friend class bitsery::Access; + + public: + Impl() + : unique_vertices_{ PointSet3D::create() }, + cell_barycenters_{ PointSet3D::create() } + { + } + + void set_unique_vertices( std::unique_ptr< PointSet3D > vertices ) + { + check_point_set( vertices.get(), "unique vertices" ); + unique_vertices_ = std::move( vertices ); + } + + void set_cell_barycenters( std::unique_ptr< PointSet3D > barycenters ) + { + check_point_set( barycenters.get(), "cell barycenters" ); + cell_barycenters_ = std::move( barycenters ); + } + + void apply_to( const BRep& model ) const + { + 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() ) + { + 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() ); + } + } + + private: + template < typename Archive > + void serialize( Archive& archive ) + { + 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: + std::unique_ptr< PointSet3D > unique_vertices_; + std::unique_ptr< PointSet3D > cell_barycenters_; + }; + + ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers() = default; + + ExternalTimeSeriesHelpers::ExternalTimeSeriesHelpers( + ExternalTimeSeriesHelpers&& ) noexcept = default; + + ExternalTimeSeriesHelpers& ExternalTimeSeriesHelpers::operator=( + ExternalTimeSeriesHelpers&& ) noexcept = default; + + ExternalTimeSeriesHelpers::~ExternalTimeSeriesHelpers() = default; + + void ExternalTimeSeriesHelpers::set_unique_vertices( + std::unique_ptr< PointSet3D > vertices ) + { + impl_->set_unique_vertices( std::move( vertices ) ); + } + + void ExternalTimeSeriesHelpers::set_cell_barycenters( + std::unique_ptr< PointSet3D > barycenters ) + { + impl_->set_cell_barycenters( std::move( barycenters ) ); + } + + 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/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/CMakeLists.txt b/tests/model/CMakeLists.txt index 334bcb02c..cf1b310ac 100644 --- a/tests/model/CMakeLists.txt +++ b/tests/model/CMakeLists.txt @@ -81,6 +81,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-builder-robustness.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..e4a0c305a --- /dev/null +++ b/tests/model/test-external-time-series-helpers.cpp @@ -0,0 +1,361 @@ +/* + * 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 + +#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. }; + // 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 ) + { + return unique_vertex + time; + } + + std::array< double, 3 > velocity( geode::index_t polyhedron ) + { + return { static_cast< double >( polyhedron ), 1., 2. }; + } + + 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 ) + { + 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 ) + { + auto attribute = create_time_step< double >( + vertices->vertex_attribute_manager(), PRESSURE, time ); + for( const auto point : geode::Indices{ unique_vertices } ) + { + attribute->set_value( + point, pressure( unique_vertices[point], time ) ); + } + } + 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() } ) + { + 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() ) + { + 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(), ": ", series.nb_time_steps() ); + 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 ); + } + 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 ) + { + auto other = geode::load_brep( + absl::StrCat( geode::DATA_PATH, "layers.og_brep" ) ); + 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" ); + } + + // 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" ); + } + + // 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; + 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 ) + { + 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, "test_mesh3.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 ); + 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, "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 ); + check_tolerance( block_id ); + check_colocated_points( block_id ); + check_null_point_set(); + check_wrong_extension( helpers ); +} + +OPENGEODE_TEST( "external-time-series-helpers" )