#include "fesa/model/shell_geometry.h" #include #include #include #include #include #include #include #include namespace { using Vector3 = std::array; fesa::Node Node(fesa::EntityIndex label, Vector3 coordinates) { return {{"Shell-Instance", static_cast(label), std::to_string(label)}, coordinates, {"shell-geometry.inp", static_cast(label + 1U)}}; } fesa::Mitc4ShellDefinition Element( fesa::EntityIndex label, std::array node_indices) { return {{"Shell-Instance", static_cast(label), std::to_string(label)}, fesa::ShellSourceElementType::kS4, node_indices, 0U, 0U, {"shell-geometry.inp", static_cast(100U + label)}}; } std::vector Sections(double thickness = 0.2) { return {{"Section", thickness, 0U, {"shell-geometry.inp", 90U}}}; } double Dot(const Vector3& left, const Vector3& right) { return left[0] * right[0] + left[1] * right[1] + left[2] * right[2]; } Vector3 Cross(const Vector3& left, const Vector3& right) { return {left[1] * right[2] - left[2] * right[1], left[2] * right[0] - left[0] * right[2], left[0] * right[1] - left[1] * right[0]}; } double Norm(const Vector3& value) { return std::sqrt(Dot(value, value)); } void ExpectVectorNear(const Vector3& actual, const Vector3& expected, double tolerance = 1.0e-12) { for (std::size_t component = 0U; component < actual.size(); ++component) { EXPECT_NEAR(actual[component], expected[component], tolerance); } } void ExpectRightHandedFrame(const fesa::ShellNodeInitialFrame& frame) { EXPECT_NEAR(Norm(frame.director), 1.0, 1.0e-12); EXPECT_NEAR(Norm(frame.tangent_a), 1.0, 1.0e-12); EXPECT_NEAR(Norm(frame.tangent_b), 1.0, 1.0e-12); EXPECT_NEAR(Dot(frame.director, frame.tangent_a), 0.0, 1.0e-12); EXPECT_NEAR(Dot(frame.director, frame.tangent_b), 0.0, 1.0e-12); EXPECT_NEAR(Dot(frame.tangent_a, frame.tangent_b), 0.0, 1.0e-12); ExpectVectorNear(Cross(frame.tangent_a, frame.tangent_b), frame.director); } const fesa::ShellNodeInitialFrame& FrameFor(const fesa::ShellGeometry& geometry, fesa::EntityIndex node_index) { const auto found = std::find_if(geometry.nodal_frames.begin(), geometry.nodal_frames.end(), [node_index](const fesa::ShellNodeInitialFrame& frame) { return frame.node_index == node_index; }); EXPECT_NE(found, geometry.nodal_frames.end()); return *found; } void ExpectFailureCode(const fesa::Result& result, const std::string& code) { ASSERT_FALSE(result.HasValue()); EXPECT_EQ(result.GetStatus().Category(), fesa::FailureCategory::kModel); ASSERT_EQ(result.GetStatus().Diagnostics().size(), 1U); EXPECT_EQ(result.GetStatus().Diagnostics()[0].code, code); } } // namespace // MITC4-GEO-001 TEST(Mitc4Geometry, BuildsDeterministicFramesForPlanarRotatedAndWarpedElements) { const std::vector planar_nodes{ Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {1.0, 1.0, 0.0}), Node(3U, {0.0, 1.0, 0.0})}; auto planar = fesa::PreprocessShellGeometry( planar_nodes, {Element(10U, {0U, 1U, 2U, 3U})}, Sections()); ASSERT_TRUE(planar.HasValue()); ASSERT_EQ(planar.Value().element_data.size(), 1U); ExpectVectorNear(planar.Value().element_data[0].normal_candidate, {0.0, 0.0, 1.0}); EXPECT_NEAR(planar.Value().element_data[0].surface_area_weight, 1.0, 1.0e-12); ASSERT_EQ(planar.Value().nodal_frames.size(), 4U); for (const auto& frame : planar.Value().nodal_frames) { ExpectVectorNear(frame.director, {0.0, 0.0, 1.0}); ExpectVectorNear(frame.tangent_a, {1.0, 0.0, 0.0}); ExpectVectorNear(frame.tangent_b, {0.0, 1.0, 0.0}); ExpectRightHandedFrame(frame); } const std::vector rotated_nodes{ Node(0U, {0.0, 0.0, 0.0}), Node(1U, {0.0, 1.0, 0.0}), Node(2U, {0.0, 1.0, 1.0}), Node(3U, {0.0, 0.0, 1.0})}; auto rotated = fesa::PreprocessShellGeometry( rotated_nodes, {Element(11U, {0U, 1U, 2U, 3U})}, Sections()); ASSERT_TRUE(rotated.HasValue()); const auto& rotated_frame = FrameFor(rotated.Value(), 0U); ExpectVectorNear(rotated_frame.director, {1.0, 0.0, 0.0}); ExpectVectorNear(rotated_frame.tangent_a, {0.0, 1.0, 0.0}); ExpectVectorNear(rotated_frame.tangent_b, {0.0, 0.0, 1.0}); ExpectRightHandedFrame(rotated_frame); const std::vector warped_nodes{ Node(0U, {0.0, 0.0, 0.0}), Node(1U, {2.0, 0.0, 0.0}), Node(2U, {2.0, 1.0, 0.2}), Node(3U, {0.0, 1.0, 0.0})}; auto warped = fesa::PreprocessShellGeometry( warped_nodes, {Element(12U, {0U, 1U, 2U, 3U})}, Sections()); ASSERT_TRUE(warped.HasValue()); EXPECT_GT(warped.Value().element_data[0].surface_area_weight, 2.0); for (const auto& frame : warped.Value().nodal_frames) { ExpectRightHandedFrame(frame); } } // MITC4-GEO-002 TEST(Mitc4Geometry, AreaWeightsSharedDirectorsInStableSourceIdentityOrder) { const std::vector nodes{ Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {1.0, 1.0, 0.0}), Node(3U, {0.0, 1.0, 0.0}), Node(4U, {2.0, 0.0, 1.0}), Node(5U, {2.0, 1.0, 1.0})}; const auto flat = Element(10U, {0U, 1U, 2U, 3U}); const auto tilted = Element(20U, {1U, 4U, 5U, 2U}); auto first = fesa::PreprocessShellGeometry(nodes, {tilted, flat}, Sections()); auto second = fesa::PreprocessShellGeometry(nodes, {flat, tilted}, Sections()); ASSERT_TRUE(first.HasValue()); ASSERT_TRUE(second.HasValue()); const Vector3 expected_shared_director{-1.0 / std::sqrt(5.0), 0.0, 2.0 / std::sqrt(5.0)}; for (const auto sharedNode : {1U, 2U}) { const auto& firstFrame = FrameFor(first.Value(), sharedNode); const auto& secondFrame = FrameFor(second.Value(), sharedNode); ExpectVectorNear(firstFrame.director, expected_shared_director); ExpectVectorNear(firstFrame.director, secondFrame.director, 0.0); ExpectVectorNear(firstFrame.tangent_a, {0.0, 1.0, 0.0}); ExpectRightHandedFrame(firstFrame); } } // MITC4-GEO-003 TEST(Mitc4Geometry, RejectsInvalidSurfaceJacobianAndIncidentOrientationCases) { const auto valid_element = Element(10U, {0U, 1U, 2U, 3U}); ExpectFailureCode(fesa::PreprocessShellGeometry( {Node(0U, {0.0, 0.0, 0.0}), Node(1U, {0.0, 0.0, 0.0}), Node(2U, {1.0, 1.0, 0.0}), Node(3U, {0.0, 1.0, 0.0})}, {valid_element}, Sections()), "invalid-shell-geometry"); ExpectFailureCode(fesa::PreprocessShellGeometry( {Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 1.0, 0.0}), Node(2U, {0.0, 1.0, 0.0}), Node(3U, {1.0, 0.0, 0.0})}, {valid_element}, Sections()), "invalid-shell-geometry"); ExpectFailureCode(fesa::PreprocessShellGeometry( {Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {2.0, 0.0, 0.0}), Node(3U, {3.0, 0.0, 0.0})}, {valid_element}, Sections()), "invalid-shell-geometry"); ExpectFailureCode( fesa::PreprocessShellGeometry( {Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {0.05, 0.05, 0.0}), Node(3U, {0.0, 1.0, 0.0})}, {valid_element}, Sections()), "invalid-shell-geometry"); ExpectFailureCode( fesa::PreprocessShellGeometry( {Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {1.0, std::numeric_limits::quiet_NaN(), 0.0}), Node(3U, {0.0, 1.0, 0.0})}, {valid_element}, Sections()), "invalid-shell-geometry"); ExpectFailureCode(fesa::PreprocessShellGeometry( {Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {1.0, 1.0, 0.0}), Node(3U, {0.0, 1.0, 0.0})}, {valid_element}, Sections(0.0)), "invalid-shell-jacobian"); const std::vector opposed_nodes{ Node(0U, {0.0, 0.0, 0.0}), Node(1U, {1.0, 0.0, 0.0}), Node(2U, {1.0, 1.0, 0.0}), Node(3U, {0.0, 1.0, 0.0})}; ExpectFailureCode( fesa::PreprocessShellGeometry( opposed_nodes, {Element(10U, {0U, 1U, 2U, 3U}), Element(20U, {0U, 3U, 2U, 1U})}, Sections()), "opposed-incident-normal"); } // MITC4-GEO-004 TEST(Mitc4Geometry, ExposesTheCompleteRequiredValidationPointInventory) { const auto& points = fesa::ShellGeometryValidationPoints(); ASSERT_EQ(points.size(), 17U); EXPECT_EQ(std::count_if(points.begin(), points.end(), [](const auto& point) { return point.kind == fesa::ShellGeometryPointKind::kCenter; }), 1); EXPECT_EQ(std::count_if(points.begin(), points.end(), [](const auto& point) { return point.kind == fesa::ShellGeometryPointKind::kStiffness; }), 8); EXPECT_EQ(std::count_if(points.begin(), points.end(), [](const auto& point) { return point.kind == fesa::ShellGeometryPointKind::kTying; }), 4); EXPECT_EQ(std::count_if(points.begin(), points.end(), [](const auto& point) { return point.kind == fesa::ShellGeometryPointKind::kRecovery; }), 4); EXPECT_EQ(points.front().natural_coordinates, (Vector3{0.0, 0.0, 0.0})); const double gauss = 1.0 / std::sqrt(3.0); EXPECT_EQ(points[1].natural_coordinates, (Vector3{-gauss, -gauss, -gauss})); EXPECT_EQ(points[8].natural_coordinates, (Vector3{-gauss, gauss, gauss})); EXPECT_EQ(points[9].natural_coordinates, (Vector3{0.0, -1.0, 0.0})); EXPECT_EQ(points[12].natural_coordinates, (Vector3{1.0, 0.0, 0.0})); EXPECT_EQ(points[13].natural_coordinates, (Vector3{-gauss, -gauss, 0.0})); EXPECT_EQ(points[16].natural_coordinates, (Vector3{-gauss, gauss, 0.0})); }