feat: adopt lumped mass and reduced-coordinate shell HHT

Integrate Hex8 mass verification and fixed-null-basis shell dynamics with numerical and physics handoff evidence. Preserve interrupted reference and full-validation status; full integration validation is deferred until after performance optimization.
This commit is contained in:
KOKO\Mimi
2026-10-13 16:57:19 +09:00
parent ede137c6b4
commit eaa5cbeb21
110 changed files with 15274 additions and 971 deletions
+211 -55
View File
@@ -26,6 +26,7 @@ using EngineeringStress = std::array<double, 5>;
struct Geometry {
std::array<Vector3, 3> reference_covariant;
std::array<Vector3, 3> current_covariant;
std::array<Vector3, 3> covariant_increment;
std::array<Vector3, 3> reciprocal;
Mitc4LocalFrame local_frame;
double reference_jacobian;
@@ -145,6 +146,31 @@ bool MakeLocalFrame(const Vector3& midsurface_xi, const Vector3& midsurface_eta,
return true;
}
/// @brief Forms (R(phi)-I)D0 without adding and subtracting the baseline.
Vector3 DirectorIncrement(const Vector3& phi, const Vector3& initial) {
const double squared_norm = phi.Dot(phi);
double first = 1.0;
double second = 0.5;
if (squared_norm < 0.25) {
double power = 1.0;
double first_coefficient = 1.0;
double second_coefficient = 0.5;
for (unsigned int k = 1U; k <= 9U; ++k) {
power *= squared_norm;
first_coefficient /= -static_cast<double>((2U * k) * (2U * k + 1U));
second_coefficient /= -static_cast<double>((2U * k + 1U) * (2U * k + 2U));
first += first_coefficient * power;
second += second_coefficient * power;
}
} else {
const double norm = std::sqrt(squared_norm);
first = std::sin(norm) / norm;
second = (1.0 - std::cos(norm)) / squared_norm;
}
const Vector3 crossed = phi.Cross(initial);
return first * crossed + second * phi.Cross(crossed);
}
/// @brief Evaluates fixed-reference and trial covariant geometry together.
bool EvaluateGeometry(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
@@ -169,6 +195,18 @@ bool EvaluateGeometry(const std::array<Vector3, kNodeCount>& coordinates,
const Vector3 director_eta = DerivativeSum(shape.eta_derivatives, directors);
const Vector3 director = WeightedSum(shape.values, directors);
const double half_thickness = 0.5 * thickness;
std::array<Vector3, kNodeCount> director_increment{};
for (std::size_t node = 0U; node < kNodeCount; ++node)
director_increment[node] =
DirectorIncrement(trial.rotation_vector[node], initial_directors[node]);
geometry.covariant_increment[0U] =
displacement_xi + half_thickness * zeta *
DerivativeSum(shape.xi_derivatives, director_increment);
geometry.covariant_increment[1U] =
displacement_eta + half_thickness * zeta *
DerivativeSum(shape.eta_derivatives, director_increment);
geometry.covariant_increment[2U] =
half_thickness * WeightedSum(shape.values, director_increment);
geometry.reference_covariant[0U] =
reference_xi + half_thickness * zeta * initial_director_xi;
@@ -215,10 +253,13 @@ Tensor DirectStrain(const Geometry& geometry) {
Tensor strain{};
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
strain[row][column] = 0.5 * (geometry.current_covariant[row].Dot(
geometry.current_covariant[column]) -
geometry.reference_covariant[row].Dot(
geometry.reference_covariant[column]));
strain[row][column] =
0.5 * (geometry.reference_covariant[row].Dot(
geometry.covariant_increment[column]) +
geometry.covariant_increment[row].Dot(
geometry.reference_covariant[column]) +
geometry.covariant_increment[row].Dot(
geometry.covariant_increment[column]));
}
}
strain[2U][2U] = 0.0;
@@ -278,8 +319,11 @@ EngineeringStress Stress(const EngineeringStrain& strain, const double youngs,
shear * strain[4U]};
}
/// @brief Distinguishes invalid geometry from arithmetic recovery failure.
enum class PointStatus { kValid, kInvalidGeometry, kNonfiniteRecovery };
/// @brief Evaluates direct strain at one mandatory edge-midpoint tying point.
bool EvaluateTyingSample(
PointStatus EvaluateTyingSample(
const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const std::array<Vector3, kNodeCount>& directors,
@@ -288,14 +332,14 @@ bool EvaluateTyingSample(
Geometry geometry{};
if (!EvaluateGeometry(coordinates, initial_directors, directors, trial,
thickness, xi, eta, 0.0, geometry)) {
return false;
return PointStatus::kInvalidGeometry;
}
strain = DirectStrain(geometry);
return IsFinite(strain);
return IsFinite(strain) ? PointStatus::kValid : PointStatus::kNonfiniteRecovery;
}
/// @brief Computes one MITC-projected local strain and material stress point.
bool EvaluatePoint(const std::array<Vector3, kNodeCount>& coordinates,
PointStatus EvaluatePoint(const std::array<Vector3, kNodeCount>& coordinates,
const std::array<Vector3, kNodeCount>& initial_directors,
const std::array<Vector3, kNodeCount>& directors,
const NonlinearMitc4TrialDofs& trial,
@@ -305,7 +349,7 @@ bool EvaluatePoint(const std::array<Vector3, kNodeCount>& coordinates,
const double zeta, PointEvaluation& point) {
if (!EvaluateGeometry(coordinates, initial_directors, directors, trial,
thickness, xi, eta, zeta, point.geometry)) {
return false;
return PointStatus::kInvalidGeometry;
}
Tensor covariant = DirectStrain(point.geometry);
const Mitc4TyingWeights weights = Mitc4Shell::TyingWeights(xi, eta);
@@ -320,7 +364,9 @@ bool EvaluatePoint(const std::array<Vector3, kNodeCount>& coordinates,
point.strain = LocalEngineering(cartesian, point.geometry.local_frame);
point.stress = Stress(point.strain, youngs, poisson);
return IsFinite(covariant) && IsFinite(cartesian) && IsFinite(point.strain) &&
IsFinite(point.stress);
IsFinite(point.stress)
? PointStatus::kValid
: PointStatus::kNonfiniteRecovery;
}
/// @brief Tests the fixed-order physical recovery inventory for finite values.
@@ -787,7 +833,8 @@ JetMatrix3 Inverse(const JetMatrix3& matrix) {
}
/// @brief Evaluates Rodrigues' map with an analytic near-zero series branch.
JetMatrix3 ExpSkew(const JetVector3& rotation) {
JetMatrix3 ExpSkew(const JetVector3& rotation, const bool increment_only = false) {
const JetMatrix3 identity = increment_only ? JetMatrix3{} : Identity();
const Jet squared_norm = Dot(rotation, rotation);
Jet sine_over_norm{};
Jet one_minus_cosine_over_norm_squared{};
@@ -807,12 +854,12 @@ JetMatrix3 ExpSkew(const JetVector3& rotation) {
}
const JetMatrix3 skew = Skew(rotation);
JetMatrix3 result =
Add(Add(Identity(), Scale(sine_over_norm, skew)),
Add(Add(identity, Scale(sine_over_norm, skew)),
Scale(one_minus_cosine_over_norm_squared, Multiply(skew, skew)));
if (squared_norm.value < 0.25) {
// Third derivatives amplify cancellation in 1-cos(theta). Preserve the
// established static coefficients and use a stable series for the new
// directional derivatives of the complete rotation tensor.
// Near the original small-angle boundary, cancellation in 1-cos(theta)
// also corrupts the physical-force gradient and Hessian. Use the existing
// stable series consistently for the complete Jet and rotation tensor.
Jet sine_series = Constant(1.0);
Jet cosine_series = Constant(0.5);
Jet power = Constant(1.0);
@@ -825,18 +872,9 @@ JetMatrix3 ExpSkew(const JetVector3& rotation) {
sine_series = sine_series + sine_coefficient * power;
cosine_series = cosine_series + cosine_coefficient * power;
}
const JetMatrix3 stable = Add(Add(Identity(), Scale(sine_series, skew)),
const JetMatrix3 stable = Add(Add(identity, Scale(sine_series, skew)),
Scale(cosine_series, Multiply(skew, skew)));
for (std::size_t row = 0U; row < 3U; ++row) {
for (std::size_t column = 0U; column < 3U; ++column) {
result[row][column].directional_first =
stable[row][column].directional_first;
result[row][column].directional_second =
stable[row][column].directional_second;
result[row][column].mixed_first = stable[row][column].mixed_first;
result[row][column].mixed_second = stable[row][column].mixed_second;
}
}
result = stable;
}
return result;
}
@@ -845,7 +883,7 @@ JetMatrix3 ExpSkew(const JetVector3& rotation) {
struct JetTrial {
std::array<JetVector3, kNodeCount> displacement;
std::array<JetVector3, kNodeCount> rotation;
std::array<JetVector3, kNodeCount> director;
std::array<JetVector3, kNodeCount> director_increment;
};
/// @brief Lifts one validated nonlinear trial into independent Jet variables.
@@ -871,8 +909,8 @@ JetTrial MakeJetTrial(const NonlinearMitc4TrialDofs& trial,
}
const JetVector3 initial_director{frame[0U][2U], frame[1U][2U],
frame[2U][2U]};
result.director[node] =
Multiply(ExpSkew(result.rotation[node]), initial_director);
result.director_increment[node] =
Multiply(ExpSkew(result.rotation[node], true), initial_director);
}
return result;
}
@@ -882,13 +920,13 @@ std::array<JetVector3, 3> CovariantIncrement(
const std::array<Vector3, kNodeCount>& initial_directors,
const JetTrial& trial, const double thickness, const double xi,
const double eta, const double zeta) {
static_cast<void>(initial_directors);
const auto shape = Mitc4Shell::ShapeFunctions(xi, eta);
std::array<JetVector3, 3> increment{};
for (std::size_t node = 0U; node < kNodeCount; ++node) {
const auto displacement =
Subtract(trial.displacement[node], trial.displacement[0U]);
const auto director =
Subtract(trial.director[node], ConstantVector(initial_directors[node]));
const auto director = trial.director_increment[node];
const auto layer =
Add(displacement, Scale(0.5 * thickness * zeta, director));
increment[0U] =
@@ -1043,8 +1081,8 @@ JetMatrix3 PolarRotation(const JetMatrix3& deformation) {
return result;
}
/// @brief Returns the speed of physical spin for a unit chart null vector.
Jet DrillSpinRate(const JetVector3& phi, const Vector3& director) {
/// @brief Returns the unnormalized physical drill-null chart vector.
JetVector3 DrillNullPreimage(const JetVector3& phi, const Vector3& director) {
const Jet square = Dot(phi, phi);
Jet coefficient{};
if (square.value < 0.25) {
@@ -1069,7 +1107,19 @@ Jet DrillSpinRate(const JetVector3& phi, const Vector3& director) {
const auto cross = Cross(phi, d0);
const auto inverse_action =
Add(Add(d0, Scale(0.5, cross)), Scale(coefficient, Cross(phi, cross)));
return Reciprocal(Sqrt(Dot(inverse_action, inverse_action)));
return inverse_action;
}
/// @brief Returns the normalized physical drill-null chart vector.
JetVector3 DrillNull(const JetVector3& phi, const Vector3& director) {
const JetVector3 preimage = DrillNullPreimage(phi, director);
return Scale(Reciprocal(Sqrt(Dot(preimage, preimage))), preimage);
}
/// @brief Returns the speed of physical spin for a unit chart null vector.
Jet DrillSpinRate(const JetVector3& phi, const Vector3& director) {
const JetVector3 preimage = DrillNullPreimage(phi, director);
return Reciprocal(Sqrt(Dot(preimage, preimage)));
}
/// @brief Evaluates the objective numerical drilling potential and variations.
@@ -1332,14 +1382,17 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
std::array<double, 2>{-1.0, 0.0}, std::array<double, 2>{1.0, 0.0}};
std::array<Tensor, 4> tying_samples{};
for (std::size_t point = 0U; point < tying_locations.size(); ++point) {
if (!EvaluateTyingSample(coordinates_, initial_directors_, directors, trial,
thickness_, tying_locations[point][0U],
tying_locations[point][1U],
tying_samples[point])) {
const auto point_status = EvaluateTyingSample(
coordinates_, initial_directors_, directors, trial, thickness_,
tying_locations[point][0U], tying_locations[point][1U],
tying_samples[point]);
if (point_status != PointStatus::kValid) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 tying-point reference or trial geometry is "
"invalid.");
point_status == PointStatus::kInvalidGeometry
? "invalid-nonlinear-shell-geometry"
: "invalid-nonlinear-shell-recovery",
source_location_, identity_,
"Nonlinear MITC4 tying-point geometry or strain is invalid.");
}
}
@@ -1374,14 +1427,18 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
for (const double thickness_sign : thickness_gauss) {
PointEvaluation point{};
if (!EvaluatePoint(coordinates_, initial_directors_, directors, trial,
tying_samples, thickness_, youngs_modulus_,
poisson_ratio_, row.natural_coordinates[0U],
row.natural_coordinates[1U], thickness_sign * gauss,
point)) {
const auto point_status = EvaluatePoint(
coordinates_, initial_directors_, directors, trial, tying_samples,
thickness_, youngs_modulus_, poisson_ratio_,
row.natural_coordinates[0U], row.natural_coordinates[1U],
thickness_sign * gauss, point);
if (point_status != PointStatus::kValid) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 volume recovery geometry is invalid.");
point_status == PointStatus::kInvalidGeometry
? "invalid-nonlinear-shell-geometry"
: "invalid-nonlinear-shell-recovery",
source_location_, identity_,
"Nonlinear MITC4 volume recovery geometry or values are invalid.");
}
for (std::size_t component = 0U; component < 3U; ++component) {
row.generalized_strain[component] += 0.5 * point.strain[component];
@@ -1410,14 +1467,18 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::EvaluateKinematics(
for (std::size_t position = 0U; position < stress_zeta.size(); ++position) {
PointEvaluation point{};
if (!EvaluatePoint(coordinates_, initial_directors_, directors, trial,
tying_samples, thickness_, youngs_modulus_,
poisson_ratio_, row.natural_coordinates[0U],
row.natural_coordinates[1U], stress_zeta[position],
point)) {
const auto point_status = EvaluatePoint(
coordinates_, initial_directors_, directors, trial, tying_samples,
thickness_, youngs_modulus_, poisson_ratio_,
row.natural_coordinates[0U], row.natural_coordinates[1U],
stress_zeta[position], point);
if (point_status != PointStatus::kValid) {
return Failure<NonlinearMitc4Recovery>(
"invalid-nonlinear-shell-geometry", source_location_, identity_,
"Nonlinear MITC4 section-position recovery geometry is invalid.");
point_status == PointStatus::kInvalidGeometry
? "invalid-nonlinear-shell-geometry"
: "invalid-nonlinear-shell-recovery",
source_location_, identity_,
"Nonlinear MITC4 section recovery geometry or values are invalid.");
}
row.stress[position] = {
stress_position[position],
@@ -1439,6 +1500,97 @@ Result<NonlinearMitc4Recovery> NonlinearMitc4Shell::RecoverState(
return EvaluateKinematics(trial);
}
Result<NonlinearMitc4ExternalDrillingConstraint>
NonlinearMitc4Shell::EvaluateExternalDrillingConstraint(
const std::size_t node, const Vector3& phi, const Vector3& phi_dot,
const Vector3& force, const Vector3& force_rate,
const Vector3& force_acceleration) const {
if (node >= kNodeCount || !phi_dot.IsFinite() || !force.IsFinite() ||
!force_rate.IsFinite() || !force_acceleration.IsFinite()) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
"invalid-nonlinear-shell-external-constraint", source_location_,
identity_, "External drilling constraint inputs must be finite and "
"identify one source-local node.");
}
const auto rotation = MakeFiniteRotationState(phi, initial_frames_[node]);
if (!rotation.HasValue()) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
"invalid-nonlinear-shell-rotation", source_location_, identity_,
"External drilling constraint rotations must remain in the principal "
"chart.");
}
JetVector3 jet_phi{};
for (std::size_t component = 0U; component < 3U; ++component) {
jet_phi[component] = Variable(phi[component], component,
phi_dot[component]);
}
const JetVector3 null_direction = DrillNull(jet_phi, initial_directors_[node]);
const Jet scalar = -1.0 * Dot(null_direction, ConstantVector(force));
const Jet scalar_time =
-1.0 * Dot(null_direction, ConstantVector(force_rate));
const Jet scalar_acceleration =
-1.0 * Dot(null_direction, ConstantVector(force_acceleration));
if (!IsFinite(scalar) || !IsFinite(scalar_time) ||
!IsFinite(scalar_acceleration)) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
"invalid-nonlinear-shell-external-constraint", source_location_,
identity_, "External drilling constraint derivatives must remain "
"finite.");
}
NonlinearMitc4ExternalDrillingConstraint result{};
result.value = scalar.value;
result.gradient = Vector{3U};
result.hessian = Matrix{3U, 3U};
result.gradient_directional_first = Vector{3U};
result.gradient_directional_second = Vector{3U};
result.directional_first =
scalar.directional_first + scalar_time.value;
result.directional_second = scalar.directional_second +
2.0 * scalar_time.directional_first +
scalar_acceleration.value;
for (std::size_t row = 0U; row < 3U; ++row) {
result.gradient[row] = scalar.first[row];
result.gradient_directional_first[row] =
scalar.mixed_first[row] + scalar_time.first[row];
result.gradient_directional_second[row] =
scalar.mixed_second[row] +
2.0 * scalar_time.mixed_first[row] + scalar_acceleration.first[row];
for (std::size_t column = 0U; column < 3U; ++column) {
result.hessian(row, column) =
scalar.second[row * kGlobalDofCount + column];
}
}
if (!std::isfinite(result.value) || !std::isfinite(result.directional_first) ||
!std::isfinite(result.directional_second)) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
"invalid-nonlinear-shell-external-constraint", source_location_,
identity_, "External drilling constraint derivatives must remain "
"finite.");
}
for (std::size_t row = 0U; row < 3U; ++row) {
if (!std::isfinite(result.gradient[row]) ||
!std::isfinite(result.gradient_directional_first[row]) ||
!std::isfinite(result.gradient_directional_second[row])) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
"invalid-nonlinear-shell-external-constraint", source_location_,
identity_, "External drilling constraint derivatives must remain "
"finite.");
}
for (std::size_t column = 0U; column < 3U; ++column) {
if (!std::isfinite(result.hessian(row, column))) {
return Failure<NonlinearMitc4ExternalDrillingConstraint>(
"invalid-nonlinear-shell-external-constraint", source_location_,
identity_, "External drilling constraint derivatives must remain "
"finite.");
}
}
}
return Result<NonlinearMitc4ExternalDrillingConstraint>::Success(
std::move(result));
}
Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
const NonlinearMitc4Shell& element, const NonlinearMitc4TrialDofs& trial,
const Vector* coordinate_direction) {
@@ -1526,9 +1678,13 @@ Result<NonlinearMitc4InternalEquilibrium> EvaluateInternalResidualAndTangent(
equilibrium.drilling_constraint[node] = constraints[node].value;
equilibrium.drilling_constraint_hessian[node] =
Matrix{kGlobalDofCount, kGlobalDofCount};
equilibrium.drilling_constraint_third_directional_gradient[node] =
Vector{kGlobalDofCount};
for (std::size_t i = 0U; i < kGlobalDofCount; ++i) {
equilibrium.drilling_constraint_gradient(node, i) =
constraints[node].first[i];
equilibrium.drilling_constraint_third_directional_gradient[node][i] =
constraints[node].mixed_second[i];
for (std::size_t j = 0U; j < kGlobalDofCount; ++j)
equilibrium.drilling_constraint_hessian[node](i, j) =
constraints[node].second[i * kGlobalDofCount + j];