Add MITC4 drilling diagnostics output and comparison contract

This commit is contained in:
KOKO\Mimi
2026-10-20 09:45:58 +09:00
parent 3d5c296651
commit a73d3fc9ca
31 changed files with 1175 additions and 45 deletions
@@ -20,6 +20,8 @@
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
#include "mitc4_drilling_reader.h"
namespace fesa::test {
namespace {
@@ -385,6 +387,9 @@ Result<EigenReferenceComparisonReport> EigenReferenceComparison::Compare(
}
EigenReferenceComparisonReport report{};
report.drilling_diagnostics = ReadMitc4DrillingReport(file.Get());
if (report.drilling_diagnostics && !report.drilling_diagnostics->frames.empty())
Fail("schema-mismatch", "Eigen drilling diagnostics must have no equilibrium frames.");
report.case_id = reference_case.case_id;
report.source_step_name = frequency.source_step_name_raw;
report.requested_mode_count = requested;
@@ -7,6 +7,7 @@
#include <vector>
#include "fesa/core/status.h"
#include "mitc4_drilling_report.h"
#include "reference_tolerance_policy.h"
namespace fesa::test {
@@ -36,6 +37,7 @@ struct EigenReferenceComparisonReport {
bool passed;
std::string policy_id;
double relative_rms_tolerance;
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
};
/// @brief Compares modal eigenvalues without changing the approved input or
@@ -123,6 +123,25 @@ TEST(EigenReferenceComparison,
EXPECT_EQ(ok.Value().policy_id, "mitc4-eigen-first-implementation");
EXPECT_DOUBLE_EQ(ok.Value().relative_rms_tolerance, 0.05);
EXPECT_EQ(ok.Value().families.size(), 2U);
EXPECT_FALSE(ok.Value().drilling_diagnostics);
for (const std::string mode : {"direct-initial-director-spring", "legacy-corotated-twist"}) {
f = H5Fopen(h5.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT);
m = H5Gopen2(f, "/metadata", H5P_DEFAULT);
if (H5Aexists(m, "mitc4_drilling_formulation") > 0) H5Adelete(m, "mitc4_drilling_formulation");
if (H5Aexists(m, "drilling_diagnostics_version") > 0) H5Adelete(m, "drilling_diagnostics_version");
Str(m, "mitc4_drilling_formulation", mode);
U(m, "drilling_diagnostics_version", 1U);
H5Gclose(m);
H5Fclose(f);
const auto compared = fesa::test::EigenReferenceComparison::Compare(reference_case);
ASSERT_TRUE(compared.HasValue());
ASSERT_TRUE(compared.Value().drilling_diagnostics);
EXPECT_EQ(compared.Value().drilling_diagnostics->formulation, mode);
EXPECT_EQ(compared.Value().drilling_diagnostics->version, 1U);
EXPECT_TRUE(compared.Value().drilling_diagnostics->frames.empty());
EXPECT_EQ(compared.Value().families.size(), 2U);
}
// Only synthetic FESA output is changed; each TXT mode remains an independent
// scalar family, including at both inclusive five-percent boundaries.
for (const auto& sample :
+21
View File
@@ -0,0 +1,21 @@
#ifndef FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
#define FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
#include "mitc4_drilling_report.h"
#include "io/hdf5/mitc4_drilling_diagnostics.h"
namespace fesa::test {
inline std::optional<Mitc4DrillingDiagnosticsReport> ReadMitc4DrillingReport(hid_t file) {
const auto stored = hdf5_internal::ReadDrillingDiagnostics(file);
if (!stored) return std::nullopt;
Mitc4DrillingDiagnosticsReport result{stored->formulation, stored->version, {}};
for (const auto& frame : stored->frames)
result.frames.push_back({frame.storage_step, frame.frame_index, frame.raw_equilibrium,
frame.drilling_moment, frame.equilibrium_with_drilling});
return result;
}
} // namespace fesa::test
#endif // FESA_TESTS_REFERENCE_MITC4_DRILLING_READER_H_
+58
View File
@@ -0,0 +1,58 @@
#ifndef FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
#define FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
#include <array>
#include <cstddef>
#include <cstdint>
#include <optional>
#include <ostream>
#include <string>
#include <vector>
namespace fesa::test {
struct Mitc4DrillingFrameReport {
std::string storage_step;
std::uint64_t frame_index;
std::array<double, 6> raw_equilibrium;
std::array<double, 3> drilling_moment;
std::array<double, 6> equilibrium_with_drilling;
};
struct Mitc4DrillingDiagnosticsReport {
std::string formulation;
std::uint64_t version;
std::vector<Mitc4DrillingFrameReport> frames;
};
inline void WriteDrillingDiagnosticsJson(
std::ostream& stream, const std::optional<Mitc4DrillingDiagnosticsReport>& report) {
if (!report) { stream << "null"; return; }
stream << "{\"formulation\":\"" << report->formulation
<< "\",\"version\":" << report->version << ",\"frames\":[";
const auto numbers = [&](const auto& values) {
stream << '[';
for (std::size_t i = 0; i < values.size(); ++i) {
if (i != 0U) stream << ',';
stream << values[i];
}
stream << ']';
};
for (std::size_t i = 0; i < report->frames.size(); ++i) {
if (i != 0U) stream << ',';
const auto& frame = report->frames[i];
stream << "{\"storage_step\":\"Step-1\",\"frame_index\":" << frame.frame_index
<< ",\"raw_equilibrium\":";
numbers(frame.raw_equilibrium);
stream << ",\"drilling_moment\":";
numbers(frame.drilling_moment);
stream << ",\"equilibrium_with_drilling\":";
numbers(frame.equilibrium_with_drilling);
stream << '}';
}
stream << "]}";
}
} // namespace fesa::test
#endif // FESA_TESTS_REFERENCE_MITC4_DRILLING_REPORT_H_
@@ -26,6 +26,8 @@
#include "reference_tolerance_policy.h"
#include "mitc4_drilling_reader.h"
namespace fesa::test {
namespace {
@@ -573,6 +575,7 @@ std::vector<double> ReadDisplacement(const hid_t file,
}
struct Hdf5Projection {
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
std::vector<WideRow> rows;
std::string source_element_type;
std::string internal_formulation;
@@ -619,7 +622,7 @@ Hdf5Projection ReadHdf5(const Mitc4ReferenceCase& reference_case) {
values.begin() + static_cast<std::ptrdiff_t>(row * kComponents.size()),
kComponents.size(), rows[row].values.begin());
}
return {std::move(rows), reference_case.expected_source_element_type,
return {ReadMitc4DrillingReport(file.Get()), std::move(rows), reference_case.expected_source_element_type,
kInternalFormulation, kIntegrationRule};
}
@@ -739,6 +742,7 @@ Result<Mitc4ComparisonReport> Mitc4ReferenceComparison::Compare(
}
Mitc4ComparisonReport report{};
report.drilling_diagnostics = std::move(hdf5.drilling_diagnostics);
report.case_id = reference_case.case_id;
report.source_element_type = std::move(hdf5.source_element_type);
report.internal_formulation = std::move(hdf5.internal_formulation);
@@ -889,7 +893,9 @@ Status Mitc4ReferenceComparison::WriteDeterministicJson(
}
stream.imbue(std::locale::classic());
stream << std::setprecision(std::numeric_limits<double>::max_digits10);
stream << "{\"case_id\":\"" << JsonEscape(report.case_id)
stream << "{\"drilling_diagnostics\":";
WriteDrillingDiagnosticsJson(stream, report.drilling_diagnostics);
stream << ",\"case_id\":\"" << JsonEscape(report.case_id)
<< "\",\"source_element_type\":\""
<< JsonEscape(report.source_element_type)
<< "\",\"internal_formulation\":\""
@@ -8,6 +8,7 @@
#include <vector>
#include "fesa/core/status.h"
#include "mitc4_drilling_report.h"
namespace fesa::test {
@@ -77,6 +78,7 @@ struct Mitc4ComparisonReport {
std::vector<Mitc4Warning> warnings;
std::size_t worst_row;
bool passed;
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
};
class Mitc4ReferenceComparison {
@@ -22,6 +22,8 @@
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
#endif
#include "io/hdf5/mitc4_drilling_diagnostics.h"
namespace {
constexpr const char* kInstanceName = "PART-1-1";
@@ -771,3 +773,56 @@ TEST(Mitc4ReferenceComparison, RequiresOnlyDeclaredInputCsvAndHdf5) {
}
} // namespace
TEST(Mitc4ReferenceComparison, DrillingOldMarkerlessReportIsNull) {
ContractFixture fixture{"drilling-old"};
const auto compared = fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
ASSERT_TRUE(compared.HasValue());
EXPECT_FALSE(compared.Value().drilling_diagnostics);
const auto json = fixture.Root() / "drilling-old.json";
ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(compared.Value(), json).IsOk());
EXPECT_NE(ReadBytes(json).find("\"drilling_diagnostics\":null"), std::string::npos);
}
TEST(Mitc4ReferenceComparison, DrillingNewMarkersPreserveAllFramesWithoutNewVerdict) {
for (const auto mode : {fesa::Mitc4DrillingMode::kDirectSpring,
fesa::Mitc4DrillingMode::kLegacyCorotatedTwist}) {
ContractFixture fixture{"drilling-new"};
{
Hdf5Handle file{H5Fopen(fixture.Results().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
fesa::hdf5_internal::WriteDrillingMetadata(file.Get(), mode);
// Nonzero raw balance is diagnostic only for the linear displacement comparator.
for (const int index : {0, 2, 10}) {
const std::string root = "/steps/Step-1/frames/" + std::to_string(index);
(void)fesa::hdf5_internal::CreateGroup(file.Get(), (root + "/global").c_str());
const std::array<double, 6> raw{1., 2., 3., 4., 5., 6.};
const fesa::Mitc4DrillingEvidence evidence{mode, {4., 5., 6.}, {1., 2., 3., 0., 0., 0.}};
fesa::hdf5_internal::WriteModelDoubleDataset(file.Get(), root + "/global/equilibrium", {6U},
raw.data(), 6U, "", "", "", "");
fesa::hdf5_internal::WriteDrillingFrame(file.Get(), root, evidence);
}
}
const auto compared = fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase());
ASSERT_TRUE(compared.HasValue());
EXPECT_TRUE(compared.Value().passed);
EXPECT_EQ(compared.Value().metrics.size(), 2U);
const auto& diagnostics = compared.Value().drilling_diagnostics;
ASSERT_TRUE(diagnostics);
EXPECT_EQ(diagnostics->formulation, fesa::Mitc4DrillingModeName(mode));
ASSERT_EQ(diagnostics->frames.size(), 3U);
EXPECT_EQ(diagnostics->frames[0].frame_index, 0U);
EXPECT_EQ(diagnostics->frames[1].frame_index, 2U);
EXPECT_EQ(diagnostics->frames[2].frame_index, 10U);
const auto json = fixture.Root() / "drilling-new.json";
ASSERT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(compared.Value(), json).IsOk());
const auto bytes = ReadBytes(json);
EXPECT_NE(bytes.find("\"drilling_diagnostics\":{\"formulation\":\"" +
diagnostics->formulation + "\",\"version\":1,\"frames\":["), std::string::npos);
{
Hdf5Handle file{H5Fopen(fixture.Results().string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/2/global/drilling_moment", H5P_DEFAULT), 0);
}
EXPECT_FALSE(fesa::test::Mitc4ReferenceComparison::Compare(fixture.ReferenceCase()).HasValue());
}
}
@@ -21,6 +21,8 @@
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
#endif
#include "io/hdf5/mitc4_drilling_diagnostics.h"
namespace {
std::string ReadBytes(const std::filesystem::path& path) {
@@ -409,3 +411,75 @@ TEST(NonlinearStaticReference,
EXPECT_FALSE(nonzero.normalized_metric.has_value()) << identity;
}
}
TEST(NonlinearStaticReference, DrillingSyntheticMomentSelectsOnlyExistingInternalFamily) {
const auto reference_case = MakeCase("shellnl-s4-final");
const auto copied = fesa::test::CopyInputToExecutionDirectory(reference_case);
ASSERT_TRUE(copied.HasValue());
const auto generated = fesa::test::RunFesaApplicationOnCopiedInput(reference_case, copied.Value());
ASSERT_TRUE(generated.HasValue());
const auto& path = generated.Value().generated_results_hdf5;
const auto baseline = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path);
ASSERT_TRUE(baseline.HasValue());
ASSERT_TRUE(baseline.Value().drilling_diagnostics);
const auto final_index = baseline.Value().drilling_diagnostics->frames.back().frame_index;
const std::string root = "/steps/Step-1/frames/" + std::to_string(final_index) + "/global/";
const auto moment_gate = [](const auto& report) -> const auto& {
const auto found = std::find_if(report.internal_equilibrium.begin(), report.internal_equilibrium.end(),
[](const auto& family) { return family.family_identity == "coarse.global_moment_balance"; });
if (found == report.internal_equilibrium.end()) throw std::runtime_error{"Missing existing moment family."};
return *found;
};
// This mutates only a generated working output into an algebra fixture.
// Its comparator verdict is not physics reference evidence.
for (const std::string mode : {"direct-initial-director-spring", "legacy-corotated-twist", "markerless"}) {
{
fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
fesa::hdf5_internal::Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
ASSERT_GE(H5Adelete(metadata.Get(), "mitc4_drilling_formulation"), 0);
ASSERT_GE(H5Adelete(metadata.Get(), "drilling_diagnostics_version"), 0);
if (mode != "markerless") {
fesa::hdf5_internal::WriteStringAttribute(metadata.Get(), "mitc4_drilling_formulation", mode);
fesa::hdf5_internal::WriteUint64Attribute(metadata.Get(), "drilling_diagnostics_version", 1U);
}
for (const auto& entry : std::vector<std::pair<std::string, std::vector<double>>>{
{"equilibrium", {0.,0.,0.,1e6,0.,0.}}, {"drilling_moment", {1e6,0.,0.}},
{"equilibrium_with_drilling", {0.,0.,0.,0.,0.,0.}}}) {
fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), (root + entry.first).c_str(), H5P_DEFAULT), H5Dclose};
ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, entry.second.data()), 0);
}
}
const auto compared = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path);
ASSERT_TRUE(compared.HasValue());
EXPECT_TRUE(compared.Value().external_blocking_passed);
EXPECT_EQ(compared.Value().families.size(), baseline.Value().families.size());
EXPECT_EQ(compared.Value().internal_equilibrium.size(), baseline.Value().internal_equilibrium.size());
const auto& gate = moment_gate(compared.Value());
EXPECT_EQ(gate.scale, moment_gate(baseline.Value()).scale);
EXPECT_EQ(gate.numerator, mode == "direct-initial-director-spring" ? 0.0 : 1e6);
EXPECT_EQ(gate.passed, mode == "direct-initial-director-spring");
EXPECT_EQ(compared.Value().drilling_diagnostics.has_value(), mode != "markerless");
}
// Restore new metadata, retain exact raw-minus-M algebra, and expose a real imbalance.
{
fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
fesa::hdf5_internal::WriteDrillingMetadata(file.Get(), fesa::Mitc4DrillingMode::kDirectSpring);
for (const auto& entry : std::vector<std::pair<std::string, std::vector<double>>>{
{"equilibrium", {0.,0.,0.,2e6,0.,0.}}, {"equilibrium_with_drilling", {0.,0.,0.,1e6,0.,0.}}}) {
fesa::hdf5_internal::Hdf5Handle dataset{H5Dopen2(file.Get(), (root + entry.first).c_str(), H5P_DEFAULT), H5Dclose};
ASSERT_GE(H5Dwrite(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
H5P_DEFAULT, entry.second.data()), 0);
}
}
const auto imbalanced = fesa::test::CompareHdf5ToReferenceCsv(reference_case, path);
ASSERT_TRUE(imbalanced.HasValue());
EXPECT_FALSE(moment_gate(imbalanced.Value()).passed);
EXPECT_EQ(moment_gate(imbalanced.Value()).scale, moment_gate(baseline.Value()).scale);
{
fesa::hdf5_internal::Hdf5Handle file{H5Fopen(path.string().c_str(), H5F_ACC_RDWR, H5P_DEFAULT), H5Fclose};
ASSERT_GE(H5Ldelete(file.Get(), "/steps/Step-1/frames/0/global/drilling_moment", H5P_DEFAULT), 0);
}
EXPECT_FALSE(fesa::test::CompareHdf5ToReferenceCsv(reference_case, path).HasValue());
}
@@ -37,6 +37,8 @@
#error FESA_TEST_BINARY_DIR must identify the CMake binary root.
#endif
#include "mitc4_drilling_reader.h"
namespace fesa::test {
namespace {
@@ -896,6 +898,8 @@ std::vector<std::uint8_t> ReadConstraintMask(const hid_t file,
}
struct Hdf5Projection {
std::uint64_t selected_frame_index;
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
std::filesystem::path copied_input_path;
std::vector<Hdf5Node> nodes;
std::vector<double> displacement;
@@ -972,7 +976,7 @@ Hdf5Projection ReadNonlinearHdf5(
const std::uint64_t frame_index = ReadFactorOneFrame(file.Get());
const std::string root = "/steps/Step-1/frames/" +
std::to_string(frame_index) + "/nodal/";
return {copied_input,
return {frame_index, ReadMitc4DrillingReport(file.Get()), copied_input,
nodes,
ReadNodalDataset(file.Get(), root + "displacement",
"UX,UY,UZ,URX,URY,URZ", contract.node_count),
@@ -1320,6 +1324,16 @@ void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report,
applied_moment_scale += applied_resultant.Norm();
reaction_moment_scale += constrained_resultant.Norm();
}
const std::array<double, 6>* moment_balance = &projection.global_equilibrium;
if (projection.drilling_diagnostics &&
projection.drilling_diagnostics->formulation == "direct-initial-director-spring") {
const auto& frames = projection.drilling_diagnostics->frames;
const auto selected = std::find_if(frames.begin(), frames.end(), [&](const auto& frame) {
return frame.frame_index == projection.selected_frame_index;
});
if (selected == frames.end()) Fail("schema-mismatch", "Missing factor-one drilling diagnostics.");
moment_balance = &selected->equilibrium_with_drilling;
}
report->constrained_reaction_resultant = {
reaction_force.Components(), reaction_moment.Components()};
const std::string prefix = ContractFor(reference_case).family_prefix;
@@ -1329,9 +1343,9 @@ void AppendInternalEquilibrium(NonlinearReferenceComparisonReport* report,
projection.global_equilibrium[2U]},
applied_force_scale, reaction_force_scale);
AppendGlobalBalance(report, prefix + "global_moment_balance",
{projection.global_equilibrium[3U],
projection.global_equilibrium[4U],
projection.global_equilibrium[5U]},
{(*moment_balance)[3U],
(*moment_balance)[4U],
(*moment_balance)[5U]},
applied_moment_scale, reaction_moment_scale);
AppendFreeResidual(report, prefix + "accepted_free_force_residual",
projection, false);
@@ -1385,6 +1399,7 @@ void CompareLinearCase(NonlinearReferenceComparisonReport& report,
: diagnostics.front().message;
Fail("schema-mismatch", message);
}
report.drilling_diagnostics = legacy.Value().drilling_diagnostics;
for (const auto& metric : legacy.Value().metrics) {
ReferenceToleranceFamilyDecision tolerance{};
tolerance.identity = metric.blocking ? "linear.translation" : "linear.rotation";
@@ -1418,6 +1433,7 @@ void CompareNonlinearCase(NonlinearReferenceComparisonReport& report,
report.generated_results_hdf5);
report.copied_input_path = hdf5.copied_input_path;
report.generated_log_path = DerivedLogPath(hdf5.copied_input_path);
report.drilling_diagnostics = hdf5.drilling_diagnostics;
RequireExactNodeLabels(hdf5.nodes, contract.node_count);
const auto displacement_rows = ReadCsvWideRows(
reference_case.displacement_csv_path, true, kDisplacementComponents,
@@ -1475,6 +1491,7 @@ void ComparePressureCase(NonlinearReferenceComparisonReport& report,
ReadNonlinearHdf5(reference_case, report.generated_results_hdf5);
report.copied_input_path = hdf5.copied_input_path;
report.generated_log_path = DerivedLogPath(hdf5.copied_input_path);
report.drilling_diagnostics = hdf5.drilling_diagnostics;
RequireExactNodeLabels(hdf5.nodes, contract.node_count);
RequirePressureInstance(hdf5.nodes);
const auto displacement_rows =
@@ -10,6 +10,7 @@
#include <vector>
#include "fesa/core/status.h"
#include "mitc4_drilling_report.h"
#include "reference_tolerance_policy.h"
namespace fesa::test {
@@ -114,6 +115,7 @@ struct NonlinearReferenceComparisonReport {
ConstrainedReactionResultantReport constrained_reaction_resultant;
bool external_blocking_passed;
bool internal_blocking_passed;
std::optional<Mitc4DrillingDiagnosticsReport> drilling_diagnostics;
};
enum class NonlinearReferenceClassification {