feat: add eigen2 frequency pipeline and fail-closed validation
Implement MITC4 mass, constrained whitening, MKL FEAST, modal HDF5, and deterministic Value comparison. MSVC Debug builds; 394 of 395 tests pass. Original-model all-c residual and the documented S3 static contract conflict remain unresolved; this is an implementation checkpoint, not feature acceptance.
This commit is contained in:
@@ -157,6 +157,9 @@ TEST(DomainMappingComponents,
|
||||
EXPECT_EQ(domain.Steps()[0].Loads().size(), 1U);
|
||||
ASSERT_EQ(domain.Warnings().size(), 3U);
|
||||
EXPECT_EQ(domain.Warnings()[0].keyword, "PREPRINT");
|
||||
EXPECT_EQ(
|
||||
domain.Warnings()[0].message,
|
||||
"The allowlisted Abaqus keyword is ignored without semantic effect.");
|
||||
EXPECT_EQ(domain.Warnings()[1].keyword, "OUTPUT");
|
||||
EXPECT_EQ(domain.Warnings()[2].keyword, "NODE OUTPUT");
|
||||
}
|
||||
|
||||
@@ -0,0 +1,207 @@
|
||||
#include <gtest/gtest.h>
|
||||
|
||||
#include <algorithm>
|
||||
#include <filesystem>
|
||||
#include <fstream>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
|
||||
#include "fesa/io/abaqus/domain_mapper.h"
|
||||
#include "fesa/io/abaqus/input_reader.h"
|
||||
|
||||
namespace {
|
||||
|
||||
class TemporaryInputFile {
|
||||
public:
|
||||
TemporaryInputFile(const std::string& stem, const std::string& content)
|
||||
: path_{std::filesystem::temp_directory_path() /
|
||||
("fesa-eigen-domain-" + stem + ".inp")} {
|
||||
std::ofstream stream{path_, std::ios::binary | std::ios::trunc};
|
||||
stream.write(content.data(), static_cast<std::streamsize>(content.size()));
|
||||
if (!stream) {
|
||||
throw std::runtime_error{"Unable to create eigen domain fixture."};
|
||||
}
|
||||
}
|
||||
|
||||
~TemporaryInputFile() {
|
||||
std::error_code error;
|
||||
std::filesystem::remove(path_, error);
|
||||
}
|
||||
|
||||
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||
|
||||
private:
|
||||
std::filesystem::path path_;
|
||||
};
|
||||
|
||||
fesa::Result<fesa::Domain> MapText(const std::string& stem,
|
||||
const std::string& content) {
|
||||
const TemporaryInputFile input{stem, content};
|
||||
auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
|
||||
if (!parsed.HasValue()) {
|
||||
return fesa::Result<fesa::Domain>::Failure(parsed.GetStatus());
|
||||
}
|
||||
return fesa::AbaqusDomainMapper{}.Map(parsed.Value());
|
||||
}
|
||||
|
||||
std::string EigenDeck(const std::string& density = "2700.,",
|
||||
const std::string& frequency = "10, , , , ,") {
|
||||
return R"inp(*Part, name=ShellPart
|
||||
*Node
|
||||
1, 0., 0., 0.
|
||||
2, 1., 0., 0.
|
||||
3, 1., 1., 0.
|
||||
4, 0., 1., 0.
|
||||
*Element, type=S4R
|
||||
1, 1, 2, 3, 4
|
||||
*Elset, elset=ShellSet
|
||||
1
|
||||
*Shell Section, elset=ShellSet, material=Steel
|
||||
0.5, 5
|
||||
*End Part
|
||||
*Assembly, name=Assembly
|
||||
*Instance, name=Shell-1, part=ShellPart
|
||||
*End Instance
|
||||
*Nset, nset=Root, instance=Shell-1
|
||||
1
|
||||
*End Assembly
|
||||
*Material, name=Steel
|
||||
*Density
|
||||
)inp" + density +
|
||||
R"inp(
|
||||
*Elastic
|
||||
2.1e11, 0.3
|
||||
*Boundary
|
||||
Root, 1, 6
|
||||
*Step, name=Step-2, nlgeom=NO, perturbation
|
||||
*Frequency, eigensolver=Lanczos, sim, acoustic coupling=on, normalization=mass
|
||||
)inp" + frequency +
|
||||
R"inp(
|
||||
*Restart, write, frequency=0
|
||||
*Output, field
|
||||
*Node Output, variable=PRESELECT
|
||||
*Output, history
|
||||
*Energy Output, variable=ALL
|
||||
*End Step
|
||||
)inp";
|
||||
}
|
||||
|
||||
const fesa::Diagnostic* FindDiagnostic(const fesa::Status& status,
|
||||
const std::string& code) {
|
||||
for (const auto& diagnostic : status.Diagnostics()) {
|
||||
if (diagnostic.code == code) {
|
||||
return &diagnostic;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, PreservesFrequencyDensityAndSourceStepIdentity) {
|
||||
auto result = MapText("accepted", EigenDeck());
|
||||
ASSERT_TRUE(result.HasValue());
|
||||
const fesa::Domain& domain = result.Value();
|
||||
|
||||
ASSERT_EQ(domain.Densities().size(), 1U);
|
||||
EXPECT_EQ(domain.Densities()[0].material_index, fesa::EntityIndex{0U});
|
||||
EXPECT_DOUBLE_EQ(domain.Densities()[0].rho, 2700.0);
|
||||
ASSERT_NE(domain.Frequency(), nullptr);
|
||||
EXPECT_EQ(domain.Frequency()->source_step_name_raw, "Step-2");
|
||||
EXPECT_EQ(domain.Frequency()->source_step_name_canonical, "STEP-2");
|
||||
EXPECT_EQ(domain.Frequency()->requested_mode_count, 10);
|
||||
EXPECT_TRUE(domain.Frequency()->perturbation_explicit);
|
||||
EXPECT_TRUE(domain.Frequency()->sim_explicit);
|
||||
ASSERT_EQ(domain.Steps().Size(), 1U);
|
||||
EXPECT_TRUE(domain.Steps()[0U].IsFrequency());
|
||||
EXPECT_EQ(domain.Steps()[0U].Frequency(), domain.Frequency());
|
||||
EXPECT_EQ(domain.Steps()[0U].Name(), "Step-2");
|
||||
ASSERT_EQ(domain.Steps()[0U].BoundaryConditions().size(), 6U);
|
||||
EXPECT_TRUE(domain.Steps()[0U].Loads().empty());
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsInvalidDensityAndFrequencySlots) {
|
||||
auto density = MapText("bad-density", EigenDeck("2700., 1."));
|
||||
ASSERT_FALSE(density.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(density.GetStatus(), "invalid-density-data"),
|
||||
nullptr);
|
||||
|
||||
auto slots = MapText("bad-slots", EigenDeck("2700.,", "10, 0"));
|
||||
ASSERT_FALSE(slots.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(slots.GetStatus(), "invalid-frequency-data"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsMissingActiveDensityAndNonzeroBoundary) {
|
||||
std::string missing_deck = EigenDeck();
|
||||
const std::string density_block = "*Density\n2700.,\n";
|
||||
const std::size_t density_location = missing_deck.find(density_block);
|
||||
ASSERT_NE(density_location, std::string::npos);
|
||||
missing_deck.erase(density_location, density_block.size());
|
||||
auto missing = MapText("missing-density", missing_deck);
|
||||
ASSERT_FALSE(missing.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(missing.GetStatus(), "missing-material-density"),
|
||||
nullptr);
|
||||
|
||||
std::string nonzero = EigenDeck();
|
||||
const std::string source = "Root, 1, 6";
|
||||
const std::size_t location = nonzero.rfind(source);
|
||||
ASSERT_NE(location, std::string::npos);
|
||||
nonzero.replace(location, source.size(), "Root, 1, 6, 1.");
|
||||
auto boundary = MapText("nonzero-boundary", nonzero);
|
||||
ASSERT_FALSE(boundary.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(boundary.GetStatus(), "unsupported-eigen-boundary"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsMixedProceduresInvalidOutputAndSurface) {
|
||||
std::string mixed = EigenDeck();
|
||||
mixed.insert(mixed.find("*Restart"), "*Static\n0.1, 1., 0.01, 1.\n");
|
||||
auto mixed_result = MapText("mixed-procedures", mixed);
|
||||
ASSERT_FALSE(mixed_result.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(mixed_result.GetStatus(),
|
||||
"unsupported-analysis-procedure"),
|
||||
nullptr);
|
||||
|
||||
auto wrong_output = MapText(
|
||||
"wrong-output",
|
||||
EigenDeck().replace(EigenDeck().find("*Output, field\n*Node Output"),
|
||||
std::string{"*Output, field\n*Node Output"}.size(),
|
||||
"*Output, history\n*Node Output"));
|
||||
ASSERT_FALSE(wrong_output.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(wrong_output.GetStatus(), "unsupported-keyword"),
|
||||
nullptr);
|
||||
|
||||
std::string surface = EigenDeck();
|
||||
surface.insert(surface.find("*Material"),
|
||||
"*Surface, name=Face, type=ELEMENT\n");
|
||||
auto surface_result = MapText("surface", surface);
|
||||
ASSERT_FALSE(surface_result.HasValue());
|
||||
}
|
||||
|
||||
TEST(EigenDomainMapping, RejectsInvalidPreprintAndAcceptsStrictNoOp) {
|
||||
const std::string preprint =
|
||||
"*Preprint, echo=NO, model=NO, history=NO, contact=NO\n";
|
||||
auto accepted = MapText("preprint", preprint + EigenDeck());
|
||||
ASSERT_TRUE(accepted.HasValue());
|
||||
const auto warning = std::find_if(accepted.Value().Warnings().begin(),
|
||||
accepted.Value().Warnings().end(),
|
||||
[](const fesa::Diagnostic& diagnostic) {
|
||||
return diagnostic.keyword == "PREPRINT";
|
||||
});
|
||||
ASSERT_NE(warning, accepted.Value().Warnings().end());
|
||||
EXPECT_EQ(warning->message, "mandatory-eigen-output-unchanged");
|
||||
|
||||
auto rejected =
|
||||
MapText("bad-preprint", "*Preprint, echo=YES\n" + EigenDeck());
|
||||
ASSERT_FALSE(rejected.HasValue());
|
||||
EXPECT_NE(FindDiagnostic(rejected.GetStatus(), "unsupported-keyword"),
|
||||
nullptr);
|
||||
|
||||
auto missing_parameters =
|
||||
MapText("bare-preprint", "*Preprint\n" + EigenDeck());
|
||||
ASSERT_FALSE(missing_parameters.HasValue());
|
||||
EXPECT_NE(
|
||||
FindDiagnostic(missing_parameters.GetStatus(), "unsupported-keyword"),
|
||||
nullptr);
|
||||
}
|
||||
|
||||
} // namespace
|
||||
Reference in New Issue
Block a user