#include "fesa/results/result_recovery.hpp" #include "fesa/analysis/analysis_model.hpp" #include "fesa/analysis/analysis_state.hpp" #include "fesa/assembly/parallel_for.hpp" #include "fesa/assembly/sparse_assembler.hpp" #include "fesa/fem/dof_manager.hpp" #include "fesa/model/domain.hpp" #include #include #include #include #include #include #include #include #include #include #include #include namespace { constexpr double kYoungsModulus = 100.0; constexpr double kPoissonRatio = 0.25; constexpr double kLength = 2.0; struct RecoveryFixture { std::unique_ptr domain; std::unique_ptr model; std::unique_ptr dofs; std::unique_ptr stiffness; }; fesa::ModelDefinition makeDefinition( const bool twoElements = false, std::vector> sectionPoints = {}, std::vector loads = {}, const bool reverseSecond = false, const bool sectionJump = false, const bool nonzeroPrescription = true) { const std::filesystem::path source{"models/result-recovery.inp"}; fesa::ModelDefinition definition{}; definition.sourcePath = source; definition.sourceContentIdentity = "fnv1a64:0123456789abcdef"; definition.nodes = { {{"Beam-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 10U}}, {{"Beam-1", 2, "2"}, {kLength, 0.0, 0.0}, {source, 11U}}}; if (twoElements) { definition.nodes.push_back( {{"Beam-1", 3, "3"}, {2.0 * kLength, 0.0, 0.0}, {source, 12U}}); } definition.materials = { {"Material", kYoungsModulus, kPoissonRatio, {source, 20U}}}; definition.sections = {{ "Section", 2.0, 3.0, 0.0, 4.0, 5.0, {0.0, 1.0, 0.0}, std::move(sectionPoints), {source, 30U}}}; if (sectionJump) { auto secondSection = definition.sections.front(); secondSection.name = "Section-2"; secondSection.area = 2.5; secondSection.location.line = 31U; definition.sections.push_back(std::move(secondSection)); } definition.elements = { {{"Beam-1", 10, "10"}, {0U, 1U}, 0U, 0U, {source, 40U}}}; if (twoElements) { definition.elements.push_back({ {"Beam-1", 20, "20"}, reverseSecond ? std::array{2U, 1U} : std::array{1U, 2U}, 0U, sectionJump ? 1U : 0U, {source, 41U}}); } definition.steps = {{ "Step-1", {{"1", 1, 1, nonzeroPrescription ? 0.1 : 0.0, {source, 50U}}, {"1", 2, 6, 0.0, {source, 51U}}}, std::move(loads), 0.1, 1.0, 0.01, 1.0, {source, 49U}}}; return definition; } RecoveryFixture makeFixture( const bool twoElements = false, std::vector> sectionPoints = {}, std::vector loads = {}, const bool reverseSecond = false, const bool sectionJump = false, const bool nonzeroPrescription = true) { auto domainResult = fesa::Domain::create(makeDefinition( twoElements, std::move(sectionPoints), std::move(loads), reverseSecond, sectionJump, nonzeroPrescription)); if (!domainResult.hasValue()) { throw std::runtime_error{"Recovery fixture Domain construction failed."}; } auto domain = std::make_unique( std::move(domainResult.value())); auto modelResult = fesa::AnalysisModel::create(*domain); if (!modelResult.hasValue()) { throw std::runtime_error{"Recovery fixture AnalysisModel construction failed."}; } auto model = std::make_unique( std::move(modelResult.value())); auto dofsResult = fesa::DofManager::create(*model); if (!dofsResult.hasValue()) { throw std::runtime_error{"Recovery fixture DofManager construction failed."}; } auto dofs = std::make_unique( std::move(dofsResult.value())); fesa::SerialParallelFor serial; auto stiffnessResult = fesa::SparseAssembler::assembleStiffness( *model, *dofs, serial); if (!stiffnessResult.hasValue()) { throw std::runtime_error{"Recovery fixture stiffness assembly failed."}; } auto stiffness = std::make_unique( std::move(stiffnessResult.value())); return { std::move(domain), std::move(model), std::move(dofs), std::move(stiffness)}; } fesa::AnalysisState makeAxialEquilibriumState(const RecoveryFixture& fixture) { auto state = fesa::AnalysisState::create( *fixture.dofs, {"Step-1", 0U}); state.displacement()[0U] = 0.1; state.displacement()[6U] = 0.3; const fesa::Vector internal = fixture.stiffness->multiply(state.displacement()); for (const std::size_t fullDof : fixture.dofs->freeDofs()) { state.externalForce()[fullDof] = internal[fullDof]; } return state; } fesa::AnalysisState makePatchState( const RecoveryFixture& fixture, const double epsilon, const double twist, const double kappaY, const double kappaZ) { auto state = fesa::AnalysisState::create( *fixture.dofs, {"Step-1", 0U}); state.displacement()[0U] = 0.1; state.displacement()[6U] = 0.1 + epsilon * kLength; state.displacement()[7U] = 0.5 * kappaZ * kLength * kLength; state.displacement()[8U] = -0.5 * kappaY * kLength * kLength; state.displacement()[9U] = twist * kLength; state.displacement()[10U] = kappaY * kLength; state.displacement()[11U] = kappaZ * kLength; state.externalForce() = fixture.stiffness->multiply(state.displacement()); return state; } void expectStatusCode(const fesa::Status& status, const std::string& code) { ASSERT_FALSE(status.isOk()); EXPECT_EQ(status.failureCategory(), fesa::FailureCategory::model); ASSERT_EQ(status.diagnostics().size(), 1U); EXPECT_EQ(status.diagnostics()[0U].code, code); } void expectScaledNear( const double actual, const double expected, const double relativeTolerance = 1.0e-12) { ASSERT_TRUE(std::isfinite(actual)); ASSERT_TRUE(std::isfinite(expected)); EXPECT_LE( std::abs(actual - expected), relativeTolerance * (std::max)(std::abs(expected), 1.0)); } std::vector makeStationRows( const RecoveryFixture& fixture) { const auto& nodes = fixture.domain->nodes(); const auto& elements = fixture.domain->elements(); return { {0U, 0, nodes[elements[0U].nodeIndices[0U]].sourceId, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {1.0, 2.0, 3.0, 4.0}}, {0U, 1, nodes[elements[0U].nodeIndices[1U]].sourceId, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {5.0, 6.0, 7.0, 8.0}}, {1U, 0, nodes[elements[1U].nodeIndices[0U]].sourceId, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {5.0, 6.0, 7.0, 8.0}}, {1U, 1, nodes[elements[1U].nodeIndices[1U]].sourceId, {0.0, 0.0, 0.0, 0.0, 0.0, 0.0}, {9.0, 10.0, 11.0, 12.0}}}; } } // namespace TEST(ResultRecovery, ComputesResidualReactionForNonzeroPrescription) { const auto fixture = makeFixture(); auto state = makeAxialEquilibriumState(fixture); const fesa::Status status = fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, state); ASSERT_TRUE(status.isOk()); EXPECT_DOUBLE_EQ(state.internalForce()[0U], -20.0); EXPECT_DOUBLE_EQ(state.internalForce()[6U], 20.0); EXPECT_DOUBLE_EQ(state.residual()[0U], -20.0); EXPECT_DOUBLE_EQ(state.residual()[6U], 0.0); EXPECT_DOUBLE_EQ(state.reaction()[0U], -20.0); for (const std::size_t fullDof : fixture.dofs->freeDofs()) { EXPECT_DOUBLE_EQ(state.reaction()[fullDof], 0.0); } } TEST(ResultRecovery, EnforcesNormalizedFreeResidual) { const auto fixture = makeFixture(); auto failed = makeAxialEquilibriumState(fixture); failed.internalForce()[0U] = 91.0; failed.residual()[0U] = 92.0; failed.reaction()[0U] = 93.0; failed.reaction()[6U] = 94.0; failed.endpointResults().push_back({}); failed.externalForce()[6U] += 1.0e-7; expectStatusCode( fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, failed), "free-residual-tolerance-failure"); EXPECT_DOUBLE_EQ(failed.internalForce()[0U], 91.0); EXPECT_DOUBLE_EQ(failed.residual()[0U], 92.0); EXPECT_DOUBLE_EQ(failed.reaction()[0U], 93.0); EXPECT_DOUBLE_EQ(failed.reaction()[6U], 94.0); EXPECT_EQ(failed.endpointResults().size(), 1U); auto thresholdPass = makeAxialEquilibriumState(fixture); thresholdPass.externalForce()[6U] += 1.0e-10 * 20.0 * 0.5; const fesa::Status thresholdStatus = fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, thresholdPass); ASSERT_TRUE(thresholdStatus.isOk()); EXPECT_NE(thresholdPass.residual()[6U], 0.0); EXPECT_DOUBLE_EQ( thresholdPass.reaction()[6U], thresholdPass.residual()[6U]); const auto zeroFixture = makeFixture(false, {}, {}, false, false, false); auto zeroEquilibrium = fesa::AnalysisState::create( *zeroFixture.dofs, {"Step-1", 0U}); EXPECT_TRUE(fesa::ResultRecovery::recover( *zeroFixture.model, *zeroFixture.dofs, *zeroFixture.stiffness, zeroEquilibrium) .isOk()); auto wrongPrescription = makeAxialEquilibriumState(fixture); wrongPrescription.displacement()[0U] = 0.0; expectStatusCode( fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, wrongPrescription), "invalid-recovery-state"); auto nonfinite = makeAxialEquilibriumState(fixture); nonfinite.displacement()[6U] = std::numeric_limits::quiet_NaN(); expectStatusCode( fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, nonfinite), "nonfinite-recovery-value"); const auto wrongFixture = makeFixture(true); auto wrongState = fesa::AnalysisState::create( *wrongFixture.dofs, {"Step-1", 0U}); expectStatusCode( fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, wrongState), "invalid-recovery-dimensions"); } TEST(ResultRecovery, KeepsEndActionSectionAndGaussResultsDistinct) { const auto fixture = makeFixture(); auto state = makePatchState(fixture, 0.02, 0.03, -0.04, 0.05); ASSERT_TRUE(fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, state) .isOk()); ASSERT_EQ(state.endpointResults().size(), 2U); ASSERT_EQ(state.gaussResults().size(), 2U); EXPECT_EQ(state.endpointResults()[0U].endpoint, 0); EXPECT_EQ(state.endpointResults()[1U].endpoint, 1); EXPECT_EQ(state.gaussResults()[0U].gaussPoint, 1); EXPECT_EQ(state.gaussResults()[1U].gaussPoint, 2); EXPECT_DOUBLE_EQ( state.endpointResults()[0U].endAction[0U], -state.endpointResults()[0U].sectionResultant[0U]); EXPECT_DOUBLE_EQ( state.endpointResults()[1U].endAction[0U], state.endpointResults()[1U].sectionResultant[0U]); EXPECT_DOUBLE_EQ( state.gaussResults()[0U].generalizedResultant[0U], state.endpointResults()[0U].sectionResultant[0U]); } TEST(ResultRecovery, MatchesAxialTorsionAndTwoPlaneEndSigns) { const auto fixture = makeFixture(); const double epsilon = 0.02; const double twist = -0.03; const double kappaY = 0.04; const double kappaZ = -0.05; auto state = makePatchState(fixture, epsilon, twist, kappaY, kappaZ); ASSERT_TRUE(fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, state) .isOk()); const double shearModulus = kYoungsModulus / (2.0 * (1.0 + kPoissonRatio)); const std::array expected = { kYoungsModulus * 2.0 * epsilon, shearModulus * 5.0 * twist, kYoungsModulus * 3.0 * kappaY, kYoungsModulus * 4.0 * kappaZ}; const std::array endComponents = {0U, 3U, 4U, 5U}; for (std::size_t component = 0U; component < expected.size(); ++component) { expectScaledNear( state.endpointResults()[0U].sectionResultant[component], expected[component]); expectScaledNear( state.endpointResults()[1U].sectionResultant[component], expected[component]); expectScaledNear( state.endpointResults()[0U].endAction[endComponents[component]], -expected[component]); expectScaledNear( state.endpointResults()[1U].endAction[endComponents[component]], expected[component]); } } TEST(ResultRecovery, OrdersStressPointsAndDefaultCentroid) { const std::vector> sectionPoints = { {0.25, -0.5}, {-0.4, 0.3}}; const auto fixture = makeFixture(false, sectionPoints); auto state = makePatchState(fixture, 0.01, 0.0, 0.02, -0.03); ASSERT_TRUE(fesa::ResultRecovery::recover( *fixture.model, *fixture.dofs, *fixture.stiffness, state) .isOk()); ASSERT_EQ(state.stressResults().size(), 4U); for (std::size_t gauss = 0U; gauss < 2U; ++gauss) { for (std::size_t point = 0U; point < sectionPoints.size(); ++point) { const auto& row = state.stressResults()[gauss * 2U + point]; EXPECT_EQ(row.element, 0U); EXPECT_EQ(row.gaussPoint, static_cast(gauss + 1U)); EXPECT_EQ(row.sectionPoint, point + 1U); EXPECT_DOUBLE_EQ(row.x1, sectionPoints[point][0U]); EXPECT_DOUBLE_EQ(row.x2, sectionPoints[point][1U]); EXPECT_EQ(row.source, "input"); expectScaledNear( row.s11, kYoungsModulus * (0.01 + row.x2 * 0.02 - row.x1 * -0.03)); } } const auto defaultFixture = makeFixture(); auto defaultState = makePatchState(defaultFixture, 0.01, 0.0, 0.0, 0.0); ASSERT_TRUE(fesa::ResultRecovery::recover( *defaultFixture.model, *defaultFixture.dofs, *defaultFixture.stiffness, defaultState) .isOk()); ASSERT_EQ(defaultState.stressResults().size(), 2U); for (const auto& row : defaultState.stressResults()) { EXPECT_EQ(row.sectionPoint, 0U); EXPECT_DOUBLE_EQ(row.x1, 0.0); EXPECT_DOUBLE_EQ(row.x2, 0.0); EXPECT_EQ(row.source, "fesa-default"); } } TEST(ResultRecovery, RequiresInteriorEndpointConsistencyWithoutAveraging) { const auto fixture = makeFixture(true); const std::array tolerances = {1.0e-6, 1.0e-6, 1.0e-6, 1.0e-6}; auto rows = makeStationRows(fixture); rows[2U].sectionResultant[0U] += 0.5e-6; auto normalized = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *fixture.model, rows, tolerances); ASSERT_TRUE(normalized.hasValue()); ASSERT_EQ(normalized.value().size(), 3U); EXPECT_EQ(normalized.value()[1U].representativeElement, 0U); EXPECT_DOUBLE_EQ(normalized.value()[1U].sectionResultant[0U], 5.0); rows[2U].sectionResultant[0U] = 5.0 + 2.0e-6; auto mismatch = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *fixture.model, rows, tolerances); ASSERT_FALSE(mismatch.hasValue()); expectStatusCode(mismatch.status(), "node-station-tolerance-failure"); rows = makeStationRows(fixture); rows[2U].sectionResultant[1U] = std::numeric_limits::infinity(); auto nonfinite = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *fixture.model, rows, tolerances); ASSERT_FALSE(nonfinite.hasValue()); expectStatusCode(nonfinite.status(), "nonfinite-node-station-value"); auto invalidTolerance = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *fixture.model, makeStationRows(fixture), {1.0e-6, -1.0, 1.0e-6, 1.0e-6}); ASSERT_FALSE(invalidTolerance.hasValue()); expectStatusCode( invalidTolerance.status(), "invalid-node-station-tolerance"); const std::filesystem::path source{"models/result-recovery.inp"}; const auto loadedFixture = makeFixture( true, {}, {{"2", 2, 1.0, {source, 60U}}}); auto loaded = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *loadedFixture.model, makeStationRows(loadedFixture), tolerances); ASSERT_FALSE(loaded.hasValue()); expectStatusCode(loaded.status(), "ineligible-node-station"); const auto reversedFixture = makeFixture(true, {}, {}, true); auto reversed = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *reversedFixture.model, makeStationRows(reversedFixture), tolerances); ASSERT_FALSE(reversed.hasValue()); expectStatusCode(reversed.status(), "ineligible-node-station"); const auto jumpFixture = makeFixture(true, {}, {}, false, true); auto jumped = fesa::ResultRecovery::normalizeSectionResultantsToNodeStations( *jumpFixture.model, makeStationRows(jumpFixture), tolerances); ASSERT_FALSE(jumped.hasValue()); expectStatusCode(jumped.status(), "ineligible-node-station"); }