#include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #if defined(_M_X64) || defined(_M_IX86) || defined(__SSE__) #include #endif #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 { class RestoreTrialEnvironment { public: RestoreTrialEnvironment() : rounding_(std::fegetround()), dynamic_(mkl_get_dynamic()), tls_(mkl_set_num_threads_local(0)) { #if defined(_M_X64) mxcsr_ = _mm_getcsr(); _mm_setcsr((_mm_getcsr() | 0x1F80U) & ~0x8040U); #endif static_cast(std::fesetround(FE_TONEAREST)); mkl_set_dynamic(0); } ~RestoreTrialEnvironment() { mkl_set_dynamic(dynamic_); static_cast(mkl_set_num_threads_local(tls_)); static_cast(std::fesetround(rounding_)); #if defined(_M_X64) _mm_setcsr(mxcsr_); #endif } private: int rounding_; int dynamic_; int tls_; #if defined(_M_X64) unsigned int mxcsr_; #endif }; void ExpectSameBits(const double left, const double right) { EXPECT_EQ(std::memcmp(&left, &right, sizeof(double)), 0); } void ExpectSameOperators( const fesa::hht_reduced_internal::ConstraintOperators& left, const fesa::hht_reduced_internal::ConstraintOperators& right) { ASSERT_EQ(left.rows.size(), right.rows.size()); for (std::size_t row = 0U; row < left.rows.size(); ++row) { const auto& a = left.rows[row]; const auto& b = right.rows[row]; ExpectSameBits(a.value, b.value); ExpectSameBits(a.value_scale, b.value_scale); ExpectSameBits(a.time_first, b.time_first); ExpectSameBits(a.time_second, b.time_second); for (const auto& pair : {std::make_pair(&a.gradient, &b.gradient), std::make_pair(&a.time_gradient, &b.time_gradient), std::make_pair(&a.third_directional_gradient, &b.third_directional_gradient)}) { ASSERT_EQ(pair.first->size(), pair.second->size()); for (std::size_t i = 0U; i < pair.first->size(); ++i) { EXPECT_EQ((*pair.first)[i].dof, (*pair.second)[i].dof); ExpectSameBits((*pair.first)[i].value, (*pair.second)[i].value); } } ASSERT_EQ(a.hessian.size(), b.hessian.size()); for (std::size_t i = 0U; i < a.hessian.size(); ++i) { EXPECT_EQ(a.hessian[i].row, b.hessian[i].row); EXPECT_EQ(a.hessian[i].column, b.hessian[i].column); ExpectSameBits(a.hessian[i].value, b.hessian[i].value); } } } struct Fixture { std::unique_ptr domain; fesa::DofManager dofs; }; Fixture MakeFixture(const bool partial_rotation, const bool warped_shared = false) { fesa::ModelDefinition definition{}; definition.source_path = "hht-reduced.inp"; definition.nodes = {{{"Plate", 1, "1"}, {-1, -1, 0}, {definition.source_path, 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, 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}}}; definition.shell_elements = {{{"Plate", 1, "1"}, fesa::ShellSourceElementType::kS4, {0U, 1U, 2U, 3U}, 0U, 0U, {definition.source_path, 8U}}}; if (warped_shared) { definition.nodes[2U].coordinates[2U] = 0.1; definition.nodes.push_back( {{"Plate", 5, "5"}, {3, -1, 0}, {definition.source_path, 11U}}); definition.nodes.push_back( {{"Plate", 6, "6"}, {3, 1, 0}, {definition.source_path, 12U}}); definition.shell_elements.push_back( {{"Plate", 2, "2"}, fesa::ShellSourceElementType::kS4, {1U, 4U, 5U, 2U}, 0U, 0U, {definition.source_path, 13U}}); } fesa::DynamicStepDefinition step{}; step.source_step_name_raw = "Dynamic"; step.source_step_name_canonical = "DYNAMIC"; step.initial_increment = step.time_period = 0.1; step.increment_count = step.maximum_increments = 1U; for (std::size_t i = 0U; i < 4U; ++i) { definition.shell_node_initial_frames.push_back( {static_cast(i), {0, 0, 1}, {1, 0, 0}, {0, 1, 0}}); const std::string name = "Node" + std::to_string(i); definition.node_sets.push_back({name, std::nullopt, {static_cast(i)}, {definition.source_path, 9U}}); if (partial_rotation && i == 0U) step.boundaries.push_back({name, 6, 6, 0.0, {definition.source_path, 10U}}); } if (warped_shared) { for (std::size_t i = 4U; i < 6U; ++i) { definition.shell_node_initial_frames.push_back( {static_cast(i), {0, 0, 1}, {1, 0, 0}, {0, 1, 0}}); const std::string name = "Node" + std::to_string(i); definition.node_sets.push_back({name, std::nullopt, {static_cast(i)}, {definition.source_path, 14U}}); } } definition.dynamic_step = step; auto created = fesa::Domain::Create(std::move(definition)); if (!created.HasValue()) throw std::runtime_error{"Invalid reduced fixture."}; auto domain = std::make_unique(std::move(created.Value())); auto model = fesa::AnalysisModel::Create(*domain); if (!model.HasValue()) throw std::runtime_error{"Invalid reduced model."}; auto dofs = fesa::DofManager::Create(model.Value()); if (!dofs.HasValue()) throw std::runtime_error{"Invalid reduced dofs."}; return {std::move(domain), std::move(dofs.Value())}; } std::vector Rotations(const std::size_t nodes) { std::vector rotations; for (std::size_t node = 0U; node < nodes; ++node) { auto rotation = fesa::EvaluateDynamicRotation({}, {0, 0, 1}, {}, {}); if (!rotation.HasValue()) throw std::runtime_error{"Invalid rotation."}; rotations.push_back(std::move(rotation.Value())); } return rotations; } fesa::Result Assemble( const Fixture& fixture, const std::vector& shells, const fesa::Vector& q, const fesa::Vector& v, const fesa::Vector& a, const fesa::Vector& external, const bool directional) { fesa::DynamicAcceptedFrameState frame{}; frame.generalized_coordinates = q; frame.velocity = v; frame.acceleration = a; frame.algorithmic_acceleration = fesa::Vector(q.Size()); frame.external_force = external; fesa::SerialParallelFor parallel; const auto parameters = fesa::MakeHhtParameters(-0.05); if (!parameters.HasValue()) return fesa::Result::Failure( parameters.GetStatus()); return fesa::hht_internal::EvaluateAssembly( *fixture.domain, fixture.dofs, shells, std::move(frame), fesa::Vector(q.Size()), 0.1, parameters.Value(), parallel, directional); } void SetComponentColumns(fesa::hht_reduced_internal::FixedChart* chart) { chart->p_columns.clear(); chart->z_columns.clear(); for (const auto full : chart->p_dofs) chart->p_columns.push_back({full / 6U, full % 6U < 3U ? 0U : 1U, false, {{full, 1.0}}, {0.0}}); for (const auto full : chart->z_dofs) chart->z_columns.push_back({full / 6U, 1U, false, {{full, 1.0}}, {0.0}}); } std::size_t PIndex(const fesa::hht_reduced_internal::FixedChart& chart, const std::size_t full) { const auto found = std::find(chart.p_dofs.begin(), chart.p_dofs.end(), full); if (found == chart.p_dofs.end()) throw std::runtime_error{"Missing p DOF."}; return static_cast(found - chart.p_dofs.begin()); } std::array Hidden( const fesa::hht_reduced_internal::ConstraintRowOperators& row, const fesa::Vector& velocity, const fesa::Vector& acceleration) { double first = row.time_first; double second = row.time_second; for (const auto& entry : row.gradient) { first += entry.value * velocity[entry.dof]; second += entry.value * acceleration[entry.dof]; } for (const auto& entry : row.time_gradient) second += 2.0 * entry.value * velocity[entry.dof]; for (const auto& entry : row.hessian) second += entry.value * velocity[entry.row] * velocity[entry.column]; return {row.value, first, second}; } TEST(HhtReducedChart, SelectsCanonicalPivotAndLiftsCoordinates) { const auto fixture = MakeFixture(false); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, Rotations(fixture.domain->Nodes().size())); ASSERT_TRUE(chart.HasValue()); ASSERT_EQ(chart.Value().null_nodes.size(), 4U); for (std::size_t node = 0U; node < 4U; ++node) { EXPECT_EQ(chart.Value().pivot_components[node], 2U); EXPECT_EQ(chart.Value().z_dofs[node], 6U * node + 5U); } fesa::Vector p{chart.Value().p_dofs.size()}; fesa::Vector z{chart.Value().z_dofs.size()}; for (std::size_t i = 0U; i < p.Size(); ++i) p[i] = static_cast(i + 1U); for (std::size_t i = 0U; i < z.Size(); ++i) z[i] = -static_cast(i + 1U); const auto full = fesa::hht_reduced_internal::Lift(chart.Value(), p, z); ASSERT_TRUE(full.HasValue()); for (std::size_t i = 0U; i < p.Size(); ++i) EXPECT_DOUBLE_EQ(full.Value()[chart.Value().p_dofs[i]], p[i]); for (std::size_t i = 0U; i < z.Size(); ++i) EXPECT_DOUBLE_EQ(full.Value()[chart.Value().z_dofs[i]], z[i]); } TEST(HhtReducedChart, RetainsNodeWhenItsPartialBoundaryCutsTheNull) { const auto fixture = MakeFixture(true); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, Rotations(fixture.domain->Nodes().size())); ASSERT_TRUE(chart.HasValue()); ASSERT_EQ(chart.Value().null_nodes.size(), 3U); EXPECT_TRUE(std::find(chart.Value().null_nodes.begin(), chart.Value().null_nodes.end(), 0U) == chart.Value().null_nodes.end()); EXPECT_TRUE(std::find(chart.Value().p_dofs.begin(), chart.Value().p_dofs.end(), 3U) != chart.Value().p_dofs.end()); EXPECT_TRUE(std::find(chart.Value().p_dofs.begin(), chart.Value().p_dofs.end(), 4U) != chart.Value().p_dofs.end()); const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); ASSERT_TRUE(shells.HasValue()); const fesa::Vector zero(fixture.dofs.FullDofCount()); const auto assembly = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); ASSERT_TRUE(assembly.HasValue()); const auto operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), assembly.Value(), chart.Value(), zero, zero); ASSERT_TRUE(operators.HasValue()); EXPECT_EQ(operators.Value().rows.size(), 3U); } TEST(HhtReducedChart, UsesActualFiniteInitialNullAndPreservesTransposeActions) { const auto fixture = MakeFixture(false); auto rotations = Rotations(4U); for (auto& rotation : rotations) { const auto finite = fesa::EvaluateDynamicRotation({0.31, -0.22, 0.17}, {0, 0, 1}, {}, {}); ASSERT_TRUE(finite.HasValue()); rotation = finite.Value(); } const auto built = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, rotations); ASSERT_TRUE(built.HasValue()); const auto& chart = built.Value(); const auto& raw = rotations.front().drill_null; const double norm = std::hypot(raw[0U], raw[1U], raw[2U]); std::size_t axis = 0U; for (std::size_t i = 1U; i < 3U; ++i) if (std::abs(raw[i]) < std::abs(raw[axis])) axis = i; EXPECT_EQ(chart.minimum_axes.front(), axis); ASSERT_EQ(chart.z_columns.front().support.size(), 3U); for (std::size_t i = 0U; i < 3U; ++i) { const auto& column = chart.z_columns.front(); EXPECT_NEAR(column.support[i].value, raw[i] / norm, 1e-15); EXPECT_TRUE(std::isfinite(column.support_radii[i])); EXPECT_GE(column.support_radii[i], 0.0); } EXPECT_GT(std::abs(chart.z_columns.front().support[0U].value), 0.01); fesa::Vector p(chart.p_columns.size()), z(chart.z_columns.size()); for (std::size_t i = 0U; i < p.Size(); ++i) p[i] = 0.015 * (i + 1U); for (std::size_t i = 0U; i < z.Size(); ++i) z[i] = -0.02 * (i + 1U); const auto full = fesa::hht_reduced_internal::Lift(chart, p, z); ASSERT_TRUE(full.HasValue()); const auto projected_p = fesa::hht_reduced_internal::ProjectPhysical(chart, full.Value()); const auto projected_z = fesa::hht_reduced_internal::ProjectAlgebraic(chart, full.Value()); ASSERT_TRUE(projected_p.HasValue()); ASSERT_TRUE(projected_z.HasValue()); for (std::size_t i = 0U; i < p.Size(); ++i) EXPECT_NEAR(projected_p.Value()[i], p[i], 1e-15); for (std::size_t i = 0U; i < z.Size(); ++i) EXPECT_NEAR(projected_z.Value()[i], z[i], 1e-15); } TEST(HhtReducedChart, MinimumAxisTieAndPartialBoundaryRetainExactZeros) { const auto fixture = MakeFixture(true); const auto chart = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, Rotations(4U)); ASSERT_TRUE(chart.HasValue()); for (const auto axis : chart.Value().minimum_axes) EXPECT_EQ(axis, 0U); const auto full = fesa::hht_reduced_internal::Lift( chart.Value(), fesa::Vector(chart.Value().p_columns.size(), 0.4), fesa::Vector(chart.Value().z_columns.size(), -0.2)); ASSERT_TRUE(full.HasValue()); EXPECT_DOUBLE_EQ(full.Value()[5U], 0.0); for (const auto& column : chart.Value().p_columns) if (column.node == 0U) EXPECT_FALSE(column.fixed_plane); } TEST(HhtReducedChart, ProperRotationRelatesRetainedPlanesByConstantOrthogonalMap) { const auto fixture = MakeFixture(false); const std::array, 3U> q{{ {{0.36, -0.48, 0.8}}, {{0.8, 0.6, 0.0}}, {{-0.48, 0.64, 0.6}}}}; const auto rotate = [&q](const std::array& x) { std::array y{}; for (std::size_t i = 0U; i < 3U; ++i) for (std::size_t j = 0U; j < 3U; ++j) y[i] += q[i][j] * x[j]; return y; }; const std::array phi{{0.31, -0.22, 0.17}}; const auto phi_q = rotate(phi), director_q = rotate({0.0, 0.0, 1.0}); const auto original = fesa::EvaluateDynamicRotation( {phi[0U], phi[1U], phi[2U]}, {0, 0, 1}, {}, {}); const auto transformed = fesa::EvaluateDynamicRotation( {phi_q[0U], phi_q[1U], phi_q[2U]}, {director_q[0U], director_q[1U], director_q[2U]}, {}, {}); ASSERT_TRUE(original.HasValue()); ASSERT_TRUE(transformed.HasValue()); const auto source = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, std::vector(4U, original.Value())); const auto target = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, std::vector(4U, transformed.Value())); ASSERT_TRUE(source.HasValue()); ASSERT_TRUE(target.HasValue()); std::array, 2U> source_plane{}, target_plane{}; std::size_t si = 0U, ti = 0U; for (const auto& column : source.Value().p_columns) if (column.node == 0U && column.channel == 1U) { for (const auto& entry : column.support) source_plane[si][entry.dof - 3U] = entry.value; ++si; } for (const auto& column : target.Value().p_columns) if (column.node == 0U && column.channel == 1U) { for (const auto& entry : column.support) target_plane[ti][entry.dof - 3U] = entry.value; ++ti; } ASSERT_EQ(si, 2U); ASSERT_EQ(ti, 2U); std::array, 2U> t{}; for (std::size_t j = 0U; j < 2U; ++j) { const auto rotated = rotate(source_plane[j]); for (std::size_t i = 0U; i < 2U; ++i) for (std::size_t k = 0U; k < 3U; ++k) t[i][j] += target_plane[i][k] * rotated[k]; for (std::size_t k = 0U; k < 3U; ++k) EXPECT_NEAR(target_plane[0U][k] * t[0U][j] + target_plane[1U][k] * t[1U][j], rotated[k], 1e-14); } for (std::size_t i = 0U; i < 2U; ++i) for (std::size_t j = 0U; j < 2U; ++j) EXPECT_NEAR(t[0U][i] * t[0U][j] + t[1U][i] * t[1U][j], i == j ? 1.0 : 0.0, 1e-14); } TEST(HhtReducedChart, RejectsSubnormalNormWithoutAValidCoefficientCertificate) { const auto fixture = MakeFixture(false); auto rotations = Rotations(4U); const double tiny = std::numeric_limits::denorm_min(); rotations.front().drill_null = {tiny, tiny, tiny}; EXPECT_FALSE(fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, rotations).HasValue()); } TEST(HhtReducedChart, NativeCoefficientRadiiExportFixedInputReplay) { const auto fixture = MakeFixture(false); const auto finite = fesa::EvaluateDynamicRotation({0.31, -0.22, 0.17}, {0, 0, 1}, {}, {}); ASSERT_TRUE(finite.HasValue()); const double tiny = std::numeric_limits::denorm_min(); const std::array, 7U> inputs{{ {{0.0, 0.0, 1.0}}, {{1.0, 1.0, 1.0}}, finite.Value().drill_null.Components(), {{1e300, -7e299, 3e299}}, {{1e-300, 7e-301, -3e-301}}, {{1.0, tiny, -1e-320}}, {{-1.0, 1e-160, 1e-170}}}}; std::cout << "FBNATIVE environment round=" << std::fegetround(); #if defined(_M_X64) || defined(_M_IX86) || defined(__SSE__) std::cout << " mxcsr=" << _mm_getcsr(); #endif std::cout << '\n'; for (std::size_t sample = 0U; sample < inputs.size(); ++sample) { auto rotations = Rotations(4U); rotations.front().drill_null = fesa::Vector3(inputs[sample]); const auto chart = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, rotations); ASSERT_TRUE(chart.HasValue()) << sample; std::cout << "FBNATIVE case=" << sample << " axis=" << chart.Value().minimum_axes.front() << std::hexfloat << " raw="; for (const auto value : inputs[sample]) std::cout << value << ','; std::array basis{}; std::size_t column = 0U; for (const auto& p : chart.Value().p_columns) if (p.node == 0U && p.channel == 1U) basis[column++] = &p; ASSERT_EQ(column, 2U); basis[2U] = &chart.Value().z_columns.front(); std::cout << " values="; for (const auto* p : basis) { ASSERT_EQ(p->support.size(), 3U); for (const auto& entry : p->support) std::cout << entry.value << ','; } std::cout << " radii="; for (const auto* p : basis) for (const auto radius : p->support_radii) { EXPECT_TRUE(std::isfinite(radius)); EXPECT_GE(radius, 0.0); std::cout << radius << ','; } std::cout << std::defaultfloat << '\n'; } auto invalid = Rotations(4U); invalid.front().drill_null = {}; EXPECT_FALSE(fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, invalid).HasValue()); } TEST(HhtReducedChart, InvertibleGraphDoesNotCertifySelectorTransversality) { const auto fixture = MakeFixture(false); const auto chart = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, Rotations(4U)); ASSERT_TRUE(chart.HasValue()); fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(4U); for (std::size_t i = 0U; i < 4U; ++i) operators.rows[i].gradient.push_back({6U * i + 5U, 1.0}); fesa::MklPardisoTangentSolver solver; EXPECT_TRUE(fesa::hht_reduced_internal::SolveGraphSystem( fesa::hht_reduced_internal::GraphJacobian(operators, chart.Value()), fesa::Vector(4U, 1.0), solver).HasValue()); const auto shells = fesa::hht_internal::BuildRuntimeShells(*fixture.domain, fixture.dofs); ASSERT_TRUE(shells.HasValue()); const fesa::Vector zero(fixture.dofs.FullDofCount()); const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero, zero, false); ASSERT_TRUE(assembled.HasValue()); auto assembly = assembled.Value(); assembly.rotations = Rotations(4U); for (auto& rotation : assembly.rotations) rotation.drill_null = {1.0, 0.0, 0.0}; EXPECT_FALSE(fesa::hht_reduced_internal::CheckSelectorTransversality( assembly, chart.Value(), solver).HasValue()); for (auto& rotation : assembly.rotations) rotation.drill_null = {1.0, 0.0, 1e-12}; EXPECT_FALSE(fesa::hht_reduced_internal::CheckSelectorTransversality( assembly, chart.Value(), solver).HasValue()); } TEST(HhtReducedOperators, CompleteTrialConstraintDataMatchesFreshAndOwnsCopies) { RestoreTrialEnvironment restore; 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; auto v = zero; auto force = zero; auto rate = zero; auto acceleration = zero; for (std::size_t i = 0U; i < zero.Size(); ++i) { q[i] = 0.001 * static_cast((i % 5U) + 1U); v[i] = 0.002 * static_cast((i % 7U) + 1U); if (i % 6U >= 3U) { force[i] = 0.03; rate[i] = -0.02; acceleration[i] = 0.01; } } auto assembled = Assemble(fixture, shells.Value(), q, v, zero, force, true); ASSERT_TRUE(assembled.HasValue()); ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); ASSERT_EQ(assembled.Value().trial_constraints->shells.size(), shells.Value().size()); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, assembled.Value().rotations); ASSERT_TRUE(chart.HasValue()); for (std::size_t e = 0U; e < shells.Value().size(); ++e) { const auto& saved = assembled.Value().trial_constraints->shells[e]; ASSERT_TRUE(saved.directional_internal); fesa::NonlinearMitc4TrialDofs trial{}; fesa::Vector direction{24U}; for (std::size_t node = 0U; node < 4U; ++node) { const auto first = 6U * node; trial.displacement[node] = fesa::Vector3{saved.q[first], saved.q[first + 1U], saved.q[first + 2U]}; trial.rotation_vector[node] = fesa::Vector3{ saved.q[first + 3U], saved.q[first + 4U], saved.q[first + 5U]}; } for (std::size_t i = 0U; i < 24U; ++i) direction[i] = saved.v[i]; const auto response = shells.Value()[e].internal->EvaluateTrial(trial, &direction); ASSERT_TRUE(response.HasValue()); for (std::size_t node = 0U; node < 4U; ++node) { ExpectSameBits(saved.drilling_constraint[node], response.Value().drilling_constraint[node]); for (std::size_t i = 0U; i < 24U; ++i) { ExpectSameBits(saved.drilling_constraint_gradient(node, i), response.Value().drilling_constraint_gradient(node, i)); ExpectSameBits( saved.drilling_constraint_third_directional_gradient[node][i], response.Value() .drilling_constraint_third_directional_gradient[node][i]); for (std::size_t j = 0U; j < 24U; ++j) ExpectSameBits( saved.drilling_constraint_hessian[node](i, j), response.Value().drilling_constraint_hessian[node](i, j)); } } } const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { return fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), rate, acceleration); }; auto fresh = assembled.Value(); fresh.trial_constraints.reset(); const auto reference = evaluate(fresh); const auto reused = evaluate(assembled.Value()); ASSERT_TRUE(reference.HasValue()); ASSERT_TRUE(reused.HasValue()); ExpectSameOperators(reused.Value(), reference.Value()); auto copy = assembled.Value(); copy.trial_constraints->shells[0U].drilling_constraint[0U] += 7.0; // Test-only finite marker proves the selected route actually reads its owner. const auto marker = evaluate(copy); ASSERT_TRUE(marker.HasValue()); EXPECT_NE(marker.Value().rows[0U].value, reused.Value().rows[0U].value); EXPECT_NE( copy.trial_constraints->shells[0U].drilling_constraint[0U], assembled.Value().trial_constraints->shells[0U].drilling_constraint[0U]); auto moved = std::move(assembled.Value()); const auto moved_result = evaluate(moved); ASSERT_TRUE(moved_result.HasValue()); ExpectSameOperators(moved_result.Value(), reference.Value()); fesa::DynamicAcceptedFrameState frame{}; frame.generalized_coordinates = q; frame.velocity = v; frame.acceleration = zero; frame.algorithmic_acceleration = zero; frame.external_force = force; 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()); const auto tbb_result = evaluate(tbb.Value()); ASSERT_TRUE(tbb_result.HasValue()); ExpectSameOperators(tbb_result.Value(), reference.Value()); } TEST(HhtReducedOperators, ChangedTrialConstraintContextFallsBackInOriginalPosition) { RestoreTrialEnvironment restore; 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()); const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); ASSERT_TRUE(assembled.HasValue()); ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); ASSERT_TRUE( assembled.Value().trial_constraints->shells[0U].directional_internal); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, assembled.Value().rotations); ASSERT_TRUE(chart.HasValue()); const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { return fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), zero, zero); }; using Assembly = fesa::hht_internal::DynamicAssembly; std::vector> changes{ [](Assembly& a) { a.frame.generalized_coordinates[2U] = 0.001; }, [](Assembly& a) { a.frame.velocity[3U] = 0.003; }, [](Assembly& a) { a.frame.time = 0.1; }, [](Assembly& a) { ++a.frame.frame_index; }, [](Assembly& a) { ++a.frame.increment_index; }, [](Assembly& a) { ++a.frame.iteration_count; }, [](Assembly& a) { a.frame.is_initial = true; }, [](Assembly& a) { a.trial_constraints->source_path = "different.inp"; }, [](Assembly& a) { ++a.trial_constraints->shells[0U].element_index; }, [](Assembly& a) { ++a.trial_constraints->shells[0U].scatter[0U]; }, [](Assembly& a) { a.trial_constraints->shells[0U].directional_internal = false; }, [](Assembly& a) { a.trial_constraints->shells[0U].node_source_ids[0U].instance_name += "x"; }, [](Assembly& a) { ++a.trial_constraints->shells[0U].node_source_ids[0U].source_label; }, [](Assembly& a) { a.trial_constraints->shells[0U].node_source_ids[0U].source_label_text += "x"; }, [](Assembly& a) { ++a.trial_constraints->shells[0U].configuration.source.line; }, [](Assembly& a) { a.trial_constraints->shells[0U].configuration.source.file = "different.inp"; }, [](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) { a.trial_constraints->shells[0U].drilling_constraint_gradient = fesa::Matrix{0U, 0U}; }, [](Assembly& a) { a.trial_constraints->shells[0U].drilling_constraint_hessian[2U] = fesa::Matrix{0U, 0U}; }, [](Assembly& a) { a.trial_constraints->shells[0U] .drilling_constraint_third_directional_gradient[1U] = fesa::Vector{0U}; }, [](Assembly& a) { a.trial_constraints->shells[0U].drilling_constraint_hessian[0U]( 0U, 0U) = std::numeric_limits::infinity(); }, [](Assembly& a) { a.trial_constraints->shells[0U] .evaluation_context.dense_policy.blas_max_threads = 2; }, [](Assembly& a) { a.trial_constraints->shells[0U] .evaluation_context.dense_policy.dynamic = 1; }, [](Assembly& a) { a.trial_constraints->shells[0U] .evaluation_context.dense_policy.dynamic = -1; }, [](Assembly& a) { a.trial_constraints->shells[0U].evaluation_context.mxcsr_control ^= 0x8000U; }, [](Assembly& a) { a.trial_constraints->shells[0U].evaluation_context.supported = false; }}; for (std::size_t i = 0U; i < 64U; ++i) changes.push_back([i](Assembly& a) { a.trial_constraints->shells[0U].configuration.values[i] += 0.125; }); for (const double invalid : {std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::infinity()}) changes.push_back([invalid](Assembly& a) { a.trial_constraints->shells[0U].configuration.values[60U] = invalid; }); for (std::size_t i = 0U; i < changes.size(); ++i) { SCOPED_TRACE(i); auto candidate = assembled.Value(); candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0; changes[i](candidate); auto fresh = candidate; fresh.trial_constraints.reset(); const auto reference = evaluate(fresh); const auto actual = evaluate(candidate); ASSERT_TRUE(reference.HasValue()); ASSERT_TRUE(actual.HasValue()); ExpectSameOperators(actual.Value(), reference.Value()); } for (std::size_t kind = 0U; kind < 2U; ++kind) { auto candidate = assembled.Value(); if (kind == 0U) candidate.trial_constraints->shells.clear(); if (kind == 1U) candidate.trial_constraints.reset(); auto fresh = assembled.Value(); fresh.trial_constraints.reset(); const auto reference = evaluate(fresh); const auto actual = evaluate(candidate); ASSERT_TRUE(reference.HasValue()); ASSERT_TRUE(actual.HasValue()); ExpectSameOperators(actual.Value(), reference.Value()); } // Change the actual consumer environment, preserving the original fallback. for (std::size_t mode = 0U; mode < 5U; ++mode) { SCOPED_TRACE(mode); auto candidate = assembled.Value(); candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0; auto fresh = candidate; fresh.trial_constraints.reset(); #if defined(_M_X64) const auto csr = _mm_getcsr(); if (mode == 0U) _mm_setcsr(csr ^ 0x8000U); if (mode == 1U) _mm_setcsr(csr ^ 0x0040U); #endif const int rounding = std::fegetround(); if (mode == 2U) static_cast(std::fesetround(FE_DOWNWARD)); if (mode == 3U) static_cast(mkl_set_num_threads_local(2)); if (mode == 4U) mkl_set_dynamic(1); const auto reference = evaluate(fresh); const auto actual = evaluate(candidate); if (mode == 4U) mkl_set_dynamic(0); if (mode == 3U) EXPECT_EQ(mkl_set_num_threads_local(0), 2); static_cast(std::fesetround(rounding)); #if defined(_M_X64) _mm_setcsr(csr); #endif ASSERT_TRUE(reference.HasValue()); ASSERT_TRUE(actual.HasValue()); ExpectSameOperators(actual.Value(), reference.Value()); } auto failed_trial = assembled.Value(); failed_trial.frame.generalized_coordinates[0U] = std::numeric_limits::infinity(); auto fresh_failure = failed_trial; fresh_failure.trial_constraints.reset(); const auto failed = evaluate(failed_trial); const auto original = evaluate(fresh_failure); ASSERT_FALSE(failed.HasValue()); ASSERT_FALSE(original.HasValue()); ASSERT_EQ(failed.GetStatus().Diagnostics().size(), original.GetStatus().Diagnostics().size()); EXPECT_EQ(failed.GetStatus().Diagnostics()[0U].code, original.GetStatus().Diagnostics()[0U].code); EXPECT_EQ(failed.GetStatus().Diagnostics()[0U].message, original.GetStatus().Diagnostics()[0U].message); const auto rerun = evaluate(assembled.Value()); auto fresh = assembled.Value(); fresh.trial_constraints.reset(); const auto reference = evaluate(fresh); ASSERT_TRUE(rerun.HasValue()); ASSERT_TRUE(reference.HasValue()); ExpectSameOperators(rerun.Value(), reference.Value()); } TEST(HhtReducedOperators, TrialContextControlsAndInitialFalseFailureLifetime) { RestoreTrialEnvironment restore; const auto context = fesa::hht_internal::ReadTrialEvaluationContext(); ASSERT_TRUE(fesa::hht_internal::ReusableTrialContext(context, context)); for (const unsigned int bit : {0x0040U, 0x8000U, 0x2000U, 0x0080U}) { auto changed = context; changed.mxcsr_control ^= bit; EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(context, changed)); } auto unmasked = context; unmasked.mxcsr_control &= ~0x0080U; EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(unmasked, unmasked)); static_cast(mkl_set_num_threads_local(2)); const auto positive = fesa::hht_internal::ReadTrialEvaluationContext(); EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(positive, positive)); EXPECT_EQ(mkl_set_num_threads_local(0), 2); mkl_set_dynamic(1); const auto dynamic = fesa::hht_internal::ReadTrialEvaluationContext(); EXPECT_FALSE(fesa::hht_internal::ReusableTrialContext(dynamic, dynamic)); mkl_set_dynamic(0); 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()); const auto position = Assemble(fixture, shells.Value(), zero, zero, zero, zero, false); ASSERT_TRUE(position.HasValue()); EXPECT_FALSE(position.Value().trial_constraints.has_value()); mkl_set_dynamic(1); const auto dynamic_trial = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); mkl_set_dynamic(0); ASSERT_TRUE(dynamic_trial.HasValue()); ASSERT_TRUE(dynamic_trial.Value().trial_constraints.has_value()); EXPECT_FALSE( dynamic_trial.Value().trial_constraints->shells[0U].directional_internal); static_cast(mkl_set_num_threads_local(2)); const auto positive_trial = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); EXPECT_EQ(mkl_set_num_threads_local(0), 2); ASSERT_TRUE(positive_trial.HasValue()); ASSERT_TRUE(positive_trial.Value().trial_constraints.has_value()); EXPECT_FALSE(positive_trial.Value() .trial_constraints->shells[0U] .directional_internal); fesa::DynamicAcceptedFrameState frame{}; frame.is_initial = true; for (auto* vector : {&frame.generalized_coordinates, &frame.velocity, &frame.acceleration, &frame.algorithmic_acceleration, &frame.external_force}) *vector = zero; fesa::SerialParallelFor parallel; const auto parameters = fesa::MakeHhtParameters(-0.05); ASSERT_TRUE(parameters.HasValue()); const auto initial = fesa::hht_internal::EvaluateAssembly( *fixture.domain, fixture.dofs, shells.Value(), frame, zero, 0.1, parameters.Value(), parallel, true); ASSERT_TRUE(initial.HasValue()); EXPECT_FALSE(initial.Value().trial_constraints.has_value()); auto invalid = zero; invalid[0U] = std::numeric_limits::quiet_NaN(); const auto failure = Assemble(fixture, shells.Value(), invalid, zero, zero, zero, true); EXPECT_FALSE(failure.HasValue()); const auto current = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); const auto next = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); ASSERT_TRUE(current.HasValue()); ASSERT_TRUE(next.HasValue()); ASSERT_EQ(current.Value().trial_constraints->shells.size(), shells.Value().size()); ASSERT_EQ(next.Value().trial_constraints->shells.size(), shells.Value().size()); EXPECT_NE(¤t.Value().trial_constraints->shells[0U], &next.Value().trial_constraints->shells[0U]); } TEST(HhtReducedOperators, NonfiniteMatchingTrialKeysPreserveOriginalFailure) { RestoreTrialEnvironment restore; 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()); const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); ASSERT_TRUE(assembled.HasValue()); ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); ASSERT_TRUE( assembled.Value().trial_constraints->shells[0U].directional_internal); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, assembled.Value().rotations); ASSERT_TRUE(chart.HasValue()); const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { return fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), zero, zero); }; const std::array invalid{std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::infinity()}; for (std::size_t channel = 0U; channel < 2U; ++channel) for (std::size_t i = 0U; i < invalid.size(); ++i) { SCOPED_TRACE(channel); SCOPED_TRACE(i); auto candidate = assembled.Value(); auto& saved = candidate.trial_constraints->shells[0U]; if (channel == 0U) { candidate.frame.generalized_coordinates[0U] = invalid[i]; saved.q[0U] = invalid[i]; } else { candidate.frame.velocity[0U] = invalid[i]; saved.v[0U] = invalid[i]; } auto fresh = candidate; fresh.trial_constraints.reset(); const auto original = evaluate(fresh); const auto actual = evaluate(candidate); ASSERT_FALSE(original.HasValue()); ASSERT_FALSE(actual.HasValue()); EXPECT_EQ(actual.GetStatus().Category(), original.GetStatus().Category()); const auto& a = actual.GetStatus().Diagnostics(); const auto& b = original.GetStatus().Diagnostics(); ASSERT_EQ(a.size(), b.size()); for (std::size_t j = 0U; j < a.size(); ++j) { EXPECT_EQ(a[j].severity, b[j].severity); EXPECT_EQ(a[j].code, b[j].code); EXPECT_EQ(a[j].location.file, b[j].location.file); EXPECT_EQ(a[j].location.line, b[j].location.line); EXPECT_EQ(a[j].keyword, b[j].keyword); EXPECT_EQ(a[j].entity_identity, b[j].entity_identity); EXPECT_EQ(a[j].message, b[j].message); } } } TEST(HhtReducedOperators, NonfiniteMatchingTimeKeyMissPreservesFreshSuccess) { RestoreTrialEnvironment restore; 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()); const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero, zero, true); ASSERT_TRUE(assembled.HasValue()); ASSERT_TRUE(assembled.Value().trial_constraints.has_value()); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, assembled.Value().rotations); ASSERT_TRUE(chart.HasValue()); const auto evaluate = [&](const fesa::hht_internal::DynamicAssembly& value) { return fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), value, chart.Value(), zero, zero); }; const auto parameters = fesa::MakeHhtParameters(-0.05); ASSERT_TRUE(parameters.HasValue()); fesa::SerialParallelFor parallel; for (const double bad_time : {std::numeric_limits::quiet_NaN(), std::numeric_limits::infinity(), -std::numeric_limits::infinity()}) { auto candidate = assembled.Value(); candidate.frame.time = bad_time; candidate.trial_constraints->time = bad_time; candidate.trial_constraints->shells[0U].drilling_constraint[0U] = 17.0; auto fresh = candidate; fresh.trial_constraints.reset(); const auto reference = evaluate(fresh); const auto actual = evaluate(candidate); ASSERT_TRUE(reference.HasValue()); ASSERT_TRUE(actual.HasValue()); ExpectSameOperators(actual.Value(), reference.Value()); const auto producer = fesa::hht_internal::EvaluateAssembly( *fixture.domain, fixture.dofs, shells.Value(), candidate.frame, zero, 0.1, parameters.Value(), parallel, true); ASSERT_TRUE(producer.HasValue()); EXPECT_FALSE(producer.Value().trial_constraints.has_value()); const auto producer_result = evaluate(producer.Value()); ASSERT_TRUE(producer_result.HasValue()); ExpectSameOperators(producer_result.Value(), reference.Value()); } } 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(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, &frame.external_force}) *values = fesa::Vector(fixture.dofs.FullDofCount()); const auto parameters = fesa::MakeHhtParameters(-0.05); ASSERT_TRUE(parameters.HasValue()); fesa::SerialParallelFor parallel; const auto assembly = fesa::hht_internal::EvaluateAssembly( *fixture.domain, fixture.dofs, shells.Value(), frame, fesa::Vector(fixture.dofs.FullDofCount()), 0.1, parameters.Value(), parallel, true); ASSERT_TRUE(assembly.HasValue()); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, assembly.Value().rotations); ASSERT_TRUE(chart.HasValue()); const auto operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), assembly.Value(), chart.Value(), fesa::Vector(fixture.dofs.FullDofCount()), fesa::Vector(fixture.dofs.FullDofCount())); ASSERT_TRUE(operators.HasValue()); ASSERT_EQ(operators.Value().rows.size(), 4U); fesa::MklPardisoTangentSolver solver; const auto graph_condition = fesa::hht_reduced_internal::CheckGraphRegularity( assembly.Value(), chart.Value(), operators.Value(), solver); ASSERT_TRUE(graph_condition.HasValue()); EXPECT_GE(graph_condition.Value(), std::ldexp(1.0, -32)); for (std::size_t row_index = 0U; row_index < operators.Value().rows.size(); ++row_index) { const auto& row = operators.Value().rows[row_index]; EXPECT_DOUBLE_EQ(row.value, 0.0); double expected_scale = 0.0; const auto node = chart.Value().null_nodes[row_index]; for (std::size_t component = 0U; component < 3U; ++component) expected_scale += std::abs(assembly.Value().rotations[node].drill_null[component]) * assembly.Value().frame.residual_scale[6U * node + 3U + component]; EXPECT_DOUBLE_EQ(row.value_scale, expected_scale); EXPECT_FALSE(row.gradient.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()); EXPECT_LT(entry.column, fixture.dofs.FullDofCount()); } } } 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( fixture.dofs, Rotations(fixture.domain->Nodes().size())); ASSERT_TRUE(chart.HasValue()); fesa::MklPardisoTangentSolver solver; const auto root = fesa::hht_reduced_internal::SolveEmbeddingRoot( chart.Value(), fesa::Vector(chart.Value().p_dofs.size()), fesa::Vector(chart.Value().z_dofs.size()), [&chart](const fesa::Vector&) { fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(chart.Value().z_dofs.size()); operators.rows.front().value = std::numeric_limits::quiet_NaN(); return fesa::Result:: Success(std::move(operators)); }, solver); ASSERT_FALSE(root.HasValue()); ASSERT_FALSE(root.GetStatus().Diagnostics().empty()); EXPECT_EQ(root.GetStatus().Diagnostics().front().code, "dynamic-invalid-state"); } TEST(HhtReducedEmbedding, ReturnsExactTwoCycleForFullHiddenCertification) { // This deliberately nonphysical graph models a binary64 Newton two-cycle. // The root helper must return an already evaluated state; it must not claim // graph convergence. The procedure's original hidden-row gates remain the // only authority that can accept the returned trial. fesa::hht_reduced_internal::FixedChart chart{}; chart.full_dof_count = 1U; chart.z_dofs = {0U}; SetComponentColumns(&chart); fesa::MklPardisoTangentSolver solver; const auto root = fesa::hht_reduced_internal::SolveEmbeddingRoot( chart, fesa::Vector(0U), fesa::Vector{1U, 1.0}, [](const fesa::Vector& q) { fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(1U); operators.rows[0U].value = 2.0 * q[0U]; operators.rows[0U].gradient.push_back({0U, 1.0}); return fesa::Result:: Success(std::move(operators)); }, solver); ASSERT_TRUE(root.HasValue()); EXPECT_DOUBLE_EQ(root.Value().z[0U], -1.0); EXPECT_DOUBLE_EQ(root.Value().q[0U], -1.0); EXPECT_DOUBLE_EQ(root.Value().operators.rows[0U].value, -2.0); } TEST(HhtReducedEmbedding, ReturnsEvaluatedTrialWhenLiftAbsorbsAChangedPrivateRoot) { const auto fixture = MakeFixture(false); auto rotations = Rotations(4U); const double c = std::sqrt(0.5); rotations.front().drill_null = {c, -c, 0.0}; const auto built = fesa::hht_reduced_internal::BuildFixedChart(fixture.dofs, rotations); ASSERT_TRUE(built.HasValue()); const auto& chart = built.Value(); fesa::Vector p(chart.p_columns.size()), seed(chart.z_columns.size()); std::size_t angular = 0U; for (std::size_t i = 0U; i < chart.p_columns.size(); ++i) if (chart.p_columns[i].node == 0U && chart.p_columns[i].channel == 1U) if (angular++ == 1U) p[i] = 0.4; ASSERT_EQ(angular, 2U); const auto original = fesa::hht_reduced_internal::Lift(chart, p, seed); ASSERT_TRUE(original.HasValue()); auto changed = seed; changed[0U] = 1e-20 / 0.003; EXPECT_NE(changed[0U], seed[0U]); const auto absorbed = fesa::hht_reduced_internal::Lift(chart, p, changed); ASSERT_TRUE(absorbed.HasValue()); for (std::size_t i = 0U; i < original.Value().Size(); ++i) EXPECT_EQ(original.Value()[i], absorbed.Value()[i]); fesa::MklPardisoTangentSolver solver; const auto root = fesa::hht_reduced_internal::SolveEmbeddingRoot( chart, p, seed, [&chart](const fesa::Vector& q) { fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(chart.z_columns.size()); for (std::size_t row = 0U; row < operators.rows.size(); ++row) { const double factor = row == 0U ? 0.003 : 1.0; auto& out = operators.rows[row]; for (const auto& entry : chart.z_columns[row].support) { out.gradient.push_back({entry.dof, factor * entry.value}); out.value += factor * entry.value * q[entry.dof]; } if (row == 0U) out.value -= 1e-20; out.value_scale = std::abs(out.value); } return fesa::Result::Success( std::move(operators)); }, solver); ASSERT_TRUE(root.HasValue()); // A returned arithmetic-stagnation trial is not a converged graph. The // procedure must still apply the original full endpoint/hidden gates. EXPECT_EQ(root.Value().z[0U], seed[0U]); EXPECT_NE(root.Value().operators.rows[0U].value, 0.0); EXPECT_GT(root.Value().graph_residual_relative, 1e-10); EXPECT_EQ(root.Value().graph_iteration_count, 0U); for (std::size_t i = 0U; i < original.Value().Size(); ++i) EXPECT_EQ(root.Value().q[i], original.Value()[i]); } TEST(HhtReducedEmbedding, StopsAtExistingScaledPositionCertificate) { fesa::hht_reduced_internal::FixedChart chart{}; chart.full_dof_count = 1U; chart.z_dofs = {0U}; SetComponentColumns(&chart); fesa::MklPardisoTangentSolver solver; const auto root = fesa::hht_reduced_internal::SolveEmbeddingRoot( chart, fesa::Vector(0U), fesa::Vector{1U, 0.0}, [](const fesa::Vector& q) { fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(1U); operators.rows[0U].value = 5.0e-11; operators.rows[0U].value_scale = 1.0; operators.rows[0U].gradient.push_back({0U, 1.0}); return fesa::Result:: Success(std::move(operators)); }, solver); ASSERT_TRUE(root.HasValue()); EXPECT_DOUBLE_EQ(root.Value().z[0U], 0.0); EXPECT_DOUBLE_EQ(root.Value().graph_residual_infinity, 5.0e-11); EXPECT_DOUBLE_EQ(root.Value().graph_residual_relative, 5.0e-11); EXPECT_DOUBLE_EQ(root.Value().graph_correction_infinity, 0.0); } TEST(HhtReducedEmbedding, RequiresExactZeroWhenPositionScaleIsZero) { fesa::hht_reduced_internal::FixedChart chart{}; chart.full_dof_count = 1U; chart.z_dofs = {0U}; SetComponentColumns(&chart); fesa::MklPardisoTangentSolver solver; const auto root = fesa::hht_reduced_internal::SolveEmbeddingRoot( chart, fesa::Vector(0U), fesa::Vector{1U, 1.0}, [](const fesa::Vector& q) { fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(1U); operators.rows[0U].value = 5.0e-11 * q[0U]; operators.rows[0U].gradient.push_back({0U, 5.0e-11}); return fesa::Result:: Success(std::move(operators)); }, solver); ASSERT_TRUE(root.HasValue()); EXPECT_DOUBLE_EQ(root.Value().z[0U], 0.0); EXPECT_DOUBLE_EQ(root.Value().operators.rows[0U].value, 0.0); } TEST(HhtReducedEmbedding, ReconstructsRatesAcrossSelectedPartitionCancellation) { // The exact RHS has the binary64 terms (+1e16, +1, -1e16). A plain // left-to-right sum loses +1; the private rate partition must retain it. fesa::hht_reduced_internal::FixedChart chart{}; chart.full_dof_count = 3U; chart.p_dofs = {0U, 1U}; chart.z_dofs = {2U}; SetComponentColumns(&chart); fesa::hht_reduced_internal::EmbeddingRoot root{}; root.p = fesa::Vector{2U}; root.z = fesa::Vector{1U}; root.q = fesa::Vector{3U}; root.operators.rows.resize(1U); auto& row = root.operators.rows.front(); row.time_first = -1.0e16; row.time_second = 1.0e16; row.gradient = {{2U, 1.0}, {0U, -1.0}, {1U, 1.0e16}}; row.time_gradient = {{0U, -0.5}}; row.hessian = {{0U, 0U, -1.0e16}}; fesa::Vector vp{2U}; vp[0U] = vp[1U] = 1.0; const fesa::Vector ap{2U}; double plain_velocity_rhs = -row.time_first; for (const auto& entry : row.gradient) if (entry.dof < 2U) plain_velocity_rhs -= entry.value * vp[entry.dof]; double plain_acceleration_rhs = -row.hessian.front().value * vp[0U] * vp[0U]; plain_acceleration_rhs -= 2.0 * row.time_gradient.front().value * vp[0U]; plain_acceleration_rhs -= row.time_second; EXPECT_DOUBLE_EQ(plain_velocity_rhs, 0.0); EXPECT_DOUBLE_EQ(plain_acceleration_rhs, 0.0); fesa::MklPardisoTangentSolver solver; const auto rates = fesa::hht_reduced_internal::ReconstructPhysicalRates( chart, root, vp, ap, solver); ASSERT_TRUE(rates.HasValue()); EXPECT_DOUBLE_EQ(rates.Value().velocity[0U], 1.0); EXPECT_DOUBLE_EQ(rates.Value().velocity[1U], 1.0); EXPECT_DOUBLE_EQ(rates.Value().velocity[2U], 1.0); EXPECT_DOUBLE_EQ(rates.Value().acceleration[0U], 0.0); EXPECT_DOUBLE_EQ(rates.Value().acceleration[1U], 0.0); EXPECT_DOUBLE_EQ(rates.Value().acceleration[2U], 1.0); EXPECT_DOUBLE_EQ(rates.Value().z_velocity[0U], 1.0); EXPECT_DOUBLE_EQ(rates.Value().z_acceleration[0U], 1.0); } 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(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); ASSERT_TRUE(initial.HasValue()); auto initial_rotations = initial.Value().rotations; for (auto& rotation : initial_rotations) { auto finite = fesa::EvaluateDynamicRotation({0.12, -0.08, 0.05}, {0, 0, 1}, {}, {}); ASSERT_TRUE(finite.HasValue()); rotation = finite.Value(); } const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, initial_rotations); ASSERT_TRUE(chart.HasValue()); fesa::Vector force_rate(fixture.dofs.FullDofCount()); fesa::Vector force_acceleration(fixture.dofs.FullDofCount()); fesa::Vector force(fixture.dofs.FullDofCount()); // A finite dead drilling moment keeps the external n(q)^T F and its // position derivatives active without driving this local graph beyond its // principal-coordinate branch during the off-graph tangent probe. force[5U] = 1.0e-2; force_rate[5U] = 0.2; force_acceleration[5U] = -0.1; const auto position_operator = [&](const fesa::Vector& q) { const auto position = Assemble(fixture, shells.Value(), q, zero, zero, force, true); if (!position.HasValue()) return fesa::Result:: Failure(position.GetStatus()); return fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), position.Value(), chart.Value(), force_rate, force_acceleration); }; fesa::Vector p(chart.Value().p_dofs.size()); p[PIndex(chart.Value(), 2U)] = 0.01; p[PIndex(chart.Value(), 3U)] = 0.02; fesa::MklPardisoTangentSolver solver; const auto root = fesa::hht_reduced_internal::SolveEmbeddingRoot( chart.Value(), p, fesa::Vector(chart.Value().z_dofs.size()), position_operator, solver); ASSERT_TRUE(root.HasValue()) << (root.HasValue() || root.GetStatus().Diagnostics().empty() ? "" : root.GetStatus().Diagnostics().front().message); EXPECT_DOUBLE_EQ(root.Value().q[2U], 0.01); fesa::Vector vp(chart.Value().p_dofs.size()); fesa::Vector ap(chart.Value().p_dofs.size()); vp[PIndex(chart.Value(), 3U)] = 0.03; ap[PIndex(chart.Value(), 4U)] = -0.02; const auto rates = fesa::hht_reduced_internal::ReconstructPhysicalRates( chart.Value(), root.Value(), vp, ap, solver); ASSERT_TRUE(rates.HasValue()); const auto actual = Assemble(fixture, shells.Value(), root.Value().q, rates.Value().velocity, rates.Value().acceleration, force, true); ASSERT_TRUE(actual.HasValue()); const auto operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), actual.Value(), chart.Value(), force_rate, force_acceleration); ASSERT_TRUE(operators.HasValue()); bool saw_time_term = false; for (const auto& row : operators.Value().rows) { const auto hidden = Hidden(row, rates.Value().velocity, rates.Value().acceleration); saw_time_term = saw_time_term || row.time_first != 0.0 || row.time_second != 0.0; EXPECT_NEAR(hidden[0U], 0.0, 1.0e-9); EXPECT_NEAR(hidden[1U], 0.0, 1.0e-9); EXPECT_NEAR(hidden[2U], 0.0, 1.0e-8); } EXPECT_TRUE(saw_time_term); const auto parameters = fesa::MakeHhtParameters(-0.05); ASSERT_TRUE(parameters.HasValue()); const double cv = parameters.Value().gamma / (parameters.Value().beta * 0.1); const double ca = 1.0 / ((1.0 + parameters.Value().alpha) * parameters.Value().beta * 0.01); const auto reference_length = fesa::hht_internal::ReferenceLength(*fixture.domain); ASSERT_TRUE(reference_length.HasValue()); const double h = fixture.domain->Dynamic()->initial_increment; const auto system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( actual.Value(), chart.Value(), operators.Value(), cv, ca, reference_length.Value(), h); ASSERT_TRUE(system.HasValue()); ASSERT_EQ(system.Value().physical_count, chart.Value().p_dofs.size()); ASSERT_EQ(system.Value().algebraic_count, chart.Value().z_dofs.size()); fesa::Vector correction(system.Value().rhs.Size()); correction[0U] = 0.004; correction[system.Value().physical_count] = -0.003; correction[system.Value().physical_count + system.Value().algebraic_count] = 0.002; correction[system.Value().physical_count + 2U * system.Value().algebraic_count] = -0.001; for (std::size_t i = 0U; i < correction.Size(); ++i) correction[i] /= system.Value().unknown_scale[i]; const auto mapped = fesa::hht_reduced_internal::MapAugmentedCorrection( chart.Value(), system.Value(), correction, cv, ca); ASSERT_TRUE(mapped.HasValue()); EXPECT_DOUBLE_EQ(mapped.Value().coordinate[0U], 0.004); EXPECT_DOUBLE_EQ(mapped.Value().velocity[0U], cv * 0.004); EXPECT_DOUBLE_EQ(mapped.Value().acceleration[0U], ca * 0.004); const auto& z_column = chart.Value().z_columns.front(); for (const auto& entry : z_column.support) { EXPECT_NEAR(mapped.Value().coordinate[entry.dof], -0.003 * entry.value, 1e-17); EXPECT_NEAR(mapped.Value().velocity[entry.dof], 0.002 * entry.value, 1e-17); EXPECT_NEAR(mapped.Value().acceleration[entry.dof], -0.001 * entry.value, 1e-17); } const auto residual = [&](const fesa::Vector& q, const fesa::Vector& v, const fesa::Vector& a) -> fesa::Result { const auto trial = Assemble(fixture, shells.Value(), q, v, a, force, true); if (!trial.HasValue()) return fesa::Result::Failure(trial.GetStatus()); const auto trial_operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), trial.Value(), chart.Value(), force_rate, force_acceleration); if (!trial_operators.HasValue()) return fesa::Result::Failure(trial_operators.GetStatus()); fesa::Vector result(system.Value().rhs.Size()); for (std::size_t i = 0U; i < chart.Value().p_columns.size(); ++i) for (const auto& entry : chart.Value().p_columns[i].support) result[i] += entry.value * trial.Value().frame.residual[entry.dof]; for (std::size_t row = 0U; row < chart.Value().z_dofs.size(); ++row) { const auto hidden = Hidden(trial_operators.Value().rows[row], v, a); result[system.Value().physical_count + row] = hidden[0U]; result[system.Value().physical_count + system.Value().algebraic_count + row] = hidden[1U]; result[system.Value().physical_count + 2U * system.Value().algebraic_count + row] = hidden[2U]; } for (std::size_t i = 0U; i < result.Size(); ++i) result[i] *= system.Value().row_scale[i]; return fesa::Result::Success(std::move(result)); }; const auto base = residual(root.Value().q, rates.Value().velocity, rates.Value().acceleration); ASSERT_TRUE(base.HasValue()); const auto projected_base = fesa::hht_reduced_internal::ProjectPhysical( chart.Value(), actual.Value().frame.residual); ASSERT_TRUE(projected_base.HasValue()); for (std::size_t i = 0U; i < projected_base.Value().Size(); ++i) { double uncancelled = 0.0; for (const auto& entry : chart.Value().p_columns[i].support) uncancelled += std::abs(entry.value * actual.Value().frame.residual[entry.dof]); EXPECT_NEAR(base.Value()[i], system.Value().row_scale[i] * projected_base.Value()[i], 1e-13 * system.Value().row_scale[i] * uncancelled); } const fesa::Vector input_scale(system.Value().rhs.Size(), 1.0); const fesa::Vector output_scale(system.Value().rhs.Size(), 1.0); const auto block_norm = [&](const fesa::Vector& values, const std::size_t block, const fesa::hht_reduced_internal::AugmentedReducedSystem& active) { const std::size_t begin = block == 0U ? 0U : active.physical_count + (block - 1U) * active.algebraic_count; const std::size_t end = block == 0U ? active.physical_count : begin + active.algebraic_count; double squared = 0.0; for (std::size_t row = begin; row < end; ++row) squared += std::pow(output_scale[row] * values[row], 2.0); return std::sqrt(squared); }; const auto sweep = [&](const char* name, const fesa::Vector& direction, const fesa::hht_reduced_internal::AugmentedReducedSystem& active, const fesa::Vector& base_velocity, const fesa::Vector& base_acceleration, const bool expected_pass) { fesa::Vector normalized(direction); double direction_norm = 0.0; for (std::size_t i = 0U; i < normalized.Size(); ++i) direction_norm += std::pow(input_scale[i] * normalized[i], 2.0); direction_norm = std::sqrt(direction_norm); ASSERT_GT(direction_norm, 0.0) << name; for (std::size_t i = 0U; i < normalized.Size(); ++i) normalized[i] /= direction_norm; const auto mapped_direction = fesa::hht_reduced_internal::MapAugmentedCorrection( chart.Value(), active, normalized, cv, ca); ASSERT_TRUE(mapped_direction.HasValue()) << name; const auto analytic = active.tangent.Multiply(normalized); double tangent_frobenius = 0.0; for (std::size_t row = 0U; row < active.tangent.Rows(); ++row) for (std::size_t offset = active.tangent.RowOffsets()[row]; offset < active.tangent.RowOffsets()[row + 1U]; ++offset) { const auto column = active.tangent.ColumnIndices()[offset]; const double value = output_scale[row] * active.tangent.Values()[offset] / input_scale[column]; tangent_frobenius += value * value; } tangent_frobenius = std::sqrt(tangent_frobenius); ASSERT_GT(tangent_frobenius, 0.0) << name; std::array passed{}; const std::array exponents{{8, 10, 12, 14, 16, 18, 20}}; for (std::size_t sample = 0U; sample < exponents.size(); ++sample) { const double step = std::ldexp(1.0, -exponents[sample]); fesa::Vector plus_q(root.Value().q), minus_q(root.Value().q); fesa::Vector plus_v(base_velocity), minus_v(base_velocity); fesa::Vector plus_a(base_acceleration), minus_a(base_acceleration); for (std::size_t i = 0U; i < plus_q.Size(); ++i) { plus_q[i] += step * mapped_direction.Value().coordinate[i]; minus_q[i] -= step * mapped_direction.Value().coordinate[i]; plus_v[i] += step * mapped_direction.Value().velocity[i]; minus_v[i] -= step * mapped_direction.Value().velocity[i]; plus_a[i] += step * mapped_direction.Value().acceleration[i]; minus_a[i] -= step * mapped_direction.Value().acceleration[i]; } const auto plus = residual(plus_q, plus_v, plus_a); const auto minus = residual(minus_q, minus_v, minus_a); ASSERT_TRUE(plus.HasValue()) << name << " h=" << step; ASSERT_TRUE(minus.HasValue()) << name << " h=" << step; fesa::Vector difference(analytic.Size()); for (std::size_t row = 0U; row < difference.Size(); ++row) difference[row] = (plus.Value()[row] - minus.Value()[row]) / (2.0 * step); fesa::Vector error(analytic.Size()); for (std::size_t row = 0U; row < error.Size(); ++row) error[row] = analytic[row] - difference[row]; passed[sample] = true; std::cout << "N5 " << name << " j=" << exponents[sample] << " h=" << step; for (std::size_t block = 0U; block < 4U; ++block) { const double analytic_norm = block_norm(analytic, block, active); const double fd_norm = block_norm(difference, block, active); const double error_norm = block_norm(error, block, active); const double metric = analytic_norm == 0.0 ? error_norm / tangent_frobenius : error_norm / (analytic_norm + fd_norm); const bool block_pass = analytic_norm == 0.0 ? metric <= 1.0e-10 : metric <= 1.0e-6; passed[sample] = passed[sample] && block_pass; std::cout << " b" << block << "=(a:" << analytic_norm << ",fd:" << fd_norm << ",e:" << error_norm << ",m:" << metric << ")"; } std::cout << '\n'; } bool consecutive = false; for (std::size_t sample = 1U; sample <= 4U; ++sample) consecutive = consecutive || (passed[sample] && passed[sample + 1U]); EXPECT_EQ(consecutive, expected_pass) << name; }; const auto total = system.Value().rhs.Size(); fesa::Vector u_only(total), phi_only(total), z_only(total), vz_only(total), az_only(total), mixed(total); u_only[PIndex(chart.Value(), 0U)] = 0.7; phi_only[PIndex(chart.Value(), 3U)] = -0.6; z_only[system.Value().physical_count] = 0.5; vz_only[system.Value().physical_count + system.Value().algebraic_count] = -0.4; az_only[system.Value().physical_count + 2U * system.Value().algebraic_count] = 0.3; mixed[PIndex(chart.Value(), 0U)] = 0.25; mixed[PIndex(chart.Value(), 3U)] = -0.35; mixed[system.Value().physical_count] = 0.2; mixed[system.Value().physical_count + system.Value().algebraic_count] = -0.15; mixed[system.Value().physical_count + 2U * system.Value().algebraic_count] = 0.1; sweep("u", u_only, system.Value(), rates.Value().velocity, rates.Value().acceleration, true); sweep("phi", phi_only, system.Value(), rates.Value().velocity, rates.Value().acceleration, true); sweep("z", z_only, system.Value(), rates.Value().velocity, rates.Value().acceleration, true); sweep("vz", vz_only, system.Value(), rates.Value().velocity, rates.Value().acceleration, true); sweep("az", az_only, system.Value(), rates.Value().velocity, rates.Value().acceleration, true); sweep("mixed", mixed, system.Value(), rates.Value().velocity, rates.Value().acceleration, true); // Keep the finite F-position derivative large enough that deleting it is // observable under the existing all-block, consecutive-step N5 gate. The // root and graph-regularity probe for this warped shared-node state // converges at this load without changing its principal-coordinate branch. const auto no_force_actual = Assemble( fixture, shells.Value(), root.Value().q, rates.Value().velocity, rates.Value().acceleration, zero, true); ASSERT_TRUE(no_force_actual.HasValue()); const auto no_force_operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), no_force_actual.Value(), chart.Value(), force_rate, force_acceleration); ASSERT_TRUE(no_force_operators.HasValue()); auto omitted_external_gradient = operators.Value(); ASSERT_EQ(omitted_external_gradient.rows.size(), no_force_operators.Value().rows.size()); bool saw_external_position_gradient = false; for (std::size_t row = 0U; row < omitted_external_gradient.rows.size(); ++row) { auto& omitted = omitted_external_gradient.rows[row].gradient; const auto& no_force = no_force_operators.Value().rows[row].gradient; 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); const auto omitted_external_gradient_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( actual.Value(), chart.Value(), omitted_external_gradient, cv, ca, reference_length.Value(), h); ASSERT_TRUE(omitted_external_gradient_system.HasValue()); sweep("mutant-omit-external-gradient", mixed, omitted_external_gradient_system.Value(), rates.Value().velocity, rates.Value().acceleration, false); const auto zero_rates = fesa::Vector(fixture.dofs.FullDofCount()); const auto ha_actual = Assemble(fixture, shells.Value(), root.Value().q, zero_rates, rates.Value().acceleration, force, true); ASSERT_TRUE(ha_actual.HasValue()); const auto ha_operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), ha_actual.Value(), chart.Value(), force_rate, force_acceleration); ASSERT_TRUE(ha_operators.HasValue()); const auto ha_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( ha_actual.Value(), chart.Value(), ha_operators.Value(), cv, ca, reference_length.Value(), h); ASSERT_TRUE(ha_system.HasValue()); sweep("Ha", mixed, ha_system.Value(), zero_rates, rates.Value().acceleration, true); auto omitted_ha = ha_operators.Value(); for (auto& row : omitted_ha.rows) row.hessian.clear(); const auto omitted_ha_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( ha_actual.Value(), chart.Value(), omitted_ha, cv, ca, reference_length.Value(), h); ASSERT_TRUE(omitted_ha_system.HasValue()); sweep("mutant-omit-Ha", mixed, omitted_ha_system.Value(), zero_rates, rates.Value().acceleration, false); const auto hw_actual = Assemble(fixture, shells.Value(), root.Value().q, rates.Value().velocity, zero_rates, force, true); ASSERT_TRUE(hw_actual.HasValue()); const auto hw_operators = fesa::hht_reduced_internal::EvaluateConstraintOperators( *fixture.domain, fixture.dofs, shells.Value(), hw_actual.Value(), chart.Value(), force_rate, force_acceleration); ASSERT_TRUE(hw_operators.HasValue()); const auto hw_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( hw_actual.Value(), chart.Value(), hw_operators.Value(), cv, ca, reference_length.Value(), h); ASSERT_TRUE(hw_system.HasValue()); sweep("HW-2W", mixed, hw_system.Value(), rates.Value().velocity, zero_rates, true); auto omitted_hw = hw_operators.Value(); for (auto& row : omitted_hw.rows) row.hessian.clear(); const auto omitted_hw_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( hw_actual.Value(), chart.Value(), omitted_hw, cv, ca, reference_length.Value(), h); ASSERT_TRUE(omitted_hw_system.HasValue()); sweep("mutant-omit-HW-2W", mixed, omitted_hw_system.Value(), rates.Value().velocity, zero_rates, false); auto omitted_third = operators.Value(); for (auto& row : omitted_third.rows) row.third_directional_gradient.clear(); const auto third_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( actual.Value(), chart.Value(), omitted_third, cv, ca, reference_length.Value(), h); ASSERT_TRUE(third_system.HasValue()); sweep("mutant-omit-third", mixed, third_system.Value(), rates.Value().velocity, rates.Value().acceleration, false); auto omitted_mixed_time = operators.Value(); for (auto& row : omitted_mixed_time.rows) row.time_gradient.clear(); const auto mixed_time_system = fesa::hht_reduced_internal::AssembleAugmentedReducedSystem( actual.Value(), chart.Value(), omitted_mixed_time, cv, ca, reference_length.Value(), h); ASSERT_TRUE(mixed_time_system.HasValue()); sweep("mutant-omit-psi-qt", mixed, mixed_time_system.Value(), rates.Value().velocity, rates.Value().acceleration, false); auto omitted_second_time_root = root.Value(); for (auto& row : omitted_second_time_root.operators.rows) row.time_second = 0.0; const auto omitted_second_time_rates = fesa::hht_reduced_internal::ReconstructPhysicalRates( chart.Value(), omitted_second_time_root, vp, ap, solver); ASSERT_TRUE(omitted_second_time_rates.HasValue()); double omitted_second_time_hidden = 0.0; for (const auto& row : operators.Value().rows) omitted_second_time_hidden = (std::max)( omitted_second_time_hidden, std::abs(Hidden(row, omitted_second_time_rates.Value().velocity, omitted_second_time_rates.Value().acceleration)[2U])); 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; chart.p_dofs = {0U, 3U, 4U}; chart.z_dofs = {5U}; SetComponentColumns(&chart); const std::array zero{}; const auto refine = [&](const std::array& residual, const std::array& correction, const bool private_pass = true) { return fesa::hht_reduced_internal::ShouldRefineAngular( chart, residual, correction, private_pass); }; EXPECT_TRUE(refine({1e-10, 2e-10}, zero)); EXPECT_FALSE(refine({2e-10, 2e-10}, zero)); // Fresh U failure thaws. EXPECT_FALSE(refine({0.0, 1e-10}, zero)); // Phi correction-only is excluded. EXPECT_FALSE(refine({0.0, 2e-10}, zero, false)); for (std::size_t i = 0U; i < zero.size(); ++i) { auto correction = zero; correction[i] = std::nextafter(1e-10, 1.0); EXPECT_FALSE(refine({0.0, 2e-10}, correction)); correction[i] = std::numeric_limits::quiet_NaN(); EXPECT_FALSE(refine({0.0, 2e-10}, correction)); } EXPECT_FALSE(refine({std::numeric_limits::infinity(), 2e-10}, zero)); EXPECT_FALSE(refine({0.0, std::numeric_limits::quiet_NaN()}, zero)); chart.p_dofs = {3U, 4U}; SetComponentColumns(&chart); EXPECT_FALSE(refine({0.0, 2e-10}, zero)); chart.p_dofs = {0U}; chart.z_dofs.clear(); SetComponentColumns(&chart); EXPECT_FALSE(refine({0.0, 2e-10}, zero)); } TEST(HhtReducedChannelNewton, RetainsAllAuxiliaryBlocksAndFullModelDefect) { fesa::hht_reduced_internal::FixedChart chart{}; chart.full_dof_count = 6U; chart.p_dofs = {0U, 3U, 4U}; chart.z_dofs = {5U}; SetComponentColumns(&chart); std::vector entries; for (std::size_t i = 0U; i < 6U; ++i) entries.push_back({i, i, static_cast(i + 2U), 0U, i}); entries.push_back({0U, 1U, 0.25, 0U, 6U}); entries.push_back({1U, 0U, 0.5, 0U, 7U}); entries.push_back({3U, 1U, 0.7, 0U, 8U}); entries.push_back({1U, 3U, -0.2, 0U, 9U}); entries.push_back({2U, 4U, 1.5, 0U, 10U}); entries.push_back({4U, 2U, 0.1, 0U, 11U}); entries.push_back({5U, 1U, 0.3, 0U, 12U}); auto tangent = fesa::hht_internal::MakeSparse(6U, 6U, std::move(entries)); ASSERT_TRUE(tangent.HasValue()); fesa::Vector expected{6U}; expected[1U] = 2.0; expected[2U] = -1.0; expected[3U] = 0.4; expected[4U] = 0.2; expected[5U] = -0.3; auto rhs = tangent.Value().Multiply(expected); rhs[0U] += 1.0; // Omitted row is not silently certified by the J solve. const fesa::hht_reduced_internal::AugmentedReducedSystem full{ tangent.Value(), rhs, 3U, 1U, fesa::Vector(6U, 1.0), fesa::Vector(6U, 1.0)}; auto selected = fesa::hht_reduced_internal::SelectAugmentedSolve(full, chart, true); ASSERT_TRUE(selected.HasValue()); EXPECT_EQ(selected.Value().full_indices, (std::vector{1U, 2U, 3U, 4U, 5U})); fesa::MklPardisoTangentSolver solver; ASSERT_TRUE(solver.FactorizeFresh(selected.Value().tangent).IsOk()); fesa::Vector step{5U}; ASSERT_TRUE(solver.SolveCorrection(selected.Value().rhs, step).IsOk()); EXPECT_TRUE(fesa::hht_internal::CheckLinearSolve( selected.Value().tangent, selected.Value().rhs, step).IsOk()); auto embedded = fesa::hht_reduced_internal::EmbedAugmentedSolution( selected.Value(), step, 6U); ASSERT_TRUE(embedded.HasValue()); EXPECT_DOUBLE_EQ(embedded.Value()[0U], 0.0); for (std::size_t i = 1U; i < 6U; ++i) EXPECT_NEAR(embedded.Value()[i], expected[i], 1e-14); EXPECT_FALSE(fesa::hht_internal::CheckLinearSolve( full.tangent, full.rhs, embedded.Value()).IsOk()); auto mapped = fesa::hht_reduced_internal::MapAugmentedCorrection( chart, full, embedded.Value(), 2.0, 3.0); ASSERT_TRUE(mapped.HasValue()); EXPECT_DOUBLE_EQ(mapped.Value().coordinate[0U], 0.0); EXPECT_DOUBLE_EQ(mapped.Value().velocity[0U], 0.0); EXPECT_DOUBLE_EQ(mapped.Value().acceleration[0U], 0.0); EXPECT_NEAR(mapped.Value().coordinate[5U], 0.4, 1e-14); EXPECT_NEAR(mapped.Value().velocity[5U], 0.2, 1e-14); EXPECT_NEAR(mapped.Value().acceleration[5U], -0.3, 1e-14); auto thawed = fesa::hht_reduced_internal::SelectAugmentedSolve(full, chart, false); ASSERT_TRUE(thawed.HasValue()); EXPECT_EQ(thawed.Value().full_indices.size(), 6U); EXPECT_DOUBLE_EQ(thawed.Value().rhs[0U], rhs[0U]); auto invalid = full; invalid.rhs[0U] = std::numeric_limits::infinity(); EXPECT_FALSE(fesa::hht_reduced_internal::SelectAugmentedSolve( invalid, chart, true).HasValue()); step[0U] = std::numeric_limits::quiet_NaN(); EXPECT_FALSE(fesa::hht_reduced_internal::EmbedAugmentedSolution( selected.Value(), step, 6U).HasValue()); } TEST(HhtReducedChannelNewton, RestrictedSingularityIsNotFullInvertibility) { fesa::hht_reduced_internal::FixedChart chart{}; chart.full_dof_count = 6U; chart.p_dofs = {0U, 3U}; SetComponentColumns(&chart); auto matrix = fesa::hht_internal::MakeSparse( 2U, 2U, {{0U, 1U, 1.0, 0U, 0U}, {1U, 0U, 1.0, 0U, 1U}}); ASSERT_TRUE(matrix.HasValue()); const fesa::hht_reduced_internal::AugmentedReducedSystem full{ matrix.Value(), fesa::Vector{2U}, 2U, 0U, fesa::Vector(2U, 1.0), fesa::Vector(2U, 1.0)}; auto restricted = fesa::hht_reduced_internal::SelectAugmentedSolve(full, chart, true); ASSERT_TRUE(restricted.HasValue()); fesa::MklPardisoTangentSolver solver; ASSERT_TRUE(solver.FactorizeFresh(full.tangent).IsOk()); EXPECT_FALSE(solver.FactorizeFresh(restricted.Value().tangent).IsOk()); } TEST(HhtReducedOperators, NullMetricKeepsFiniteLeftAssociatedProducts) { 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()); const auto assembled = Assemble(fixture, shells.Value(), zero, zero, zero, zero, false); ASSERT_TRUE(assembled.HasValue()); auto assembly = assembled.Value(); const double maximum = (std::numeric_limits::max)(); const double epsilon = std::ldexp(1.0, -26); const double t = std::sqrt(1.0 - epsilon * epsilon); const std::array n{epsilon, 0.6 * t, 0.8 * t}; const std::array block{0.0, maximum, maximum, maximum, 0.0, 0.0, maximum, 0.0, 0.0}; double original_metric = 0.0; for (std::size_t i = 0U; i < 3U; ++i) for (std::size_t j = 0U; j < 3U; ++j) original_metric += (n[i] * block[3U * i + j]) * n[j]; ASSERT_TRUE(std::isfinite(original_metric)); ASSERT_GT(original_metric, 0.0); ASSERT_FALSE(std::isfinite(block[1U] * n[1U] + block[2U] * n[2U])); std::vector entries; for (std::size_t node = 0U; node < assembly.rotations.size(); ++node) { assembly.rotations[node].drill_null = fesa::Vector3(n); for (std::size_t i = 0U; i < 3U; ++i) for (std::size_t j = 0U; j < 3U; ++j) entries.push_back({6U * node + 3U + i, 6U * node + 3U + j, block[3U * i + j], node, 3U * i + j}); } const auto metric = fesa::hht_internal::MakeSparse( fixture.dofs.FullDofCount(), fixture.dofs.FullDofCount(), std::move(entries)); ASSERT_TRUE(metric.HasValue()); assembly.drilling_metric = metric.Value(); const auto chart = fesa::hht_reduced_internal::BuildFixedChart( fixture.dofs, assembly.rotations); ASSERT_TRUE(chart.HasValue()); fesa::hht_reduced_internal::ConstraintOperators operators; operators.rows.resize(chart.Value().null_nodes.size()); for (std::size_t row = 0U; row < operators.rows.size(); ++row) for (std::size_t component = 0U; component < 3U; ++component) operators.rows[row].gradient.push_back( {6U * chart.Value().null_nodes[row] + 3U + component, original_metric * n[component]}); fesa::MklPardisoTangentSolver solver; const auto checked = fesa::hht_reduced_internal::CheckGraphRegularity( assembly, chart.Value(), operators, solver); ASSERT_TRUE(checked.HasValue()); EXPECT_GE(checked.Value(), std::ldexp(1.0, -32)); } } // 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(node); q[6U * node + 3U] = 0.13 + 0.01 * static_cast(node); q[6U * node + 4U] = -0.09; q[6U * node + 5U] = 0.21 - 0.01 * static_cast(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 nodes{}; std::array, 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( &fixture.domain->Properties()[definition.section_index]); const auto* material = dynamic_cast( &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(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); } const auto accepted = frame; for (const bool missing : {true, false}) { auto invalid = assembly.Value(); if (missing) invalid.hidden_constraint = fesa::Vector(0U); else invalid.hidden_constraint[0U] = std::numeric_limits::infinity(); const auto rejected = fesa::hht_internal::CompleteConstraintEvidence( *fixture.domain, fixture.dofs, invalid, length.Value(), null_nodes, positive, tangent, &frame); EXPECT_FALSE(rejected.IsOk()); for (const auto& pair : { std::make_pair(&frame.hidden_constraint, &accepted.hidden_constraint), std::make_pair(&frame.hidden_velocity_defect, &accepted.hidden_velocity_defect), std::make_pair(&frame.hidden_acceleration_defect, &accepted.hidden_acceleration_defect)}) { ASSERT_EQ(pair.first->Size(), pair.second->Size()); for (std::size_t row = 0U; row < pair.first->Size(); ++row) ExpectSameBits((*pair.first)[row], (*pair.second)[row]); } } }