feat(linear-static-mitc4-shell): step 12 - shell-linear-static-flow
This commit is contained in:
@@ -11,6 +11,7 @@
|
||||
#include <cstddef>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <limits>
|
||||
#include <memory>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
@@ -95,6 +96,60 @@ Tip, 1, )inp" + std::to_string(tipForce) + R"inp(
|
||||
)inp";
|
||||
}
|
||||
|
||||
std::string shellDeck(
|
||||
const std::string& boundaryBlock,
|
||||
const std::string& loadBlock = {}) {
|
||||
return std::string{R"inp(*Part, name=ShellPart
|
||||
*Node
|
||||
1, 0., 0., 0.
|
||||
2, 1., 0., 0.
|
||||
3, 1., 1., 0.
|
||||
4, 0., 1., 0.
|
||||
*Element, type=S4
|
||||
1, 1, 2, 3, 4
|
||||
*Elset, elset=ShellSet
|
||||
1
|
||||
*Shell Section, elset=ShellSet, material=Steel
|
||||
0.1
|
||||
*End Part
|
||||
*Assembly, name=Assembly
|
||||
*Instance, name=Shell-1, part=ShellPart
|
||||
*End Instance
|
||||
*Nset, nset=N1, instance=Shell-1
|
||||
1
|
||||
*Nset, nset=N2, instance=Shell-1
|
||||
2
|
||||
*Nset, nset=N3, instance=Shell-1
|
||||
3
|
||||
*Nset, nset=N4, instance=Shell-1
|
||||
4
|
||||
*Nset, nset=All, instance=Shell-1
|
||||
1, 2, 3, 4
|
||||
*End Assembly
|
||||
*Material, name=Steel
|
||||
*Elastic
|
||||
1000., 0.25
|
||||
)inp"} + boundaryBlock + R"inp(*Step, name=Load, nlgeom=NO
|
||||
*Static
|
||||
0.1, 1., 0.01, 1.
|
||||
)inp" + loadBlock + R"inp(*End Step
|
||||
)inp";
|
||||
}
|
||||
|
||||
std::string allConstrainedShellDeck() {
|
||||
return shellDeck("*Boundary\nAll, 1, 6\n");
|
||||
}
|
||||
|
||||
std::string prescribedShellDeck() {
|
||||
return shellDeck(R"inp(*Boundary
|
||||
N1, 1, 6
|
||||
N2, 1, 1, 0.1
|
||||
N2, 2, 6
|
||||
N3, 2, 6
|
||||
N4, 1, 6
|
||||
)inp");
|
||||
}
|
||||
|
||||
// The pure Template Method spy makes the eight public lifecycle hooks observable
|
||||
// without coupling the ordering assertion to any solver backend.
|
||||
class SpyAnalysis final : public fesa::Analysis {
|
||||
@@ -167,6 +222,61 @@ private:
|
||||
mutable int solveCalls_{0};
|
||||
};
|
||||
|
||||
class RecordingMklSolver final : public fesa::LinearSolver {
|
||||
public:
|
||||
fesa::Status factorize(const fesa::SparseMatrix& matrix) override {
|
||||
++factorizeCalls_;
|
||||
factorizedDimension_ = matrix.rows();
|
||||
if (matrix.rows() == 1U && matrix.columns() == 1U &&
|
||||
matrix.values().size() == 1U) {
|
||||
scalarStiffness_ = matrix.values()[0U];
|
||||
}
|
||||
return backend_.factorize(matrix);
|
||||
}
|
||||
|
||||
fesa::Status solve(
|
||||
const fesa::Vector& rhs, fesa::Vector& solution) const override {
|
||||
++solveCalls_;
|
||||
if (rhs.size() == 0U) {
|
||||
rhs_.clear();
|
||||
} else {
|
||||
rhs_.assign(rhs.data(), rhs.data() + rhs.size());
|
||||
}
|
||||
return backend_.solve(rhs, solution);
|
||||
}
|
||||
|
||||
int factorizeCalls() const noexcept { return factorizeCalls_; }
|
||||
int solveCalls() const noexcept { return solveCalls_; }
|
||||
std::size_t factorizedDimension() const noexcept {
|
||||
return factorizedDimension_;
|
||||
}
|
||||
double scalarStiffness() const noexcept { return scalarStiffness_; }
|
||||
const std::vector<double>& rhs() const noexcept { return rhs_; }
|
||||
|
||||
private:
|
||||
fesa::MklPardisoSolver backend_;
|
||||
int factorizeCalls_{0};
|
||||
mutable int solveCalls_{0};
|
||||
std::size_t factorizedDimension_{0U};
|
||||
double scalarStiffness_{0.0};
|
||||
mutable std::vector<double> rhs_;
|
||||
};
|
||||
|
||||
class NonfiniteLinearSolver final : public fesa::LinearSolver {
|
||||
public:
|
||||
fesa::Status factorize(const fesa::SparseMatrix&) override {
|
||||
return fesa::Status::ok();
|
||||
}
|
||||
|
||||
fesa::Status solve(
|
||||
const fesa::Vector&, fesa::Vector& solution) const override {
|
||||
for (std::size_t index = 0U; index < solution.size(); ++index) {
|
||||
solution[index] = (std::numeric_limits<double>::quiet_NaN)();
|
||||
}
|
||||
return fesa::Status::ok();
|
||||
}
|
||||
};
|
||||
|
||||
class SpyResultsWriter final : public fesa::ResultsWriter {
|
||||
public:
|
||||
explicit SpyResultsWriter(std::vector<std::string>& events)
|
||||
@@ -198,6 +308,7 @@ public:
|
||||
const std::vector<fesa::Diagnostic>& diagnostics) override {
|
||||
outputPath_ = outputPath;
|
||||
nodeCount_ = domain.nodes().size();
|
||||
shellElementCount_ = domain.shellElements().size();
|
||||
state_ = std::make_unique<fesa::AnalysisState>(state);
|
||||
diagnostics_ = diagnostics;
|
||||
return fesa::Status::ok();
|
||||
@@ -214,6 +325,9 @@ public:
|
||||
return outputPath_;
|
||||
}
|
||||
std::size_t nodeCount() const noexcept { return nodeCount_; }
|
||||
std::size_t shellElementCount() const noexcept {
|
||||
return shellElementCount_;
|
||||
}
|
||||
const std::vector<fesa::Diagnostic>& diagnostics() const noexcept {
|
||||
return diagnostics_;
|
||||
}
|
||||
@@ -221,6 +335,7 @@ public:
|
||||
private:
|
||||
std::filesystem::path outputPath_;
|
||||
std::size_t nodeCount_{0U};
|
||||
std::size_t shellElementCount_{0U};
|
||||
std::unique_ptr<fesa::AnalysisState> state_;
|
||||
std::vector<fesa::Diagnostic> diagnostics_;
|
||||
};
|
||||
@@ -298,3 +413,120 @@ TEST(LinearStaticCli, RealPipelineHandlesAnalyticalAndNonzeroPrescription) {
|
||||
EXPECT_EQ(state.gaussResults().size(), 2U);
|
||||
EXPECT_EQ(state.stressResults().size(), 2U);
|
||||
}
|
||||
|
||||
// MITC4-FLOW-001
|
||||
TEST(Mitc4ShellCli, UsesExistingLifecycleAndExactlyOneFactorization) {
|
||||
TempDirectory directory{"shell-order"};
|
||||
const auto input = directory.path() / "all-constrained-shell.inp";
|
||||
const auto output = directory.path() / "results.h5";
|
||||
writeText(input, allConstrainedShellDeck());
|
||||
|
||||
std::vector<std::string> adapterEvents;
|
||||
fesa::SerialParallelFor serial;
|
||||
SpyLinearSolver solver{adapterEvents};
|
||||
SpyResultsWriter writer{adapterEvents};
|
||||
fesa::LinearStaticAnalysis analysis{serial, solver, writer};
|
||||
|
||||
ASSERT_TRUE(analysis.run({input, output}).isOk());
|
||||
EXPECT_EQ(solver.factorizeCalls(), 1);
|
||||
EXPECT_EQ(solver.solveCalls(), 1);
|
||||
EXPECT_EQ(writer.writeCalls(), 1);
|
||||
EXPECT_EQ(
|
||||
adapterEvents,
|
||||
(std::vector<std::string>{
|
||||
"solver-factorize", "solver-solve", "writer-write"}));
|
||||
}
|
||||
|
||||
// MITC4-FLOW-002
|
||||
TEST(Mitc4ShellCli, AppliesKfcForNonzeroPrescribedDisplacement) {
|
||||
TempDirectory directory{"shell-prescribed"};
|
||||
const auto input = directory.path() / "prescribed-shell.inp";
|
||||
const auto output = directory.path() / "captured-results.h5";
|
||||
writeText(input, prescribedShellDeck());
|
||||
|
||||
fesa::SerialParallelFor serial;
|
||||
RecordingMklSolver solver;
|
||||
CapturingResultsWriter writer;
|
||||
fesa::LinearStaticAnalysis analysis{serial, solver, writer};
|
||||
|
||||
const auto status = analysis.run({input, output});
|
||||
for (const auto& diagnostic : status.diagnostics()) {
|
||||
EXPECT_TRUE(status.isOk())
|
||||
<< diagnostic.code << ": " << diagnostic.message;
|
||||
}
|
||||
ASSERT_TRUE(status.isOk());
|
||||
ASSERT_EQ(solver.factorizeCalls(), 1);
|
||||
ASSERT_EQ(solver.solveCalls(), 1);
|
||||
ASSERT_EQ(solver.factorizedDimension(), 1U);
|
||||
ASSERT_EQ(solver.rhs().size(), 1U);
|
||||
EXPECT_NEAR(solver.scalarStiffness(), 440.0 / 9.0, 1.0e-12);
|
||||
// Ff is exactly zero, so this nonzero RHS is solely -Kfc*dc.
|
||||
EXPECT_NEAR(solver.rhs()[0U], -4.0 / 9.0, 1.0e-12);
|
||||
|
||||
EXPECT_EQ(writer.outputPath(), output);
|
||||
EXPECT_EQ(writer.nodeCount(), 4U);
|
||||
EXPECT_EQ(writer.shellElementCount(), 1U);
|
||||
const auto& state = writer.state();
|
||||
ASSERT_EQ(state.displacement().size(), 24U);
|
||||
EXPECT_NEAR(state.displacement()[6U], 0.1, 1.0e-12);
|
||||
EXPECT_NEAR(state.displacement()[12U], -1.0 / 110.0, 1.0e-12);
|
||||
EXPECT_NEAR(state.verificationMetrics()[0U], 0.0, 1.0e-10);
|
||||
EXPECT_EQ(state.shellResults().size(), 4U);
|
||||
EXPECT_GT(state.physicalStrainEnergy(), 0.0);
|
||||
}
|
||||
|
||||
// MITC4-FLOW-003
|
||||
TEST(Mitc4ShellCli, RejectsSingularAndAcceptsZeroByZeroFreeSystem) {
|
||||
TempDirectory directory{"shell-singular-all"};
|
||||
const auto singularInput = directory.path() / "singular-shell.inp";
|
||||
const auto constrainedInput = directory.path() / "constrained-shell.inp";
|
||||
writeText(singularInput, shellDeck(""));
|
||||
writeText(constrainedInput, allConstrainedShellDeck());
|
||||
|
||||
fesa::SerialParallelFor serial;
|
||||
fesa::MklPardisoSolver singularSolver;
|
||||
std::vector<std::string> singularEvents;
|
||||
SpyResultsWriter singularWriter{singularEvents};
|
||||
fesa::LinearStaticAnalysis singularAnalysis{
|
||||
serial, singularSolver, singularWriter};
|
||||
const auto singular = singularAnalysis.run(
|
||||
{singularInput, directory.path() / "singular.h5"});
|
||||
ASSERT_FALSE(singular.isOk());
|
||||
EXPECT_EQ(singular.failureCategory(), fesa::FailureCategory::solver);
|
||||
EXPECT_EQ(singularWriter.writeCalls(), 0);
|
||||
|
||||
RecordingMklSolver constrainedSolver;
|
||||
CapturingResultsWriter constrainedWriter;
|
||||
fesa::LinearStaticAnalysis constrainedAnalysis{
|
||||
serial, constrainedSolver, constrainedWriter};
|
||||
const auto constrained = constrainedAnalysis.run(
|
||||
{constrainedInput, directory.path() / "constrained.h5"});
|
||||
ASSERT_TRUE(constrained.isOk());
|
||||
EXPECT_EQ(constrainedSolver.factorizeCalls(), 1);
|
||||
EXPECT_EQ(constrainedSolver.factorizedDimension(), 0U);
|
||||
EXPECT_EQ(constrainedSolver.solveCalls(), 1);
|
||||
EXPECT_TRUE(constrainedSolver.rhs().empty());
|
||||
EXPECT_EQ(constrainedWriter.state().shellResults().size(), 4U);
|
||||
}
|
||||
|
||||
// MITC4-FLOW-004
|
||||
TEST(Mitc4ShellCli, DoesNotWriteAnInvalidRecoveryCandidate) {
|
||||
TempDirectory directory{"shell-invalid-candidate"};
|
||||
const auto input = directory.path() / "invalid-recovery-shell.inp";
|
||||
writeText(input, prescribedShellDeck());
|
||||
|
||||
fesa::SerialParallelFor serial;
|
||||
NonfiniteLinearSolver solver;
|
||||
std::vector<std::string> adapterEvents;
|
||||
SpyResultsWriter writer{adapterEvents};
|
||||
fesa::LinearStaticAnalysis analysis{serial, solver, writer};
|
||||
const auto status = analysis.run(
|
||||
{input, directory.path() / "must-not-exist.h5"});
|
||||
|
||||
ASSERT_FALSE(status.isOk());
|
||||
EXPECT_EQ(status.failureCategory(), fesa::FailureCategory::model);
|
||||
ASSERT_FALSE(status.diagnostics().empty());
|
||||
EXPECT_EQ(status.diagnostics().front().code, "nonfinite-recovery-value");
|
||||
EXPECT_EQ(writer.writeCalls(), 0);
|
||||
EXPECT_TRUE(adapterEvents.empty());
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user