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:
@@ -140,6 +140,23 @@ fesa::DynamicAcceptedFrameState MakeFrame(const fesa::Domain& domain,
|
||||
return frame;
|
||||
}
|
||||
|
||||
TEST(DynamicAnalysisState, RejectsUnknownOrChangingIntegrationFormulation) {
|
||||
auto fixture = MakeFixture();
|
||||
auto initial = MakeFrame(*fixture.domain, 0U);
|
||||
initial.time_integration_formulation =
|
||||
static_cast<fesa::DynamicTimeIntegrationFormulation>(99);
|
||||
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(initial).IsOk());
|
||||
EXPECT_TRUE(fixture.state.AcceptedFrames().empty());
|
||||
initial.time_integration_formulation =
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht;
|
||||
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(initial).IsOk());
|
||||
auto next = MakeFrame(*fixture.domain, 1U);
|
||||
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(next).IsOk());
|
||||
EXPECT_EQ(fixture.state.AcceptedFrames().size(), 1U);
|
||||
next.time_integration_formulation = initial.time_integration_formulation;
|
||||
EXPECT_TRUE(fixture.state.CommitAcceptedFrame(next).IsOk());
|
||||
}
|
||||
|
||||
TEST(DynamicAnalysisState, DefaultStateRejectsTrialAndCommit) {
|
||||
fesa::DynamicAnalysisState state;
|
||||
EXPECT_FALSE(state.IsInitialized());
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <optional>
|
||||
#include <stdexcept>
|
||||
@@ -11,6 +13,7 @@
|
||||
|
||||
#include "analysis/hht_dynamic_internal.h"
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/math/matrix.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
@@ -24,7 +27,8 @@ struct Fixture {
|
||||
|
||||
Fixture MakeFixture(const std::size_t free_nodes = 1U,
|
||||
const bool partial_rotation = false,
|
||||
const bool translations_free = true) {
|
||||
const bool translations_free = true,
|
||||
const double youngs_modulus = 1000.0) {
|
||||
fesa::ModelDefinition definition{};
|
||||
definition.source_path = "hht-algebra.inp";
|
||||
definition.nodes = {
|
||||
@@ -32,7 +36,8 @@ Fixture MakeFixture(const std::size_t free_nodes = 1U,
|
||||
{{"Plate", 2, "2"}, {1, -1, 0}, {definition.source_path, 2U}},
|
||||
{{"Plate", 3, "3"}, {1, 1, 0}, {definition.source_path, 3U}},
|
||||
{{"Plate", 4, "4"}, {-1, 1, 0}, {definition.source_path, 4U}}};
|
||||
definition.materials = {{"Elastic", 1000, 0.3, {definition.source_path, 5U}}};
|
||||
definition.materials = {
|
||||
{"Elastic", youngs_modulus, 0.3, {definition.source_path, 5U}}};
|
||||
definition.densities = {{0U, 1.0, {definition.source_path, 6U}}};
|
||||
definition.shell_sections = {
|
||||
{"Plate", 0.2, 0U, {definition.source_path, 7U}}};
|
||||
@@ -106,8 +111,11 @@ fesa::hht_internal::DynamicAssembly Assembly(
|
||||
frame.external_force[0] = 2.0;
|
||||
frame.residual_scale[0] = 2.0;
|
||||
fesa::Matrix mass{24U, 24U};
|
||||
fesa::Matrix position{24U, 24U};
|
||||
fesa::Matrix velocity{24U, 24U};
|
||||
fesa::Matrix stiffness{24U, 24U};
|
||||
fesa::Matrix metric{24U, 24U};
|
||||
fesa::Matrix hidden_gradient{4U, 24U};
|
||||
std::vector<fesa::DynamicRotationKinematics> rotations;
|
||||
for (std::size_t i = 0; i < 4; ++i) {
|
||||
const std::size_t base = 6U * i;
|
||||
@@ -119,12 +127,16 @@ fesa::hht_internal::DynamicAssembly Assembly(
|
||||
stiffness(base + 5U, base) = -0.7 * k;
|
||||
stiffness(base + 5U, base + 5U) = k;
|
||||
metric(base + 5U, base + 5U) = 3.0;
|
||||
hidden_gradient(i, base) = -0.7 * k;
|
||||
hidden_gradient(i, base + 5U) = k;
|
||||
auto rotation = fesa::EvaluateDynamicRotation({}, {0, 0, 1}, {}, {});
|
||||
EXPECT_TRUE(rotation.HasValue());
|
||||
rotations.push_back(std::move(rotation.Value()));
|
||||
}
|
||||
return {std::move(frame),
|
||||
Sparse(mass),
|
||||
Sparse(position),
|
||||
Sparse(velocity),
|
||||
Sparse(stiffness),
|
||||
Sparse(stiffness),
|
||||
Sparse(metric),
|
||||
@@ -134,7 +146,318 @@ fesa::hht_internal::DynamicAssembly Assembly(
|
||||
0.0,
|
||||
0.0,
|
||||
fesa::Vector{4U},
|
||||
fesa::Vector{4U}};
|
||||
fesa::Vector{4U},
|
||||
Sparse(hidden_gradient)};
|
||||
}
|
||||
|
||||
fesa::DynamicAcceptedFrameState EmptyTrial(const Fixture& fixture) {
|
||||
fesa::DynamicAcceptedFrameState frame{};
|
||||
for (auto* values :
|
||||
{&frame.generalized_coordinates, &frame.velocity, &frame.acceleration,
|
||||
&frame.algorithmic_acceleration, &frame.external_force})
|
||||
*values = fesa::Vector(fixture.dofs.FullDofCount());
|
||||
return frame;
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ValidPredictorPreservesRatesFixedDofsAndOldState) {
|
||||
const auto fixture = MakeFixture(4U, true);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
auto old = EmptyTrial(fixture);
|
||||
old.velocity[2U] = 0.2;
|
||||
old.velocity[5U] = 0.1;
|
||||
old.acceleration[2U] = 0.3;
|
||||
old.algorithmic_acceleration[2U] = 0.3;
|
||||
old.iteration_count = 7U;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
const auto predictor = fesa::PredictHht(
|
||||
old.generalized_coordinates, old.velocity, old.algorithmic_acceleration,
|
||||
0.1, parameters).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
const auto& frame = start.Value().frame;
|
||||
EXPECT_GT(frame.generalized_coordinates[2U], 0.0);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], predictor.coordinates[i]);
|
||||
EXPECT_DOUBLE_EQ(frame.velocity[i], predictor.velocity[i]);
|
||||
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.acceleration[i],
|
||||
parameters.alpha * old.acceleration[i] /
|
||||
(1.0 + parameters.alpha));
|
||||
EXPECT_DOUBLE_EQ(old.generalized_coordinates[i], 0.0);
|
||||
}
|
||||
for (const auto i : fixture.dofs.ConstrainedDofs()) {
|
||||
EXPECT_DOUBLE_EQ(frame.generalized_coordinates[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.velocity[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.acceleration[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[i], 0.0);
|
||||
}
|
||||
EXPECT_DOUBLE_EQ(old.velocity[2U], 0.2);
|
||||
EXPECT_DOUBLE_EQ(old.acceleration[2U], 0.3);
|
||||
EXPECT_EQ(old.iteration_count, 7U);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, InvalidPredictorGeometryUsesUnchangedAcceptedCoordinates) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
for (const int invalid_case : {0, 1, 2}) {
|
||||
SCOPED_TRACE(invalid_case);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
if (invalid_case == 0) {
|
||||
predictor.coordinates[5U] = 3.5;
|
||||
} else if (invalid_case == 1) {
|
||||
predictor.coordinates[6U] = -2.0;
|
||||
predictor.coordinates[12U] = -2.0;
|
||||
} else {
|
||||
predictor.coordinates[3U] =
|
||||
std::acos(-1.0) - 2.0 * std::sqrt(std::numeric_limits<double>::epsilon());
|
||||
}
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[i], 0.0);
|
||||
EXPECT_DOUBLE_EQ(old.generalized_coordinates[i], 0.0);
|
||||
const double auxiliary =
|
||||
-predictor.coordinates[i] / (parameters.beta * 0.1 * 0.1);
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.algorithmic_acceleration[i], auxiliary);
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.acceleration[i],
|
||||
auxiliary / (1.0 + parameters.alpha));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ValidChartRateOverflowIsNotReplacedByAcceptedGeometry) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[3U] = 0.1;
|
||||
predictor.velocity[3U] = 1e200;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 1e-160,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"invalid-dynamic-rotation");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, LaterOutsideChartNodeDoesNotHideEarlierRateOverflow) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[3U] = 0.1;
|
||||
predictor.velocity[3U] = 1e200;
|
||||
predictor.coordinates[11U] = 3.5;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 1e-160,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"invalid-dynamic-rotation");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, UsesTheExistingPrincipalChartMarginForInitialSelection) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
const double margin = std::sqrt(std::numeric_limits<double>::epsilon());
|
||||
fesa::SerialParallelFor parallel;
|
||||
for (const bool inside : {true, false}) {
|
||||
SCOPED_TRACE(inside);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
const double angle = std::acos(-1.0) - (inside ? 2.0 : 0.5) * margin;
|
||||
for (std::size_t node = 0U; node < 4U; ++node) {
|
||||
const auto& x = fixture.domain->Nodes()[node].coordinates;
|
||||
predictor.coordinates[6U * node] =
|
||||
(std::cos(angle) - 1.0) * x[0U] - std::sin(angle) * x[1U];
|
||||
predictor.coordinates[6U * node + 1U] =
|
||||
std::sin(angle) * x[0U] + (std::cos(angle) - 1.0) * x[1U];
|
||||
predictor.coordinates[6U * node + 5U] = angle;
|
||||
}
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U],
|
||||
inside ? predictor.coordinates[5U] : 0.0);
|
||||
}
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, CommonEnergyScalingSelectsTheSmallerFreeResidual) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
auto probe = old;
|
||||
probe.generalized_coordinates[5U] = 0.25;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto evaluated = fesa::hht_internal::EvaluateAssembly(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
|
||||
0.1, parameters, parallel, false);
|
||||
ASSERT_TRUE(evaluated.HasValue());
|
||||
double drill_residual = 0.0;
|
||||
for (std::size_t i = 3U; i < 24U; i += 6U)
|
||||
for (std::size_t j = 0U; j < 3U; ++j)
|
||||
drill_residual = std::max(
|
||||
drill_residual, std::abs(evaluated.Value().frame.residual[i + j]));
|
||||
ASSERT_GT(drill_residual, 0.0);
|
||||
// A rigid translation gives m*a per node, m=rho*t*area/4=0.2.
|
||||
// The accepted-start force is 0.75 of the predictor torque. Multiplying
|
||||
// force by ell=2 reverses the unscaled force-versus-torque comparison.
|
||||
const double translation =
|
||||
0.75 * drill_residual * (1.0 + parameters.alpha) * parameters.beta *
|
||||
0.1 * 0.1 / 0.2;
|
||||
fesa::HhtPredictor predictor{probe.generalized_coordinates, fesa::Vector(24U)};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
predictor.coordinates[6U * node] = translation;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U], 0.25);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ConstrainedReactionDoesNotChangeNontrivialFreeResidualTie) {
|
||||
const auto fixture = MakeFixture();
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
auto old = EmptyTrial(fixture);
|
||||
auto probe = old;
|
||||
probe.generalized_coordinates[5U] = 0.25;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto evaluated = fesa::hht_internal::EvaluateAssembly(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
|
||||
0.1, parameters, parallel, false);
|
||||
ASSERT_TRUE(evaluated.HasValue());
|
||||
fesa::Vector load_scale(24U);
|
||||
for (std::size_t i = 0U; i < 24U; ++i) {
|
||||
old.external_force[i] = 0.5 * evaluated.Value().frame.internal_force[i];
|
||||
load_scale[i] = std::abs(old.external_force[i]);
|
||||
}
|
||||
// Pure drill spin has zero physical inertia at both starting points, so
|
||||
// the free residuals are exact opposites. A support reaction is excluded.
|
||||
old.external_force[6U] = load_scale[6U] = 1e308;
|
||||
fesa::HhtPredictor predictor{probe.generalized_coordinates, fesa::Vector(24U)};
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, load_scale, 0.1, parameters, parallel);
|
||||
ASSERT_TRUE(start.HasValue());
|
||||
EXPECT_DOUBLE_EQ(start.Value().frame.generalized_coordinates[5U], 0.25);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ScaledResidualOverflowRemainsAFailure) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
auto old = EmptyTrial(fixture);
|
||||
old.external_force[0U] = 1e308;
|
||||
fesa::Vector load_scale(24U);
|
||||
load_scale[0U] = 1e308;
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[5U] = 0.25;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, load_scale, 0.1,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-invalid-state");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, AcceptedStartArithmeticFailureIsNotIgnoredDuringComparison) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
predictor.coordinates[6U * node] = 1e10;
|
||||
const double time_increment = 1e-150;
|
||||
const auto parameters = fesa::MakeHhtParameters(-0.05).Value();
|
||||
fesa::SerialParallelFor parallel;
|
||||
auto probe = old;
|
||||
probe.generalized_coordinates = predictor.coordinates;
|
||||
const auto valid_predictor = fesa::hht_internal::EvaluateAssembly(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), probe, fesa::Vector(24U),
|
||||
time_increment, parameters, parallel, false);
|
||||
ASSERT_TRUE(valid_predictor.HasValue());
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), time_increment, parameters, parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-invalid-state");
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, RejectedPredictorDoesNotHideInvalidAcceptedStartRates) {
|
||||
const auto fixture = MakeFixture(4U);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
predictor.coordinates[5U] = 3.5;
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 1e-160,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-invalid-state");
|
||||
EXPECT_DOUBLE_EQ(old.generalized_coordinates[5U], 0.0);
|
||||
}
|
||||
|
||||
TEST(HhtNewtonStart, ValidGeometryStressOverflowRemainsARecoveryFailure) {
|
||||
const auto fixture = MakeFixture(4U, false, true, 1e307);
|
||||
const auto shells = fesa::hht_internal::BuildRuntimeShells(
|
||||
*fixture.domain, fixture.dofs);
|
||||
ASSERT_TRUE(shells.HasValue());
|
||||
const auto old = EmptyTrial(fixture);
|
||||
fesa::HhtPredictor predictor{fesa::Vector(24U), fesa::Vector(24U)};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t component = 0U; component < 2U; ++component)
|
||||
predictor.coordinates[6U * node + component] =
|
||||
9.0 * fixture.domain->Nodes()[node].coordinates[component];
|
||||
fesa::SerialParallelFor parallel;
|
||||
const auto start = fesa::hht_internal::EvaluateNewtonStart(
|
||||
*fixture.domain, fixture.dofs, shells.Value(), old, predictor,
|
||||
old.acceleration, fesa::Vector(24U), 0.1,
|
||||
fesa::MakeHhtParameters(-0.05).Value(), parallel);
|
||||
ASSERT_FALSE(start.HasValue());
|
||||
EXPECT_EQ(start.GetStatus().Diagnostics().front().code,
|
||||
"invalid-nonlinear-shell-recovery");
|
||||
}
|
||||
|
||||
TEST(HhtDynamicAlgebra,
|
||||
@@ -169,6 +492,20 @@ TEST(HhtDynamicAlgebra,
|
||||
EXPECT_FALSE(pseudoinverse.IsOk());
|
||||
}
|
||||
|
||||
TEST(HhtDynamicAlgebra, RejectsMissingCanonicalHiddenGradient) {
|
||||
const auto fixture = MakeFixture();
|
||||
auto assembly = Assembly();
|
||||
assembly.hidden_gradient.reset();
|
||||
fesa::MklPardisoSolver positive;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
const auto acceleration = fesa::hht_internal::InitializeAcceleration(
|
||||
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
|
||||
ASSERT_FALSE(acceleration.HasValue());
|
||||
ASSERT_FALSE(acceleration.GetStatus().Diagnostics().empty());
|
||||
EXPECT_EQ(acceleration.GetStatus().Diagnostics().front().code,
|
||||
"dynamic-algebraic-gradient");
|
||||
}
|
||||
|
||||
TEST(HhtDynamicAlgebra, ChecksInitialConstraintAndVelocityWithoutRepair) {
|
||||
const auto fixture = MakeFixture();
|
||||
fesa::MklPardisoSolver positive;
|
||||
@@ -240,6 +577,11 @@ TEST(HhtDynamicAlgebra, AppliesTheExactRegularityConditionBoundary) {
|
||||
}
|
||||
assembly.internal_tangent = Sparse(stiffness);
|
||||
assembly.drilling_metric = Sparse(metric);
|
||||
fesa::Matrix hidden_gradient{4U, 24U};
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
hidden_gradient(node, 6U * node + 5U) =
|
||||
node == 1U ? std::ldexp(1.0, -exponent) : 1.0;
|
||||
assembly.hidden_gradient = Sparse(hidden_gradient);
|
||||
const auto result = fesa::hht_internal::InitializeAcceleration(
|
||||
*fixture.domain, fixture.dofs, assembly, 2.0, positive, tangent);
|
||||
EXPECT_EQ(result.HasValue(), exponent <= 32)
|
||||
|
||||
@@ -2,15 +2,291 @@
|
||||
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <iterator>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/analysis/newton_iteration_log.h"
|
||||
#include "fesa/assembly/parallel_for.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
|
||||
namespace {
|
||||
fesa::ModelDefinition MakeDefinition(const bool fixed = true) {
|
||||
fesa::Status InjectedTangentFailure() {
|
||||
return fesa::Status::Failure(
|
||||
fesa::FailureCategory::kSolver,
|
||||
{{fesa::Severity::kError, "injected-tangent-backend", {}, "DYNAMIC",
|
||||
{}, "Injected tangent backend failure."}});
|
||||
}
|
||||
|
||||
class FailingTangent final : public fesa::TangentSolver {
|
||||
public:
|
||||
fesa::Status FactorizeFresh(const fesa::SparseMatrix&) override {
|
||||
return InjectedTangentFailure();
|
||||
}
|
||||
fesa::Status SolveCorrection(const fesa::Vector&, fesa::Vector&) const override {
|
||||
return InjectedTangentFailure();
|
||||
}
|
||||
};
|
||||
|
||||
std::string FailureText(const fesa::Status& status) {
|
||||
std::string result;
|
||||
for (const auto& diagnostic : status.Diagnostics()) {
|
||||
if (!result.empty()) result += " | ";
|
||||
result += diagnostic.code + ": " + diagnostic.message;
|
||||
}
|
||||
return result;
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeDefinition(bool fixed = true);
|
||||
|
||||
fesa::ModelDefinition MakeRigidTranslationDefinition() {
|
||||
auto definition = MakeDefinition(false);
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
definition.nodes.insert(
|
||||
definition.nodes.end(),
|
||||
{{{"Shell-B", 50, "0050"}, {2.0, 0.0, 0.0}, {source, 14U}},
|
||||
{{"Shell-B", 60, "0060"}, {3.0, 0.0, 0.0}, {source, 15U}},
|
||||
{{"Shell-B", 70, "0070"}, {3.0, 1.0, 0.0}, {source, 16U}},
|
||||
{{"Shell-B", 80, "0080"}, {2.0, 1.0, 0.0}, {source, 17U}}});
|
||||
definition.shell_elements.push_back(
|
||||
{{"Shell-B", 19, "0019"}, fesa::ShellSourceElementType::kS4,
|
||||
{4U, 5U, 6U, 7U}, 0U, 0U, {source, 18U}});
|
||||
for (std::size_t node = 4U; node < 8U; ++node) {
|
||||
definition.shell_node_initial_frames.push_back(
|
||||
{static_cast<fesa::EntityIndex>(node), {0.0, 0.0, 1.0},
|
||||
{1.0, 0.0, 0.0}, {0.0, 1.0, 0.0}});
|
||||
}
|
||||
definition.node_sets[0U] =
|
||||
{"All-A", std::string{"Shell-A"}, {0U, 1U, 2U, 3U}, {source, 9U}};
|
||||
definition.node_sets.push_back(
|
||||
{"All-B", std::string{"Shell-B"}, {4U, 5U, 6U, 7U}, {source, 19U}});
|
||||
definition.dynamic_step->loads = {{"All-A", 1, 1.0, {source, 20U}},
|
||||
{"All-B", 1, 1.0, {source, 21U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
std::array<double, 3> RotateGlobal(const std::array<double, 3>& value) {
|
||||
constexpr double kCosZ = 0.7648421872844885;
|
||||
constexpr double kSinZ = 0.6442176872376910;
|
||||
constexpr double kCosX = 0.9210609940028851;
|
||||
constexpr double kSinX = 0.3894183423086505;
|
||||
return {kCosZ * value[0U] - kSinZ * kCosX * value[1U] +
|
||||
kSinZ * kSinX * value[2U],
|
||||
kSinZ * value[0U] + kCosZ * kCosX * value[1U] -
|
||||
kCosZ * kSinX * value[2U],
|
||||
kSinX * value[1U] + kCosX * value[2U]};
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeRotatedRigidTranslationDefinition() {
|
||||
auto definition = MakeRigidTranslationDefinition();
|
||||
for (auto& node : definition.nodes) node.coordinates = RotateGlobal(node.coordinates);
|
||||
for (auto& frame : definition.shell_node_initial_frames) {
|
||||
frame.director = RotateGlobal(frame.director);
|
||||
frame.tangent_a = RotateGlobal(frame.tangent_a);
|
||||
frame.tangent_b = RotateGlobal(frame.tangent_b);
|
||||
}
|
||||
const auto load = RotateGlobal({1.0, 0.0, 0.0});
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
definition.dynamic_step->loads = {{"All-A", 1, load[0U], {source, 22U}},
|
||||
{"All-A", 2, load[1U], {source, 23U}},
|
||||
{"All-A", 3, load[2U], {source, 24U}},
|
||||
{"All-B", 1, load[0U], {source, 25U}},
|
||||
{"All-B", 2, load[1U], {source, 26U}},
|
||||
{"All-B", 3, load[2U], {source, 27U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeFiniteRotationDefinition() {
|
||||
auto definition = MakeDefinition(false);
|
||||
definition.dynamic_step->initial_increment = 0.01;
|
||||
definition.dynamic_step->time_period = 0.02;
|
||||
definition.dynamic_step->increment_count = 2U;
|
||||
definition.dynamic_step->loads = {
|
||||
{"Loaded", 3, 1.0, {"dynamic-state.inp", 23U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeRotatedFiniteRotationDefinition() {
|
||||
auto definition = MakeFiniteRotationDefinition();
|
||||
for (auto& node : definition.nodes) node.coordinates = RotateGlobal(node.coordinates);
|
||||
for (auto& frame : definition.shell_node_initial_frames) {
|
||||
frame.director = RotateGlobal(frame.director);
|
||||
frame.tangent_a = RotateGlobal(frame.tangent_a);
|
||||
frame.tangent_b = RotateGlobal(frame.tangent_b);
|
||||
}
|
||||
const auto load = RotateGlobal({0.0, 0.0, 1.0});
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
definition.dynamic_step->loads = {{"Loaded", 1, load[0U], {source, 24U}},
|
||||
{"Loaded", 2, load[1U], {source, 25U}},
|
||||
{"Loaded", 3, load[2U], {source, 26U}}};
|
||||
return definition;
|
||||
}
|
||||
|
||||
struct RigidTranslationRun {
|
||||
fesa::DynamicExecutionResult execution;
|
||||
std::string log;
|
||||
};
|
||||
|
||||
void ExpectSameSourceIds(const std::vector<fesa::SourceEntityId>& left,
|
||||
const std::vector<fesa::SourceEntityId>& right) {
|
||||
ASSERT_EQ(left.size(), right.size());
|
||||
for (std::size_t i = 0U; i < left.size(); ++i) {
|
||||
EXPECT_EQ(left[i].instance_name, right[i].instance_name);
|
||||
EXPECT_EQ(left[i].source_label, right[i].source_label);
|
||||
EXPECT_EQ(left[i].source_label_text, right[i].source_label_text);
|
||||
}
|
||||
}
|
||||
|
||||
fesa::Result<RigidTranslationRun> RunRigidTranslation(
|
||||
const fesa::ModelDefinition& definition, const fesa::ParallelFor& parallel,
|
||||
const std::string& label) {
|
||||
auto domain = fesa::Domain::Create(definition);
|
||||
if (!domain.HasValue()) return fesa::Result<RigidTranslationRun>::Failure(
|
||||
domain.GetStatus());
|
||||
fesa::MklPardisoSolver mass;
|
||||
fesa::MklPardisoTangentSolver tangent;
|
||||
fesa::NewtonIterationLogWriter log;
|
||||
const auto path = std::filesystem::temp_directory_path() /
|
||||
("fesa-hht-rigid-" + label + ".log");
|
||||
auto status = log.Open(path);
|
||||
if (!status.IsOk()) return fesa::Result<RigidTranslationRun>::Failure(status);
|
||||
auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value(), log);
|
||||
status = log.Close();
|
||||
std::string log_text;
|
||||
{
|
||||
std::ifstream input{path, std::ios::binary};
|
||||
log_text.assign(std::istreambuf_iterator<char>{input}, {});
|
||||
}
|
||||
std::filesystem::remove(path);
|
||||
if (!status.IsOk()) return fesa::Result<RigidTranslationRun>::Failure(status);
|
||||
if (!result.HasValue()) return fesa::Result<RigidTranslationRun>::Failure(
|
||||
result.GetStatus());
|
||||
return fesa::Result<RigidTranslationRun>::Success(
|
||||
{std::move(result.Value()), log_text});
|
||||
}
|
||||
|
||||
void ExpectSameDynamicHistory(const RigidTranslationRun& left,
|
||||
const RigidTranslationRun& right) {
|
||||
EXPECT_EQ(left.log, right.log);
|
||||
const auto& left_frames = left.execution.state.AcceptedFrames();
|
||||
const auto& right_frames = right.execution.state.AcceptedFrames();
|
||||
ASSERT_EQ(left_frames.size(), right_frames.size());
|
||||
for (std::size_t frame = 0U; frame < left_frames.size(); ++frame) {
|
||||
const auto& lhs = left_frames[frame];
|
||||
const auto& rhs = right_frames[frame];
|
||||
EXPECT_EQ(lhs.time, rhs.time);
|
||||
EXPECT_EQ(lhs.iteration_count, rhs.iteration_count);
|
||||
EXPECT_EQ(lhs.nodal_internal_order, rhs.nodal_internal_order);
|
||||
ExpectSameSourceIds(lhs.nodal_source_ids, rhs.nodal_source_ids);
|
||||
EXPECT_EQ(lhs.nodal_triad, rhs.nodal_triad);
|
||||
EXPECT_EQ(lhs.nodal_director, rhs.nodal_director);
|
||||
for (const auto* values : {&lhs.generalized_coordinates, &lhs.velocity,
|
||||
&lhs.acceleration, &lhs.algorithmic_acceleration,
|
||||
&lhs.inertia_force, &lhs.internal_force,
|
||||
&lhs.external_force, &lhs.residual, &lhs.reaction}) {
|
||||
const auto* other = values == &lhs.generalized_coordinates
|
||||
? &rhs.generalized_coordinates
|
||||
: values == &lhs.velocity ? &rhs.velocity
|
||||
: values == &lhs.acceleration ? &rhs.acceleration
|
||||
: values == &lhs.algorithmic_acceleration
|
||||
? &rhs.algorithmic_acceleration
|
||||
: values == &lhs.inertia_force ? &rhs.inertia_force
|
||||
: values == &lhs.internal_force ? &rhs.internal_force
|
||||
: values == &lhs.external_force ? &rhs.external_force
|
||||
: values == &lhs.residual ? &rhs.residual
|
||||
: &rhs.reaction;
|
||||
ASSERT_EQ(values->Size(), other->Size());
|
||||
for (std::size_t i = 0U; i < values->Size(); ++i)
|
||||
EXPECT_DOUBLE_EQ((*values)[i], (*other)[i]);
|
||||
}
|
||||
EXPECT_DOUBLE_EQ(lhs.kinetic_energy, rhs.kinetic_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.physical_strain_energy, rhs.physical_strain_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.drilling_energy, rhs.drilling_energy);
|
||||
EXPECT_DOUBLE_EQ(lhs.external_work, rhs.external_work);
|
||||
EXPECT_DOUBLE_EQ(lhs.energy_defect, rhs.energy_defect);
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectRotatedDofVector(const fesa::Vector& source, const fesa::Vector& target,
|
||||
const double scale) {
|
||||
ASSERT_EQ(source.Size(), target.Size());
|
||||
ASSERT_EQ(source.Size() % 6U, 0U);
|
||||
for (std::size_t node = 0U; node < source.Size() / 6U; ++node)
|
||||
for (const std::size_t offset : {0U, 3U}) {
|
||||
const auto rotated = RotateGlobal(
|
||||
{source[6U * node + offset], source[6U * node + offset + 1U],
|
||||
source[6U * node + offset + 2U]});
|
||||
for (std::size_t component = 0U; component < 3U; ++component)
|
||||
EXPECT_LE(std::abs(rotated[component] -
|
||||
target[6U * node + offset + component]) /
|
||||
scale,
|
||||
1e-10);
|
||||
}
|
||||
}
|
||||
|
||||
void ExpectCovariantDynamicHistory(const RigidTranslationRun& original,
|
||||
const RigidTranslationRun& rotated,
|
||||
const double time_scale,
|
||||
const double reference_energy) {
|
||||
constexpr double kCoordinateScale = 1.0;
|
||||
const double velocity_scale = kCoordinateScale / time_scale;
|
||||
const double acceleration_scale = kCoordinateScale / (time_scale * time_scale);
|
||||
const auto& original_frames = original.execution.state.AcceptedFrames();
|
||||
const auto& rotated_frames = rotated.execution.state.AcceptedFrames();
|
||||
ASSERT_EQ(original_frames.size(), rotated_frames.size());
|
||||
for (std::size_t frame = 0U; frame < original_frames.size(); ++frame) {
|
||||
const auto& source = original_frames[frame];
|
||||
const auto& target = rotated_frames[frame];
|
||||
ExpectSameSourceIds(source.nodal_source_ids, target.nodal_source_ids);
|
||||
EXPECT_LE(std::abs(source.kinetic_energy - target.kinetic_energy) /
|
||||
reference_energy,
|
||||
1e-10);
|
||||
EXPECT_LE(std::abs(source.physical_strain_energy -
|
||||
target.physical_strain_energy) /
|
||||
reference_energy,
|
||||
1e-10);
|
||||
EXPECT_LE(std::abs(source.drilling_energy - target.drilling_energy) /
|
||||
reference_energy,
|
||||
1e-10);
|
||||
ExpectRotatedDofVector(source.generalized_coordinates,
|
||||
target.generalized_coordinates, kCoordinateScale);
|
||||
ExpectRotatedDofVector(source.velocity, target.velocity, velocity_scale);
|
||||
ExpectRotatedDofVector(source.acceleration, target.acceleration,
|
||||
acceleration_scale);
|
||||
ExpectRotatedDofVector(source.algorithmic_acceleration,
|
||||
target.algorithmic_acceleration, acceleration_scale);
|
||||
for (const auto* values : {&source.inertia_force, &source.internal_force,
|
||||
&source.external_force, &source.residual,
|
||||
&source.reaction}) {
|
||||
const auto* other = values == &source.inertia_force ? &target.inertia_force
|
||||
: values == &source.internal_force ? &target.internal_force
|
||||
: values == &source.external_force ? &target.external_force
|
||||
: values == &source.residual ? &target.residual
|
||||
: &target.reaction;
|
||||
ExpectRotatedDofVector(*values, *other, 1.0);
|
||||
}
|
||||
for (std::size_t node = 0U; node < source.nodal_triad.size(); ++node)
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
const auto triad = RotateGlobal(source.nodal_triad[node][row]);
|
||||
const auto director = RotateGlobal(source.nodal_director[node]);
|
||||
for (std::size_t component = 0U; component < 3U; ++component) {
|
||||
EXPECT_LE(std::abs(triad[component] -
|
||||
target.nodal_triad[node][row][component]),
|
||||
1e-10);
|
||||
EXPECT_LE(std::abs(director[component] -
|
||||
target.nodal_director[node][component]),
|
||||
1e-10);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fesa::ModelDefinition MakeDefinition(const bool fixed) {
|
||||
const std::filesystem::path source{"dynamic-state.inp"};
|
||||
fesa::ModelDefinition definition{};
|
||||
definition.source_path = source;
|
||||
@@ -71,6 +347,8 @@ TEST(HhtDynamicProcedure, ClampedStepLoadHasPhysicalReactionAndZeroMotion) {
|
||||
: result.GetStatus().Diagnostics().front().message);
|
||||
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
|
||||
for (const auto& frame : result.Value().state.AcceptedFrames()) {
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
EXPECT_DOUBLE_EQ(frame.reaction[2U], -4.0);
|
||||
EXPECT_DOUBLE_EQ(frame.external_force[2U], 4.0);
|
||||
@@ -113,6 +391,8 @@ TEST(HhtDynamicProcedure, UnloadedFreeChannelsRemainExactlyAtRest) {
|
||||
: result.GetStatus().Diagnostics().front().message);
|
||||
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 3U);
|
||||
for (const auto& frame : result.Value().state.AcceptedFrames()) {
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
EXPECT_EQ(frame.iteration_count, 0U);
|
||||
for (const auto* values :
|
||||
{&frame.generalized_coordinates, &frame.velocity,
|
||||
@@ -167,7 +447,125 @@ TEST(HhtDynamicProcedure, LargeDirectStepStartsFromAcceptedGeometry) {
|
||||
result.Value().state.CommittedState().free_residual_metrics)
|
||||
EXPECT_LE(metric, 1e-10);
|
||||
EXPECT_GT(result.Value().state.CommittedState().iteration_count, 0U);
|
||||
const auto& frames = result.Value().state.AcceptedFrames();
|
||||
const double alpha = domain.Value().Dynamic()->alpha;
|
||||
for (std::size_t frame_index = 1U; frame_index < frames.size(); ++frame_index) {
|
||||
const auto& frame = frames[frame_index];
|
||||
EXPECT_EQ(frame.time_integration_formulation,
|
||||
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
|
||||
for (std::size_t dof = 0U; dof < frame.acceleration.Size(); ++dof) {
|
||||
EXPECT_DOUBLE_EQ(frame.algorithmic_acceleration[dof],
|
||||
(1.0 + alpha) * frame.acceleration[dof] -
|
||||
alpha * frames[frame_index - 1U].acceleration[dof]);
|
||||
}
|
||||
}
|
||||
std::filesystem::remove(path);
|
||||
}
|
||||
|
||||
TEST(HhtDynamicProcedure, PreservesBackendCauseAndAddsInitialFailureContext) {
|
||||
auto definition = MakeDefinition(false);
|
||||
definition.dynamic_step->dynamic_location = {"dynamic-state.inp", 13U};
|
||||
auto domain = fesa::Domain::Create(std::move(definition));
|
||||
ASSERT_TRUE(domain.HasValue());
|
||||
fesa::SerialParallelFor parallel;
|
||||
fesa::MklPardisoSolver mass;
|
||||
FailingTangent tangent;
|
||||
fesa::NewtonIterationLogWriter log;
|
||||
const auto path =
|
||||
std::filesystem::temp_directory_path() / "fesa-hht-context.log";
|
||||
ASSERT_TRUE(log.Open(path).IsOk());
|
||||
const auto result = fesa::HhtDynamicProcedureExecutor(parallel, mass, tangent)
|
||||
.Execute(domain.Value(), log);
|
||||
ASSERT_TRUE(log.Close().IsOk());
|
||||
std::filesystem::remove(path);
|
||||
ASSERT_FALSE(result.HasValue());
|
||||
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
||||
EXPECT_EQ(result.GetStatus().Diagnostics().front().code,
|
||||
"injected-tangent-backend");
|
||||
const auto found = std::find_if(
|
||||
result.GetStatus().Diagnostics().begin(), result.GetStatus().Diagnostics().end(),
|
||||
[](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.code == "dynamic-procedure-context";
|
||||
});
|
||||
ASSERT_NE(found, result.GetStatus().Diagnostics().end());
|
||||
EXPECT_EQ(found->location.file, std::filesystem::path{"dynamic-state.inp"});
|
||||
EXPECT_EQ(found->location.line, 13U);
|
||||
EXPECT_EQ(found->entity_identity, "Transient Load");
|
||||
EXPECT_NE(found->message.find("stage=initial"), std::string::npos);
|
||||
EXPECT_NE(found->message.find("increment=0"), std::string::npos);
|
||||
EXPECT_NE(found->message.find("time=0"), std::string::npos);
|
||||
}
|
||||
|
||||
TEST(HhtDynamicProcedure,
|
||||
UniformTranslationHasNoShellStrainAndIsDeterministicAcrossPartitions) {
|
||||
const auto definition = MakeRigidTranslationDefinition();
|
||||
const auto rotated_definition = MakeRotatedRigidTranslationDefinition();
|
||||
const auto finite_rotation_definition = MakeFiniteRotationDefinition();
|
||||
const auto rotated_finite_rotation_definition =
|
||||
MakeRotatedFiniteRotationDefinition();
|
||||
const fesa::SerialParallelFor serial;
|
||||
const fesa::TbbParallelFor tbb;
|
||||
auto first = RunRigidTranslation(definition, serial, "serial-first");
|
||||
auto partitioned = RunRigidTranslation(definition, tbb, "tbb");
|
||||
auto second = RunRigidTranslation(definition, serial, "serial-second");
|
||||
auto rotated = RunRigidTranslation(rotated_definition, serial, "rotated");
|
||||
auto finite_rotation = RunRigidTranslation(finite_rotation_definition, serial,
|
||||
"finite-rotation");
|
||||
auto rotated_finite_rotation = RunRigidTranslation(
|
||||
rotated_finite_rotation_definition, serial, "rotated-finite-rotation");
|
||||
ASSERT_TRUE(first.HasValue())
|
||||
<< (first.HasValue() ? "" : FailureText(first.GetStatus()));
|
||||
ASSERT_TRUE(partitioned.HasValue())
|
||||
<< (partitioned.HasValue() ? "" : FailureText(partitioned.GetStatus()));
|
||||
ASSERT_TRUE(second.HasValue())
|
||||
<< (second.HasValue() ? "" : FailureText(second.GetStatus()));
|
||||
ASSERT_TRUE(rotated.HasValue())
|
||||
<< (rotated.HasValue() ? "" : FailureText(rotated.GetStatus()));
|
||||
ASSERT_TRUE(finite_rotation.HasValue())
|
||||
<< (finite_rotation.HasValue() ? "" : FailureText(finite_rotation.GetStatus()));
|
||||
ASSERT_TRUE(rotated_finite_rotation.HasValue())
|
||||
<< (rotated_finite_rotation.HasValue()
|
||||
? ""
|
||||
: FailureText(rotated_finite_rotation.GetStatus()));
|
||||
ExpectSameDynamicHistory(first.Value(), partitioned.Value());
|
||||
ExpectSameDynamicHistory(first.Value(), second.Value());
|
||||
ExpectCovariantDynamicHistory(first.Value(), rotated.Value(), 0.5,
|
||||
2.0 * 1000.0 * 0.1);
|
||||
ExpectCovariantDynamicHistory(finite_rotation.Value(),
|
||||
rotated_finite_rotation.Value(), 0.02,
|
||||
1000.0 * 0.1);
|
||||
const auto& frames = first.Value().execution.state.AcceptedFrames();
|
||||
ASSERT_EQ(frames.size(), 3U);
|
||||
constexpr double kReferenceEnergy = 2.0 * 1000.0 * 0.1;
|
||||
constexpr double kCoordinateScale = 1.0;
|
||||
for (const auto& frame : frames) {
|
||||
EXPECT_LE(std::abs(frame.physical_strain_energy) / kReferenceEnergy, 1e-10);
|
||||
EXPECT_LE(std::abs(frame.drilling_energy) / kReferenceEnergy, 1e-10);
|
||||
for (std::size_t node = 1U; node < 8U; ++node)
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node] -
|
||||
frame.generalized_coordinates[0U]) /
|
||||
kCoordinateScale,
|
||||
1e-12);
|
||||
for (std::size_t node = 0U; node < 8U; ++node) {
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + 1U]) /
|
||||
kCoordinateScale,
|
||||
1e-12);
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + 2U]) /
|
||||
kCoordinateScale,
|
||||
1e-12);
|
||||
for (std::size_t rotation = 3U; rotation < 6U; ++rotation)
|
||||
EXPECT_LE(std::abs(frame.generalized_coordinates[6U * node + rotation]),
|
||||
1e-12);
|
||||
}
|
||||
}
|
||||
EXPECT_GT(frames.back().generalized_coordinates[0U], 0.0);
|
||||
double maximum_rotation = 0.0;
|
||||
for (const auto& frame : finite_rotation.Value().execution.state.AcceptedFrames())
|
||||
for (std::size_t node = 0U; node < 4U; ++node)
|
||||
for (std::size_t component = 3U; component < 6U; ++component)
|
||||
maximum_rotation = (std::max)(
|
||||
maximum_rotation, std::abs(frame.generalized_coordinates[6U * node + component]));
|
||||
EXPECT_GT(maximum_rotation, 0.0);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -90,7 +90,16 @@ double GradientIntegral(std::size_t a, std::size_t b, std::size_t i,
|
||||
}
|
||||
std::vector<double> DenseOracle(const std::vector<std::size_t>& free) {
|
||||
const auto n = free.size();
|
||||
std::vector<double> k(n * n, 0.0), m(n * n, 0.0), eigen(n, 0.0);
|
||||
std::vector<double> k(n * n, 0.0), full_mass(24U * 24U, 0.0),
|
||||
m(n * n, 0.0), eigen(n, 0.0);
|
||||
for (std::size_t a = 0; a < 8; ++a)
|
||||
for (std::size_t b = 0; b < 8; ++b)
|
||||
for (std::size_t i = 0; i < 3; ++i) {
|
||||
double value = 2.0 * 48.0 / 64.0;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
value *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0;
|
||||
full_mass[(3U * a + i) * 24U + 3U * b + i] = value;
|
||||
}
|
||||
for (std::size_t row = 0; row < n; ++row)
|
||||
for (std::size_t col = 0; col < n; ++col) {
|
||||
const auto a = free[row] / 3U, b = free[col] / 3U;
|
||||
@@ -104,12 +113,12 @@ std::vector<double> DenseOracle(const std::vector<std::size_t>& free) {
|
||||
GradientIntegral(a, b, j, i) + (i == j ? gradient_dot : 0.0)) +
|
||||
(80.0 / 48.0) * (48.0 * kSigns[a][i] / (8.0 * (i + 1))) *
|
||||
(48.0 * kSigns[b][j] / (8.0 * (j + 1)));
|
||||
if (i == j) {
|
||||
m[row * n + col] = 2.0 * 48.0 / 64.0;
|
||||
for (std::size_t d = 0; d < 3; ++d)
|
||||
m[row * n + col] *= 1.0 + kSigns[a][d] * kSigns[b][d] / 3.0;
|
||||
}
|
||||
}
|
||||
for (std::size_t row = 0; row < n; ++row) {
|
||||
const auto full_row = free[row];
|
||||
for (std::size_t col = 0; col < 24U; ++col)
|
||||
m[row * n + row] += full_mass[full_row * 24U + col];
|
||||
}
|
||||
EXPECT_EQ(
|
||||
LAPACKE_dsygv(LAPACK_ROW_MAJOR, 1, 'N', 'U', static_cast<MKL_INT>(n),
|
||||
k.data(), static_cast<MKL_INT>(n), m.data(),
|
||||
@@ -126,7 +135,7 @@ std::vector<std::size_t> FaceFree() {
|
||||
}
|
||||
} // namespace
|
||||
TEST(SolidFrequencyProcedure,
|
||||
OriginalConsistentPencilMatchesIndependentDenseOracle) {
|
||||
RowSumLumpedPencilMatchesIndependentDenseOracle) {
|
||||
for (const bool constrained : {false, true}) {
|
||||
const std::size_t count = constrained ? 12U : 24U;
|
||||
auto domain = Brick(count, constrained ? kFaceFixed : "");
|
||||
@@ -282,9 +291,9 @@ TEST(SolidFrequencyProcedure,
|
||||
|
||||
TEST(SolidFrequencyProcedure, RejectsOverflowingOriginalPencilValidationScale) {
|
||||
// Every local K entry, eigenvalue, and spectral enclosure is representable.
|
||||
// The all-odd x Walsh mode still overflows the original residual/energy
|
||||
// denominators: finite / infinity must never become passing zero evidence.
|
||||
auto domain = Brick(24U, "", 1.0, 2.0, 3e306);
|
||||
// With M_L=12*I, the highest mode makes ||K||_F+|lambda|*||M||_F
|
||||
// overflow: finite / infinity must never become passing zero evidence.
|
||||
auto domain = Brick(24U, "", 1.0, 2.0, 4e306);
|
||||
ASSERT_TRUE(domain.HasValue()) << StatusText(domain.GetStatus());
|
||||
const auto result =
|
||||
fesa::SolidFrequencyProcedureExecutor{}.Execute(domain.Value());
|
||||
|
||||
@@ -304,7 +304,7 @@ TEST(SolidHhtPairedKinematics, InvalidLowInputsFailWithoutChangingPredictor) {
|
||||
.HasValue());
|
||||
}
|
||||
|
||||
TEST(SolidHhtPairedKinematics, ConsistentMassKeepsSubUlpActionAndCancellation) {
|
||||
TEST(SolidHhtPairedKinematics, MassMultiplyKeepsSubUlpActionAndCancellation) {
|
||||
fesa::SparsePattern pattern;
|
||||
pattern.row_offsets = {0, 2, 4};
|
||||
pattern.column_indices = {0, 1, 0, 1};
|
||||
@@ -569,7 +569,7 @@ TEST(SolidHhtDynamics, RampStartsAtZeroAndAuxiliaryAccelerationIsDistinct) {
|
||||
}
|
||||
|
||||
TEST(SolidHhtDynamics,
|
||||
OneFreeTranslationHasAnalyticConsistentInertialSupportReactions) {
|
||||
OneFreeTranslationHasAnalyticLumpedInertialSupportReactions) {
|
||||
auto result = RunDynamics(OneFree());
|
||||
ASSERT_TRUE(result.HasValue()) << solid_hht_test::Message(result.GetStatus());
|
||||
const auto d = solid_hht_test::Brick();
|
||||
@@ -581,12 +581,11 @@ TEST(SolidHhtDynamics,
|
||||
const auto& f = frames[n];
|
||||
for (unsigned b = 0; b < 8; ++b) {
|
||||
std::array<double, 3> product{}, sign{};
|
||||
double scalar_mass = 2.0 / 216;
|
||||
double scalar_mass = b == 0 ? .25 : 0.0;
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const bool same = d.nodes[b].coordinates[c] == 0;
|
||||
product[c] = same ? 1.0 / 3 : 1.0 / 6;
|
||||
sign[c] = same ? -1 : 1;
|
||||
scalar_mass *= same ? 2 : 1;
|
||||
}
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
double stiffness;
|
||||
@@ -620,7 +619,7 @@ TEST(SolidHhtDynamics,
|
||||
}
|
||||
if (n != 0) {
|
||||
const auto& old = frames[n - 1];
|
||||
const double mass = 2.0 / 27;
|
||||
const double mass = .25;
|
||||
const double weighted =
|
||||
mass * f.algorithmic_acceleration[0] +
|
||||
.95 * (f.internal_force[0] - f.external_force[0]) +
|
||||
@@ -665,7 +664,7 @@ TEST(SolidHhtDynamics,
|
||||
EXPECT_EQ(state.Value().CommittedState().increment_index, 0U);
|
||||
EXPECT_DOUBLE_EQ(state.Value().CommittedState().displacement[0], 0);
|
||||
EXPECT_NEAR(state.Value().CommittedState().acceleration[0],
|
||||
.01 / (2.0 / 27.0), 1e-14);
|
||||
.01 / .25, 1e-14);
|
||||
ASSERT_FALSE(state.Value().IterationRecords().empty());
|
||||
EXPECT_EQ(state.Value().IterationRecords().back().stage, "failed");
|
||||
EXPECT_FALSE(state.Value().IterationRecords().back().failure_code.empty());
|
||||
|
||||
@@ -79,8 +79,8 @@ fesa::ModelDefinition TwoBricks(double h, unsigned count, bool nonlinear) {
|
||||
fesa::SourceLocation{});
|
||||
return d;
|
||||
}
|
||||
// Closed polynomial brick integrals: integral Na*X and integral grad Na.
|
||||
// These do not call the production stiffness, mass, quadrature or response.
|
||||
// Closed polynomial brick integrals and pre-restriction row-sum masses. These
|
||||
// do not call the production stiffness, mass, quadrature or response.
|
||||
std::vector<double> ExactForce(const fesa::ModelDefinition& d, double t,
|
||||
bool nonlinear) {
|
||||
const auto m = Motion(t, nonlinear);
|
||||
@@ -89,14 +89,14 @@ std::vector<double> ExactForce(const fesa::ModelDefinition& d, double t,
|
||||
const auto& nodes = d.solid_elements[e].node_indices;
|
||||
for (unsigned a = 0; a < 8; ++a) {
|
||||
const auto& x = d.nodes[nodes[a]].coordinates;
|
||||
std::array<double, 3> integral_x{}, gradient{};
|
||||
std::array<double, 3> gradient{};
|
||||
for (unsigned c = 0; c < 3; ++c) {
|
||||
const double origin = c == 0 ? static_cast<double>(e) : 0;
|
||||
const double local = x[c] - origin;
|
||||
integral_x[c] = 2 * (origin / 8 + (1 + local) / 24);
|
||||
gradient[c] = (2 * local - 1) / 4;
|
||||
}
|
||||
const auto inertia = Apply(m.acceleration, integral_x);
|
||||
const auto inertia = Apply(
|
||||
m.acceleration, {.25 * x[0], .25 * x[1], .25 * x[2]});
|
||||
const auto internal = Apply(m.piola, gradient);
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
f[3 * nodes[a] + c] += inertia[c] + internal[c];
|
||||
@@ -110,16 +110,8 @@ std::vector<double> ExactMassProduct(const fesa::Domain& d,
|
||||
for (unsigned e = 0; e < 2; ++e) {
|
||||
const auto& nodes = d.SolidElements()[e].node_indices;
|
||||
for (unsigned a = 0; a < 8; ++a)
|
||||
for (unsigned b = 0; b < 8; ++b) {
|
||||
double coefficient = 2.0 / 216;
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
coefficient *= d.Nodes()[nodes[a]].coordinates[c] ==
|
||||
d.Nodes()[nodes[b]].coordinates[c]
|
||||
? 2
|
||||
: 1;
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
result[3 * nodes[a] + c] += coefficient * v[3 * nodes[b] + c];
|
||||
}
|
||||
for (unsigned c = 0; c < 3; ++c)
|
||||
result[3 * nodes[a] + c] += .25 * v[3 * nodes[a] + c];
|
||||
}
|
||||
return result;
|
||||
}
|
||||
@@ -505,8 +497,8 @@ TEST(SolidHhtTemporal,
|
||||
}
|
||||
|
||||
TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
// Node0's three translations are free. Exact polynomial integrals yield
|
||||
// M=(rho/27)I, Kii=41/18, Kij=19/30 for mu=4.8 and direct kappa=8.
|
||||
// Node0's three translations are free. Pre-restriction row-sum lumping
|
||||
// yields M=(rho/8)I, Kii=41/18, Kij=19/30 for mu=4.8 and kappa=8.
|
||||
std::array<std::array<double, 3>, 3> errors{};
|
||||
for (unsigned level = 0; level < 3; ++level) {
|
||||
const double h = .02 / static_cast<double>(1U << level);
|
||||
@@ -528,7 +520,7 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
ASSERT_TRUE(result.HasValue())
|
||||
<< solid_hht_test::Message(result.GetStatus());
|
||||
const auto& f = result.Value().state.CommittedState();
|
||||
const double m = 2.0 / 27;
|
||||
const double m = .25;
|
||||
const double diagonal = 41.0 / 18;
|
||||
const double off = 19.0 / 30;
|
||||
const std::array<double, 2> k{diagonal + 2 * off, diagonal - off};
|
||||
@@ -562,7 +554,7 @@ TEST(SolidHhtTemporal, IndependentThreeDofBrickOscillatorHasSecondOrderUVA) {
|
||||
|
||||
TEST(SolidHhtTemporal, IndependentScalarBrickOscillatorHasSecondOrderUVA) {
|
||||
std::array<std::array<double, 3>, 3> errors{};
|
||||
const double m = 2.0 / 27;
|
||||
const double m = .25;
|
||||
const double k = 41.0 / 18;
|
||||
const double w = std::sqrt(k / m);
|
||||
for (unsigned level = 0; level < 3; ++level) {
|
||||
|
||||
Reference in New Issue
Block a user