feat(shell-surface-pressure-load): step 4 - integrate-linear-shell-pressure

This commit is contained in:
KOKO\Mimi
2026-09-01 15:03:28 +09:00
parent 6630cb1006
commit 4b6d023680
9 changed files with 1017 additions and 4 deletions
+3
View File
@@ -11,6 +11,7 @@ add_executable(
unit/analysis/newton_iteration_log_test.cpp
unit/assembly/parallel_for_test.cpp
unit/assembly/load_assembler_test.cpp
unit/assembly/surface_pressure_assembler_test.cpp
unit/assembly/sparse_assembler_test.cpp
unit/app/output_paths_test.cpp
unit/constraints/boundary_condition_test.cpp
@@ -74,8 +75,10 @@ target_link_libraries(
add_executable(
fesa_integration_tests
integration/analysis/linear_static_analysis_test.cpp
integration/analysis/shell_surface_pressure_linear_test.cpp
integration/analysis/nonlinear_static_procedure_test.cpp
integration/app/fesa_application_test.cpp
integration/app/shell_surface_pressure_application_test.cpp
)
target_link_libraries(
@@ -0,0 +1,272 @@
#include "fesa/analysis/linear_static_analysis.h"
#include <gtest/gtest.h>
#include <array>
#include <atomic>
#include <chrono>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <fstream>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include <hdf5.h>
#include "fesa/analysis/analysis.h"
#include "fesa/app/fesa_application.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/results/results_writer.h"
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
namespace {
class TempDirectory {
public:
explicit TempDirectory(const std::string& label) {
static std::atomic<unsigned long long> sequence{0U};
const auto tick =
std::chrono::steady_clock::now().time_since_epoch().count();
path_ = std::filesystem::temp_directory_path() /
("fesa-surface-pressure-linear-" + label + "-" +
std::to_string(tick) + "-" +
std::to_string(sequence.fetch_add(1U)));
std::error_code error;
if (!std::filesystem::create_directory(path_, error) || error) {
throw std::runtime_error{"Unable to create surface pressure fixture."};
}
}
TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete;
~TempDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
void WriteText(const std::filesystem::path& path, const std::string& text) {
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
stream.write(text.data(), static_cast<std::streamsize>(text.size()));
if (!stream) {
throw std::runtime_error{"Unable to write surface pressure input."};
}
}
std::string ShellDeck(const std::string& source_type,
const std::string& connectivity,
const bool prescribed_displacement,
const bool with_pressure) {
std::string boundary;
if (prescribed_displacement) {
boundary = R"inp(*Boundary
N1, 1, 6
N2, 1, 2
N2, 3, 3, 5.0
N2, 4, 6
N3, 1, 6
N4, 1, 6
)inp";
} else {
boundary = "*Boundary\nAll, 1, 6\n";
}
const std::string surface = with_pressure
? "*Surface, TYPE=ELEMENT, NAME=Pressure\n"
"Shell-1.1, SPOS\n"
: "";
const std::string dsload = with_pressure
? "*Dsload\nPressure, P, 4.0\n"
: "";
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=)inp"} +
source_type + "\n1, " + connectivity + R"inp(
*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
)inp" +
surface + R"inp(*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" +
boundary + R"inp(*Step, name=Load, nlgeom=NO
*Static
0.1, 1., 0.01, 1.
)inp" +
dsload + "*End Step\n";
}
class CapturingResultsWriter final : public fesa::ResultsWriter {
public:
fesa::Status Write(const std::filesystem::path&, const fesa::Domain&,
const fesa::AnalysisState& state,
const std::vector<fesa::Diagnostic>&) override {
state_ = std::make_unique<fesa::AnalysisState>(state);
return fesa::Status::Ok();
}
const fesa::AnalysisState& State() const {
if (!state_) {
throw std::logic_error{"No linear state was captured."};
}
return *state_;
}
private:
std::unique_ptr<fesa::AnalysisState> state_;
};
fesa::AnalysisState RunLinear(const std::filesystem::path& input,
const std::filesystem::path& output) {
fesa::SerialParallelFor serial;
fesa::MklPardisoSolver solver;
CapturingResultsWriter writer;
std::unique_ptr<fesa::Analysis> analysis =
std::make_unique<fesa::LinearStaticAnalysis>(serial, solver, writer);
const fesa::Status status = analysis->Run({input, output});
if (!status.IsOk()) {
throw std::runtime_error{status.Diagnostics().empty()
? "Linear pressure analysis failed."
: status.Diagnostics().front().code};
}
return writer.State();
}
std::array<double, 3> SumTranslation(const fesa::Vector& values) {
std::array<double, 3> sum{};
for (std::size_t node = 0U; node < values.Size() / 6U; ++node) {
for (std::size_t component = 0U; component < 3U; ++component) {
sum[component] += values[node * 6U + component];
}
}
return sum;
}
} // namespace
TEST(LinearShellSurfacePressure,
UsesReferenceNormalAndAreaAfterPrescribedDisplacement) {
TempDirectory directory{"reference-configuration"};
const auto input = directory.Path() / "prescribed.inp";
WriteText(input, ShellDeck("S4", "1, 2, 3, 4", true, true));
const fesa::AnalysisState state =
RunLinear(input, directory.Path() / "captured.h5");
EXPECT_DOUBLE_EQ(state.Displacement()[8U], 5.0);
EXPECT_EQ(SumTranslation(state.ExternalForce()),
(std::array<double, 3>{0.0, 0.0, -4.0}));
}
TEST(LinearShellSurfacePressure,
AssemblesBeforeProjectionAndIncludesConstrainedReaction) {
TempDirectory directory{"reaction"};
const auto input = directory.Path() / "reaction.inp";
WriteText(input, ShellDeck("S4", "1, 2, 3, 4", false, true));
const fesa::AnalysisState state =
RunLinear(input, directory.Path() / "captured.h5");
EXPECT_EQ(SumTranslation(state.ExternalForce()),
(std::array<double, 3>{0.0, 0.0, -4.0}));
EXPECT_EQ(SumTranslation(state.Reaction()),
(std::array<double, 3>{0.0, 0.0, 4.0}));
}
TEST(LinearShellSurfacePressure,
SatisfiesFreeResidualGlobalResultantAndMoment) {
TempDirectory directory{"equilibrium"};
const auto input = directory.Path() / "equilibrium.inp";
WriteText(input, ShellDeck("S4", "1, 2, 3, 4", false, true));
const fesa::AnalysisState state =
RunLinear(input, directory.Path() / "captured.h5");
for (const double value : state.Equilibrium()) {
EXPECT_NEAR(value, 0.0, 1.0e-8);
}
EXPECT_NEAR(state.VerificationMetrics()[0U], 0.0, 1.0e-10);
}
TEST(LinearShellSurfacePressure, PreservesS4S4rMeshAndCyclicOrderInvariance) {
TempDirectory directory{"source-invariance"};
const auto s4_input = directory.Path() / "s4.inp";
const auto s4r_input = directory.Path() / "s4r.inp";
const auto cyclic_input = directory.Path() / "cyclic.inp";
WriteText(s4_input, ShellDeck("S4", "1, 2, 3, 4", false, true));
WriteText(s4r_input, ShellDeck("S4R", "1, 2, 3, 4", false, true));
WriteText(cyclic_input, ShellDeck("S4", "2, 3, 4, 1", false, true));
const fesa::AnalysisState s4 =
RunLinear(s4_input, directory.Path() / "s4.h5");
const fesa::AnalysisState s4r =
RunLinear(s4r_input, directory.Path() / "s4r.h5");
const fesa::AnalysisState cyclic =
RunLinear(cyclic_input, directory.Path() / "cyclic.h5");
ASSERT_EQ(s4.ExternalForce().Size(), s4r.ExternalForce().Size());
ASSERT_EQ(s4.ExternalForce().Size(), cyclic.ExternalForce().Size());
for (std::size_t index = 0U; index < s4.ExternalForce().Size(); ++index) {
EXPECT_DOUBLE_EQ(s4.ExternalForce()[index], s4r.ExternalForce()[index]);
EXPECT_DOUBLE_EQ(s4.ExternalForce()[index], cyclic.ExternalForce()[index]);
EXPECT_DOUBLE_EQ(s4.Reaction()[index], s4r.Reaction()[index]);
EXPECT_DOUBLE_EQ(s4.Reaction()[index], cyclic.Reaction()[index]);
}
}
TEST(LinearShellSurfacePressure, WritesSchemaZeroWithoutPressureDataset) {
TempDirectory directory{"schema-zero"};
const auto input = directory.Path() / "pressure.inp";
const auto output = directory.Path() / "pressure_results.h5";
WriteText(input, ShellDeck("S4", "1, 2, 3, 4", false, true));
EXPECT_EQ(fesa::FesaApplication{}.Run({input.string()}), 0);
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
const hid_t file =
H5Fopen(output.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
ASSERT_GE(file, 0);
const hid_t metadata = H5Gopen2(file, "/metadata", H5P_DEFAULT);
ASSERT_GE(metadata, 0);
const hid_t schema = H5Aopen(metadata, "schema_version", H5P_DEFAULT);
ASSERT_GE(schema, 0);
std::uint64_t schema_version = 0U;
ASSERT_GE(H5Aread(schema, H5T_NATIVE_UINT64, &schema_version), 0);
EXPECT_EQ(schema_version, 0U);
EXPECT_EQ(H5Lexists(file, "/steps/Step-1/frames/0/surface_pressure",
H5P_DEFAULT),
0);
EXPECT_EQ(H5Lexists(file, "/pressure", H5P_DEFAULT), 0);
EXPECT_GE(H5Aclose(schema), 0);
EXPECT_GE(H5Gclose(metadata), 0);
EXPECT_GE(H5Fclose(file), 0);
}
@@ -0,0 +1,145 @@
#include "fesa/app/fesa_application.h"
#include <gtest/gtest.h>
#include <atomic>
#include <chrono>
#include <filesystem>
#include <fstream>
#include <iterator>
#include <stdexcept>
#include <string>
#include <vector>
#include <hdf5.h>
namespace {
class TempDirectory {
public:
explicit TempDirectory(const std::string& label) {
static std::atomic<unsigned long long> sequence{0U};
const auto tick =
std::chrono::steady_clock::now().time_since_epoch().count();
path_ = std::filesystem::temp_directory_path() /
("fesa-surface-pressure-app-" + label + "-" +
std::to_string(tick) + "-" +
std::to_string(sequence.fetch_add(1U)));
std::error_code error;
if (!std::filesystem::create_directory(path_, error) || error) {
throw std::runtime_error{"Unable to create application fixture."};
}
}
TempDirectory(const TempDirectory&) = delete;
TempDirectory& operator=(const TempDirectory&) = delete;
~TempDirectory() {
std::error_code ignored;
std::filesystem::remove_all(path_, ignored);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
std::filesystem::path path_;
};
void WriteText(const std::filesystem::path& path, const std::string& text) {
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
stream.write(text.data(), static_cast<std::streamsize>(text.size()));
if (!stream) {
throw std::runtime_error{"Unable to write application input."};
}
}
std::vector<char> ReadBytes(const std::filesystem::path& path) {
std::ifstream stream{path, std::ios::binary};
return {std::istreambuf_iterator<char>{stream},
std::istreambuf_iterator<char>{}};
}
std::string ShellDeck(const bool with_pressure, const bool collapsed) {
const std::string third_node = collapsed ? "3, 1., 0., 0.\n"
: "3, 1., 1., 0.\n";
const std::string surface = with_pressure
? "*Surface, TYPE=ELEMENT, NAME=Pressure\n"
"Shell-1.1, SPOS\n"
: "";
const std::string dsload = with_pressure
? "*Dsload\nPressure, P, 4.0\n"
: "";
return std::string{R"inp(*Part, name=ShellPart
*Node
1, 0., 0., 0.
2, 1., 0., 0.
)inp"} +
third_node + R"inp(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
)inp" +
surface + R"inp(*Nset, nset=All, instance=Shell-1
1, 2, 3, 4
*End Assembly
*Material, name=Steel
*Elastic
1000., 0.25
*Boundary
All, 1, 6
*Step, name=Load, nlgeom=NO
*Static
0.1, 1., 0.01, 1.
)inp" +
dsload + "*End Step\n";
}
} // namespace
TEST(ShellSurfacePressureApplication,
LinearFailurePreservesExistingFinalAndNoCandidate) {
TempDirectory directory{"preserve-final"};
const auto input = directory.Path() / "invalid-pressure.inp";
const auto output = directory.Path() / "invalid-pressure_results.h5";
WriteText(input, ShellDeck(true, true));
const std::vector<char> previous{'p', 'r', 'i', 'o', 'r'};
{
std::ofstream stream{output, std::ios::binary | std::ios::trunc};
stream.write(previous.data(),
static_cast<std::streamsize>(previous.size()));
}
const int exit_code = fesa::FesaApplication{}.Run({input.string()});
EXPECT_NE(exit_code, 0);
EXPECT_EQ(ReadBytes(output), previous);
}
TEST(ShellSurfacePressureApplication,
InputWithoutPressureRetainsExistingLinearBehavior) {
TempDirectory directory{"no-pressure"};
const auto input = directory.Path() / "no-pressure.inp";
const auto output = directory.Path() / "no-pressure_results.h5";
WriteText(input, ShellDeck(false, false));
EXPECT_EQ(fesa::FesaApplication{}.Run({input.string()}), 0);
ASSERT_TRUE(std::filesystem::exists(output));
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
const hid_t file =
H5Fopen(output.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
ASSERT_GE(file, 0);
EXPECT_GT(H5Lexists(file, "/steps/Step-1/frames/0/nodal/displacement",
H5P_DEFAULT),
0);
EXPECT_EQ(H5Lexists(file, "/steps/Step-1/frames/0/surface_pressure",
H5P_DEFAULT),
0);
EXPECT_GE(H5Fclose(file), 0);
}
@@ -0,0 +1,195 @@
#include "fesa/assembly/surface_pressure_assembler.h"
#include <gtest/gtest.h>
#include <array>
#include <cstddef>
#include <cstdint>
#include <filesystem>
#include <memory>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/assembly/parallel_for.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/model/domain.h"
namespace {
struct PressureFixture {
std::unique_ptr<fesa::Domain> domain;
std::unique_ptr<fesa::AnalysisModel> model;
std::unique_ptr<fesa::DofManager> dofs;
};
PressureFixture MakeFixture(const std::vector<double>& magnitudes) {
const std::filesystem::path source{"surface-pressure-assembly.inp"};
fesa::ModelDefinition definition{};
definition.source_path = source;
definition.source_content_identity = "fnv1a64:surfacepressure";
definition.nodes = {
{{"Shell-1", 1, "1"}, {0.0, 0.0, 0.0}, {source, 2U}},
{{"Shell-1", 2, "2"}, {1.0, 0.0, 0.0}, {source, 3U}},
{{"Shell-1", 3, "3"}, {1.0, 1.0, 0.0}, {source, 4U}},
{{"Shell-1", 4, "4"}, {0.0, 1.0, 0.0}, {source, 5U}}};
definition.materials = {{"Steel", 1000.0, 0.25, {source, 6U}}};
definition.shell_sections = {{"Shell", 0.1, 0U, {source, 7U}}};
definition.shell_elements = {{{"Shell-1", 1, "1"},
fesa::ShellSourceElementType::kS4,
{0U, 1U, 2U, 3U},
0U,
0U,
{source, 8U}}};
for (std::size_t node = 0U; node < definition.nodes.size(); ++node) {
definition.shell_node_initial_frames.push_back(
{static_cast<fesa::EntityIndex>(node),
{0.0, 0.0, 1.0},
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0}});
}
definition.element_surfaces = {
{"Pressure-Surface",
"PRESSURE-SURFACE",
{{0U,
0U,
"ShellPart",
"SHELLPART",
"Shell-1",
"SHELL-1",
1,
"1",
"S4",
fesa::ShellSurfaceSide::kSpos,
{source, 12U},
0U,
fesa::SurfaceMemberKind::kDirect,
"",
""}},
{source, 11U},
0U}};
std::vector<fesa::SurfacePressureLoadDefinition> pressure_loads;
pressure_loads.reserve(magnitudes.size());
for (std::size_t ordinal = 0U; ordinal < magnitudes.size(); ++ordinal) {
pressure_loads.push_back({"Step-1",
"STEP-1",
0U,
"Pressure-Surface",
"PRESSURE-SURFACE",
"P",
magnitudes[ordinal],
std::to_string(magnitudes[ordinal]),
{source, 20U + ordinal},
{source, 21U + ordinal},
ordinal,
ordinal});
}
definition.steps.emplace_back(
"Step-1", std::vector<fesa::PrescribedDisplacementDefinition>{},
std::vector<fesa::NodalLoad>{}, std::move(pressure_loads),
fesa::StaticProcedureKind::kLinearSmallDisplacement, false, 0.1, 1.0,
0.01, 1.0, fesa::SourceLocation{source, 19U});
auto domain_result = fesa::Domain::Create(std::move(definition));
if (!domain_result.HasValue()) {
throw std::runtime_error{"Unable to construct pressure Domain fixture."};
}
auto domain =
std::make_unique<fesa::Domain>(std::move(domain_result.Value()));
auto model_result = fesa::AnalysisModel::Create(*domain);
if (!model_result.HasValue()) {
throw std::runtime_error{"Unable to construct pressure AnalysisModel."};
}
auto model =
std::make_unique<fesa::AnalysisModel>(std::move(model_result.Value()));
auto dofs_result = fesa::DofManager::Create(*model);
if (!dofs_result.HasValue()) {
throw std::runtime_error{"Unable to construct pressure DofManager."};
}
auto dofs =
std::make_unique<fesa::DofManager>(std::move(dofs_result.Value()));
return {std::move(domain), std::move(model), std::move(dofs)};
}
std::vector<double> Values(const fesa::Vector& vector) {
return {vector.Data(), vector.Data() + vector.Size()};
}
} // namespace
TEST(SurfacePressureAssembly, AssemblesFullDofsInPressureFacetAndRecordOrder) {
auto fixture = MakeFixture({2.0});
fesa::Vector coordinates{fixture.dofs->FullDofCount()};
coordinates[2U] = 11.0;
fesa::SerialParallelFor serial;
const auto result = fesa::AssembleSurfacePressure(
*fixture.model, *fixture.dofs, coordinates, 1.0,
fesa::SurfacePressureConfiguration::kReference, serial);
ASSERT_TRUE(result.HasValue());
EXPECT_TRUE(result.Value().diagnostics.empty());
ASSERT_EQ(result.Value().full_external_force.Size(), 24U);
for (std::size_t node = 0U; node < 4U; ++node) {
const std::size_t offset = node * 6U;
EXPECT_DOUBLE_EQ(result.Value().full_external_force[offset], 0.0);
EXPECT_DOUBLE_EQ(result.Value().full_external_force[offset + 1U], 0.0);
EXPECT_DOUBLE_EQ(result.Value().full_external_force[offset + 2U], -0.5);
EXPECT_DOUBLE_EQ(result.Value().full_external_force[offset + 3U], 0.0);
EXPECT_DOUBLE_EQ(result.Value().full_external_force[offset + 4U], 0.0);
EXPECT_DOUBLE_EQ(result.Value().full_external_force[offset + 5U], 0.0);
}
EXPECT_EQ(result.Value().full_external_tangent.Rows(), 24U);
EXPECT_EQ(result.Value().full_external_tangent.Columns(), 24U);
for (const double value : result.Value().full_external_tangent.Values()) {
EXPECT_EQ(value, 0.0);
}
}
TEST(SurfacePressureAssembly,
AddsRepeatedRecordsAndAppliesUnitFactorExactlyOnce) {
auto fixture = MakeFixture({6.0, 2.0});
fesa::Vector coordinates{fixture.dofs->FullDofCount()};
fesa::SerialParallelFor serial;
const auto result = fesa::AssembleSurfacePressure(
*fixture.model, *fixture.dofs, coordinates, 0.5,
fesa::SurfacePressureConfiguration::kReference, serial);
ASSERT_TRUE(result.HasValue());
for (std::size_t node = 0U; node < 4U; ++node) {
EXPECT_DOUBLE_EQ(result.Value().full_external_force[node * 6U + 2U],
-1.0);
}
}
TEST(SurfacePressureAssembly,
IsBitwiseStableForSerialAndParallelLocalEvaluation) {
auto fixture = MakeFixture({1.0e16, -1.0e16, 3.5});
fesa::Vector coordinates{fixture.dofs->FullDofCount()};
fesa::SerialParallelFor serial;
fesa::TbbParallelFor parallel;
const auto serial_result = fesa::AssembleSurfacePressure(
*fixture.model, *fixture.dofs, coordinates, 1.0,
fesa::SurfacePressureConfiguration::kReference, serial);
const auto parallel_result = fesa::AssembleSurfacePressure(
*fixture.model, *fixture.dofs, coordinates, 1.0,
fesa::SurfacePressureConfiguration::kReference, parallel);
ASSERT_TRUE(serial_result.HasValue());
ASSERT_TRUE(parallel_result.HasValue());
EXPECT_EQ(Values(serial_result.Value().full_external_force),
Values(parallel_result.Value().full_external_force));
EXPECT_EQ(serial_result.Value().full_external_tangent.RowOffsets(),
parallel_result.Value().full_external_tangent.RowOffsets());
EXPECT_EQ(serial_result.Value().full_external_tangent.ColumnIndices(),
parallel_result.Value().full_external_tangent.ColumnIndices());
EXPECT_EQ(serial_result.Value().full_external_tangent.Values(),
parallel_result.Value().full_external_tangent.Values());
EXPECT_TRUE(serial_result.Value().diagnostics.empty());
EXPECT_TRUE(parallel_result.Value().diagnostics.empty());
}
@@ -989,6 +989,9 @@ TEST(Hdf5ResultsWriter, WritesExactSchemaShapesAttributesAndIdentity) {
"/steps/Step-1/frames/0/element/transverse_shear_stress",
H5P_DEFAULT),
0);
EXPECT_EQ(H5Lexists(file.Get(), "/steps/Step-1/frames/0/surface_pressure",
H5P_DEFAULT),
0);
}
TEST(Hdf5ResultsWriter, WritesMandatoryOutputsDespiteOutputRequests) {