diff --git a/doc/documentation/src/analysis_guide_templates/geometrydefinition.rst.j2 b/doc/documentation/src/analysis_guide_templates/geometrydefinition.rst.j2 index d156d6d8914..adc87995ef8 100644 --- a/doc/documentation/src/analysis_guide_templates/geometrydefinition.rst.j2 +++ b/doc/documentation/src/analysis_guide_templates/geometrydefinition.rst.j2 @@ -53,9 +53,9 @@ The nodes are read in by the section ``NODE COORDS``. Within this section, the nodes are read one per line. There are different types of nodes: -- General nodes (NODE) -- Control points for nurbs geometry (CP) -- Nodes with addition fiber information (FNODE) +- General nodes (`NODE`) +- Control points for nurbs geometry (`CP`) +- Nodes with addition fiber information (`FNODE`) All node coordinates must be given with three coordinates, even if the problem is 1D or 2D: @@ -64,8 +64,14 @@ All node coordinates must be given with three coordinates, even if the problem i NODE COORDS: - "NODE COORD [ROTANGLE ]" - "CP COORD " - - "FNODE COORD [FIBER1|FIBER2|FIBER3|CIR|TAN|RAD|HELIX|TRANS] " + - "FNODE COORD FIBER1 [FIBER2 [FIBER3 ]]" + - "FNODE "COORD CIR TAN HELIX TRANS " +For ``FNODE`` in combination with fiber directions, the directions must be numbered consecutively starting with ``FIBER1``. +For the problemtype `Cardiac_Monodomain`, the fiber directions can be entered alternatively +using ``CIR``, ``TAN``, ``HELIX``, and ``TRANS``. +All nodes of an element must provide the same set of parameters, either explicit or cardiac fiber directions. +Their direction vectors and angle values may vary between nodes. .. _geometrysets: @@ -177,4 +183,3 @@ while the parameter :math:`\sigma` is defined by the parameter ``RADIUSDISTRIBUT PARTICLE DYNAMIC/DEM: INITIAL_RADIUS: RadiusFromParticleInput|NormalRadiusDistribution|LogNormalRadiusDistribution RADIUSDISTRIBUTION_SIGMA: # variation sigma for a radius distribution - diff --git a/src/core/fem/src/general/node/4C_fem_general_fiber_node_holder.cpp b/src/core/fem/src/general/node/4C_fem_general_fiber_node_holder.cpp index ba33068453f..94fc83a7f46 100644 --- a/src/core/fem/src/general/node/4C_fem_general_fiber_node_holder.cpp +++ b/src/core/fem/src/general/node/4C_fem_general_fiber_node_holder.cpp @@ -9,6 +9,7 @@ #include "4C_comm_parobject.hpp" #include "4C_fem_general_fiber_node.hpp" +#include "4C_utils_exceptions.hpp" FOUR_C_NAMESPACE_OPEN @@ -60,7 +61,13 @@ void Core::Nodes::NodalFiberHolder::set_angle(AngleType type, const std::vector< const std::vector& Core::Nodes::NodalFiberHolder::get_angle( Core::Nodes::AngleType type) const { - return angles_.at(type); + const auto angle = angles_.find(type); + if (angle == angles_.end()) + { + FOUR_C_THROW("Nodal fiber data does not contain the requested {} angle.", + type == AngleType::Helix ? "HELIX" : "TRANS"); + } + return angle->second; } std::size_t Core::Nodes::NodalFiberHolder::fibers_size() const { return fibers_.size(); } diff --git a/src/core/fem/src/general/node/4C_fem_general_fiber_node_utils.cpp b/src/core/fem/src/general/node/4C_fem_general_fiber_node_utils.cpp index 099332f8346..2960d8be5c5 100644 --- a/src/core/fem/src/general/node/4C_fem_general_fiber_node_utils.cpp +++ b/src/core/fem/src/general/node/4C_fem_general_fiber_node_utils.cpp @@ -36,6 +36,43 @@ void Core::Nodes::project_fibers_to_gauss_points(const Core::Nodes::Node* const* FOUR_C_THROW("At least one node of the element does not provide fibers."); } + if (inode > 0) + { + if (fiberNodes[inode]->fibers().size() != fiberNodes[0]->fibers().size()) + { + FOUR_C_THROW( + "All nodes of an element must define the same number of FIBER directions. Node {} " + "defines {}, but node {} defines {}.", + fiberNodes[inode]->id(), fiberNodes[inode]->fibers().size(), fiberNodes[0]->id(), + fiberNodes[0]->fibers().size()); + } + + const auto& reference_directions = fiberNodes[0]->coordinate_system_directions(); + const auto& node_directions = fiberNodes[inode]->coordinate_system_directions(); + const bool same_coordinate_directions = + reference_directions.size() == node_directions.size() && + std::all_of(reference_directions.begin(), reference_directions.end(), + [&node_directions](const auto& direction) + { return node_directions.contains(direction.first); }); + if (!same_coordinate_directions) + { + FOUR_C_THROW( + "All nodes of an element must define the same set of coordinate system direction " + "types (CIR and TAN)."); + } + + const auto& reference_angles = fiberNodes[0]->angles(); + const auto& node_angles = fiberNodes[inode]->angles(); + const bool same_angles = + reference_angles.size() == node_angles.size() && + std::all_of(reference_angles.begin(), reference_angles.end(), + [&node_angles](const auto& angle) { return node_angles.contains(angle.first); }); + if (!same_angles) + { + FOUR_C_THROW("All nodes of an element must define the same angle types."); + } + } + for (const auto& pair : fiberNodes[inode]->coordinate_system_directions()) { coordinateSystemDirections[pair.first][inode] = pair.second; @@ -120,21 +157,6 @@ void Core::Nodes::project_fibers_to_gauss_points(const Core::Nodes::Node* const* tan[gp] += -tancir * cir[gp]; tan[gp] *= 1.0 / Core::LinAlg::norm2(tan[gp]); } - - // orthogonalize radial vector, preserve circular and tangential direction - if (gpFiberHolder.contains_coordinate_system_direction(CoordinateSystemDirection::Radial)) - { - std::vector>& rad = - gpFiberHolder.get_coordinate_system_direction_mutual(CoordinateSystemDirection::Radial); - for (std::size_t gp = 0; gp < tan.size(); ++gp) - { - double radcir = rad[gp] * cir[gp]; - double radtan = rad[gp] * tan[gp]; - // double - rad[gp] += -radcir * cir[gp] - radtan * tan[gp]; - rad[gp] *= 1.0 / Core::LinAlg::norm2(rad[gp]); - } - } } } diff --git a/src/core/io/src/4C_io_meshreader.cpp b/src/core/io/src/4C_io_meshreader.cpp index 0f00a0b432f..1b68fefe2cf 100644 --- a/src/core/io/src/4C_io_meshreader.cpp +++ b/src/core/io/src/4C_io_meshreader.cpp @@ -66,6 +66,74 @@ namespace Core::IO::Internal */ std::optional> filtered_mesh_on_rank_zero{}; }; + + FiberNodeData read_fiber_node_data(Core::IO::ValueParser& parser) + { + FiberNodeData data; + + while (!parser.at_end()) + { + const auto next = parser.read(); + + if (next.starts_with("FIBER")) + { + const std::string expected = "FIBER" + std::to_string(data.fibers.size() + 1); + if (next != expected) + { + FOUR_C_THROW( + "Fiber directions in FNODE must be numbered consecutively. Expected '{}', found " + "'{}'.", + expected, next); + } + data.fibers.emplace_back(parser.read>()); + } + else if (next == "CIR") + { + data.coordinate_system_directions[Core::Nodes::CoordinateSystemDirection::Circular] = + parser.read>(); + } + else if (next == "TAN") + { + data.coordinate_system_directions[Core::Nodes::CoordinateSystemDirection::Tangential] = + parser.read>(); + } + else if (next == "HELIX") + { + data.angles[Core::Nodes::AngleType::Helix] = parser.read(); + } + else if (next == "TRANS") + { + data.angles[Core::Nodes::AngleType::Transverse] = parser.read(); + } + else + { + FOUR_C_THROW("Unknown FNODE parameter '{}'.", next); + } + } + + const bool has_circular = data.coordinate_system_directions.contains( + Core::Nodes::CoordinateSystemDirection::Circular); + const bool has_tangential = data.coordinate_system_directions.contains( + Core::Nodes::CoordinateSystemDirection::Tangential); + const bool has_helix = data.angles.contains(Core::Nodes::AngleType::Helix); + const bool has_transverse = data.angles.contains(Core::Nodes::AngleType::Transverse); + const bool has_cardiac_directions = + has_circular || has_tangential || has_helix || has_transverse; + + if (!data.fibers.empty() && has_cardiac_directions) + { + FOUR_C_THROW( + "Fiber directions in FNODE must be defined either by CIR/TAN/HELIX/TRANS or by FIBER1, " + "FIBER2, etc., but not by both."); + } + + if (has_cardiac_directions && !(has_circular && has_tangential && has_helix && has_transverse)) + { + FOUR_C_THROW("Cardiac fiber directions in FNODE require all of CIR, TAN, HELIX, and TRANS."); + } + + return data; + } } // namespace Core::IO::Internal namespace @@ -453,56 +521,11 @@ namespace // this is a special node with additional fiber information else if (type == "FNODE") { - enum class FiberType - { - Unknown, - Angle, - Fiber, - CosyDirection - }; - - // read fiber node - std::map> cosyDirections; - std::vector> fibers; - std::map angles; - int nodeid = parser.read() - 1; parser.consume("COORD"); auto coords = parser.read>(3); max_node_id = std::max(max_node_id, nodeid) + 1; - - while (!parser.at_end()) - { - auto next = parser.read(); - - if (next == "FIBER" + std::to_string(1 + fibers.size())) - { - fibers.emplace_back(parser.read>()); - } - else if (next == "CIR") - { - cosyDirections[Core::Nodes::CoordinateSystemDirection::Circular] = - parser.read>(); - } - else if (next == "TAN") - { - cosyDirections[Core::Nodes::CoordinateSystemDirection::Tangential] = - parser.read>(); - } - else if (next == "RAD") - { - cosyDirections[Core::Nodes::CoordinateSystemDirection::Radial] = - parser.read>(); - } - else if (next == "HELIX") - { - angles[Core::Nodes::AngleType::Helix] = parser.read(); - } - else if (next == "TRANS") - { - angles[Core::Nodes::AngleType::Transverse] = parser.read(); - } - } + auto fiber_node_data = Core::IO::Internal::read_fiber_node_data(parser); // add fiber information to node std::vector> discretizations = @@ -510,8 +533,9 @@ namespace for (auto& dis : discretizations) { sanitize_node_coordinates(nodeid, dis->n_dim(), coords); - auto node = std::make_shared( - nodeid, coords, cosyDirections, fibers, angles, myrank); + auto node = std::make_shared(nodeid, coords, + fiber_node_data.coordinate_system_directions, fiber_node_data.fibers, + fiber_node_data.angles, myrank); dis->add_node(coords, nodeid, node); } } diff --git a/src/core/io/src/4C_io_meshreader.hpp b/src/core/io/src/4C_io_meshreader.hpp index 1b9c5b8e6c0..8b440742c2d 100644 --- a/src/core/io/src/4C_io_meshreader.hpp +++ b/src/core/io/src/4C_io_meshreader.hpp @@ -10,11 +10,13 @@ #include "4C_config.hpp" +#include "4C_fem_general_fiber_node.hpp" #include "4C_linalg_graph.hpp" #include "4C_rebalance.hpp" #include +#include #include #include #include @@ -29,6 +31,7 @@ namespace Core::FE namespace Core::IO { class InputFile; + class ValueParser; namespace MeshInput { @@ -39,7 +42,17 @@ namespace Core::IO namespace Internal { struct MeshReader; - } + + struct FiberNodeData + { + std::map> + coordinate_system_directions; + std::vector> fibers; + std::map angles; + }; + + FiberNodeData read_fiber_node_data(Core::IO::ValueParser& parser); + } // namespace Internal /** diff --git a/src/solid_ele/4C_solid_ele_fiber.cpp b/src/solid_ele/4C_solid_ele_fiber.cpp index fd7342a8260..12cba57d1ce 100644 --- a/src/solid_ele/4C_solid_ele_fiber.cpp +++ b/src/solid_ele/4C_solid_ele_fiber.cpp @@ -14,11 +14,45 @@ #include FOUR_C_NAMESPACE_OPEN +namespace +{ + bool has_nonempty_optional_vector_parameter( + const Core::IO::InputParameterContainer& input_data, const std::string& name) + { + const auto* parameter = input_data.get_if>>(name); + return parameter != nullptr && parameter->has_value() && !parameter->value().empty(); + } + bool has_nonempty_optional_vector(const std::optional>* parameter) + { + return parameter != nullptr && parameter->has_value() && !parameter->value().empty(); + } + + void validate_direction_input(const Core::IO::InputParameterContainer& input_data) + { + const bool has_coordinate_system = has_nonempty_optional_vector_parameter(input_data, "RAD") || + has_nonempty_optional_vector_parameter(input_data, "AXI") || + has_nonempty_optional_vector_parameter(input_data, "CIR"); + const bool has_fiber1 = has_nonempty_optional_vector_parameter(input_data, "FIBER1"); + const bool has_fiber2 = has_nonempty_optional_vector_parameter(input_data, "FIBER2"); + const bool has_fiber3 = has_nonempty_optional_vector_parameter(input_data, "FIBER3"); + const bool has_fibers = has_fiber1 || has_fiber2 || has_fiber3; + + FOUR_C_ASSERT_ALWAYS(!(has_coordinate_system && has_fibers), + "Material directions must be specified either by RAD/AXI/CIR or by FIBER1, FIBER2, " + "FIBER3, but not by both."); + FOUR_C_ASSERT_ALWAYS(!has_fiber2 || has_fiber1, + "Fiber directions must be numbered consecutively: FIBER2 requires FIBER1."); + FOUR_C_ASSERT_ALWAYS(!has_fiber3 || has_fiber2, + "Fiber directions must be numbered consecutively: FIBER3 requires FIBER2."); + } +} // namespace Discret::Elements::Fibers Discret::Elements::read_fibers( const Core::IO::InputParameterContainer& input_data) { + validate_direction_input(input_data); + Fibers fibers{}; // for now only support the old style fiber input via FIBER1, FIBER2, ... keywords @@ -27,7 +61,7 @@ Discret::Elements::Fibers Discret::Elements::read_fibers( const std::string fiber_name = "FIBER" + std::to_string(i); const auto* fiber_ptr = input_data.get_if>>(fiber_name); - if (!fiber_ptr || !fiber_ptr->has_value()) + if (!has_nonempty_optional_vector(fiber_ptr)) { break; } @@ -38,26 +72,26 @@ Discret::Elements::Fibers Discret::Elements::read_fibers( fibers.element_fibers.emplace_back(tensor); } - return fibers; } std::optional Discret::Elements::read_coordinate_system( const Core::IO::InputParameterContainer& input_data) { + validate_direction_input(input_data); + // for now only support the old style fiber input via RAD, AXI, CIR keywords const std::array rad_axi_cir = {input_data.get_if>>("RAD"), input_data.get_if>>("AXI"), input_data.get_if>>("CIR")}; - if (std::ranges::none_of(rad_axi_cir, [](const auto* p) { return p && p->has_value(); })) + if (std::ranges::none_of(rad_axi_cir, has_nonempty_optional_vector)) { // no local coordinate system defined return std::nullopt; } - FOUR_C_ASSERT_ALWAYS( - std::ranges::all_of(rad_axi_cir, [](const auto* p) { return p && p->has_value(); }), + FOUR_C_ASSERT_ALWAYS(std::ranges::all_of(rad_axi_cir, has_nonempty_optional_vector), "If you specify a coordinate system, you need to define all of RAD, AXI and CIR!"); CoordinateSystem coord_sys{}; diff --git a/unittests/io/4C_meshreader_fiber_node_test.cpp b/unittests/io/4C_meshreader_fiber_node_test.cpp new file mode 100644 index 00000000000..e5ce298d9b9 --- /dev/null +++ b/unittests/io/4C_meshreader_fiber_node_test.cpp @@ -0,0 +1,105 @@ +// This file is part of 4C multiphysics licensed under the +// GNU Lesser General Public License v3.0 or later. +// +// See the LICENSE.md file in the top-level for license information. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "4C_fem_general_fiber_node.hpp" +#include "4C_fem_general_fiber_node_holder.hpp" +#include "4C_fem_general_fiber_node_utils.hpp" +#include "4C_io_meshreader.hpp" +#include "4C_io_value_parser.hpp" +#include "4C_unittest_utils_assertions_test.hpp" +#include "4C_utils_exceptions.hpp" + +namespace +{ + using namespace FourC; + + TEST(MeshReaderFiberNodeTest, RejectsFiberNumberingGap) + { + Core::IO::ValueParser parser("FIBER2 0.0 1.0 0.0"); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE( + Core::IO::Internal::read_fiber_node_data(parser), Core::Exception, "Expected 'FIBER1'"); + } + + TEST(MeshReaderFiberNodeTest, RejectsFiberAndCoordinateSystem) + { + Core::IO::ValueParser parser( + "FIBER1 1.0 0.0 0.0 CIR 1.0 0.0 0.0 TAN 0.0 1.0 0.0 HELIX 0.0 TRANS 0.0"); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE(Core::IO::Internal::read_fiber_node_data(parser), + Core::Exception, "either by CIR/TAN/HELIX/TRANS or by FIBER1"); + } + + TEST(MeshReaderFiberNodeTest, RejectsIncompleteCardiacFiberDirections) + { + for (const std::string input : { + "TAN 0.0 1.0 0.0 HELIX 0.0 TRANS 0.0", + "CIR 1.0 0.0 0.0 HELIX 0.0 TRANS 0.0", + "CIR 1.0 0.0 0.0 TAN 0.0 1.0 0.0 TRANS 0.0", + "CIR 1.0 0.0 0.0 TAN 0.0 1.0 0.0 HELIX 0.0", + }) + { + Core::IO::ValueParser parser(input); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE(Core::IO::Internal::read_fiber_node_data(parser), + Core::Exception, "require all of CIR, TAN, HELIX, and TRANS"); + } + } + + TEST(MeshReaderFiberNodeTest, AcceptsConsecutiveFibers) + { + Core::IO::ValueParser parser("FIBER1 1.0 0.0 0.0 FIBER2 0.0 1.0 0.0 FIBER3 0.0 0.0 1.0"); + + const auto data = Core::IO::Internal::read_fiber_node_data(parser); + + EXPECT_EQ(data.fibers.size(), 3); + EXPECT_TRUE(data.coordinate_system_directions.empty()); + } + + TEST(MeshReaderFiberNodeTest, AcceptsCardiacFiberDirections) + { + Core::IO::ValueParser parser("CIR 1.0 0.0 0.0 TAN 0.0 1.0 0.0 HELIX 0.0 TRANS 0.0"); + + const auto data = Core::IO::Internal::read_fiber_node_data(parser); + + EXPECT_EQ(data.coordinate_system_directions.size(), 2); + EXPECT_EQ(data.angles.size(), 2); + EXPECT_TRUE(data.fibers.empty()); + } + + TEST(MeshReaderFiberNodeTest, ReportsMissingNodalFiberAngle) + { + Core::Nodes::NodalFiberHolder holder; + + FOUR_C_EXPECT_THROW_WITH_MESSAGE(holder.get_angle(Core::Nodes::AngleType::Helix), + Core::Exception, "does not contain the requested HELIX angle"); + FOUR_C_EXPECT_THROW_WITH_MESSAGE(holder.get_angle(Core::Nodes::AngleType::Transverse), + Core::Exception, "does not contain the requested TRANS angle"); + } + + TEST(MeshReaderFiberNodeTest, RejectsDifferentFiberCountsWithinElement) + { + const std::array coordinates{}; + const std::array fiber{1.0, 0.0, 0.0}; + std::array fiber_nodes{ + Core::Nodes::FiberNode(0, coordinates, {}, {fiber}, {}, 0), + Core::Nodes::FiberNode(1, coordinates, {}, {fiber, fiber}, {}, 0), + Core::Nodes::FiberNode(2, coordinates, {}, {fiber}, {}, 0), + Core::Nodes::FiberNode(3, coordinates, {}, {fiber}, {}, 0)}; + std::array nodes{ + &fiber_nodes[0], &fiber_nodes[1], &fiber_nodes[2], &fiber_nodes[3]}; + Core::Nodes::NodalFiberHolder holder; + const std::vector> shape_functions; + + FOUR_C_EXPECT_THROW_WITH_MESSAGE( + Core::Nodes::project_fibers_to_gauss_points( + nodes.data(), shape_functions, holder), + Core::Exception, "same number of FIBER directions"); + } +} // namespace diff --git a/unittests/solid_ele/4C_solid_ele_fiber_test.cpp b/unittests/solid_ele/4C_solid_ele_fiber_test.cpp new file mode 100644 index 00000000000..cf7dc1a5a7a --- /dev/null +++ b/unittests/solid_ele/4C_solid_ele_fiber_test.cpp @@ -0,0 +1,93 @@ +// This file is part of 4C multiphysics licensed under the +// GNU Lesser General Public License v3.0 or later. +// +// See the LICENSE.md file in the top-level for license information. +// +// SPDX-License-Identifier: LGPL-3.0-or-later + +#include + +#include "4C_solid_ele_fibers.hpp" +#include "4C_unittest_utils_assertions_test.hpp" +#include "4C_utils_exceptions.hpp" + +namespace +{ + using namespace FourC; + + Core::IO::InputParameterContainer direction_input_with_fiber_and_coordinate_system() + { + Core::IO::InputParameterContainer input; + input.add("FIBER1", std::optional>{{1.0, 0.0, 0.0}}); + input.add("RAD", std::optional>{{1.0, 0.0, 0.0}}); + input.add("AXI", std::optional>{{0.0, 1.0, 0.0}}); + input.add("CIR", std::optional>{{0.0, 0.0, 1.0}}); + return input; + } + + void add_empty_optional_direction_defaults(Core::IO::InputParameterContainer& input) + { + for (const char* direction : {"RAD", "AXI", "CIR", "FIBER1", "FIBER2", "FIBER3"}) + input.add(direction, std::optional>{std::vector{}}); + } + + TEST(SolidElementFiberTest, RejectsFiberAndCoordinateSystemWhenReadingFibers) + { + const auto input = direction_input_with_fiber_and_coordinate_system(); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE(Discret::Elements::read_fibers(input), Core::Exception, + "either by RAD/AXI/CIR or by FIBER1, FIBER2, FIBER3"); + } + + TEST(SolidElementFiberTest, RejectsFiberAndCoordinateSystemWhenReadingCoordinateSystem) + { + const auto input = direction_input_with_fiber_and_coordinate_system(); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE(Discret::Elements::read_coordinate_system(input), + Core::Exception, "either by RAD/AXI/CIR or by FIBER1, FIBER2, FIBER3"); + } + + TEST(SolidElementFiberTest, RejectsFiber2WithoutFiber1) + { + Core::IO::InputParameterContainer input; + input.add("FIBER2", std::optional>{{0.0, 1.0, 0.0}}); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE( + Discret::Elements::read_fibers(input), Core::Exception, "FIBER2 requires FIBER1"); + } + + TEST(SolidElementFiberTest, RejectsFiber3WithoutFiber2) + { + Core::IO::InputParameterContainer input; + input.add("FIBER1", std::optional>{{1.0, 0.0, 0.0}}); + input.add("FIBER3", std::optional>{{0.0, 0.0, 1.0}}); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE( + Discret::Elements::read_fibers(input), Core::Exception, "FIBER3 requires FIBER2"); + } + + TEST(SolidElementFiberTest, RejectsIncompleteCoordinateSystem) + { + for (const char* direction : {"RAD", "AXI", "CIR"}) + { + Core::IO::InputParameterContainer input; + add_empty_optional_direction_defaults(input); + input.add(direction, std::optional>{{1.0, 0.0, 0.0}}); + + FOUR_C_EXPECT_THROW_WITH_MESSAGE(Discret::Elements::read_coordinate_system(input), + Core::Exception, "need to define all of RAD, AXI and CIR"); + } + } + + TEST(SolidElementFiberTest, IgnoresEmptyOptionalFiberDefaults) + { + Core::IO::InputParameterContainer input; + add_empty_optional_direction_defaults(input); + input.add("RAD", std::optional>{{1.0, 0.0, 0.0}}); + input.add("AXI", std::optional>{{0.0, 1.0, 0.0}}); + input.add("CIR", std::optional>{{0.0, 0.0, 1.0}}); + + EXPECT_NO_THROW(Discret::Elements::read_coordinate_system(input)); + EXPECT_TRUE(Discret::Elements::read_fibers(input).element_fibers.empty()); + } +} // namespace