Connect MITC4 drilling evidence across analysis procedures

This commit is contained in:
KOKO\Mimi
2026-10-20 09:21:39 +09:00
parent 294ffbf8a1
commit c6188b3ad7
30 changed files with 763 additions and 28 deletions
@@ -18,6 +18,9 @@
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/assembly/sparse_assembler.h"
#include "fesa/elements/element_factory.h"
#include "fesa/results/result_recovery.h"
#include "elements/mitc4_drilling_internal.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/model/domain.h"
#include "fesa/model/model_types.h"
@@ -132,6 +135,8 @@ TEST(LinearFrequencyProcedureExecutor,
fesa::LinearFrequencyProcedureExecutor{}.Execute(domain.Value());
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
const auto& state = result.Value();
ASSERT_TRUE(state.drilling_mode);
EXPECT_EQ(*state.drilling_mode, fesa::Mitc4DrillingMode::kDirectSpring);
ASSERT_EQ(state.modes.size(), 1U);
EXPECT_EQ(state.mass_null_dimension, 0U);
EXPECT_EQ(state.finite_dimension, 1U);
@@ -189,10 +194,10 @@ TEST(LinearFrequencyProcedureExecutor,
}
}
// E2-C09 is a pencil oracle. The public static contract conflict is recorded
// separately; direct covariance does not certify spatial moment equilibrium.
// E2-C09 keeps raw spatial imbalance visible while the direct-spring gate
// subtracts only the separately recovered artificial internal moment.
TEST(LinearFrequencyProcedureExecutor,
PartialBoundaryStaticRejectsUnbalancedDrillingMoment) {
PartialBoundaryStaticSeparatesArtificialMomentAndPreservesLegacyGate) {
const auto domain = PanelDomain(false, false, true);
ASSERT_TRUE(domain.HasValue());
const fesa::SerialParallelFor serial;
@@ -200,9 +205,32 @@ TEST(LinearFrequencyProcedureExecutor,
const auto result =
fesa::LinearStaticProcedureExecutor{serial, solver}.Execute(
domain.Value());
ASSERT_FALSE(result.HasValue());
EXPECT_NE(StatusText(result.GetStatus())
.find("global-equilibrium-tolerance-failure"),
ASSERT_TRUE(result.HasValue()) << StatusText(result.GetStatus());
const auto& state = result.Value().state;
ASSERT_TRUE(state.Mitc4Drilling());
EXPECT_NE(state.Equilibrium()[5U], 0.0);
for (std::size_t c = 0U; c < 3U; ++c) {
EXPECT_NEAR(state.Equilibrium()[3U + c], state.Mitc4Drilling()->drilling_moment[c], 1e-14);
EXPECT_NEAR(state.Mitc4Drilling()->equilibrium_with_drilling[3U + c], 0.0, 1e-14);
}
auto model = fesa::AnalysisModel::Create(domain.Value());
ASSERT_TRUE(model.HasValue());
auto dofs = fesa::DofManager::Create(model.Value());
ASSERT_TRUE(dofs.HasValue());
auto element = fesa::ElementFactory{}.Create(domain.Value().Elements()[0U], domain.Value());
ASSERT_TRUE(element.HasValue());
auto* shell = dynamic_cast<fesa::Mitc4Shell*>(element.Value().get());
ASSERT_NE(shell, nullptr);
*shell = fesa::mitc4_drilling_internal::TestAccess::WithMode(
*shell, fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
fesa::ElementView view{std::cref(*shell)};
auto stiffness = fesa::SparseAssembler::Assemble(view, dofs.Value(), serial);
ASSERT_TRUE(stiffness.HasValue());
auto legacy_state = fesa::AnalysisState::Create(dofs.Value(), {"Step-1", 0U});
const auto legacy = fesa::ResultRecovery::Recover(model.Value(), view, dofs.Value(),
stiffness.Value(), state.Displacement(), state.ExternalForce(), legacy_state);
ASSERT_FALSE(legacy.IsOk());
EXPECT_NE(StatusText(legacy).find("global-equilibrium-tolerance-failure"),
std::string::npos);
}
@@ -16,6 +16,13 @@
#include <vector>
#include "fesa/elements/finite_rotation.h"
#include "fesa/analysis/prepared_analysis.h"
#include "fesa/analysis/nonlinear_static_procedure.h"
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/element_factory.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
#include "fesa/math/vector3.h"
namespace {
@@ -343,3 +350,57 @@ TEST(NonlinearStaticProcedure,
}
} // namespace
TEST(Mitc4DrillingEvidence, NonlinearAndPressureFramesRetainSignedTorqueOracle) {
for (const bool pressure : {false, true}) {
SCOPED_TRACE(pressure);
TempDirectory directory{"drilling"};
auto deck = AllConstrainedNonlinearShellDeck();
const auto boundary = deck.find("All, 1, 6");
ASSERT_NE(boundary, std::string::npos);
deck.replace(boundary, std::string{"All, 1, 6"}.size(), "All, 1, 5\nAll, 6, 6, 0.125");
if (pressure) {
const auto assembly_end = deck.find("*End Assembly");
deck.insert(assembly_end, "*Surface, TYPE=ELEMENT, NAME=Pressure\nShell-1.1, SPOS\n");
deck.insert(deck.find("*End Step"), "*Dsload\nPressure, P, 4.0\n");
}
const auto path = directory.Path() / "drilling.inp";
WriteText(path, deck);
auto prepared = fesa::PrepareAnalysisInput(path);
ASSERT_TRUE(prepared.HasValue());
const auto& domain = prepared.Value().domain;
auto element = fesa::ElementFactory{}.Create(domain.Elements()[0U], domain);
ASSERT_TRUE(element.HasValue());
const auto* shell = dynamic_cast<const fesa::Mitc4Shell*>(element.Value().get());
ASSERT_NE(shell, nullptr);
const auto stiffness = shell->Stiffness();
ASSERT_TRUE(stiffness.HasValue());
double trace = 0.0;
for (std::size_t node = 0U; node < 4U; ++node)
for (std::size_t c = 3U; c < 5U; ++c)
trace += stiffness.Value().physical_local20(5U * node + c, 5U * node + c);
const double kd = 1e-3 * trace / 8.0;
fesa::SerialParallelFor serial;
fesa::MklPardisoTangentSolver tangent;
fesa::NewtonIterationLogWriter log;
ASSERT_TRUE(log.Open(directory.Path() / "drilling.log").IsOk());
auto result = fesa::NonlinearStaticProcedureExecutor{serial, tangent}.Execute(domain, log);
ASSERT_TRUE(log.Close().IsOk());
ASSERT_TRUE(result.HasValue()) << (result.GetStatus().Diagnostics().empty() ? "" : result.GetStatus().Diagnostics()[0U].message);
ASSERT_EQ(result.Value().state.AcceptedFrames().size(), 10U);
for (const auto& frame : result.Value().state.AcceptedFrames()) {
ASSERT_TRUE(frame.mitc4_drilling);
EXPECT_EQ(frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
const double torque = 4.0 * kd * 0.125 * frame.load_factor;
EXPECT_NEAR(frame.global_equilibrium[5U], torque, 1e-13);
EXPECT_NEAR(frame.mitc4_drilling->drilling_moment[2U], torque, 1e-13);
for (const double value : frame.mitc4_drilling->equilibrium_with_drilling)
EXPECT_NEAR(value, 0.0, 1e-13);
EXPECT_NEAR(frame.physical_strain_energy, 0.0, 1e-20);
if (pressure) EXPECT_NE(frame.external_force[2U], 0.0);
}
ASSERT_TRUE(result.Value().state.CommittedState().mitc4_drilling);
EXPECT_EQ(result.Value().state.CommittedState().mitc4_drilling->drilling_moment,
result.Value().state.AcceptedFrames().back().mitc4_drilling->drilling_moment);
}
}
@@ -320,3 +320,27 @@ TEST(DynamicAnalysisState, CreateRejectsInvalidScheduleAndNonzeroBoundary) {
}
} // namespace
TEST(DynamicAnalysisState, DrillingEvidenceRejectsPartialCommitAndModeChanges) {
auto fixture = MakeFixture();
auto initial = MakeFrame(*fixture.domain, 0U);
initial.mitc4_drilling = fesa::Mitc4DrillingEvidence{fesa::Mitc4DrillingMode::kDirectSpring};
ASSERT_TRUE(fixture.state.CommitAcceptedFrame(initial).IsOk());
for (const int kind : {0, 1, 2, 3, 4}) {
auto next = MakeFrame(*fixture.domain, 1U);
next.mitc4_drilling = initial.mitc4_drilling;
if (kind == 0) next.mitc4_drilling.reset();
if (kind == 1) next.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
if (kind == 2) next.mitc4_drilling->mode = static_cast<fesa::Mitc4DrillingMode>(99);
if (kind == 3) next.mitc4_drilling->equilibrium_with_drilling[3U] = 1.0;
if (kind == 4) next.mitc4_drilling->drilling_moment[0U] = std::numeric_limits<double>::quiet_NaN();
EXPECT_FALSE(fixture.state.CommitAcceptedFrame(next).IsOk()) << kind;
ASSERT_EQ(fixture.state.AcceptedFrames().size(), 1U);
EXPECT_EQ(fixture.state.CommittedState().frame_index, 0U);
EXPECT_EQ(fixture.state.CommittedState().mitc4_drilling->mode,
fesa::Mitc4DrillingMode::kDirectSpring);
}
auto valid = MakeFrame(*fixture.domain, 1U);
valid.mitc4_drilling = initial.mitc4_drilling;
EXPECT_TRUE(fixture.state.CommitAcceptedFrame(valid).IsOk());
}
@@ -216,6 +216,12 @@ void ExpectSameDynamicHistory(const RigidTranslationRun& left,
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);
ASSERT_TRUE(lhs.mitc4_drilling);
ASSERT_TRUE(rhs.mitc4_drilling);
EXPECT_EQ(lhs.mitc4_drilling->mode, rhs.mitc4_drilling->mode);
EXPECT_EQ(lhs.mitc4_drilling->drilling_moment, rhs.mitc4_drilling->drilling_moment);
EXPECT_EQ(lhs.mitc4_drilling->equilibrium_with_drilling,
rhs.mitc4_drilling->equilibrium_with_drilling);
}
}
@@ -357,6 +363,10 @@ TEST(HhtDynamicProcedure, ClampedStepLoadHasPhysicalReactionAndZeroMotion) {
EXPECT_EQ(frame.time_integration_formulation,
fesa::DynamicTimeIntegrationFormulation::kReducedCoordinateHht);
EXPECT_EQ(frame.iteration_count, 0U);
ASSERT_TRUE(frame.mitc4_drilling);
EXPECT_EQ(frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
EXPECT_EQ(frame.mitc4_drilling->drilling_moment, (std::array<double, 3>{}));
EXPECT_EQ(frame.mitc4_drilling->equilibrium_with_drilling, frame.global_equilibrium);
EXPECT_DOUBLE_EQ(frame.reaction[2U], -4.0);
EXPECT_DOUBLE_EQ(frame.external_force[2U], 4.0);
EXPECT_DOUBLE_EQ(frame.kinetic_energy, 0.0);
+212 -10
View File
@@ -22,10 +22,13 @@
#include "analysis/hht_reduced_internal.h"
#include "math/dense_blas_internal.h"
#include "fesa/elements/mitc4_shell.h"
#include "elements/mitc4_drilling_internal.h"
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/hht_time_integration.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
namespace {
@@ -625,6 +628,10 @@ TEST(HhtReducedOperators,
[](Assembly& a) {
a.trial_constraints->shells[0U].configuration.identity += "x";
},
[](Assembly& a) {
a.trial_constraints->shells[0U].configuration.drilling_mode =
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
},
[](Assembly& a) { a.trial_constraints->shells[0U].q[0U] = -0.0; },
[](Assembly& a) { a.trial_constraints->shells[0U].v[0U] = -0.0; },
[](Assembly& a) {
@@ -935,11 +942,15 @@ TEST(HhtReducedOperators, NonfiniteMatchingTimeKeyMissPreservesFreshSuccess) {
}
}
TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
void CheckShellConstraintScatter(const fesa::Mitc4DrillingMode mode) {
const auto fixture = MakeFixture(false);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
fixture.dofs);
ASSERT_TRUE(shells.HasValue());
SCOPED_TRACE(static_cast<int>(mode));
for (const auto& shell : shells.Value())
*shell.internal_kernel = fesa::mitc4_drilling_internal::TestAccess::WithMode(
*shell.internal_kernel, mode);
fesa::DynamicAcceptedFrameState frame{};
for (auto* values : {&frame.generalized_coordinates, &frame.velocity,
&frame.acceleration, &frame.algorithmic_acceleration,
@@ -980,7 +991,10 @@ TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
assembly.Value().frame.residual_scale[6U * node + 3U + component];
EXPECT_DOUBLE_EQ(row.value_scale, expected_scale);
EXPECT_FALSE(row.gradient.empty());
EXPECT_FALSE(row.hessian.empty());
if (mode == fesa::Mitc4DrillingMode::kLegacyCorotatedTwist)
EXPECT_FALSE(row.hessian.empty());
else
EXPECT_TRUE(row.hessian.empty());
EXPECT_LT(row.gradient.size(), fixture.dofs.FullDofCount());
for (const auto& entry : row.hessian) {
EXPECT_LT(entry.row, fixture.dofs.FullDofCount());
@@ -989,6 +1003,14 @@ TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
}
}
TEST(HhtReducedOperators, ScatterShellDerivativesByConstraintRow) {
CheckShellConstraintScatter(fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
}
TEST(HhtReducedOperators, DirectSpringScattersZeroInitialHessianByConstraintRow) {
CheckShellConstraintScatter(fesa::Mitc4DrillingMode::kDirectSpring);
}
TEST(HhtReducedEmbedding, RejectsNonfiniteGraphResidualBeforeAcceptance) {
const auto fixture = MakeFixture(false);
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(
@@ -1184,11 +1206,15 @@ TEST(HhtReducedEmbedding,
EXPECT_DOUBLE_EQ(rates.Value().z_acceleration[0U], 1.0);
}
TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
void CheckActualShellRatesWithMixedTimeLoad(const fesa::Mitc4DrillingMode mode) {
const auto fixture = MakeFixture(false, true);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain,
fixture.dofs);
ASSERT_TRUE(shells.HasValue());
SCOPED_TRACE(static_cast<int>(mode));
for (const auto& shell : shells.Value())
*shell.internal_kernel = fesa::mitc4_drilling_internal::TestAccess::WithMode(
*shell.internal_kernel, mode);
const fesa::Vector zero(fixture.dofs.FullDofCount());
const auto initial = Assemble(fixture, shells.Value(), zero, zero, zero,
zero, true);
@@ -1470,13 +1496,26 @@ TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
++row) {
auto& omitted = omitted_external_gradient.rows[row].gradient;
const auto& no_force = no_force_operators.Value().rows[row].gradient;
ASSERT_EQ(omitted.size(), no_force.size());
for (std::size_t entry = 0U; entry < omitted.size(); ++entry) {
ASSERT_EQ(omitted[entry].dof, no_force[entry].dof);
saw_external_position_gradient =
saw_external_position_gradient ||
omitted[entry].value != no_force[entry].value;
omitted[entry].value = no_force[entry].value;
if (mode == fesa::Mitc4DrillingMode::kLegacyCorotatedTwist) {
ASSERT_EQ(omitted.size(), no_force.size());
for (std::size_t entry = 0U; entry < omitted.size(); ++entry) {
ASSERT_EQ(omitted[entry].dof, no_force[entry].dof);
saw_external_position_gradient =
saw_external_position_gradient ||
omitted[entry].value != no_force[entry].value;
omitted[entry].value = no_force[entry].value;
}
} else {
// Direct internal/external derivatives can cancel exact sparse entries.
// Compare the full union before replacing only the external contribution.
fesa::Vector with_force(fixture.dofs.FullDofCount());
fesa::Vector without_force(fixture.dofs.FullDofCount());
for (const auto& entry : omitted) with_force[entry.dof] = entry.value;
for (const auto& entry : no_force) without_force[entry.dof] = entry.value;
for (std::size_t dof = 0U; dof < with_force.Size(); ++dof)
saw_external_position_gradient = saw_external_position_gradient ||
with_force[dof] != without_force[dof];
omitted = no_force;
}
}
ASSERT_TRUE(saw_external_position_gradient);
@@ -1560,6 +1599,14 @@ TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
EXPECT_GT(omitted_second_time_hidden, 1.0e-8);
}
TEST(HhtReducedEmbedding, ReconstructsActualShellRatesWithMixedTimeLoad) {
CheckActualShellRatesWithMixedTimeLoad(fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
}
TEST(HhtReducedEmbedding, DirectSpringReconstructsActualShellRatesWithMixedTimeLoad) {
CheckActualShellRatesWithMixedTimeLoad(fesa::Mitc4DrillingMode::kDirectSpring);
}
TEST(HhtReducedChannelNewton, RequiresEveryTranslationGateAndThaws) {
fesa::hht_reduced_internal::FixedChart chart{};
chart.full_dof_count = 6U;
@@ -1737,3 +1784,158 @@ TEST(HhtReducedOperators, NullMetricKeepsFiniteLeftAssociatedProducts) {
}
} // namespace
TEST(Mitc4DrillingEvidence, HhtActualMomentMatchesClosedFormAndSourceOrder) {
const auto fixture = MakeFixture(false, true);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
auto q = zero;
for (std::size_t node = 0U; node < fixture.domain->Nodes().size(); ++node) {
q[6U * node + 0U] = 0.03 * static_cast<double>(node);
q[6U * node + 3U] = 0.13 + 0.01 * static_cast<double>(node);
q[6U * node + 4U] = -0.09;
q[6U * node + 5U] = 0.21 - 0.01 * static_cast<double>(node);
}
const auto serial = Assemble(fixture, shells.Value(), q, zero, zero, zero, true);
ASSERT_TRUE(serial.HasValue());
ASSERT_TRUE(serial.Value().frame.mitc4_drilling);
fesa::Vector3 expected;
for (std::size_t element = 0U; element < fixture.domain->ShellElements().Size(); ++element) {
const auto& definition = fixture.domain->ShellElements()[element];
std::array<const fesa::Node*, 4> nodes{};
std::array<std::array<double, 3>, 4> directors{};
for (std::size_t i = 0U; i < 4U; ++i) {
nodes[i] = &fixture.domain->Nodes()[definition.node_indices[i]];
directors[i] = fixture.domain->ShellNodeInitialFrames()[definition.node_indices[i]].director;
}
const auto* section = dynamic_cast<const fesa::ShellSection*>(
&fixture.domain->Properties()[definition.section_index]);
const auto* material = dynamic_cast<const fesa::LinearElasticMaterial*>(
&fixture.domain->Materials()[definition.material_index]);
ASSERT_NE(section, nullptr);
ASSERT_NE(material, nullptr);
const auto kernel = fesa::Mitc4Shell::Create(nodes, directors, *section, *material);
ASSERT_TRUE(kernel.HasValue());
const auto stiffness = kernel.Value().Stiffness();
ASSERT_TRUE(stiffness.HasValue());
double trace = 0.0;
for (std::size_t i = 0U; i < 4U; ++i)
for (std::size_t c = 3U; c < 5U; ++c)
trace += stiffness.Value().physical_local20(5U * i + c, 5U * i + c);
const double kd = 1e-3 * trace / 8.0;
for (std::size_t i = 0U; i < 4U; ++i) {
const auto node = definition.node_indices[i];
const fesa::Vector3 phi{q[6U * node + 3U], q[6U * node + 4U], q[6U * node + 5U]};
const fesa::Vector3 director{directors[i]};
const double angle = phi.Norm();
const double coefficient = (1.0 - 0.5 * angle / std::tan(0.5 * angle)) / (angle * angle);
// Independent closed inverse-transpose Rodrigues identity; no dense solve.
expected = expected + kd * phi.Dot(director) *
(director + 0.5 * phi.Cross(director) + coefficient * phi.Cross(phi.Cross(director)));
}
}
for (std::size_t c = 0U; c < 3U; ++c)
EXPECT_NEAR(serial.Value().frame.mitc4_drilling->drilling_moment[c], expected[c], 2e-14);
EXPECT_EQ(serial.Value().frame.mitc4_drilling->mode, fesa::Mitc4DrillingMode::kDirectSpring);
auto frame = serial.Value().frame;
const auto parameters = fesa::MakeHhtParameters(-0.05);
ASSERT_TRUE(parameters.HasValue());
fesa::TbbParallelFor parallel;
const auto tbb = fesa::hht_internal::EvaluateAssembly(*fixture.domain, fixture.dofs,
shells.Value(), frame, zero, 0.1, parameters.Value(), parallel, true);
ASSERT_TRUE(tbb.HasValue());
ASSERT_TRUE(tbb.Value().frame.mitc4_drilling);
for (std::size_t c = 0U; c < 3U; ++c)
ExpectSameBits(tbb.Value().frame.mitc4_drilling->drilling_moment[c],
serial.Value().frame.mitc4_drilling->drilling_moment[c]);
}
TEST(Mitc4DrillingEvidence, HhtCorrectedGateRejectsOppositeSignAndRealImbalance) {
const auto fixture = MakeFixture(false);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
auto q = zero;
for (std::size_t node = 0U; node < 4U; ++node) q[6U * node + 5U] = 0.125;
const auto assembly = Assemble(fixture, shells.Value(), q, zero, zero, zero, false);
ASSERT_TRUE(assembly.HasValue());
EXPECT_EQ(assembly.Value().hidden_constraint.Size(), 0U);
const auto directional = Assemble(fixture, shells.Value(), q, zero, zero, zero, true);
ASSERT_TRUE(directional.HasValue());
ASSERT_EQ(directional.Value().hidden_constraint.Size(), 4U);
for (std::size_t node = 0U; node < 4U; ++node)
EXPECT_GT(directional.Value().hidden_constraint[node], 0.0);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
auto unavailable_frame = assembly.Value().frame;
const auto unavailable = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly.Value(), 2.0,
fesa::hht_internal::NullNodes(fixture.dofs, assembly.Value().rotations),
positive, tangent, &unavailable_frame);
EXPECT_FALSE(unavailable.IsOk());
EXPECT_EQ(unavailable_frame.hidden_constraint.Size(), 0U);
auto frame = assembly.Value().frame;
// A support supplying the complete internal force gives exact static balance.
frame.reaction = frame.internal_force;
ASSERT_TRUE(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs,
assembly.Value(), &frame).IsOk());
ASSERT_TRUE(frame.mitc4_drilling);
EXPECT_GT(frame.global_equilibrium[5U], 0.0);
EXPECT_NEAR(frame.global_equilibrium[5U], frame.mitc4_drilling->drilling_moment[2U], 1e-14);
for (const double value : frame.mitc4_drilling->equilibrium_with_drilling)
EXPECT_NEAR(value, 0.0, 1e-14);
for (const int kind : {0, 1, 2, 3}) {
auto invalid = frame;
if (kind == 0) invalid.mitc4_drilling->drilling_moment[2U] *= -1.0;
if (kind == 1) invalid.reaction[0U] += 1.0;
if (kind == 2) invalid.reaction[4U] += 1.0;
if (kind == 3) invalid.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
EXPECT_FALSE(fesa::hht_internal::CompleteEquilibrium(*fixture.domain, fixture.dofs,
assembly.Value(), &invalid).IsOk()) << kind;
}
}
TEST(Mitc4DrillingEvidence, DirectHiddenCertificateUsesCanonicalPrimitive) {
const auto fixture = MakeFixture(false);
const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs);
ASSERT_TRUE(shells.HasValue());
const fesa::Vector zero(fixture.dofs.FullDofCount());
auto q = zero;
auto external = zero;
fesa::NonlinearMitc4TrialDofs trial{};
for (std::size_t node = 0U; node < 4U; ++node) {
q[6U * node] = 0.03 * static_cast<double>(node);
trial.displacement[node] = {q[6U * node], 0.0, 0.0};
trial.rotation_vector[node] = {0.00005, 0.000035, 0.00002};
for (std::size_t c = 0U; c < 3U; ++c)
q[6U * node + 3U + c] = trial.rotation_vector[node][c];
}
const auto local = shells.Value()[0U].internal->EvaluateTrial(trial, &zero);
ASSERT_TRUE(local.HasValue());
external = local.Value().drilling_force;
const auto assembly = Assemble(fixture, shells.Value(), q, zero, zero, external, true);
ASSERT_TRUE(assembly.HasValue());
const auto length = fesa::hht_internal::ReferenceLength(*fixture.domain);
ASSERT_TRUE(length.HasValue());
const auto null_nodes = fesa::hht_internal::NullNodes(fixture.dofs, assembly.Value().rotations);
ASSERT_EQ(null_nodes.size(), 4U);
fesa::MklPardisoSolver positive;
fesa::MklPardisoTangentSolver tangent;
auto frame = assembly.Value().frame;
const auto status = fesa::hht_internal::CompleteConstraintEvidence(
*fixture.domain, fixture.dofs, assembly.Value(), length.Value(), null_nodes,
positive, tangent, &frame);
ASSERT_TRUE(status.IsOk()) << (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message);
const auto chart = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, assembly.Value().rotations);
ASSERT_TRUE(chart.HasValue());
const auto canonical = fesa::hht_reduced_internal::EvaluateConstraintOperators(
*fixture.domain, fixture.dofs, shells.Value(), assembly.Value(), chart.Value(), zero, zero);
ASSERT_TRUE(canonical.HasValue());
for (std::size_t row = 0U; row < 4U; ++row) {
EXPECT_DOUBLE_EQ(frame.hidden_constraint[row], canonical.Value().rows[row].value);
EXPECT_NEAR(frame.hidden_constraint[row], 0.0, 1e-17);
EXPECT_DOUBLE_EQ(frame.hidden_velocity_defect[row], 0.0);
EXPECT_DOUBLE_EQ(frame.hidden_acceleration_defect[row], 0.0);
}
}
@@ -28,6 +28,7 @@
#include "fesa/analysis/newton_iteration_log.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/elements/finite_rotation.h"
#include "elements/mitc4_drilling_internal.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
#include "fesa/solvers/linear/mkl_pardiso_tangent_solver.h"
@@ -120,6 +121,14 @@ struct Fixture {
double length;
};
// The finite rigid-orbit and free-spin C08 fixtures prescribe the historical
// objective potential and its hidden velocity equation explicitly.
void UseLegacyDrilling(const Fixture& fixture) {
for (const auto& shell : fixture.shells)
*shell.internal_kernel = fesa::mitc4_drilling_internal::TestAccess::WithMode(
*shell.internal_kernel, fesa::Mitc4DrillingMode::kLegacyCorotatedTwist);
}
Fixture MakeFixture(const std::size_t divisions,
const bool lifecycle_translation = false) {
fesa::ModelDefinition definition{};
@@ -643,6 +652,7 @@ TEST(HhtShellPhysics, ManufacturedHiddenCountsExternalHessianOnce) {
TEST(HhtShellPhysics,
C08aRigidOrbitMatchesIndependentVolumeForcePowerAndMomentum) {
const auto fixture = MakeFixture(1U);
UseLegacyDrilling(fixture);
const auto parameters = Require(fesa::MakeHhtParameters(-0.05));
const auto initial =
Evaluate(fixture, EmptyFrame(fixture), 1.0 / 16.0, parameters, false);
@@ -3078,6 +3088,7 @@ std::array<double, 2> AccelerationSplit(const Frame& basis,
TEST(HhtShellPhysics, ReducedManufacturedControllerKeepsExactInitialOrbit) {
const auto fixture = MakeFixture(1U);
UseLegacyDrilling(fixture);
const auto grid = RunPlate(fixture, 1.0 / 64.0, true);
ASSERT_EQ(grid.history.size(), 65U);
const auto exact = RigidOrbit(fixture, 0.0);
@@ -3115,6 +3126,7 @@ TEST(HhtShellPhysics, ReducedManufacturedControllerKeepsExactInitialOrbit) {
TEST(HhtShellPhysics, C08aRigidManufacturedOrbitReducedCoordinateTimeConvergence) {
const auto fixture = MakeFixture(1U);
UseLegacyDrilling(fixture);
std::vector<GridEvidence> grids;
std::vector<std::array<double, 6>> errors;
for (const unsigned denominator : {16U, 32U, 64U, 128U, 256U, 512U, 1024U}) {
@@ -3297,6 +3309,7 @@ TEST(HhtShellPhysics, C08bInitialPhysicalSpectrumProbe) {
TEST(HhtShellPhysics,
PredictorStartCompletesPreviouslyFailingLumpedFreePlateGrid) {
const auto fixture = MakeFixture(2U);
UseLegacyDrilling(fixture);
const auto evidence = RunPlate(fixture, 1.0 / 256.0, false);
ASSERT_EQ(evidence.history.size(), 65U);
EXPECT_DOUBLE_EQ(evidence.history.back().time, kEndTime);
@@ -3307,6 +3320,7 @@ TEST(HhtShellPhysics,
TEST(HhtShellPhysics,
C08bFreeFiniteRotationPlateSelfConvergenceAndPhysicalInvariants) {
const auto fixture = MakeFixture(2U);
UseLegacyDrilling(fixture);
std::vector<GridEvidence> grids;
for (const unsigned denominator :
{64U, 128U, 256U, 512U, 1024U, 32768U, 65536U, 131072U}) {
@@ -454,3 +454,28 @@ TEST(NonlinearAnalysisState, StoresNoUnapprovedVerificationMetrics) {
EXPECT_FALSE((HasVerificationMetrics<fesa::NonlinearTrialState>::value));
EXPECT_FALSE((HasVerificationMetrics<fesa::AcceptedFrameState>::value));
}
TEST(NonlinearAnalysisState, DrillingEvidenceRetainsModeAndRejectsPartialCommit) {
const auto fixture = MakeFixture();
auto result = fesa::NonlinearAnalysisState::Create(*fixture.domain, *fixture.dofs,
MakeBaseline(*fixture.domain));
ASSERT_TRUE(result.HasValue());
auto& state = result.Value();
auto first = MakeFrame(state.CommittedState(), 0U, 1U, 0.5, 1U);
first.mitc4_drilling = fesa::Mitc4DrillingEvidence{fesa::Mitc4DrillingMode::kDirectSpring};
ASSERT_TRUE(state.CommitAcceptedFrame(first).IsOk());
for (const int kind : {0, 1, 2, 3, 4}) {
auto next = MakeFrame(state.CommittedState(), 1U, 2U, 1.0, 1U);
next.mitc4_drilling = first.mitc4_drilling;
if (kind == 0) next.mitc4_drilling.reset();
if (kind == 1) next.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
if (kind == 2) next.mitc4_drilling->mode = static_cast<fesa::Mitc4DrillingMode>(99);
if (kind == 3) next.mitc4_drilling->equilibrium_with_drilling[3U] = 1.0;
if (kind == 4) next.mitc4_drilling->drilling_moment[0U] = std::numeric_limits<double>::infinity();
EXPECT_FALSE(state.CommitAcceptedFrame(next).IsOk()) << kind;
ASSERT_EQ(state.AcceptedFrames().size(), 1U);
EXPECT_DOUBLE_EQ(state.CommittedState().load_factor, 0.5);
EXPECT_EQ(state.CommittedState().mitc4_drilling->mode,
fesa::Mitc4DrillingMode::kDirectSpring);
}
}
@@ -17,9 +17,12 @@
#include "fesa/assembly/parallel_for.h"
#include "fesa/assembly/sparse_assembler.h"
#include "fesa/elements/element.h"
#include "fesa/elements/element_factory.h"
#include "fesa/elements/mitc4_shell.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/math/vector3.h"
#include "fesa/model/domain.h"
#include "fesa/results/result_recovery.h"
#include "results/analysis_state_commit.h"
#include "results/beam_result_recovery.h"
#include "results/global_equilibrium_recovery.h"
@@ -627,3 +630,99 @@ TEST(ResultRecoveryComponents,
ExpectStatusCode(status, "nonfinite-recovery-value");
ExpectAnalysisStateEqual(state, prior);
}
TEST(Mitc4DrillingEvidence, LinearDirectSpringAcceptsKnownArtificialTorque) {
auto definition = MakeShellDefinition();
for (std::size_t node = 0U; node < 4U; ++node) {
definition.steps[0U].boundaries.push_back(
{std::to_string(node + 1U), 1, 5, 0.0, {definition.source_path, 51U}});
definition.steps[0U].boundaries.push_back(
{std::to_string(node + 1U), 6, 6, 0.125, {definition.source_path, 52U}});
}
const auto fixture = MakeShellFixture(std::move(definition));
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
for (std::size_t node = 0U; node < 4U; ++node)
state.Displacement()[6U * node + 5U] = 0.125;
const auto status = fesa::ResultRecovery::Recover(
*fixture.model, *fixture.dofs, *fixture.stiffness, state);
ASSERT_TRUE(status.IsOk()) << (status.Diagnostics().empty() ? "" : status.Diagnostics()[0U].message);
}
TEST(Mitc4DrillingEvidence, LinearTorqueOracleAndSignedGateKeepRealImbalance) {
auto definition = MakeShellDefinition();
for (std::size_t node = 0U; node < 4U; ++node) {
definition.steps[0U].boundaries.push_back(
{std::to_string(node + 1U), 1, 5, 0.0, {definition.source_path, 51U}});
definition.steps[0U].boundaries.push_back(
{std::to_string(node + 1U), 6, 6, 0.125, {definition.source_path, 52U}});
}
const auto fixture = MakeShellFixture(std::move(definition));
auto element = fesa::ElementFactory{}.Create(fixture.domain->Elements()[0U],
*fixture.domain);
ASSERT_TRUE(element.HasValue());
const auto* shell = dynamic_cast<const fesa::Mitc4Shell*>(element.Value().get());
ASSERT_NE(shell, nullptr);
const auto stiffness = shell->Stiffness();
ASSERT_TRUE(stiffness.HasValue());
double rotary_trace = 0.0;
for (std::size_t node = 0U; node < 4U; ++node)
for (std::size_t c = 3U; c < 5U; ++c)
rotary_trace += stiffness.Value().physical_local20(5U * node + c, 5U * node + c);
const double expected_torque = 4.0 * 0.125 * (1e-3 * rotary_trace / 8.0);
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
for (std::size_t node = 0U; node < 4U; ++node)
state.Displacement()[6U * node + 5U] = 0.125;
ASSERT_TRUE(fesa::ResultRecovery::Recover(*fixture.model, *fixture.dofs,
*fixture.stiffness, state).IsOk());
ASSERT_TRUE(state.Mitc4Drilling());
EXPECT_EQ(state.Mitc4Drilling()->mode, fesa::Mitc4DrillingMode::kDirectSpring);
EXPECT_NEAR(state.Equilibrium()[5U], expected_torque, 1e-14);
EXPECT_NEAR(state.Mitc4Drilling()->drilling_moment[2U], expected_torque, 1e-14);
EXPECT_DOUBLE_EQ(state.Mitc4Drilling()->drilling_moment[0U], 0.0);
EXPECT_DOUBLE_EQ(state.Mitc4Drilling()->drilling_moment[1U], 0.0);
for (double value : state.Mitc4Drilling()->equilibrium_with_drilling)
EXPECT_NEAR(value, 0.0, 1e-14);
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), 0.0);
auto global = fesa::results_internal::GlobalEquilibriumRecovery(
*fixture.domain, *fixture.dofs, *fixture.stiffness, state.Displacement(),
state.ExternalForce());
ASSERT_TRUE(global.HasValue());
for (const int kind : {0, 1, 2, 3}) {
auto changed = global.Value();
fesa::results_internal::RecoveryCandidate candidate{24U};
candidate.shell.mitc4_drilling = *state.Mitc4Drilling();
if (kind == 0) candidate.shell.mitc4_drilling->drilling_moment[2U] *= -1.0;
if (kind == 1) changed.residual[0U] += 1.0;
if (kind == 2) changed.residual[4U] += 1.0;
if (kind == 3) candidate.shell.mitc4_drilling->mode =
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
const auto rejected = fesa::results_internal::RecoverShellGlobalEvidence(
*fixture.domain, *fixture.dofs, state.ExternalForce(), changed, candidate);
EXPECT_FALSE(rejected.IsOk()) << kind;
}
}
TEST(Mitc4DrillingEvidence, StaticCommitRejectsInvalidEvidenceAtomically) {
const auto fixture = MakeShellFixture(MakeShellDefinition());
auto state = fesa::AnalysisState::Create(*fixture.dofs, {"Step-1", 0U});
fesa::ShellStateCandidate candidate{};
candidate.rows = MakeShellRows(0U, 1.0).rows;
candidate.mitc4_drilling = fesa::Mitc4DrillingEvidence{
fesa::Mitc4DrillingMode::kDirectSpring, {0.0, 0.0, 2.0}, {0, 0, 0, 0, 0, -2}};
ASSERT_TRUE(state.CommitShellResults({0U}, candidate).IsOk());
for (const int kind : {0, 1, 2, 3, 4}) {
auto invalid = candidate;
invalid.physical_strain_energy = 77.0;
if (kind == 0) invalid.mitc4_drilling->mode = static_cast<fesa::Mitc4DrillingMode>(99);
if (kind == 1) invalid.mitc4_drilling->mode = fesa::Mitc4DrillingMode::kLegacyCorotatedTwist;
if (kind == 2) invalid.mitc4_drilling->drilling_moment[0U] = std::numeric_limits<double>::infinity();
if (kind == 3) invalid.mitc4_drilling->equilibrium_with_drilling[5U] = 2.0;
if (kind == 4) invalid.mitc4_drilling.reset();
EXPECT_FALSE(state.CommitShellResults({0U}, invalid).IsOk()) << kind;
EXPECT_DOUBLE_EQ(state.PhysicalStrainEnergy(), 0.0);
ASSERT_TRUE(state.Mitc4Drilling());
EXPECT_EQ(state.Mitc4Drilling()->mode, candidate.mitc4_drilling->mode);
EXPECT_EQ(state.Mitc4Drilling()->drilling_moment, candidate.mitc4_drilling->drilling_moment);
}
}