#include "reference_comparison.h" #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/assembly/load_assembler.h" #include "fesa/fem/dof_manager.h" #include "fesa/io/abaqus/domain_mapper.h" #include "fesa/io/abaqus/input_reader.h" #include "fesa/results/result_recovery.h" namespace fesa::test { namespace { constexpr const char* kModelId = "cantilever-beam-b33"; constexpr const char* kStepName = "Step-1"; constexpr std::size_t kFrameIndex = 0U; constexpr const char* kFrameText = "Increment 1: Step Time = 1.000"; constexpr const char* kInputName = "cantilever beam.inp"; constexpr const char* kDisplacementName = "cantilever beam displacements.csv"; constexpr const char* kReactionName = "cantilever beam reactions.csv"; constexpr const char* kSectionName = "cantilever beam elemental forces.csv"; constexpr const char* kDisplacementPath = "/steps/Step-1/frames/0/nodal/displacement"; constexpr const char* kReactionPath = "/steps/Step-1/frames/0/nodal/reaction"; constexpr const char* kSectionPath = "/steps/Step-1/frames/0/element/section_resultant"; constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11"; constexpr double kKinematicFloor = 1.0e-9; constexpr double kForceMomentFloor = 1.0e-3; constexpr double kRelativeCoefficient = 1.0e-6; class ComparisonFailure final : public std::runtime_error { public: ComparisonFailure(std::string code, std::string message) : std::runtime_error{std::move(message)}, code_{std::move(code)} {} const std::string& Code() const noexcept { return code_; } private: std::string code_; }; [[noreturn]] void Fail(const std::string& code, const std::string& message) { throw ComparisonFailure{code, message}; } Status ComparisonFailureStatus(const std::string& code, const std::string& message) { return Status::Failure(FailureCategory::kModel, {{Severity::kError, code, {}, "", kModelId, message}}); } std::string Trim(const std::string& value) { const auto is_space = [](const unsigned char character) { return std::isspace(character) != 0; }; const auto begin = std::find_if_not(value.begin(), value.end(), [&](const char character) { return is_space(static_cast(character)); }); const auto end = std::find_if_not(value.rbegin(), value.rend(), [&](const char character) { return is_space(static_cast(character)); }).base(); return begin < end ? std::string{begin, end} : std::string{}; } std::string CollapseWhitespace(const std::string& value) { std::string result; bool pending_space = false; for (const char character : Trim(value)) { if (std::isspace(static_cast(character)) != 0) { pending_space = !result.empty(); } else { if (pending_space) { result.push_back(' '); } result.push_back(character); pending_space = false; } } return result; } std::string AsciiLower(std::string value) { std::transform(value.begin(), value.end(), value.begin(), [](const char character) { if (character >= 'A' && character <= 'Z') { return static_cast(character - 'A' + 'a'); } return character; }); return value; } std::vector SplitCsvLine(const std::string& line) { std::vector fields; std::size_t start = 0U; while (true) { const std::size_t comma = line.find(',', start); fields.push_back(Trim(line.substr(start, comma - start))); if (comma == std::string::npos) { break; } start = comma + 1U; } return fields; } std::int64_t ParsePositiveLabel(const std::string& field) { std::int64_t value = 0; const char* const begin = field.data(); const char* const end = begin + field.size(); const auto parsed = std::from_chars(begin, end, value); if (parsed.ec != std::errc{} || parsed.ptr != end || value <= 0) { Fail("schema-mismatch", "A CSV or HDF5 source-node label is invalid."); } return value; } double ParseFiniteDouble(const std::string& field) { if (field.empty()) { Fail("schema-mismatch", "A reference numeric field is empty."); } errno = 0; char* end = nullptr; const double value = std::strtod(field.c_str(), &end); if (errno == ERANGE || end == field.c_str() || end == nullptr || *end != '\0' || !std::isfinite(value)) { Fail("schema-mismatch", "A reference numeric field is nonfinite or invalid."); } return value; } struct WideReferenceRow { std::string instance_name; std::int64_t source_node_label; std::vector values; }; struct ReferenceTable { std::vector rows; }; ReferenceTable ReadReferenceCsv( const std::filesystem::path& path, const std::vector& expected_header) { std::ifstream stream{path}; if (!stream) { Fail("needs-reference-artifacts", "An approved reference CSV is missing."); } std::string line; if (!std::getline(stream, line)) { Fail("schema-mismatch", "An approved reference CSV is empty."); } if (!line.empty() && line.back() == '\r') { line.pop_back(); } if (SplitCsvLine(line) != expected_header) { Fail("schema-mismatch", "An approved reference CSV header is not exact."); } ReferenceTable table; while (std::getline(stream, line)) { if (!line.empty() && line.back() == '\r') { line.pop_back(); } if (line.empty()) { Fail("schema-mismatch", "Blank reference CSV rows are not allowed."); } const auto fields = SplitCsvLine(line); if (fields.size() != expected_header.size() || CollapseWhitespace(fields[0U]) != kFrameText || fields[1U].empty()) { Fail("schema-mismatch", "A reference CSV row has invalid schema or frame identity."); } WideReferenceRow row{}; row.instance_name = fields[1U]; row.source_node_label = ParsePositiveLabel(fields[2U]); row.values.reserve(fields.size() - 3U); for (std::size_t field = 3U; field < fields.size(); ++field) { row.values.push_back(ParseFiniteDouble(fields[field])); } const auto duplicate = std::find_if( table.rows.begin(), table.rows.end(), [&](const WideReferenceRow& existing) { return AsciiLower(existing.instance_name) == AsciiLower(row.instance_name) && existing.source_node_label == row.source_node_label; }); if (duplicate != table.rows.end()) { Fail("schema-mismatch", "A reference CSV row identity is duplicated."); } table.rows.push_back(std::move(row)); } if (table.rows.empty()) { Fail("schema-mismatch", "An approved reference CSV has no data rows."); } return table; } void RequireExactArtifactInventory( const std::filesystem::path& legacy_directory) { std::error_code error; if (!std::filesystem::is_directory(legacy_directory, error) || error) { Fail("needs-reference-artifacts", "The approved legacy directory is missing."); } std::vector names; for (std::filesystem::directory_iterator iterator{legacy_directory, error}, end; iterator != end && !error; iterator.increment(error)) { if (!iterator->is_regular_file(error) || error) { Fail("needs-reference-artifacts", "The legacy bundle contains a non-file entry."); } names.push_back(iterator->path().filename().string()); } if (error) { Fail("needs-reference-artifacts", "The legacy bundle cannot be inspected."); } std::sort(names.begin(), names.end()); std::vector expected = {kDisplacementName, kInputName, kReactionName, kSectionName}; std::sort(expected.begin(), expected.end()); if (names != expected) { Fail("needs-reference-artifacts", "The legacy bundle inventory is not exact."); } } Domain ReadApprovedDomain(const std::filesystem::path& input_path) { AbaqusInputReader reader; auto parsed = reader.Read(input_path); if (!parsed.HasValue()) { Fail("needs-reference-artifacts", "The approved reference input cannot be parsed."); } AbaqusDomainMapper mapper; auto domain = mapper.Map(parsed.Value()); if (!domain.HasValue()) { Fail("needs-reference-artifacts", "The approved reference input is not the required B33 model."); } return std::move(domain.Value()); } class Hdf5Handle { public: using Closer = herr_t (*)(hid_t); Hdf5Handle() = default; Hdf5Handle(const hid_t value, Closer closer) : value_{value}, closer_{closer} {} Hdf5Handle(const Hdf5Handle&) = delete; Hdf5Handle& operator=(const Hdf5Handle&) = delete; Hdf5Handle(Hdf5Handle&& other) noexcept : value_{other.value_}, closer_{other.closer_} { other.value_ = -1; other.closer_ = nullptr; } Hdf5Handle& operator=(Hdf5Handle&& other) noexcept { if (this != &other) { Reset(); value_ = other.value_; closer_ = other.closer_; other.value_ = -1; other.closer_ = nullptr; } return *this; } ~Hdf5Handle() { Reset(); } hid_t Get() const noexcept { return value_; } private: void Reset() noexcept { if (value_ >= 0 && closer_ != nullptr) { (void)closer_(value_); } value_ = -1; closer_ = nullptr; } hid_t value_{-1}; Closer closer_{nullptr}; }; class Hdf5ErrorSilencer { public: Hdf5ErrorSilencer() { if (H5Eget_auto2(H5E_DEFAULT, &callback_, &client_data_) >= 0 && H5Eset_auto2(H5E_DEFAULT, nullptr, nullptr) >= 0) { active_ = true; } } Hdf5ErrorSilencer(const Hdf5ErrorSilencer&) = delete; Hdf5ErrorSilencer& operator=(const Hdf5ErrorSilencer&) = delete; ~Hdf5ErrorSilencer() { if (active_) { (void)H5Eset_auto2(H5E_DEFAULT, callback_, client_data_); } } private: H5E_auto2_t callback_{nullptr}; void* client_data_{nullptr}; bool active_{false}; }; class Hdf5VlenReclaimer { public: Hdf5VlenReclaimer(const hid_t memory_type, const hid_t data_space, void* const data) noexcept : memory_type_{memory_type}, data_space_{data_space}, data_{data} {} Hdf5VlenReclaimer(const Hdf5VlenReclaimer&) = delete; Hdf5VlenReclaimer& operator=(const Hdf5VlenReclaimer&) = delete; ~Hdf5VlenReclaimer() { if (active_) { (void)H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_); } } herr_t Reclaim() noexcept { active_ = false; return H5Dvlen_reclaim(memory_type_, data_space_, H5P_DEFAULT, data_); } private: hid_t memory_type_; hid_t data_space_; void* data_; bool active_{true}; }; hid_t RequireId(const hid_t value, const char* message) { if (value < 0) { Fail("schema-mismatch", message); } return value; } void RequireHdf(const herr_t value, const char* message) { if (value < 0) { Fail("schema-mismatch", message); } } Hdf5Handle OpenDataset(const hid_t file, const char* path) { return {RequireId(H5Dopen2(file, path, H5P_DEFAULT), "A required HDF5 dataset is missing."), H5Dclose}; } std::vector DatasetDimensions(const hid_t dataset) { Hdf5Handle space{ RequireId(H5Dget_space(dataset), "Unable to inspect an HDF5 dataspace."), H5Sclose}; const int rank = H5Sget_simple_extent_ndims(space.Get()); if (rank < 0) { Fail("schema-mismatch", "Unable to inspect an HDF5 dataset rank."); } std::vector dimensions(static_cast(rank)); if (rank > 0) { RequireHdf( H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr), "Unable to inspect HDF5 dataset dimensions."); } return dimensions; } std::string ReadStringAttribute(const hid_t object, const char* name) { Hdf5Handle attribute{ RequireId(H5Aopen(object, name, H5P_DEFAULT), "A required HDF5 string attribute is missing."), H5Aclose}; Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), "Unable to inspect an HDF5 string attribute."), H5Tclose}; if (H5Tget_class(type.Get()) != H5T_STRING || H5Tis_variable_str(type.Get()) <= 0 || H5Tget_cset(type.Get()) != H5T_CSET_UTF8) { Fail("schema-mismatch", "An HDF5 string attribute has the wrong type."); } char* raw = nullptr; RequireHdf(H5Aread(attribute.Get(), type.Get(), &raw), "Unable to read an HDF5 string attribute."); if (raw == nullptr) { Fail("schema-mismatch", "An HDF5 string attribute is null."); } const std::string value{raw}; RequireHdf(H5free_memory(raw), "Unable to release HDF5 string memory."); return value; } std::uint64_t ReadUint64Attribute(const hid_t object, const char* name) { Hdf5Handle attribute{ RequireId(H5Aopen(object, name, H5P_DEFAULT), "A required HDF5 integer attribute is missing."), H5Aclose}; Hdf5Handle type{RequireId(H5Aget_type(attribute.Get()), "Unable to inspect an HDF5 integer attribute."), H5Tclose}; if (H5Tget_class(type.Get()) != H5T_INTEGER || H5Tget_size(type.Get()) != sizeof(std::uint64_t) || H5Tget_sign(type.Get()) != H5T_SGN_NONE) { Fail("schema-mismatch", "An HDF5 integer attribute has the wrong type."); } std::uint64_t value = 0U; RequireHdf(H5Aread(attribute.Get(), H5T_NATIVE_UINT64, &value), "Unable to read an HDF5 integer attribute."); return value; } void RequireStringAttribute(const hid_t object, const char* name, const char* expected) { if (ReadStringAttribute(object, name) != expected) { Fail("schema-mismatch", "An HDF5 string attribute has the wrong value."); } } void RequireResultAttributes(const hid_t dataset, const char* components, const char* units, const char* coordinate_system, const char* location) { RequireStringAttribute(dataset, "component_names", components); RequireStringAttribute(dataset, "component_unit_dimensions", units); RequireStringAttribute(dataset, "coordinate_system", coordinate_system); RequireStringAttribute(dataset, "location", location); RequireStringAttribute(dataset, "step_name", kStepName); if (ReadUint64Attribute(dataset, "frame_index") != kFrameIndex) { Fail("schema-mismatch", "An HDF5 result has the wrong frame identity."); } } std::vector ReadDoubleDataset( const hid_t file, const char* path, const std::vector& expected_dimensions, const char* components, const char* units, const char* coordinate_system, const char* location) { auto dataset = OpenDataset(file, path); if (DatasetDimensions(dataset.Get()) != expected_dimensions) { Fail("schema-mismatch", "An HDF5 result dataset has the wrong shape."); } Hdf5Handle type{RequireId(H5Dget_type(dataset.Get()), "Unable to inspect an HDF5 result type."), H5Tclose}; if (H5Tget_class(type.Get()) != H5T_FLOAT || H5Tget_size(type.Get()) != sizeof(double) || H5Tequal(type.Get(), H5T_IEEE_F64LE) <= 0) { Fail("schema-mismatch", "An HDF5 result dataset is not float64."); } RequireResultAttributes(dataset.Get(), components, units, coordinate_system, location); std::size_t count = 1U; for (const hsize_t dimension : expected_dimensions) { if (dimension > (std::numeric_limits::max)() / count) { Fail("schema-mismatch", "An HDF5 result shape overflows size_t."); } count *= static_cast(dimension); } std::vector values(count); if (!values.empty()) { RequireHdf(H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "Unable to read an HDF5 result dataset."); } if (!std::all_of(values.begin(), values.end(), [](const double value) { return std::isfinite(value); })) { Fail("schema-mismatch", "An HDF5 comparison value is nonfinite."); } return values; } void RequireCompoundMembers(const hid_t dataset, const std::vector& expected) { Hdf5Handle type{RequireId(H5Dget_type(dataset), "Unable to inspect an HDF5 compound type."), H5Tclose}; if (H5Tget_class(type.Get()) != H5T_COMPOUND || H5Tget_nmembers(type.Get()) != static_cast(expected.size())) { Fail("schema-mismatch", "An HDF5 compound dataset has the wrong schema."); } for (std::size_t index = 0U; index < expected.size(); ++index) { char* raw = H5Tget_member_name(type.Get(), static_cast(index)); if (raw == nullptr) { Fail("schema-mismatch", "Unable to inspect an HDF5 member name."); } const std::string actual{raw}; RequireHdf(H5free_memory(raw), "Unable to release an HDF5 member name."); if (actual != expected[index]) { Fail("schema-mismatch", "An HDF5 compound member has the wrong name."); } } } Hdf5Handle MakeVariableStringType() { Hdf5Handle type{ RequireId(H5Tcopy(H5T_C_S1), "Unable to create an HDF5 string type."), H5Tclose}; RequireHdf(H5Tset_size(type.Get(), H5T_VARIABLE), "Unable to size an HDF5 string type."); RequireHdf(H5Tset_cset(type.Get(), H5T_CSET_UTF8), "Unable to configure an HDF5 string type."); return type; } struct NodeMemoryRow { std::uint64_t internal_node_id; char* instance_name; char* source_label; double coordinates[3]; }; struct ElementMemoryRow { std::uint64_t internal_element_id; char* instance_name; char* source_label; std::uint64_t node_internal_ids[2]; double local_axes[9]; }; struct HdfNode { std::uint64_t internal_node_id; std::string instance_name; std::int64_t source_node_label; std::string source_node_label_text; std::array coordinates; }; struct HdfElement { std::uint64_t internal_element_id; std::string instance_name; std::int64_t source_element_label; std::string source_element_label_text; std::array node_internal_ids; std::array local_axes; }; std::vector ReadNodeRows(const hid_t file) { auto dataset = OpenDataset(file, "/model/nodes"); const auto dimensions = DatasetDimensions(dataset.Get()); if (dimensions.size() != 1U || dimensions[0U] == 0U) { Fail("schema-mismatch", "The HDF5 node table has the wrong shape."); } RequireCompoundMembers(dataset.Get(), {"internal_node_id", "instance_name", "source_label", "coordinates"}); RequireStringAttribute(dataset.Get(), "coordinate_system", "global-cartesian"); RequireStringAttribute(dataset.Get(), "units_label", "length"); auto string_type = MakeVariableStringType(); const hsize_t coordinate_dimensions[1] = {3U}; Hdf5Handle coordinate_type{ RequireId(H5Tarray_create2(H5T_NATIVE_DOUBLE, 1, coordinate_dimensions), "Unable to create the node coordinate memory type."), H5Tclose}; Hdf5Handle memory_type{ RequireId(H5Tcreate(H5T_COMPOUND, sizeof(NodeMemoryRow)), "Unable to create the node memory type."), H5Tclose}; RequireHdf( H5Tinsert(memory_type.Get(), "internal_node_id", HOFFSET(NodeMemoryRow, internal_node_id), H5T_NATIVE_UINT64), "Unable to define the node ID memory field."); RequireHdf( H5Tinsert(memory_type.Get(), "instance_name", HOFFSET(NodeMemoryRow, instance_name), string_type.Get()), "Unable to define the node instance memory field."); RequireHdf(H5Tinsert(memory_type.Get(), "source_label", HOFFSET(NodeMemoryRow, source_label), string_type.Get()), "Unable to define the node label memory field."); RequireHdf( H5Tinsert(memory_type.Get(), "coordinates", HOFFSET(NodeMemoryRow, coordinates), coordinate_type.Get()), "Unable to define the node coordinate memory field."); std::vector raw(static_cast(dimensions[0U])); Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), "Unable to reopen the node dataspace."), H5Sclose}; RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, raw.data()), "Unable to read the HDF5 node table."); Hdf5VlenReclaimer strings{memory_type.Get(), space.Get(), raw.data()}; std::vector rows; rows.reserve(raw.size()); for (const auto& row : raw) { if (row.instance_name == nullptr || row.source_label == nullptr) { Fail("schema-mismatch", "An HDF5 node identity is null."); } const std::array coordinates = { row.coordinates[0U], row.coordinates[1U], row.coordinates[2U]}; if (!std::all_of(coordinates.begin(), coordinates.end(), [](const double value) { return std::isfinite(value); })) { Fail("schema-mismatch", "An HDF5 node coordinate is nonfinite."); } rows.push_back({row.internal_node_id, row.instance_name, ParsePositiveLabel(row.source_label), row.source_label, coordinates}); } RequireHdf(strings.Reclaim(), "Unable to reclaim HDF5 node strings."); return rows; } std::vector ReadElementRows(const hid_t file) { auto dataset = OpenDataset(file, "/model/elements"); const auto dimensions = DatasetDimensions(dataset.Get()); if (dimensions.size() != 1U || dimensions[0U] == 0U) { Fail("schema-mismatch", "The HDF5 element table has the wrong shape."); } RequireCompoundMembers( dataset.Get(), {"internal_element_id", "instance_name", "source_label", "node_internal_ids", "local_axes"}); RequireStringAttribute(dataset.Get(), "formulation", "B33-3D-Euler-Bernoulli"); auto string_type = MakeVariableStringType(); const hsize_t connectivity_dimensions[1] = {2U}; const hsize_t axes_dimensions[2] = {3U, 3U}; Hdf5Handle connectivity_type{ RequireId(H5Tarray_create2(H5T_NATIVE_UINT64, 1, connectivity_dimensions), "Unable to create the connectivity memory type."), H5Tclose}; Hdf5Handle axes_type{ RequireId(H5Tarray_create2(H5T_NATIVE_DOUBLE, 2, axes_dimensions), "Unable to create the local-axis memory type."), H5Tclose}; Hdf5Handle memory_type{ RequireId(H5Tcreate(H5T_COMPOUND, sizeof(ElementMemoryRow)), "Unable to create the element memory type."), H5Tclose}; RequireHdf(H5Tinsert(memory_type.Get(), "internal_element_id", HOFFSET(ElementMemoryRow, internal_element_id), H5T_NATIVE_UINT64), "Unable to define the element ID memory field."); RequireHdf( H5Tinsert(memory_type.Get(), "instance_name", HOFFSET(ElementMemoryRow, instance_name), string_type.Get()), "Unable to define the element instance memory field."); RequireHdf( H5Tinsert(memory_type.Get(), "source_label", HOFFSET(ElementMemoryRow, source_label), string_type.Get()), "Unable to define the element label memory field."); RequireHdf(H5Tinsert(memory_type.Get(), "node_internal_ids", HOFFSET(ElementMemoryRow, node_internal_ids), connectivity_type.Get()), "Unable to define the connectivity memory field."); RequireHdf(H5Tinsert(memory_type.Get(), "local_axes", HOFFSET(ElementMemoryRow, local_axes), axes_type.Get()), "Unable to define the local-axis memory field."); std::vector raw(static_cast(dimensions[0U])); Hdf5Handle space{RequireId(H5Dget_space(dataset.Get()), "Unable to reopen the element dataspace."), H5Sclose}; RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, raw.data()), "Unable to read the HDF5 element table."); Hdf5VlenReclaimer strings{memory_type.Get(), space.Get(), raw.data()}; std::vector rows; rows.reserve(raw.size()); for (const auto& row : raw) { if (row.instance_name == nullptr || row.source_label == nullptr) { Fail("schema-mismatch", "An HDF5 element identity is null."); } HdfElement converted{}; converted.internal_element_id = row.internal_element_id; converted.instance_name = row.instance_name; converted.source_element_label = ParsePositiveLabel(row.source_label); converted.source_element_label_text = row.source_label; converted.node_internal_ids = {row.node_internal_ids[0U], row.node_internal_ids[1U]}; std::copy(std::begin(row.local_axes), std::end(row.local_axes), converted.local_axes.begin()); if (!std::all_of(converted.local_axes.begin(), converted.local_axes.end(), [](const double value) { return std::isfinite(value); })) { Fail("schema-mismatch", "An HDF5 local axis is nonfinite."); } rows.push_back(std::move(converted)); } RequireHdf(strings.Reclaim(), "Unable to reclaim HDF5 element strings."); return rows; } void RequireFiniteStress(const hid_t file) { auto dataset = OpenDataset(file, kStressPath); const auto dimensions = DatasetDimensions(dataset.Get()); if (dimensions.size() != 1U || dimensions[0U] == 0U) { Fail("schema-mismatch", "The mandatory stress dataset has no rows."); } RequireCompoundMembers(dataset.Get(), {"internal_element_id", "gauss_point_index", "section_point_index", "x1", "x2", "source", "S11"}); RequireResultAttributes(dataset.Get(), "S11", "force/length^2", "beam-local", "section-point"); struct StressValue { double s11; }; Hdf5Handle memory_type{RequireId(H5Tcreate(H5T_COMPOUND, sizeof(StressValue)), "Unable to create a stress memory type."), H5Tclose}; RequireHdf(H5Tinsert(memory_type.Get(), "S11", HOFFSET(StressValue, s11), H5T_NATIVE_DOUBLE), "Unable to define the stress memory field."); std::vector values(static_cast(dimensions[0U])); RequireHdf(H5Dread(dataset.Get(), memory_type.Get(), H5S_ALL, H5S_ALL, H5P_DEFAULT, values.data()), "Unable to read the stress dataset."); if (!std::all_of(values.begin(), values.end(), [](const StressValue& value) { return std::isfinite(value.s11); })) { Fail("schema-mismatch", "The mandatory stress dataset is nonfinite."); } } std::array ExpectedLocalAxes(const Domain& domain, const EulerBeam3DDefinition& element) { const auto& first = domain.Nodes()[element.node_indices[0U]].coordinates; const auto& second = domain.Nodes()[element.node_indices[1U]].coordinates; const auto& guide = domain.Sections()[element.section_index].first_axis; const std::array delta = { second[0U] - first[0U], second[1U] - first[1U], second[2U] - first[2U]}; const double length = std::hypot(delta[0U], delta[1U], delta[2U]); const std::array x = {delta[0U] / length, delta[1U] / length, delta[2U] / length}; const double projection = guide[0U] * x[0U] + guide[1U] * x[1U] + guide[2U] * x[2U]; const std::array y_trial = {guide[0U] - projection * x[0U], guide[1U] - projection * x[1U], guide[2U] - projection * x[2U]}; const double y_norm = std::hypot(y_trial[0U], y_trial[1U], y_trial[2U]); const std::array y = {y_trial[0U] / y_norm, y_trial[1U] / y_norm, y_trial[2U] / y_norm}; const std::array z = {x[1U] * y[2U] - x[2U] * y[1U], x[2U] * y[0U] - x[0U] * y[2U], x[0U] * y[1U] - x[1U] * y[0U]}; return {x[0U], x[1U], x[2U], y[0U], y[1U], y[2U], z[0U], z[1U], z[2U]}; } struct HdfProjection { std::vector nodes; std::vector elements; std::vector displacement; std::vector reaction; std::vector section_resultants; }; HdfProjection ReadHdfProjection(const std::filesystem::path& results, const std::filesystem::path& input, const Domain& domain) { std::error_code error; if (!std::filesystem::is_regular_file(results, error) || error) { Fail("needs-solver-results", "The authoritative FESA results.h5 is missing."); } Hdf5ErrorSilencer silence; if (H5Fis_hdf5(results.string().c_str()) <= 0) { Fail("schema-mismatch", "The solver result is not an HDF5 file."); } Hdf5Handle file{ RequireId(H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT), "Unable to open the solver HDF5 file read-only."), H5Fclose}; Hdf5Handle metadata{RequireId(H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), "The HDF5 metadata group is missing."), H5Gclose}; if (ReadUint64Attribute(metadata.Get(), "schema_version") != 0U || ReadUint64Attribute(metadata.Get(), "frame_index") != kFrameIndex) { Fail("schema-mismatch", "The HDF5 schema or frame version is wrong."); } RequireStringAttribute(metadata.Get(), "feature_id", "linear-static-3d-euler-beam"); RequireStringAttribute(metadata.Get(), "unit_system_label", "user-consistent-unspecified"); RequireStringAttribute(metadata.Get(), "coordinate_convention", "global-cartesian; beam-local=(t,n1,t-cross-n1)"); RequireStringAttribute(metadata.Get(), "element_formulation", "B33-3D-Euler-Bernoulli"); RequireStringAttribute(metadata.Get(), "step_name", kStepName); const std::string source_identity = ReadStringAttribute(metadata.Get(), "source_input_identity"); const std::string normalized_input = std::filesystem::absolute(input).lexically_normal().generic_u8string(); const std::string expected_identity = "path=" + normalized_input + ";content_identity=" + domain.SourceContentIdentity(); if (source_identity != expected_identity) { Fail("schema-mismatch", "The HDF5 source-input identity is inconsistent."); } HdfProjection projection{}; projection.nodes = ReadNodeRows(file.Get()); projection.elements = ReadElementRows(file.Get()); if (projection.nodes.size() != domain.Nodes().size() || projection.elements.size() != domain.Elements().size()) { Fail("schema-mismatch", "HDF5 model identity counts do not match the input."); } for (std::size_t node = 0U; node < projection.nodes.size(); ++node) { const auto& actual = projection.nodes[node]; const auto& expected = domain.Nodes()[node]; if (actual.internal_node_id != node || actual.instance_name != expected.source_id.instance_name || actual.source_node_label != expected.source_id.source_label || actual.source_node_label_text != expected.source_id.source_label_text || actual.coordinates != expected.coordinates) { Fail("schema-mismatch", "An HDF5 node identity does not match the input."); } } for (std::size_t element = 0U; element < projection.elements.size(); ++element) { const auto& actual = projection.elements[element]; const auto& expected = domain.Elements()[element]; if (actual.internal_element_id != element || actual.instance_name != expected.source_id.instance_name || actual.source_element_label != expected.source_id.source_label || actual.source_element_label_text != expected.source_id.source_label_text || actual.node_internal_ids[0U] != expected.node_indices[0U] || actual.node_internal_ids[1U] != expected.node_indices[1U]) { Fail("schema-mismatch", "An HDF5 element identity does not match the input."); } const auto axes = ExpectedLocalAxes(domain, expected); for (std::size_t component = 0U; component < axes.size(); ++component) { if (std::abs(actual.local_axes[component] - axes[component]) > 1.0e-12) { Fail("schema-mismatch", "An HDF5 local axis does not match the input."); } } } const hsize_t node_count = static_cast(projection.nodes.size()); const hsize_t element_count = static_cast(projection.elements.size()); projection.displacement = ReadDoubleDataset( file.Get(), kDisplacementPath, {node_count, 6U}, "UX,UY,UZ,URX,URY,URZ", "length,length,length,radian,radian,radian", "global-cartesian", "nodal"); projection.reaction = ReadDoubleDataset( file.Get(), kReactionPath, {node_count, 6U}, "RF1,RF2,RF3,RM1,RM2,RM3", "force,force,force,force*length,force*length,force*length", "global-cartesian", "nodal"); projection.section_resultants = ReadDoubleDataset( file.Get(), kSectionPath, {element_count, 2U, 4U}, "N,T,My,Mz", "force,force*length,force*length,force*length", "beam-local", "endpoint-positive-local-x-section-cut"); RequireFiniteStress(file.Get()); return projection; } std::vector OrderedRows( const ReferenceTable& table, const std::vector& nodes) { if (table.rows.size() != nodes.size()) { Fail("schema-mismatch", "The FESA/reference projected row sets differ."); } std::vector ordered; ordered.reserve(nodes.size()); for (const auto& node : nodes) { const auto found = std::find_if( table.rows.begin(), table.rows.end(), [&](const WideReferenceRow& row) { return AsciiLower(row.instance_name) == AsciiLower(node.instance_name) && row.source_node_label == node.source_node_label; }); if (found == table.rows.end() || found->instance_name != node.instance_name) { Fail("schema-mismatch", "A reference row identity does not match HDF5."); } ordered.push_back(&*found); } return ordered; } double TableScale(const ReferenceTable& table, const std::size_t value_index) { double scale = 0.0; for (const auto& row : table.rows) { if (value_index >= row.values.size()) { Fail("schema-mismatch", "A reference row has the wrong component arity."); } scale = (std::max)(scale, std::abs(row.values[value_index])); } return scale; } std::vector NormalizeStations( const Domain& domain, const HdfProjection& hdf, const ReferenceTable& section_table) { auto model_result = AnalysisModel::Create(domain); if (!model_result.HasValue()) { Fail("schema-mismatch", "The approved input cannot create an analysis view."); } const AnalysisModel model = std::move(model_result.Value()); const std::array tolerances = { kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 0U), kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 3U), kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 1U), kForceMomentFloor + kRelativeCoefficient * TableScale(section_table, 2U)}; std::vector endpoints; endpoints.reserve(hdf.elements.size() * 2U); for (std::size_t element = 0U; element < hdf.elements.size(); ++element) { for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) { const auto node = static_cast( hdf.elements[element].node_internal_ids[endpoint]); std::array values{}; for (std::size_t component = 0U; component < values.size(); ++component) { values[component] = hdf.section_resultants[(element * 2U + endpoint) * 4U + component]; } endpoints.push_back({static_cast(element), static_cast(endpoint), domain.Nodes()[node].source_id, {}, values}); } } auto normalized = ResultRecovery::NormalizeSectionResultantsToNodeStations( model, endpoints, tolerances); if (!normalized.HasValue()) { const auto& diagnostics = normalized.GetStatus().Diagnostics(); const std::string code = diagnostics.empty() ? std::string{} : diagnostics[0U].code; if (code == "node-station-tolerance-failure") { Fail("tolerance-failure", "Interior endpoint section resultants disagree."); } Fail("schema-mismatch", "A node station is not eligible for legacy projection."); } return std::move(normalized.Value()); } const NodeStationResultRow& FindStation( const std::vector& stations, const HdfNode& node) { const auto found = std::find_if( stations.begin(), stations.end(), [&](const NodeStationResultRow& row) { return row.node.instance_name == node.instance_name && row.node.source_label == node.source_node_label; }); if (found == stations.end()) { Fail("schema-mismatch", "A projected HDF5 node station is missing."); } return *found; } CanonicalComparisonRow CanonicalRow(const HdfNode& node, const ComparisonQuantity quantity, std::string component, const double value, std::string unit, std::string coordinate_system, std::string dataset_path) { return {kModelId, kStepName, kFrameIndex, node.instance_name, node.source_node_label, quantity, std::move(component), value, std::move(unit), std::move(coordinate_system), std::move(dataset_path)}; } void AppendNodalRows(ComparisonReport& report, const HdfProjection& hdf, const std::vector& reference, const ComparisonQuantity quantity, const std::array& components, const std::array& units, const std::vector& fesa_values, const char* dataset_path) { for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) { for (std::size_t component = 0U; component < components.size(); ++component) { auto fesa = CanonicalRow(hdf.nodes[node], quantity, components[component], fesa_values[node * 6U + component], units[component], "global-cartesian", dataset_path); auto abaqus = CanonicalRow(hdf.nodes[node], quantity, components[component], reference[node]->values[component], units[component], "global-cartesian", dataset_path); report.rows.push_back( {std::move(fesa), std::move(abaqus), 0.0, 0.0, false}); } } } void AppendSectionRows(ComparisonReport& report, const HdfProjection& hdf, const std::vector& reference, const std::vector& stations) { const std::array components = {"N", "T", "My", "Mz"}; const std::array units = {"force", "force*length", "force*length", "force*length"}; const std::array reference_columns = {0U, 3U, 1U, 2U}; for (std::size_t node = 0U; node < hdf.nodes.size(); ++node) { const auto& station = FindStation(stations, hdf.nodes[node]); for (std::size_t component = 0U; component < components.size(); ++component) { auto fesa = CanonicalRow( hdf.nodes[node], ComparisonQuantity::kSectionResultant, components[component], station.section_resultant[component], units[component], "beam-local", kSectionPath); auto abaqus = CanonicalRow(hdf.nodes[node], ComparisonQuantity::kSectionResultant, components[component], reference[node]->values[reference_columns[component]], units[component], "beam-local", kSectionPath); report.rows.push_back( {std::move(fesa), std::move(abaqus), 0.0, 0.0, false}); } } } double AbsoluteFloor(const ComparisonQuantity quantity, const std::string&) { return quantity == ComparisonQuantity::kDisplacement ? kKinematicFloor : kForceMomentFloor; } void EvaluateGroup(ComparisonReport& report, const ComparisonQuantity quantity, const std::string& component) { std::vector row_indices; for (std::size_t index = 0U; index < report.rows.size(); ++index) { if (report.rows[index].reference.quantity == quantity && report.rows[index].reference.component == component) { row_indices.push_back(index); } } if (row_indices.empty()) { Fail("schema-mismatch", "A canonical comparison component has no rows."); } double reference_scale = 0.0; for (const std::size_t index : row_indices) { reference_scale = (std::max)(reference_scale, std::abs(report.rows[index].reference.value)); } const double tolerance = AbsoluteFloor(quantity, component) + kRelativeCoefficient * reference_scale; double maximum_absolute = -1.0; double maximum_normalized = 0.0; std::size_t worst_row = row_indices.front(); long double squared_error = 0.0L; for (const std::size_t index : row_indices) { auto& row = report.rows[index]; row.absolute_error = std::abs(row.fesa.value - row.reference.value); if (!std::isfinite(row.absolute_error)) { Fail("schema-mismatch", "A canonical row error is nonfinite."); } row.tolerance = tolerance; row.passed = row.absolute_error <= tolerance; report.passed = report.passed && row.passed; const double normalized = row.absolute_error / tolerance; if (row.absolute_error > maximum_absolute) { maximum_absolute = row.absolute_error; worst_row = index; } maximum_normalized = (std::max)(maximum_normalized, normalized); const long double error = static_cast(row.absolute_error); squared_error += error * error; } const double norm_error = std::sqrt(static_cast(squared_error)); const double rms_error = std::sqrt(static_cast( squared_error / static_cast(row_indices.size()))); if (!std::isfinite(norm_error) || !std::isfinite(rms_error)) { Fail("schema-mismatch", "A component aggregate error is nonfinite."); } report.metrics.push_back({quantity, component, reference_scale, maximum_absolute, maximum_normalized, rms_error, norm_error, worst_row}); } PhysicsEvidence MakePhysicsEvidence(const Domain& domain, const HdfProjection& hdf) { auto model_result = AnalysisModel::Create(domain); if (!model_result.HasValue()) { Fail("schema-mismatch", "The approved input cannot create physics evidence."); } const AnalysisModel model = std::move(model_result.Value()); auto dofs_result = DofManager::Create(model); if (!dofs_result.HasValue()) { Fail("schema-mismatch", "The approved input cannot create a DOF map."); } const DofManager dofs = std::move(dofs_result.Value()); auto load_result = LoadAssembler::AssembleFullNodalLoad(model, dofs); if (!load_result.HasValue()) { Fail("schema-mismatch", "The approved input load cannot be assembled."); } const Vector load = std::move(load_result.Value()); if (load.Size() != hdf.reaction.size()) { Fail("schema-mismatch", "The load and reaction spaces are inconsistent."); } PhysicsEvidence evidence{}; long double residual_squared = 0.0L; for (const std::size_t free_dof : dofs.FreeDofs()) { const long double value = static_cast(hdf.reaction[free_dof]); residual_squared += value * value; } evidence.free_residual_norm = std::sqrt(static_cast(residual_squared)); for (std::size_t node = 0U; node < domain.Nodes().size(); ++node) { const auto& coordinates = domain.Nodes()[node].coordinates; const std::array applied = { load[node * 6U + 0U], load[node * 6U + 1U], load[node * 6U + 2U]}; const std::array reaction = {hdf.reaction[node * 6U + 0U], hdf.reaction[node * 6U + 1U], hdf.reaction[node * 6U + 2U]}; for (std::size_t component = 0U; component < 3U; ++component) { evidence.applied_force[component] += applied[component]; evidence.reaction_force[component] += reaction[component]; evidence.applied_moment_about_origin[component] += load[node * 6U + 3U + component]; evidence.reaction_moment_about_origin[component] += hdf.reaction[node * 6U + 3U + component]; } evidence.applied_moment_about_origin[0U] += coordinates[1U] * applied[2U] - coordinates[2U] * applied[1U]; evidence.applied_moment_about_origin[1U] += coordinates[2U] * applied[0U] - coordinates[0U] * applied[2U]; evidence.applied_moment_about_origin[2U] += coordinates[0U] * applied[1U] - coordinates[1U] * applied[0U]; evidence.reaction_moment_about_origin[0U] += coordinates[1U] * reaction[2U] - coordinates[2U] * reaction[1U]; evidence.reaction_moment_about_origin[1U] += coordinates[2U] * reaction[0U] - coordinates[0U] * reaction[2U]; evidence.reaction_moment_about_origin[2U] += coordinates[0U] * reaction[1U] - coordinates[1U] * reaction[0U]; } evidence.endpoint_consistency_passed = true; return evidence; } const char* QuantityName(const ComparisonQuantity quantity) { switch (quantity) { case ComparisonQuantity::kDisplacement: return "displacement"; case ComparisonQuantity::kReaction: return "reaction"; case ComparisonQuantity::kSectionResultant: return "section_resultant"; } return "unknown"; } void WriteJsonString(std::ostream& stream, const std::string& value) { static constexpr char kDigits[] = "0123456789abcdef"; stream.put('"'); for (const unsigned char character : value) { switch (character) { case '"': stream << "\\\""; break; case '\\': stream << "\\\\"; break; case '\b': stream << "\\b"; break; case '\f': stream << "\\f"; break; case '\n': stream << "\\n"; break; case '\r': stream << "\\r"; break; case '\t': stream << "\\t"; break; default: if (character < 0x20U) { stream << "\\u00" << kDigits[character >> 4U] << kDigits[character & 0x0fU]; } else { stream.put(static_cast(character)); } break; } } stream.put('"'); } void WriteCanonicalRow(std::ostream& stream, const CanonicalComparisonRow& row) { stream << "{\"model_id\":"; WriteJsonString(stream, row.model_id); stream << ",\"step_name\":"; WriteJsonString(stream, row.step_name); stream << ",\"frame_index\":" << row.frame_index << ",\"instance_name\":"; WriteJsonString(stream, row.instance_name); stream << ",\"source_node_label\":" << row.source_node_label << ",\"quantity\":"; WriteJsonString(stream, QuantityName(row.quantity)); stream << ",\"component\":"; WriteJsonString(stream, row.component); stream << ",\"value\":" << row.value << ",\"unit_dimension\":"; WriteJsonString(stream, row.unit_dimension); stream << ",\"coordinate_system\":"; WriteJsonString(stream, row.coordinate_system); stream << ",\"hdf5_dataset_path\":"; WriteJsonString(stream, row.hdf5_dataset_path); stream << '}'; } void WriteArray(std::ostream& stream, const std::array& values) { stream << '[' << values[0U] << ',' << values[1U] << ',' << values[2U] << ']'; } bool FiniteReport(const ComparisonReport& report) { const auto finite_array = [](const std::array& values) { return std::all_of(values.begin(), values.end(), [](const double value) { return std::isfinite(value); }); }; if (!std::isfinite(report.physics_evidence.free_residual_norm) || !finite_array(report.physics_evidence.applied_force) || !finite_array(report.physics_evidence.reaction_force) || !finite_array(report.physics_evidence.applied_moment_about_origin) || !finite_array(report.physics_evidence.reaction_moment_about_origin)) { return false; } for (const auto& row : report.rows) { if (!std::isfinite(row.fesa.value) || !std::isfinite(row.reference.value) || !std::isfinite(row.absolute_error) || !std::isfinite(row.tolerance)) { return false; } } return std::all_of(report.metrics.begin(), report.metrics.end(), [](const ComponentMetrics& metric) { return std::isfinite(metric.reference_scale) && std::isfinite(metric.maximum_absolute_error) && std::isfinite(metric.maximum_normalized_error) && std::isfinite(metric.rms_error) && std::isfinite(metric.norm_error); }); } } // namespace Result ReferenceComparison::Compare( const std::filesystem::path& results_hdf5, const std::filesystem::path& legacy_reference_directory) { try { RequireExactArtifactInventory(legacy_reference_directory); const auto input = legacy_reference_directory / kInputName; Domain domain = ReadApprovedDomain(input); const ReferenceTable displacement = ReadReferenceCsv(legacy_reference_directory / kDisplacementName, {"Frame", "Part Instance Name", "Node Label", "U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"}); const ReferenceTable reaction = ReadReferenceCsv(legacy_reference_directory / kReactionName, {"Frame", "Part Instance Name", "Node Label", "RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"}); const ReferenceTable section = ReadReferenceCsv(legacy_reference_directory / kSectionName, {"Frame", "Part Instance Name", "Node Label", "SF-SF1", "SM-SM1", "SM-SM2", "SM-SM3"}); HdfProjection hdf = ReadHdfProjection(results_hdf5, input, domain); const auto displacement_rows = OrderedRows(displacement, hdf.nodes); const auto reaction_rows = OrderedRows(reaction, hdf.nodes); const auto section_rows = OrderedRows(section, hdf.nodes); const auto stations = NormalizeStations(domain, hdf, section); if (stations.size() != hdf.nodes.size()) { Fail("schema-mismatch", "The HDF5 node-station row set is incomplete."); } ComparisonReport report{}; report.passed = true; AppendNodalRows( report, hdf, displacement_rows, ComparisonQuantity::kDisplacement, {"UX", "UY", "UZ", "URX", "URY", "URZ"}, {"length", "length", "length", "radian", "radian", "radian"}, hdf.displacement, kDisplacementPath); AppendNodalRows(report, hdf, reaction_rows, ComparisonQuantity::kReaction, {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}, {"force", "force", "force", "force*length", "force*length", "force*length"}, hdf.reaction, kReactionPath); AppendSectionRows(report, hdf, section_rows, stations); for (const std::string& component : {"UX", "UY", "UZ", "URX", "URY", "URZ"}) { EvaluateGroup(report, ComparisonQuantity::kDisplacement, component); } for (const std::string& component : {"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"}) { EvaluateGroup(report, ComparisonQuantity::kReaction, component); } for (const std::string& component : {"N", "T", "My", "Mz"}) { EvaluateGroup(report, ComparisonQuantity::kSectionResultant, component); } report.physics_evidence = MakePhysicsEvidence(domain, hdf); report.stress_comparison_applicable = false; report.stress_comparison_reason = "Abaqus beam stress comparison is N/A; analytical/unit and HDF5 " "schema tests provide stress evidence."; return Result::Success(std::move(report)); } catch (const ComparisonFailure& failure) { return Result::Failure( ComparisonFailureStatus(failure.Code(), failure.what())); } catch (const std::exception& failure) { return Result::Failure( ComparisonFailureStatus("schema-mismatch", failure.what())); } } Status ReferenceComparison::WriteDeterministicJson( const ComparisonReport& report, const std::filesystem::path& output_json) { if (output_json.empty() || output_json.filename().empty() || !FiniteReport(report)) { return Status::Failure( FailureCategory::kOutput, {{Severity::kError, "comparison-json-write-failure", {}, "", kModelId, "The deterministic comparison report or output path is invalid."}}); } std::ofstream stream{output_json, std::ios::binary | std::ios::trunc}; if (!stream) { return Status::Failure( FailureCategory::kOutput, {{Severity::kError, "comparison-json-write-failure", {}, "", kModelId, "The deterministic comparison JSON cannot be opened."}}); } stream.imbue(std::locale::classic()); stream << std::setprecision(std::numeric_limits::max_digits10); stream << "{\"rows\":["; for (std::size_t index = 0U; index < report.rows.size(); ++index) { if (index != 0U) { stream << ','; } const auto& row = report.rows[index]; stream << "{\"fesa\":"; WriteCanonicalRow(stream, row.fesa); stream << ",\"reference\":"; WriteCanonicalRow(stream, row.reference); stream << ",\"absolute_error\":" << row.absolute_error << ",\"tolerance\":" << row.tolerance << ",\"passed\":" << (row.passed ? "true" : "false") << '}'; } stream << "],\"metrics\":["; for (std::size_t index = 0U; index < report.metrics.size(); ++index) { if (index != 0U) { stream << ','; } const auto& metric = report.metrics[index]; stream << "{\"quantity\":"; WriteJsonString(stream, QuantityName(metric.quantity)); stream << ",\"component\":"; WriteJsonString(stream, metric.component); stream << ",\"reference_scale\":" << metric.reference_scale << ",\"maximum_absolute_error\":" << metric.maximum_absolute_error << ",\"maximum_normalized_error\":" << metric.maximum_normalized_error << ",\"rms_error\":" << metric.rms_error << ",\"norm_error\":" << metric.norm_error << ",\"worst_row\":" << metric.worst_row << '}'; } stream << "],\"physics_evidence\":{\"free_residual_norm\":" << report.physics_evidence.free_residual_norm << ",\"applied_force\":"; WriteArray(stream, report.physics_evidence.applied_force); stream << ",\"reaction_force\":"; WriteArray(stream, report.physics_evidence.reaction_force); stream << ",\"applied_moment_about_origin\":"; WriteArray(stream, report.physics_evidence.applied_moment_about_origin); stream << ",\"reaction_moment_about_origin\":"; WriteArray(stream, report.physics_evidence.reaction_moment_about_origin); stream << ",\"endpoint_consistency_passed\":" << (report.physics_evidence.endpoint_consistency_passed ? "true" : "false") << "},\"stress_comparison_applicable\":" << (report.stress_comparison_applicable ? "true" : "false") << ",\"stress_comparison_reason\":"; WriteJsonString(stream, report.stress_comparison_reason); stream << ",\"passed\":" << (report.passed ? "true" : "false") << "}\n"; if (!stream) { return Status::Failure( FailureCategory::kOutput, {{Severity::kError, "comparison-json-write-failure", {}, "", kModelId, "The deterministic comparison JSON write failed."}}); } return Status::Ok(); } } // namespace fesa::test