428 lines
19 KiB
C++
428 lines
19 KiB
C++
#include "fesa/model/shell_geometry.h"
|
|
|
|
#include <algorithm>
|
|
#include <cmath>
|
|
#include <tuple>
|
|
#include <utility>
|
|
|
|
#include "fesa/math/vector3.h"
|
|
|
|
namespace fesa {
|
|
namespace {
|
|
|
|
constexpr std::array<double, 4> kXiSigns{-1.0, 1.0, 1.0, -1.0};
|
|
constexpr std::array<double, 4> kEtaSigns{-1.0, -1.0, 1.0, 1.0};
|
|
|
|
struct ShapeData {
|
|
std::array<double, 4> values;
|
|
std::array<double, 4> xi_derivatives;
|
|
std::array<double, 4> eta_derivatives;
|
|
};
|
|
|
|
struct ElementWork {
|
|
std::array<Vector3, 4> coordinates;
|
|
Vector3 normal;
|
|
double area_weight;
|
|
};
|
|
|
|
ShapeData ShapeDataAt(double xi, double eta) {
|
|
ShapeData data{};
|
|
for (std::size_t node = 0U; node < 4U; ++node) {
|
|
data.values[node] =
|
|
0.25 * (1.0 + kXiSigns[node] * xi) * (1.0 + kEtaSigns[node] * eta);
|
|
data.xi_derivatives[node] =
|
|
0.25 * kXiSigns[node] * (1.0 + kEtaSigns[node] * eta);
|
|
data.eta_derivatives[node] =
|
|
0.25 * kEtaSigns[node] * (1.0 + kXiSigns[node] * xi);
|
|
}
|
|
return data;
|
|
}
|
|
|
|
Vector3 WeightedSum(const std::array<double, 4>& weights,
|
|
const std::array<Vector3, 4>& values) {
|
|
Vector3 result{};
|
|
for (std::size_t node = 0U; node < values.size(); ++node) {
|
|
result = result + weights[node] * values[node];
|
|
}
|
|
return result;
|
|
}
|
|
|
|
/// @brief Forms a translation-invariant covariant tangent in node order.
|
|
Vector3 DerivativeSum(const std::array<double, 4>& derivatives,
|
|
const std::array<Vector3, 4>& coordinates) {
|
|
// Shape derivatives sum to zero, so translating by node 1 improves the
|
|
// numerical cancellation without changing the covariant tangent.
|
|
std::array<Vector3, 4> relative{};
|
|
for (std::size_t node = 0U; node < coordinates.size(); ++node) {
|
|
relative[node] = coordinates[node] - coordinates[0];
|
|
}
|
|
return WeightedSum(derivatives, relative);
|
|
}
|
|
|
|
Result<ShellGeometry> GeometryFailure(std::string code,
|
|
const SourceLocation& location,
|
|
std::string keyword, std::string identity,
|
|
std::string message) {
|
|
return Result<ShellGeometry>::Failure(Status::Failure(
|
|
FailureCategory::kModel,
|
|
{{Severity::kError, std::move(code), location, std::move(keyword),
|
|
std::move(identity), std::move(message)}}));
|
|
}
|
|
|
|
bool SameCoordinates(const Vector3& left, const Vector3& right) {
|
|
return left == right;
|
|
}
|
|
|
|
double Orientation(const Vector3& first, const Vector3& second,
|
|
const Vector3& third, const Vector3& normal) {
|
|
return (second - first).Cross(third - first).Dot(normal);
|
|
}
|
|
|
|
bool HasOppositeSigns(double first, double second) {
|
|
return (first < 0.0 && second > 0.0) || (first > 0.0 && second < 0.0);
|
|
}
|
|
|
|
/// @brief Detects a proper projected edge crossing without a tolerance clamp.
|
|
bool SegmentsProperlyIntersect(const Vector3& first_start,
|
|
const Vector3& first_end,
|
|
const Vector3& second_start,
|
|
const Vector3& second_end,
|
|
const Vector3& normal) {
|
|
const double first_side_start =
|
|
Orientation(first_start, first_end, second_start, normal);
|
|
const double first_side_end =
|
|
Orientation(first_start, first_end, second_end, normal);
|
|
const double second_side_start =
|
|
Orientation(second_start, second_end, first_start, normal);
|
|
const double second_side_end =
|
|
Orientation(second_start, second_end, first_end, normal);
|
|
return HasOppositeSigns(first_side_start, first_side_end) &&
|
|
HasOppositeSigns(second_side_start, second_side_end);
|
|
}
|
|
|
|
/// @brief Orders incident shell contributions by stable source identity.
|
|
bool SourceIdentityLess(const Mitc4ShellDefinition& left,
|
|
std::size_t left_index,
|
|
const Mitc4ShellDefinition& right,
|
|
std::size_t right_index) {
|
|
return std::tie(left.source_id.instance_name, left.source_id.source_label,
|
|
left.source_id.source_label_text, left_index) <
|
|
std::tie(right.source_id.instance_name, right.source_id.source_label,
|
|
right.source_id.source_label_text, right_index);
|
|
}
|
|
|
|
} // namespace
|
|
|
|
const std::array<ShellGeometryValidationPoint, 17>&
|
|
ShellGeometryValidationPoints() noexcept {
|
|
static const std::array<ShellGeometryValidationPoint, 17> points = [] {
|
|
const double g = 1.0 / std::sqrt(3.0);
|
|
return std::array<ShellGeometryValidationPoint, 17>{
|
|
ShellGeometryValidationPoint{
|
|
ShellGeometryPointKind::kCenter, 0U, {0.0, 0.0, 0.0}},
|
|
{ShellGeometryPointKind::kStiffness, 0U, {-g, -g, -g}},
|
|
{ShellGeometryPointKind::kStiffness, 1U, {-g, -g, g}},
|
|
{ShellGeometryPointKind::kStiffness, 2U, {g, -g, -g}},
|
|
{ShellGeometryPointKind::kStiffness, 3U, {g, -g, g}},
|
|
{ShellGeometryPointKind::kStiffness, 4U, {g, g, -g}},
|
|
{ShellGeometryPointKind::kStiffness, 5U, {g, g, g}},
|
|
{ShellGeometryPointKind::kStiffness, 6U, {-g, g, -g}},
|
|
{ShellGeometryPointKind::kStiffness, 7U, {-g, g, g}},
|
|
{ShellGeometryPointKind::kTying, 0U, {0.0, -1.0, 0.0}},
|
|
{ShellGeometryPointKind::kTying, 1U, {0.0, 1.0, 0.0}},
|
|
{ShellGeometryPointKind::kTying, 2U, {-1.0, 0.0, 0.0}},
|
|
{ShellGeometryPointKind::kTying, 3U, {1.0, 0.0, 0.0}},
|
|
{ShellGeometryPointKind::kRecovery, 0U, {-g, -g, 0.0}},
|
|
{ShellGeometryPointKind::kRecovery, 1U, {g, -g, 0.0}},
|
|
{ShellGeometryPointKind::kRecovery, 2U, {g, g, 0.0}},
|
|
{ShellGeometryPointKind::kRecovery, 3U, {-g, g, 0.0}}};
|
|
}();
|
|
return points;
|
|
}
|
|
|
|
Result<ShellGeometry> PreprocessShellGeometry(
|
|
const std::vector<Node>& nodes,
|
|
const std::vector<Mitc4ShellDefinition>& elements,
|
|
const std::vector<ShellSection>& sections) {
|
|
ShellGeometry geometry;
|
|
geometry.element_data.reserve(elements.size());
|
|
std::vector<ElementWork> work;
|
|
work.reserve(elements.size());
|
|
|
|
const double g = 1.0 / std::sqrt(3.0);
|
|
const std::array<Vector3, 4> surface_gauss_points{
|
|
Vector3{-g, -g, 0.0}, Vector3{g, -g, 0.0}, Vector3{g, g, 0.0},
|
|
Vector3{-g, g, 0.0}};
|
|
|
|
for (std::size_t element_index = 0U; element_index < elements.size();
|
|
++element_index) {
|
|
const auto& element = elements[element_index];
|
|
ElementWork current{};
|
|
for (std::size_t local_node = 0U; local_node < 4U; ++local_node) {
|
|
if (element.node_indices[local_node] >= nodes.size()) {
|
|
return GeometryFailure(
|
|
"invalid-shell-geometry", element.location, "ELEMENT",
|
|
element.source_id.source_label_text,
|
|
"Shell geometry references an unavailable internal node.");
|
|
}
|
|
current.coordinates[local_node] =
|
|
Vector3{nodes[element.node_indices[local_node]].coordinates};
|
|
if (!current.coordinates[local_node].IsFinite()) {
|
|
return GeometryFailure(
|
|
"invalid-shell-geometry", element.location, "ELEMENT",
|
|
element.source_id.source_label_text,
|
|
"Shell geometry contains a nonfinite source coordinate.");
|
|
}
|
|
}
|
|
for (std::size_t first = 0U; first < 4U; ++first) {
|
|
for (std::size_t second = first + 1U; second < 4U; ++second) {
|
|
if (element.node_indices[first] == element.node_indices[second] ||
|
|
SameCoordinates(current.coordinates[first],
|
|
current.coordinates[second])) {
|
|
return GeometryFailure("invalid-shell-geometry", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell geometry contains duplicate nodes.");
|
|
}
|
|
}
|
|
}
|
|
|
|
const ShapeData center = ShapeDataAt(0.0, 0.0);
|
|
const Vector3 center_xi =
|
|
DerivativeSum(center.xi_derivatives, current.coordinates);
|
|
const Vector3 center_eta =
|
|
DerivativeSum(center.eta_derivatives, current.coordinates);
|
|
const Vector3 center_cross = center_xi.Cross(center_eta);
|
|
const double center_measure = center_cross.Norm();
|
|
if (!center_xi.IsFinite() || !center_eta.IsFinite() ||
|
|
!center_cross.IsFinite() || !std::isfinite(center_measure) ||
|
|
!(center_measure > 0.0)) {
|
|
return GeometryFailure(
|
|
"invalid-shell-geometry", element.location, "ELEMENT",
|
|
element.source_id.source_label_text,
|
|
"Shell center has no finite nonzero normal candidate.");
|
|
}
|
|
current.normal = (1.0 / center_measure) * center_cross;
|
|
|
|
if (SegmentsProperlyIntersect(
|
|
current.coordinates[0], current.coordinates[1],
|
|
current.coordinates[2], current.coordinates[3], current.normal) ||
|
|
SegmentsProperlyIntersect(
|
|
current.coordinates[1], current.coordinates[2],
|
|
current.coordinates[3], current.coordinates[0], current.normal)) {
|
|
return GeometryFailure("invalid-shell-geometry", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell boundary is self-intersecting in the "
|
|
"center-normal projection.");
|
|
}
|
|
|
|
double area_weight = 0.0;
|
|
for (const auto& point : surface_gauss_points) {
|
|
const ShapeData shape = ShapeDataAt(point[0], point[1]);
|
|
const Vector3 tangent_xi =
|
|
DerivativeSum(shape.xi_derivatives, current.coordinates);
|
|
const Vector3 tangent_eta =
|
|
DerivativeSum(shape.eta_derivatives, current.coordinates);
|
|
const Vector3 area_vector = tangent_xi.Cross(tangent_eta);
|
|
const double measure = area_vector.Norm();
|
|
if (!tangent_xi.IsFinite() || !tangent_eta.IsFinite() ||
|
|
!area_vector.IsFinite() || !std::isfinite(measure) ||
|
|
!(measure > 0.0) || !(area_vector.Dot(current.normal) > 0.0)) {
|
|
return GeometryFailure("invalid-shell-geometry", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell surface is zero-area or locally reversed "
|
|
"at a required point.");
|
|
}
|
|
area_weight += measure;
|
|
}
|
|
if (!std::isfinite(area_weight) || !(area_weight > 0.0)) {
|
|
return GeometryFailure("invalid-shell-geometry", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell surface-area weight is nonfinite or zero.");
|
|
}
|
|
current.area_weight = area_weight;
|
|
work.push_back(current);
|
|
geometry.element_data.push_back({static_cast<EntityIndex>(element_index),
|
|
current.normal.Components(), area_weight});
|
|
}
|
|
|
|
std::vector<std::vector<std::size_t>> incident(nodes.size());
|
|
for (std::size_t element_index = 0U; element_index < elements.size();
|
|
++element_index) {
|
|
for (const EntityIndex node_index : elements[element_index].node_indices) {
|
|
incident[node_index].push_back(element_index);
|
|
}
|
|
}
|
|
|
|
geometry.nodal_frames.reserve(nodes.size());
|
|
std::vector<const ShellNodeInitialFrame*> frame_by_node(nodes.size(),
|
|
nullptr);
|
|
for (std::size_t node_index = 0U; node_index < incident.size();
|
|
++node_index) {
|
|
auto& node_incident = incident[node_index];
|
|
if (node_incident.empty()) {
|
|
continue;
|
|
}
|
|
std::sort(node_incident.begin(), node_incident.end(),
|
|
[&elements](std::size_t left, std::size_t right) {
|
|
return SourceIdentityLess(elements[left], left, elements[right],
|
|
right);
|
|
});
|
|
for (std::size_t first = 0U; first < node_incident.size(); ++first) {
|
|
for (std::size_t second = first + 1U; second < node_incident.size();
|
|
++second) {
|
|
const double pair_dot = work[node_incident[first]].normal.Dot(
|
|
work[node_incident[second]].normal);
|
|
if (!std::isfinite(pair_dot) || !(pair_dot > 0.0)) {
|
|
return GeometryFailure("opposed-incident-normal",
|
|
nodes[node_index].location, "NODE",
|
|
nodes[node_index].source_id.source_label_text,
|
|
"Incident shell normal candidates do not "
|
|
"share a positive orientation hemisphere.");
|
|
}
|
|
}
|
|
}
|
|
|
|
double maximum_weight = 0.0;
|
|
for (const std::size_t element_index : node_incident) {
|
|
maximum_weight =
|
|
std::max(maximum_weight, work[element_index].area_weight);
|
|
}
|
|
Vector3 director_sum{};
|
|
for (const std::size_t element_index : node_incident) {
|
|
director_sum =
|
|
director_sum + (work[element_index].area_weight / maximum_weight) *
|
|
work[element_index].normal;
|
|
}
|
|
const double director_norm = director_sum.Norm();
|
|
if (!director_sum.IsFinite() || !std::isfinite(director_norm) ||
|
|
!(director_norm > 0.0)) {
|
|
return GeometryFailure(
|
|
"invalid-shell-director", nodes[node_index].location, "NODE",
|
|
nodes[node_index].source_id.source_label_text,
|
|
"Area-weighted shell director is nonfinite or zero.");
|
|
}
|
|
const Vector3 director = (1.0 / director_norm) * director_sum;
|
|
|
|
const std::array<Vector3, 3> global_axes{
|
|
Vector3{1.0, 0.0, 0.0}, Vector3{0.0, 1.0, 0.0}, Vector3{0.0, 0.0, 1.0}};
|
|
std::size_t selected_axis = 0U;
|
|
double selected_alignment = std::abs(global_axes[0].Dot(director));
|
|
for (std::size_t axis = 1U; axis < global_axes.size(); ++axis) {
|
|
const double alignment = std::abs(global_axes[axis].Dot(director));
|
|
if (alignment < selected_alignment) {
|
|
selected_alignment = alignment;
|
|
selected_axis = axis;
|
|
}
|
|
}
|
|
const Vector3 tangent_candidate =
|
|
global_axes[selected_axis] -
|
|
global_axes[selected_axis].Dot(director) * director;
|
|
const double tangent_norm = tangent_candidate.Norm();
|
|
if (!tangent_candidate.IsFinite() || !std::isfinite(tangent_norm) ||
|
|
!(tangent_norm > 0.0)) {
|
|
return GeometryFailure(
|
|
"invalid-shell-director", nodes[node_index].location, "NODE",
|
|
nodes[node_index].source_id.source_label_text,
|
|
"Least-aligned-axis tangent frame construction failed.");
|
|
}
|
|
const Vector3 tangent_a = (1.0 / tangent_norm) * tangent_candidate;
|
|
const Vector3 tangent_b = director.Cross(tangent_a);
|
|
if (!tangent_b.IsFinite() || !(tangent_b.Norm() > 0.0)) {
|
|
return GeometryFailure(
|
|
"invalid-shell-director", nodes[node_index].location, "NODE",
|
|
nodes[node_index].source_id.source_label_text,
|
|
"Right-handed shell tangent frame construction failed.");
|
|
}
|
|
geometry.nodal_frames.push_back(
|
|
{static_cast<EntityIndex>(node_index), director.Components(),
|
|
tangent_a.Components(), tangent_b.Components()});
|
|
}
|
|
|
|
// Build lookup only after frame storage is complete so later code never
|
|
// observes a pointer invalidated by vector growth.
|
|
for (const auto& frame : geometry.nodal_frames) {
|
|
frame_by_node[frame.node_index] = &frame;
|
|
}
|
|
|
|
for (std::size_t element_index = 0U; element_index < elements.size();
|
|
++element_index) {
|
|
const auto& element = elements[element_index];
|
|
if (element.section_index >= sections.size()) {
|
|
return GeometryFailure("invalid-shell-jacobian", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell geometry cannot resolve its thickness for "
|
|
"Jacobian validation.");
|
|
}
|
|
const double thickness = sections[element.section_index].thickness;
|
|
std::array<Vector3, 4> directors{};
|
|
for (std::size_t local_node = 0U; local_node < 4U; ++local_node) {
|
|
const auto* frame = frame_by_node[element.node_indices[local_node]];
|
|
if (frame == nullptr) {
|
|
return GeometryFailure("invalid-shell-director", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell element is missing a nodal director.");
|
|
}
|
|
directors[local_node] = Vector3{frame->director};
|
|
}
|
|
|
|
for (const auto& point : ShellGeometryValidationPoints()) {
|
|
const double xi = point.natural_coordinates[0];
|
|
const double eta = point.natural_coordinates[1];
|
|
const double zeta = point.natural_coordinates[2];
|
|
const ShapeData shape = ShapeDataAt(xi, eta);
|
|
const Vector3 midsurface_xi =
|
|
DerivativeSum(shape.xi_derivatives, work[element_index].coordinates);
|
|
const Vector3 midsurface_eta =
|
|
DerivativeSum(shape.eta_derivatives, work[element_index].coordinates);
|
|
const Vector3 area_vector = midsurface_xi.Cross(midsurface_eta);
|
|
const double surface_measure = area_vector.Norm();
|
|
if (!midsurface_xi.IsFinite() || !midsurface_eta.IsFinite() ||
|
|
!area_vector.IsFinite() || !std::isfinite(surface_measure) ||
|
|
!(surface_measure > 0.0) ||
|
|
!(area_vector.Dot(work[element_index].normal) > 0.0)) {
|
|
return GeometryFailure("invalid-shell-geometry", element.location,
|
|
"ELEMENT", element.source_id.source_label_text,
|
|
"Shell surface basis is nonfinite, zero, or "
|
|
"reversed at a required point.");
|
|
}
|
|
|
|
const Vector3 director_xi = WeightedSum(shape.xi_derivatives, directors);
|
|
const Vector3 director_eta =
|
|
WeightedSum(shape.eta_derivatives, directors);
|
|
const Vector3 director_value = WeightedSum(shape.values, directors);
|
|
const Vector3 covariant_xi =
|
|
midsurface_xi + 0.5 * thickness * zeta * director_xi;
|
|
const Vector3 covariant_eta =
|
|
midsurface_eta + 0.5 * thickness * zeta * director_eta;
|
|
const Vector3 covariant_zeta = 0.5 * thickness * director_value;
|
|
const double jacobian =
|
|
covariant_xi.Dot(covariant_eta.Cross(covariant_zeta));
|
|
if (!covariant_xi.IsFinite() || !covariant_eta.IsFinite() ||
|
|
!covariant_zeta.IsFinite() || !std::isfinite(jacobian) ||
|
|
!(jacobian > 0.0)) {
|
|
return GeometryFailure(
|
|
"invalid-shell-jacobian", element.location, "ELEMENT",
|
|
element.source_id.source_label_text,
|
|
"Shell Jacobian is nonfinite or nonpositive at a required point.");
|
|
}
|
|
const Vector3 reciprocal_xi =
|
|
(1.0 / jacobian) * covariant_eta.Cross(covariant_zeta);
|
|
const Vector3 reciprocal_eta =
|
|
(1.0 / jacobian) * covariant_zeta.Cross(covariant_xi);
|
|
const Vector3 reciprocal_zeta =
|
|
(1.0 / jacobian) * covariant_xi.Cross(covariant_eta);
|
|
if (!reciprocal_xi.IsFinite() || !reciprocal_eta.IsFinite() ||
|
|
!reciprocal_zeta.IsFinite()) {
|
|
return GeometryFailure(
|
|
"invalid-shell-jacobian", element.location, "ELEMENT",
|
|
element.source_id.source_label_text,
|
|
"Shell reciprocal basis is nonfinite at a required point.");
|
|
}
|
|
}
|
|
}
|
|
|
|
return Result<ShellGeometry>::Success(std::move(geometry));
|
|
}
|
|
|
|
} // namespace fesa
|