From 8342774c8254eb404f84fcf915b471b10863cfcf Mon Sep 17 00:00:00 2001 From: "KOKO\\Mimi" Date: Fri, 31 Jul 2026 15:45:02 +0900 Subject: [PATCH] =?UTF-8?q?feat(equation-and-linear-solve):=20step=200=20?= =?UTF-8?q?=E2=80=94=20symmetric-csr-assembly?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .gitignore | 1 + CMakeLists.txt | 1 + include/fesa/assembly/equation_system.hpp | 14 + include/fesa/assembly/serial_assembler.hpp | 13 + include/fesa/assembly/symmetric_csr.hpp | 18 + include/fesa/fem/dof_manager.hpp | 2 +- include/fesa/model/domain.hpp | 2 + phases/index.json | 2 +- src/fesa/assembly/serial_assembler.cpp | 238 +++++++++++ src/fesa/model/domain.cpp | 18 + tests/CMakeLists.txt | 37 ++ tests/unit/assembly/serial_assembler_test.cpp | 368 ++++++++++++++++++ 12 files changed, 712 insertions(+), 2 deletions(-) create mode 100644 include/fesa/assembly/equation_system.hpp create mode 100644 include/fesa/assembly/serial_assembler.hpp create mode 100644 include/fesa/assembly/symmetric_csr.hpp create mode 100644 src/fesa/assembly/serial_assembler.cpp create mode 100644 tests/unit/assembly/serial_assembler_test.cpp diff --git a/.gitignore b/.gitignore index f09ed83..ac8e235 100644 --- a/.gitignore +++ b/.gitignore @@ -2,5 +2,6 @@ __pycache__/ .pytest_cache/ *.py[cod] .harness/build/ +*pytest-basetemp*/ out/ .worktrees/ diff --git a/CMakeLists.txt b/CMakeLists.txt index cb1e4a4..bffc356 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -26,6 +26,7 @@ include(CTest) include(cmake/FesaDependencies.cmake) add_library(fesa_core STATIC + src/fesa/assembly/serial_assembler.cpp src/fesa/core/version.cpp src/fesa/elements/beam/beam3d2.cpp src/fesa/fem/beam_frame.cpp diff --git a/include/fesa/assembly/equation_system.hpp b/include/fesa/assembly/equation_system.hpp new file mode 100644 index 0000000..ab80a55 --- /dev/null +++ b/include/fesa/assembly/equation_system.hpp @@ -0,0 +1,14 @@ +#pragma once + +#include + +#include + +namespace fesa { + +struct EquationSystem final { + SymmetricCsr stiffness; + std::vector force; +}; + +} // namespace fesa diff --git a/include/fesa/assembly/serial_assembler.hpp b/include/fesa/assembly/serial_assembler.hpp new file mode 100644 index 0000000..c1b53e4 --- /dev/null +++ b/include/fesa/assembly/serial_assembler.hpp @@ -0,0 +1,13 @@ +#pragma once + +#include +#include +#include + +namespace fesa { + +[[nodiscard]] EquationSystem assemble_serial( + const Domain& domain, + const DofManager& dofs); + +} // namespace fesa diff --git a/include/fesa/assembly/symmetric_csr.hpp b/include/fesa/assembly/symmetric_csr.hpp new file mode 100644 index 0000000..897e0dd --- /dev/null +++ b/include/fesa/assembly/symmetric_csr.hpp @@ -0,0 +1,18 @@ +#pragma once + +#include +#include +#include + +namespace fesa { + +// Zero-based CSR containing only the upper triangle. Column indices are +// strictly increasing within each row. +struct SymmetricCsr final { + std::size_t order; + std::vector row_offsets; + std::vector column_indices; + std::vector values; +}; + +} // namespace fesa diff --git a/include/fesa/fem/dof_manager.hpp b/include/fesa/fem/dof_manager.hpp index 77736df..a844426 100644 --- a/include/fesa/fem/dof_manager.hpp +++ b/include/fesa/fem/dof_manager.hpp @@ -34,13 +34,13 @@ public: [[nodiscard]] std::size_t free_equation_count() const noexcept; [[nodiscard]] std::optional equation( DofAddress address) const; + [[nodiscard]] std::size_t full_dof(DofAddress address) const; [[nodiscard]] std::array element_full_dofs( const BeamElement& element) const; [[nodiscard]] std::vector reconstruct_full( std::span reduced) const; private: - [[nodiscard]] std::size_t full_dof(DofAddress address) const; [[nodiscard]] std::size_t node_full_dof_base(NodeId node) const; std::map node_full_dof_bases_; diff --git a/include/fesa/model/domain.hpp b/include/fesa/model/domain.hpp index 49f9620..921d70a 100644 --- a/include/fesa/model/domain.hpp +++ b/include/fesa/model/domain.hpp @@ -57,6 +57,8 @@ public: [[nodiscard]] const Node& node(NodeId id) const; [[nodiscard]] const Node& node(const EntityOrigin& origin) const; + [[nodiscard]] const IsotropicElastic& material(MaterialId id) const; + [[nodiscard]] const BeamSection& section(SectionId id) const; private: friend class DomainBuilder; diff --git a/phases/index.json b/phases/index.json index 9fcc70f..8ed475f 100644 --- a/phases/index.json +++ b/phases/index.json @@ -44,4 +44,4 @@ "status": "pending" } ] -} \ No newline at end of file +} diff --git a/src/fesa/assembly/serial_assembler.cpp b/src/fesa/assembly/serial_assembler.cpp new file mode 100644 index 0000000..0583aec --- /dev/null +++ b/src/fesa/assembly/serial_assembler.cpp @@ -0,0 +1,238 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +namespace fesa { +namespace { + +struct NumericContribution final { + std::size_t row; + std::size_t column; + const EntityOrigin* element_origin; + std::size_t local_order; + double value; +}; + +std::int32_t csr_index(const std::size_t value) { + if (value > + static_cast( + std::numeric_limits::max())) { + throw std::overflow_error{ + "Symmetric CSR exceeds the 32-bit index range."}; + } + return static_cast(value); +} + +auto origin_key(const EntityOrigin& origin) { + return std::tie( + origin.instance_name, + origin.local_label, + origin.part_name); +} + +SymmetricCsr build_sparsity_pattern( + const Domain& domain, + const DofManager& dofs) { + using Coordinate = std::pair; + std::vector coordinates; + coordinates.reserve(domain.beam_elements().size() * 78); + + for (const BeamElement& element : domain.beam_elements()) { + const std::array full_dofs = + dofs.element_full_dofs(element); + for (std::size_t local_row = 0; local_row < full_dofs.size(); + ++local_row) { + for (std::size_t local_column = local_row; + local_column < full_dofs.size(); + ++local_column) { + coordinates.emplace_back( + std::min( + full_dofs[local_row], + full_dofs[local_column]), + std::max( + full_dofs[local_row], + full_dofs[local_column])); + } + } + } + + std::ranges::sort(coordinates); + coordinates.erase( + std::ranges::unique(coordinates).begin(), + coordinates.end()); + + SymmetricCsr pattern{ + dofs.full_dof_count(), + std::vector(dofs.full_dof_count() + 1, 0), + {}, + {}, + }; + pattern.column_indices.reserve(coordinates.size()); + for (const auto [row, column] : coordinates) { + if (row >= pattern.order || column >= pattern.order) { + throw std::invalid_argument{ + "DofManager element mapping exceeds the full system order."}; + } + ++pattern.row_offsets[row + 1]; + pattern.column_indices.push_back(csr_index(column)); + } + for (std::size_t row = 0; row < pattern.order; ++row) { + const std::size_t offset = + static_cast(pattern.row_offsets[row]) + + static_cast(pattern.row_offsets[row + 1]); + pattern.row_offsets[row + 1] = csr_index(offset); + } + pattern.values.resize(pattern.column_indices.size(), 0.0); + return pattern; +} + +std::runtime_error kernel_error( + const BeamElement& element, + const BeamKernelResult& result) { + std::string message = + "Beam element " + std::to_string(element.origin.local_label) + + " kernel failed"; + for (const Diagnostic& diagnostic : result.diagnostics) { + message += ": " + diagnostic.code + " - " + diagnostic.message; + } + return std::runtime_error{std::move(message)}; +} + +std::vector collect_numeric_contributions( + const Domain& domain, + const DofManager& dofs) { + std::vector contributions; + contributions.reserve(domain.beam_elements().size() * 78); + + for (const BeamElement& element : domain.beam_elements()) { + const BeamKernelResult result = compute_beam3d2({ + { + domain.node(element.nodes[0]).position, + domain.node(element.nodes[1]).position, + }, + domain.material(element.material), + domain.section(element.section), + }); + if (!result.contribution.has_value()) { + throw kernel_error(element, result); + } + + const std::array full_dofs = + dofs.element_full_dofs(element); + for (std::size_t local_row = 0; local_row < full_dofs.size(); + ++local_row) { + for (std::size_t local_column = local_row; + local_column < full_dofs.size(); + ++local_column) { + contributions.push_back({ + std::min( + full_dofs[local_row], + full_dofs[local_column]), + std::max( + full_dofs[local_row], + full_dofs[local_column]), + &element.origin, + local_row * full_dofs.size() + local_column, + result.contribution + ->global_stiffness[local_row][local_column], + }); + } + } + } + + std::ranges::sort( + contributions, + [](const NumericContribution& left, + const NumericContribution& right) { + return std::tuple{ + left.row, + left.column, + origin_key(*left.element_origin), + left.local_order} < + std::tuple{ + right.row, + right.column, + origin_key(*right.element_origin), + right.local_order}; + }); + return contributions; +} + +void merge_numeric_contributions( + SymmetricCsr& matrix, + const std::vector& contributions) { + std::size_t contribution_index = 0; + while (contribution_index < contributions.size()) { + const NumericContribution& first = + contributions[contribution_index]; + double value = 0.0; + do { + value += contributions[contribution_index].value; + ++contribution_index; + } while ( + contribution_index < contributions.size() && + contributions[contribution_index].row == first.row && + contributions[contribution_index].column == first.column); + + const auto row_begin = + matrix.column_indices.begin() + + matrix.row_offsets[first.row]; + const auto row_end = + matrix.column_indices.begin() + + matrix.row_offsets[first.row + 1]; + const auto entry = std::lower_bound( + row_begin, + row_end, + csr_index(first.column)); + if (entry == row_end || *entry != csr_index(first.column)) { + throw std::logic_error{ + "Numeric contribution is absent from the CSR pattern."}; + } + matrix.values[static_cast( + std::distance(matrix.column_indices.begin(), entry))] = value; + } +} + +std::vector assemble_force( + const Domain& domain, + const DofManager& dofs) { + std::vector force(dofs.full_dof_count(), 0.0); + for (const NodalLoad& load : domain.step().nodal_loads) { + for (std::size_t component = 0; component < load.values.size(); + ++component) { + const auto dof = static_cast(component); + force[dofs.full_dof({load.node, dof})] += + load.values[component]; + } + } + return force; +} + +} // namespace + +EquationSystem assemble_serial( + const Domain& domain, + const DofManager& dofs) { + SymmetricCsr stiffness = build_sparsity_pattern(domain, dofs); + const std::vector contributions = + collect_numeric_contributions(domain, dofs); + merge_numeric_contributions(stiffness, contributions); + return { + std::move(stiffness), + assemble_force(domain, dofs), + }; +} + +} // namespace fesa diff --git a/src/fesa/model/domain.cpp b/src/fesa/model/domain.cpp index fbc965d..932d4dd 100644 --- a/src/fesa/model/domain.cpp +++ b/src/fesa/model/domain.cpp @@ -55,6 +55,24 @@ const Node& Domain::node(const EntityOrigin& origin) const { return nodes_[found->second]; } +const IsotropicElastic& Domain::material(const MaterialId id) const { + const auto found = material_indices_.find(id.value()); + if (found == material_indices_.end()) { + throw std::out_of_range{ + "Material ID is not present in the Domain."}; + } + return materials_[found->second]; +} + +const BeamSection& Domain::section(const SectionId id) const { + const auto found = section_indices_.find(id.value()); + if (found == section_indices_.end()) { + throw std::out_of_range{ + "Section ID is not present in the Domain."}; + } + return sections_[found->second]; +} + Domain::OriginKey Domain::origin_key(const EntityOrigin& origin) { return {origin.instance_name, origin.local_label}; } diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt index e5cebd8..cc0796a 100644 --- a/tests/CMakeLists.txt +++ b/tests/CMakeLists.txt @@ -271,3 +271,40 @@ add_test( COMMAND "$" --gtest_filter=RigidBody.* ) + +add_executable(fesa_serial_assembly_tests + unit/assembly/serial_assembler_test.cpp +) + +target_compile_features(fesa_serial_assembly_tests PRIVATE cxx_std_20) +target_compile_options( + fesa_serial_assembly_tests + PRIVATE + /W4 + /permissive- + /EHsc +) + +target_link_libraries(fesa_serial_assembly_tests + PRIVATE + fesa_core + GTest::gtest_main +) + +add_test( + NAME SparsePattern + COMMAND "$" + --gtest_filter=SparsePattern.* +) + +add_test( + NAME SerialAssembly + COMMAND "$" + --gtest_filter=SerialAssembly.* +) + +add_test( + NAME SymmetricCsr + COMMAND "$" + --gtest_filter=SymmetricCsr.* +) diff --git a/tests/unit/assembly/serial_assembler_test.cpp b/tests/unit/assembly/serial_assembler_test.cpp new file mode 100644 index 0000000..fd1b532 --- /dev/null +++ b/tests/unit/assembly/serial_assembler_test.cpp @@ -0,0 +1,368 @@ +#include + +#include +#include +#include +#include +#include +#include +#include +#include +#include + +#include + +#include + +namespace { + +fesa::IsotropicElastic unit_material() { + return { + fesa::MaterialId{0}, + "Unit", + 1.0, + 0.25, + }; +} + +fesa::BeamSection unit_section() { + return { + fesa::SectionId{0}, + "Unit", + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + 1.0, + fesa::ShearPropertySource::input, + fesa::Vec3{0.0, 1.0, 0.0}, + {}, + }; +} + +fesa::Domain finish_domain( + fesa::DomainBuilder builder, + fesa::StepDefinition step) { + builder.set_step(std::move(step)); + auto result = std::move(builder).build(); + if (!result.domain.has_value()) { + throw std::runtime_error{"Test Domain failed validation."}; + } + return std::move(*result.domain); +} + +fesa::Domain build_chain_domain(const bool reverse_storage_order) { + fesa::DomainBuilder builder; + std::array nodes{{ + { + fesa::NodeId{20}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, + fesa::Vec3{2.0, 0.0, 0.0}, + }, + { + fesa::NodeId{4}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 100}, + fesa::Vec3{0.0, 0.0, 0.0}, + }, + { + fesa::NodeId{10}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 50}, + fesa::Vec3{1.0, 0.0, 0.0}, + }, + }}; + if (reverse_storage_order) { + std::ranges::reverse(nodes); + } + for (fesa::Node& node : nodes) { + builder.add_node(std::move(node)); + } + + builder.add_material(unit_material()); + builder.add_section(unit_section()); + + std::array elements{{ + { + fesa::ElementId{1}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 200}, + {fesa::NodeId{10}, fesa::NodeId{20}}, + fesa::MaterialId{0}, + fesa::SectionId{0}, + }, + { + fesa::ElementId{0}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 10}, + {fesa::NodeId{4}, fesa::NodeId{10}}, + fesa::MaterialId{0}, + fesa::SectionId{0}, + }, + }}; + if (reverse_storage_order) { + std::ranges::reverse(elements); + } + for (fesa::BeamElement& element : elements) { + builder.add_beam_element(std::move(element)); + } + + return finish_domain( + std::move(builder), + { + "Load", + {}, + { + { + fesa::NodeId{20}, + {3.0, 4.0, 5.0, 6.0, 7.0, 8.0}, + }, + { + fesa::NodeId{10}, + {1.0, 2.0, 3.0, 4.0, 5.0, 6.0}, + }, + { + fesa::NodeId{10}, + {-1.0, 10.0, -3.0, 0.0, 0.0, 0.0}, + }, + }, + }); +} + +fesa::Domain build_parallel_domain( + const std::vector& element_labels) { + fesa::DomainBuilder builder; + builder.add_node({ + fesa::NodeId{4}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, + fesa::Vec3{0.0, 0.0, 0.0}, + }); + builder.add_node({ + fesa::NodeId{10}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, + fesa::Vec3{1.0, 0.0, 0.0}, + }); + builder.add_material(unit_material()); + builder.add_section(unit_section()); + + for (std::size_t index = 0; index < element_labels.size(); ++index) { + builder.add_beam_element({ + fesa::ElementId{static_cast(index)}, + fesa::EntityOrigin{ + "BeamPart", + "Beam-1", + element_labels[index], + }, + {fesa::NodeId{4}, fesa::NodeId{10}}, + fesa::MaterialId{0}, + fesa::SectionId{0}, + }); + } + return finish_domain(std::move(builder), {"Load", {}, {}}); +} + +double csr_value( + const fesa::SymmetricCsr& matrix, + std::size_t row, + std::size_t column) { + if (column < row) { + std::swap(row, column); + } + const auto begin = matrix.column_indices.begin() + matrix.row_offsets[row]; + const auto end = + matrix.column_indices.begin() + matrix.row_offsets[row + 1]; + const auto found = std::lower_bound( + begin, end, static_cast(column)); + if (found == end || *found != static_cast(column)) { + throw std::out_of_range{"CSR entry is not present."}; + } + return matrix.values[static_cast( + std::distance(matrix.column_indices.begin(), found))]; +} + +TEST(SparsePattern, BuildsExpectedTwoElementChainStructure) { + const fesa::Domain domain = build_chain_domain(false); + const fesa::DofManager dofs = fesa::DofManager::build(domain); + + const fesa::EquationSystem system = + fesa::assemble_serial(domain, dofs); + + EXPECT_EQ( + system.stiffness.row_offsets, + (std::vector{ + 0, + 12, + 23, + 33, + 42, + 50, + 57, + 69, + 80, + 90, + 99, + 107, + 114, + 120, + 125, + 129, + 132, + 134, + 135, + })); + + std::vector expected_columns; + for (std::int32_t row = 0; row < 18; ++row) { + const std::int32_t last_column = row < 6 ? 11 : 17; + for (std::int32_t column = row; column <= last_column; ++column) { + expected_columns.push_back(column); + } + } + EXPECT_EQ(system.stiffness.column_indices, expected_columns); +} + +TEST(SymmetricCsr, StoresSortedUpperTriangleWithValidOffsets) { + const fesa::Domain domain = build_chain_domain(false); + const fesa::DofManager dofs = fesa::DofManager::build(domain); + const fesa::SymmetricCsr matrix = + fesa::assemble_serial(domain, dofs).stiffness; + + ASSERT_EQ(matrix.order, 18); + ASSERT_EQ(matrix.row_offsets.size(), matrix.order + 1); + ASSERT_EQ(matrix.row_offsets.front(), 0); + ASSERT_EQ( + static_cast(matrix.row_offsets.back()), + matrix.column_indices.size()); + ASSERT_EQ(matrix.column_indices.size(), matrix.values.size()); + + for (std::size_t row = 0; row < matrix.order; ++row) { + const auto begin = + matrix.column_indices.begin() + matrix.row_offsets[row]; + const auto end = + matrix.column_indices.begin() + matrix.row_offsets[row + 1]; + EXPECT_TRUE(std::ranges::is_sorted(begin, end)); + EXPECT_EQ(std::adjacent_find(begin, end), end); + for (auto entry = begin; entry != end; ++entry) { + EXPECT_GE(*entry, static_cast(row)); + EXPECT_LT(*entry, static_cast(matrix.order)); + } + } +} + +TEST(SerialAssembly, AssemblesHandCalculatedAxialChainAndFullLoad) { + const fesa::Domain domain = build_chain_domain(false); + const fesa::DofManager dofs = fesa::DofManager::build(domain); + + const fesa::EquationSystem system = + fesa::assemble_serial(domain, dofs); + + EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 0, 0), 1.0); + EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 0, 6), -1.0); + EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 6, 6), 2.0); + EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 6, 12), -1.0); + EXPECT_DOUBLE_EQ(csr_value(system.stiffness, 12, 12), 1.0); + + EXPECT_EQ( + system.force, + (std::vector{ + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 0.0, + 12.0, + 0.0, + 4.0, + 5.0, + 6.0, + 3.0, + 4.0, + 5.0, + 6.0, + 7.0, + 8.0, + })); +} + +TEST(SerialAssembly, MergesDuplicateElementContributions) { + const fesa::Domain single_domain = build_parallel_domain({10}); + const fesa::Domain duplicate_domain = + build_parallel_domain({30, 10, 20}); + const fesa::EquationSystem single = fesa::assemble_serial( + single_domain, fesa::DofManager::build(single_domain)); + const fesa::EquationSystem duplicate = fesa::assemble_serial( + duplicate_domain, fesa::DofManager::build(duplicate_domain)); + + EXPECT_EQ( + duplicate.stiffness.row_offsets, + single.stiffness.row_offsets); + EXPECT_EQ( + duplicate.stiffness.column_indices, + single.stiffness.column_indices); + ASSERT_EQ(duplicate.stiffness.values.size(), single.stiffness.values.size()); + for (std::size_t index = 0; index < single.stiffness.values.size(); + ++index) { + const double value = single.stiffness.values[index]; + EXPECT_DOUBLE_EQ( + duplicate.stiffness.values[index], (value + value) + value); + } +} + +TEST(SerialAssembly, IsIndependentOfDomainStorageAndExternalLabelOrder) { + const fesa::Domain first_domain = build_chain_domain(false); + const fesa::Domain second_domain = build_chain_domain(true); + + const fesa::EquationSystem first = fesa::assemble_serial( + first_domain, fesa::DofManager::build(first_domain)); + const fesa::EquationSystem second = fesa::assemble_serial( + second_domain, fesa::DofManager::build(second_domain)); + + EXPECT_EQ(first.stiffness.order, second.stiffness.order); + EXPECT_EQ( + first.stiffness.row_offsets, second.stiffness.row_offsets); + EXPECT_EQ( + first.stiffness.column_indices, + second.stiffness.column_indices); + EXPECT_EQ(first.stiffness.values, second.stiffness.values); + EXPECT_EQ(first.force, second.force); +} + +TEST(SerialAssembly, PropagatesBeamKernelFailure) { + fesa::DomainBuilder builder; + builder.add_node({ + fesa::NodeId{0}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, + fesa::Vec3{0.0, 0.0, 0.0}, + }); + builder.add_node({ + fesa::NodeId{1}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 2}, + fesa::Vec3{1.0, 0.0, 0.0}, + }); + auto material = unit_material(); + material.young = std::numeric_limits::max(); + builder.add_material(std::move(material)); + auto section = unit_section(); + section.area = std::numeric_limits::max(); + builder.add_section(std::move(section)); + builder.add_beam_element({ + fesa::ElementId{0}, + fesa::EntityOrigin{"BeamPart", "Beam-1", 1}, + {fesa::NodeId{0}, fesa::NodeId{1}}, + fesa::MaterialId{0}, + fesa::SectionId{0}, + }); + const fesa::Domain domain = + finish_domain(std::move(builder), {"Load", {}, {}}); + + try { + static_cast(fesa::assemble_serial( + domain, fesa::DofManager::build(domain))); + FAIL() << "Expected a Beam kernel failure."; + } catch (const std::runtime_error& error) { + EXPECT_NE( + std::string{error.what()}.find("model.nonfinite_value"), + std::string::npos); + } +} + +} // namespace