feat(cpp-object-oriented-modular-refactoring): step 6 - io-application-google-style
This commit is contained in:
@@ -0,0 +1,21 @@
|
|||||||
|
#ifndef FESA_APP_FESA_APPLICATION_H_
|
||||||
|
#define FESA_APP_FESA_APPLICATION_H_
|
||||||
|
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
/// @brief Owns the argv-independent CLI contract and stable process exit codes.
|
||||||
|
class FesaApplication {
|
||||||
|
public:
|
||||||
|
/// @brief Runs one solver invocation from operands and options after argv[0].
|
||||||
|
/// @param arguments Input path and optional `--output` pair.
|
||||||
|
/// @return The stable CLI exit code for usage, input, model, solver, or
|
||||||
|
/// output status.
|
||||||
|
int Run(const std::vector<std::string>& arguments);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_APP_FESA_APPLICATION_H_
|
||||||
@@ -1,14 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace fesa {
|
|
||||||
|
|
||||||
// Owns the argv-independent command-line contract and stable process codes.
|
|
||||||
class FesaApplication {
|
|
||||||
public:
|
|
||||||
int run(const std::vector<std::string>& arguments);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef FESA_IO_ABAQUS_DOMAIN_MAPPER_H_
|
||||||
|
#define FESA_IO_ABAQUS_DOMAIN_MAPPER_H_
|
||||||
|
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/io/abaqus/input_syntax.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
/// @brief Maps syntax-only blocks into an approved immutable semantic model.
|
||||||
|
/// @note Source identity and declaration order are preserved through mapping.
|
||||||
|
class AbaqusDomainMapper {
|
||||||
|
public:
|
||||||
|
/// @brief Resolves supported Abaqus syntax into a complete Domain candidate.
|
||||||
|
/// @param input Parsed syntax whose source locations remain valid for
|
||||||
|
/// mapping.
|
||||||
|
/// @return A committed Domain or structured input/model diagnostics; partial
|
||||||
|
/// domains are never returned.
|
||||||
|
Result<Domain> Map(const ParsedInput& input) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_IO_ABAQUS_DOMAIN_MAPPER_H_
|
||||||
@@ -1,15 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "fesa/core/status.h"
|
|
||||||
#include "fesa/io/abaqus/input_syntax.hpp"
|
|
||||||
#include "fesa/model/domain.h"
|
|
||||||
|
|
||||||
namespace fesa {
|
|
||||||
|
|
||||||
// Converts syntax-only blocks into the approved immutable B33 semantic model.
|
|
||||||
class AbaqusDomainMapper {
|
|
||||||
public:
|
|
||||||
Result<Domain> map(const ParsedInput& input) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa
|
|
||||||
@@ -0,0 +1,24 @@
|
|||||||
|
#ifndef FESA_IO_ABAQUS_INPUT_READER_H_
|
||||||
|
#define FESA_IO_ABAQUS_INPUT_READER_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
#include "fesa/io/abaqus/input_syntax.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
/// @brief Reads Abaqus physical keyword, data, and comment syntax.
|
||||||
|
/// @note Semantic policy is applied later by AbaqusDomainMapper.
|
||||||
|
class AbaqusInputReader {
|
||||||
|
public:
|
||||||
|
/// @brief Parses one input file without applying semantic mapping policy.
|
||||||
|
/// @param input_path Path to the exact source bytes whose identity is
|
||||||
|
/// retained.
|
||||||
|
/// @return Parsed syntax or a structured input diagnostic.
|
||||||
|
Result<ParsedInput> Read(const std::filesystem::path& input_path) const;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_IO_ABAQUS_INPUT_READER_H_
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "fesa/core/status.h"
|
|
||||||
#include "fesa/io/abaqus/input_syntax.hpp"
|
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
|
|
||||||
namespace fesa {
|
|
||||||
|
|
||||||
// Reads only physical keyword/data/comment syntax; semantic policy is applied
|
|
||||||
// later by AbaqusDomainMapper.
|
|
||||||
class AbaqusInputReader {
|
|
||||||
public:
|
|
||||||
Result<ParsedInput> read(const std::filesystem::path& inputPath) const;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa
|
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
#ifndef FESA_IO_ABAQUS_INPUT_SYNTAX_H_
|
||||||
|
#define FESA_IO_ABAQUS_INPUT_SYNTAX_H_
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <optional>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/source_identity.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
/// @brief Stores a canonical parameter name and its optional source value.
|
||||||
|
/// @note Parameter names are canonicalized for lookup while values remain
|
||||||
|
/// source text.
|
||||||
|
struct KeywordParameter {
|
||||||
|
std::string name;
|
||||||
|
std::optional<std::string> value;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Stores one parsed data row with its source location.
|
||||||
|
struct DataLine {
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
SourceLocation location;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Stores one syntax-only keyword block and its following data rows.
|
||||||
|
struct KeywordBlock {
|
||||||
|
std::string canonical_name;
|
||||||
|
std::string original_line;
|
||||||
|
std::vector<KeywordParameter> parameters;
|
||||||
|
std::vector<DataLine> data;
|
||||||
|
SourceLocation location;
|
||||||
|
};
|
||||||
|
|
||||||
|
/// @brief Stores the parsed syntax and stable identity of one Abaqus input
|
||||||
|
/// file.
|
||||||
|
struct ParsedInput {
|
||||||
|
std::filesystem::path source_path;
|
||||||
|
std::string source_content_identity;
|
||||||
|
std::vector<KeywordBlock> blocks;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_IO_ABAQUS_INPUT_SYNTAX_H_
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "fesa/core/source_identity.h"
|
|
||||||
|
|
||||||
#include <filesystem>
|
|
||||||
#include <optional>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace fesa {
|
|
||||||
|
|
||||||
// Names are canonicalized for syntax lookup while values remain source text.
|
|
||||||
struct KeywordParameter {
|
|
||||||
std::string name;
|
|
||||||
std::optional<std::string> value;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct DataLine {
|
|
||||||
std::vector<std::string> fields;
|
|
||||||
SourceLocation location;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct KeywordBlock {
|
|
||||||
std::string canonicalName;
|
|
||||||
std::string originalLine;
|
|
||||||
std::vector<KeywordParameter> parameters;
|
|
||||||
std::vector<DataLine> data;
|
|
||||||
SourceLocation location;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ParsedInput {
|
|
||||||
std::filesystem::path sourcePath;
|
|
||||||
std::string sourceContentIdentity;
|
|
||||||
std::vector<KeywordBlock> blocks;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#ifndef FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_
|
||||||
|
#define FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_
|
||||||
|
|
||||||
|
#include "fesa/results/results_writer.h"
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
/// @brief Writes authoritative schema-v0 HDF5 results atomically.
|
||||||
|
/// @note HDF5 and platform types remain private to the implementation.
|
||||||
|
class Hdf5ResultsWriter final : public ResultsWriter {
|
||||||
|
public:
|
||||||
|
/// @brief Writes and self-checks a complete candidate before finalization.
|
||||||
|
/// @param output_path Final authoritative path; the candidate is created in
|
||||||
|
/// the same directory.
|
||||||
|
/// @param domain Immutable source and model identity.
|
||||||
|
/// @param state Fully recovered analysis state.
|
||||||
|
/// @param diagnostics Deterministically ordered run diagnostics.
|
||||||
|
/// @return Success only after atomic replacement or a structured output
|
||||||
|
/// failure.
|
||||||
|
/// @note A failed candidate does not replace an existing valid final file.
|
||||||
|
Status Write(const std::filesystem::path& output_path, const Domain& domain,
|
||||||
|
const AnalysisState& state,
|
||||||
|
const std::vector<Diagnostic>& diagnostics) override;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
|
|
||||||
|
#endif // FESA_IO_HDF5_HDF5_RESULTS_WRITER_H_
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "fesa/results/results_writer.h"
|
|
||||||
|
|
||||||
namespace fesa {
|
|
||||||
|
|
||||||
// Writes schema-v0 output while keeping backend and platform types private.
|
|
||||||
class Hdf5ResultsWriter final : public ResultsWriter {
|
|
||||||
public:
|
|
||||||
Status Write(
|
|
||||||
const std::filesystem::path& outputPath,
|
|
||||||
const Domain& domain,
|
|
||||||
const AnalysisState& state,
|
|
||||||
const std::vector<Diagnostic>& diagnostics) override;
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa
|
|
||||||
@@ -5,8 +5,8 @@
|
|||||||
#include "fesa/assembly/load_assembler.h"
|
#include "fesa/assembly/load_assembler.h"
|
||||||
#include "fesa/assembly/parallel_for.h"
|
#include "fesa/assembly/parallel_for.h"
|
||||||
#include "fesa/assembly/sparse_assembler.h"
|
#include "fesa/assembly/sparse_assembler.h"
|
||||||
#include "fesa/io/abaqus/domain_mapper.hpp"
|
#include "fesa/io/abaqus/domain_mapper.h"
|
||||||
#include "fesa/io/abaqus/input_reader.hpp"
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
#include "fesa/results/result_recovery.h"
|
#include "fesa/results/result_recovery.h"
|
||||||
#include "fesa/results/results_writer.h"
|
#include "fesa/results/results_writer.h"
|
||||||
#include "fesa/solvers/linear/linear_solver.h"
|
#include "fesa/solvers/linear/linear_solver.h"
|
||||||
@@ -65,11 +65,11 @@ Status LinearStaticAnalysis::Initialize(const AnalysisRequest& request) {
|
|||||||
diagnostics_.clear();
|
diagnostics_.clear();
|
||||||
request_ = request;
|
request_ = request;
|
||||||
|
|
||||||
const auto parsed = AbaqusInputReader{}.read(request_.input_path);
|
const auto parsed = AbaqusInputReader{}.Read(request_.input_path);
|
||||||
if (!parsed.HasValue()) {
|
if (!parsed.HasValue()) {
|
||||||
return parsed.GetStatus();
|
return parsed.GetStatus();
|
||||||
}
|
}
|
||||||
auto domain = AbaqusDomainMapper{}.map(parsed.Value());
|
auto domain = AbaqusDomainMapper{}.Map(parsed.Value());
|
||||||
if (!domain.HasValue()) {
|
if (!domain.HasValue()) {
|
||||||
return domain.GetStatus();
|
return domain.GetStatus();
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,16 +1,16 @@
|
|||||||
#include "fesa/app/fesa_application.hpp"
|
#include "fesa/app/fesa_application.h"
|
||||||
|
|
||||||
#include "fesa/analysis/linear_static_analysis.h"
|
|
||||||
#include "fesa/assembly/parallel_for.h"
|
|
||||||
#include "fesa/core/diagnostic.h"
|
|
||||||
#include "fesa/io/hdf5/hdf5_results_writer.hpp"
|
|
||||||
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
#include <iostream>
|
#include <iostream>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/linear_static_analysis.h"
|
||||||
|
#include "fesa/assembly/parallel_for.h"
|
||||||
|
#include "fesa/core/diagnostic.h"
|
||||||
|
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
||||||
|
#include "fesa/solvers/linear/mkl_pardiso_solver.h"
|
||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
@@ -21,13 +21,12 @@ constexpr int kModelExitCode = 4;
|
|||||||
constexpr int kSolverExitCode = 5;
|
constexpr int kSolverExitCode = 5;
|
||||||
constexpr int kOutputExitCode = 6;
|
constexpr int kOutputExitCode = 6;
|
||||||
|
|
||||||
bool startsWithOption(const std::string& argument) {
|
bool StartsWithOption(const std::string& argument) {
|
||||||
return !argument.empty() && argument.front() == '-';
|
return !argument.empty() && argument.front() == '-';
|
||||||
}
|
}
|
||||||
|
|
||||||
Diagnostic usageDiagnostic() {
|
Diagnostic UsageDiagnostic() {
|
||||||
return {
|
return {Severity::kError,
|
||||||
Severity::kError,
|
|
||||||
"cli-usage",
|
"cli-usage",
|
||||||
{{}, 0U},
|
{{}, 0U},
|
||||||
"",
|
"",
|
||||||
@@ -35,29 +34,26 @@ Diagnostic usageDiagnostic() {
|
|||||||
"Usage: fesa.exe <model.inp> [--output <results.h5>]."};
|
"Usage: fesa.exe <model.inp> [--output <results.h5>]."};
|
||||||
}
|
}
|
||||||
|
|
||||||
const char* severityName(const Severity severity) {
|
const char* SeverityName(const Severity severity) {
|
||||||
return severity == Severity::kWarning ? "warning" : "error";
|
return severity == Severity::kWarning ? "warning" : "error";
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeDiagnostics(std::vector<Diagnostic> diagnostics) {
|
void WriteDiagnostics(std::vector<Diagnostic> diagnostics) {
|
||||||
SortDiagnostics(diagnostics);
|
SortDiagnostics(diagnostics);
|
||||||
for (const auto& diagnostic : diagnostics) {
|
for (const auto& diagnostic : diagnostics) {
|
||||||
// Stable field labels and tab separators keep empty source fields
|
// Stable field labels and tab separators keep empty source fields
|
||||||
// explicit without depending on locale-specific formatting.
|
// explicit without depending on locale-specific formatting.
|
||||||
std::cerr
|
std::cerr << "severity=" << SeverityName(diagnostic.severity) << '\t'
|
||||||
<< "severity=" << severityName(diagnostic.severity)
|
<< "code=" << diagnostic.code << '\t'
|
||||||
<< '\t' << "code=" << diagnostic.code
|
<< "file=" << diagnostic.location.file.generic_u8string() << '\t'
|
||||||
<< '\t' << "file="
|
<< "line=" << diagnostic.location.line << '\t'
|
||||||
<< diagnostic.location.file.generic_u8string()
|
<< "keyword=" << diagnostic.keyword << '\t'
|
||||||
<< '\t' << "line=" << diagnostic.location.line
|
<< "entity_identity=" << diagnostic.entity_identity << '\t'
|
||||||
<< '\t' << "keyword=" << diagnostic.keyword
|
<< "message=" << diagnostic.message << '\n';
|
||||||
<< '\t' << "entity_identity=" << diagnostic.entity_identity
|
|
||||||
<< '\t' << "message=" << diagnostic.message
|
|
||||||
<< '\n';
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
int exitCodeFor(const Status& status) {
|
int ExitCodeFor(const Status& status) {
|
||||||
switch (status.Category().value_or(FailureCategory::kInput)) {
|
switch (status.Category().value_or(FailureCategory::kInput)) {
|
||||||
case FailureCategory::kInput:
|
case FailureCategory::kInput:
|
||||||
return kInputExitCode;
|
return kInputExitCode;
|
||||||
@@ -73,41 +69,36 @@ int exitCodeFor(const Status& status) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
int FesaApplication::run(const std::vector<std::string>& arguments) {
|
int FesaApplication::Run(const std::vector<std::string>& arguments) {
|
||||||
const bool defaultOutputForm =
|
const bool default_output_form = arguments.size() == 1U &&
|
||||||
arguments.size() == 1U &&
|
|
||||||
!arguments[0U].empty() &&
|
!arguments[0U].empty() &&
|
||||||
!startsWithOption(arguments[0U]);
|
!StartsWithOption(arguments[0U]);
|
||||||
const bool explicitOutputForm =
|
const bool explicit_output_form =
|
||||||
arguments.size() == 3U &&
|
arguments.size() == 3U && !arguments[0U].empty() &&
|
||||||
!arguments[0U].empty() &&
|
!StartsWithOption(arguments[0U]) && arguments[1U] == "--output" &&
|
||||||
!startsWithOption(arguments[0U]) &&
|
!arguments[2U].empty() && !StartsWithOption(arguments[2U]);
|
||||||
arguments[1U] == "--output" &&
|
if (!default_output_form && !explicit_output_form) {
|
||||||
!arguments[2U].empty() &&
|
WriteDiagnostics({UsageDiagnostic()});
|
||||||
!startsWithOption(arguments[2U]);
|
|
||||||
if (!defaultOutputForm && !explicitOutputForm) {
|
|
||||||
writeDiagnostics({usageDiagnostic()});
|
|
||||||
return kUsageExitCode;
|
return kUsageExitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
AnalysisRequest request;
|
AnalysisRequest request;
|
||||||
request.input_path = arguments[0U];
|
request.input_path = arguments[0U];
|
||||||
request.output_path = explicitOutputForm
|
request.output_path = explicit_output_form
|
||||||
? std::filesystem::path{arguments[2U]}
|
? std::filesystem::path{arguments[2U]}
|
||||||
: std::filesystem::current_path() / "results.h5";
|
: std::filesystem::current_path() / "results.h5";
|
||||||
|
|
||||||
TbbParallelFor parallelFor;
|
TbbParallelFor parallel_for;
|
||||||
MklPardisoSolver linearSolver;
|
MklPardisoSolver linear_solver;
|
||||||
Hdf5ResultsWriter resultsWriter;
|
Hdf5ResultsWriter results_writer;
|
||||||
LinearStaticAnalysis analysis{
|
LinearStaticAnalysis analysis{parallel_for, linear_solver, results_writer};
|
||||||
parallelFor, linearSolver, resultsWriter};
|
|
||||||
const Status status = analysis.Run(request);
|
const Status status = analysis.Run(request);
|
||||||
if (status.IsOk()) {
|
if (status.IsOk()) {
|
||||||
return kSuccessExitCode;
|
return kSuccessExitCode;
|
||||||
}
|
}
|
||||||
|
|
||||||
writeDiagnostics(status.Diagnostics());
|
WriteDiagnostics(status.Diagnostics());
|
||||||
return exitCodeFor(status);
|
return ExitCodeFor(status);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -1,9 +1,9 @@
|
|||||||
#include "fesa/app/fesa_application.hpp"
|
|
||||||
|
|
||||||
#include <cstddef>
|
#include <cstddef>
|
||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/app/fesa_application.h"
|
||||||
|
|
||||||
int main(const int argc, char* argv[]) {
|
int main(const int argc, char* argv[]) {
|
||||||
std::vector<std::string> arguments;
|
std::vector<std::string> arguments;
|
||||||
if (argc > 1) {
|
if (argc > 1) {
|
||||||
@@ -13,5 +13,5 @@ int main(const int argc, char* argv[]) {
|
|||||||
for (int index = 1; index < argc; ++index) {
|
for (int index = 1; index < argc; ++index) {
|
||||||
arguments.emplace_back(argv[index]);
|
arguments.emplace_back(argv[index]);
|
||||||
}
|
}
|
||||||
return fesa::FesaApplication{}.run(arguments);
|
return fesa::FesaApplication{}.Run(arguments);
|
||||||
}
|
}
|
||||||
|
|||||||
+1050
-1317
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
#include "fesa/io/abaqus/input_reader.hpp"
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <cstdint>
|
#include <cstdint>
|
||||||
@@ -15,30 +15,29 @@
|
|||||||
namespace fesa {
|
namespace fesa {
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
std::filesystem::path normalizedPath(const std::filesystem::path& path) {
|
std::filesystem::path NormalizedPath(const std::filesystem::path& path) {
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
const auto absolute = std::filesystem::absolute(path, error);
|
const auto absolute = std::filesystem::absolute(path, error);
|
||||||
return (error ? path : absolute).lexically_normal();
|
return (error ? path : absolute).lexically_normal();
|
||||||
}
|
}
|
||||||
|
|
||||||
bool isAsciiWhitespace(char value) noexcept {
|
bool IsAsciiWhitespace(char value) noexcept {
|
||||||
return value == ' ' || value == '\t' || value == '\r' ||
|
return value == ' ' || value == '\t' || value == '\r' || value == '\n' ||
|
||||||
value == '\n' || value == '\f' || value == '\v';
|
value == '\f' || value == '\v';
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string trim(std::string_view text) {
|
std::string Trim(std::string_view text) {
|
||||||
while (!text.empty() && isAsciiWhitespace(text.front())) {
|
while (!text.empty() && IsAsciiWhitespace(text.front())) {
|
||||||
text.remove_prefix(1U);
|
text.remove_prefix(1U);
|
||||||
}
|
}
|
||||||
while (!text.empty() && isAsciiWhitespace(text.back())) {
|
while (!text.empty() && IsAsciiWhitespace(text.back())) {
|
||||||
text.remove_suffix(1U);
|
text.remove_suffix(1U);
|
||||||
}
|
}
|
||||||
return std::string{text};
|
return std::string{text};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string uppercaseAscii(std::string value) {
|
std::string UppercaseAscii(std::string value) {
|
||||||
std::transform(
|
std::transform(value.begin(), value.end(), value.begin(), [](char character) {
|
||||||
value.begin(), value.end(), value.begin(), [](char character) {
|
|
||||||
if (character >= 'a' && character <= 'z') {
|
if (character >= 'a' && character <= 'z') {
|
||||||
return static_cast<char>(character - 'a' + 'A');
|
return static_cast<char>(character - 'a' + 'A');
|
||||||
}
|
}
|
||||||
@@ -47,167 +46,142 @@ std::string uppercaseAscii(std::string value) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> splitFields(std::string_view line) {
|
std::vector<std::string> SplitFields(std::string_view line) {
|
||||||
std::vector<std::string> fields;
|
std::vector<std::string> fields;
|
||||||
std::size_t fieldStart = 0U;
|
std::size_t field_start = 0U;
|
||||||
while (true) {
|
while (true) {
|
||||||
const std::size_t separator = line.find(',', fieldStart);
|
const std::size_t separator = line.find(',', field_start);
|
||||||
if (separator == std::string_view::npos) {
|
if (separator == std::string_view::npos) {
|
||||||
fields.push_back(trim(line.substr(fieldStart)));
|
fields.push_back(Trim(line.substr(field_start)));
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
fields.push_back(trim(line.substr(fieldStart, separator - fieldStart)));
|
fields.push_back(Trim(line.substr(field_start, separator - field_start)));
|
||||||
fieldStart = separator + 1U;
|
field_start = separator + 1U;
|
||||||
}
|
}
|
||||||
return fields;
|
return fields;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string contentIdentity(const std::string& bytes) {
|
/// @brief Computes the stable identity of the exact source bytes.
|
||||||
constexpr std::uint64_t offsetBasis = 14695981039346656037ULL;
|
/// @note This runs before line-ending handling so parser provenance is not
|
||||||
constexpr std::uint64_t prime = 1099511628211ULL;
|
/// affected by text normalization.
|
||||||
std::uint64_t hash = offsetBasis;
|
std::string ContentIdentity(const std::string& bytes) {
|
||||||
|
constexpr std::uint64_t kOffsetBasis = 14695981039346656037ULL;
|
||||||
|
constexpr std::uint64_t kPrime = 1099511628211ULL;
|
||||||
|
std::uint64_t hash = kOffsetBasis;
|
||||||
// Hash the binary input before CRLF handling so provenance follows the
|
// Hash the binary input before CRLF handling so provenance follows the
|
||||||
// exact file bytes rather than a normalized text representation.
|
// exact file bytes rather than a normalized text representation.
|
||||||
for (const unsigned char byte : bytes) {
|
for (const unsigned char byte : bytes) {
|
||||||
hash ^= static_cast<std::uint64_t>(byte);
|
hash ^= static_cast<std::uint64_t>(byte);
|
||||||
hash *= prime;
|
hash *= kPrime;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::ostringstream formatted;
|
std::ostringstream formatted;
|
||||||
formatted << "fnv1a64:" << std::hex << std::setfill('0')
|
formatted << "fnv1a64:" << std::hex << std::setfill('0') << std::setw(16)
|
||||||
<< std::setw(16) << hash;
|
<< hash;
|
||||||
return formatted.str();
|
return formatted.str();
|
||||||
}
|
}
|
||||||
|
|
||||||
Result<ParsedInput> failure(
|
Result<ParsedInput> Failure(const std::filesystem::path& source_path,
|
||||||
const std::filesystem::path& sourcePath,
|
std::size_t line, std::string code,
|
||||||
std::size_t line,
|
std::string keyword, std::string message) {
|
||||||
std::string code,
|
Diagnostic diagnostic{Severity::kError,
|
||||||
std::string keyword,
|
|
||||||
std::string message) {
|
|
||||||
Diagnostic diagnostic{
|
|
||||||
Severity::kError,
|
|
||||||
std::move(code),
|
std::move(code),
|
||||||
{sourcePath, line},
|
{source_path, line},
|
||||||
std::move(keyword),
|
std::move(keyword),
|
||||||
"",
|
"",
|
||||||
std::move(message)};
|
std::move(message)};
|
||||||
return Result<ParsedInput>::Failure(Status::Failure(
|
return Result<ParsedInput>::Failure(
|
||||||
FailureCategory::kInput, {std::move(diagnostic)}));
|
Status::Failure(FailureCategory::kInput, {std::move(diagnostic)}));
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
Result<ParsedInput> AbaqusInputReader::read(
|
Result<ParsedInput> AbaqusInputReader::Read(
|
||||||
const std::filesystem::path& inputPath) const {
|
const std::filesystem::path& input_path) const {
|
||||||
const auto sourcePath = normalizedPath(inputPath);
|
const auto source_path = NormalizedPath(input_path);
|
||||||
std::ifstream stream{sourcePath, std::ios::binary};
|
std::ifstream stream{source_path, std::ios::binary};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
return failure(
|
return Failure(source_path, 0U, "input-file-unreadable", "",
|
||||||
sourcePath,
|
|
||||||
0U,
|
|
||||||
"input-file-unreadable",
|
|
||||||
"",
|
|
||||||
"The Abaqus input file could not be opened for reading.");
|
"The Abaqus input file could not be opened for reading.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string bytes{
|
const std::string bytes{std::istreambuf_iterator<char>{stream},
|
||||||
std::istreambuf_iterator<char>{stream},
|
|
||||||
std::istreambuf_iterator<char>{}};
|
std::istreambuf_iterator<char>{}};
|
||||||
if (stream.bad()) {
|
if (stream.bad()) {
|
||||||
return failure(
|
return Failure(source_path, 0U, "input-file-unreadable", "",
|
||||||
sourcePath,
|
|
||||||
0U,
|
|
||||||
"input-file-unreadable",
|
|
||||||
"",
|
|
||||||
"The Abaqus input file could not be read completely.");
|
"The Abaqus input file could not be read completely.");
|
||||||
}
|
}
|
||||||
|
|
||||||
ParsedInput parsed{sourcePath, contentIdentity(bytes), {}};
|
ParsedInput parsed{source_path, ContentIdentity(bytes), {}};
|
||||||
std::size_t lineStart = 0U;
|
std::size_t line_start = 0U;
|
||||||
std::size_t lineNumber = 1U;
|
std::size_t line_number = 1U;
|
||||||
while (lineStart < bytes.size()) {
|
while (line_start < bytes.size()) {
|
||||||
const std::size_t newline = bytes.find('\n', lineStart);
|
const std::size_t newline = bytes.find('\n', line_start);
|
||||||
const std::size_t lineEnd =
|
const std::size_t line_end =
|
||||||
newline == std::string::npos ? bytes.size() : newline;
|
newline == std::string::npos ? bytes.size() : newline;
|
||||||
std::string originalLine = bytes.substr(lineStart, lineEnd - lineStart);
|
std::string original_line = bytes.substr(line_start, line_end - line_start);
|
||||||
if (!originalLine.empty() && originalLine.back() == '\r') {
|
if (!original_line.empty() && original_line.back() == '\r') {
|
||||||
originalLine.pop_back();
|
original_line.pop_back();
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::string trimmedLine = trim(originalLine);
|
const std::string trimmed_line = Trim(original_line);
|
||||||
if (!trimmedLine.empty() && trimmedLine.rfind("**", 0U) != 0U) {
|
if (!trimmed_line.empty() && trimmed_line.rfind("**", 0U) != 0U) {
|
||||||
if (trimmedLine.front() == '*') {
|
if (trimmed_line.front() == '*') {
|
||||||
const auto fields = splitFields(trimmedLine);
|
const auto fields = SplitFields(trimmed_line);
|
||||||
const std::string keywordText =
|
const std::string keyword_text =
|
||||||
fields.empty() ? std::string{} : trim(
|
fields.empty() ? std::string{}
|
||||||
std::string_view{fields[0]}.substr(1U));
|
: Trim(std::string_view{fields[0]}.substr(1U));
|
||||||
if (keywordText.empty()) {
|
if (keyword_text.empty()) {
|
||||||
return failure(
|
return Failure(source_path, line_number, "malformed-keyword",
|
||||||
sourcePath,
|
trimmed_line,
|
||||||
lineNumber,
|
|
||||||
"malformed-keyword",
|
|
||||||
trimmedLine,
|
|
||||||
"A keyword line requires a non-empty keyword name.");
|
"A keyword line requires a non-empty keyword name.");
|
||||||
}
|
}
|
||||||
|
|
||||||
KeywordBlock block{
|
KeywordBlock block{UppercaseAscii(keyword_text),
|
||||||
uppercaseAscii(keywordText),
|
original_line,
|
||||||
originalLine,
|
|
||||||
{},
|
{},
|
||||||
{},
|
{},
|
||||||
{sourcePath, lineNumber}};
|
{source_path, line_number}};
|
||||||
for (std::size_t index = 1U; index < fields.size(); ++index) {
|
for (std::size_t index = 1U; index < fields.size(); ++index) {
|
||||||
const std::string& field = fields[index];
|
const std::string& field = fields[index];
|
||||||
if (field.empty()) {
|
if (field.empty()) {
|
||||||
return failure(
|
return Failure(source_path, line_number, "malformed-keyword",
|
||||||
sourcePath,
|
block.canonical_name,
|
||||||
lineNumber,
|
|
||||||
"malformed-keyword",
|
|
||||||
block.canonicalName,
|
|
||||||
"A keyword parameter name cannot be empty.");
|
"A keyword parameter name cannot be empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::size_t equals = field.find('=');
|
const std::size_t equals = field.find('=');
|
||||||
const std::string parameterName = trim(std::string_view{field}.substr(
|
const std::string parameter_name =
|
||||||
0U, equals));
|
Trim(std::string_view{field}.substr(0U, equals));
|
||||||
if (parameterName.empty()) {
|
if (parameter_name.empty()) {
|
||||||
return failure(
|
return Failure(source_path, line_number, "malformed-keyword",
|
||||||
sourcePath,
|
block.canonical_name,
|
||||||
lineNumber,
|
|
||||||
"malformed-keyword",
|
|
||||||
block.canonicalName,
|
|
||||||
"A keyword parameter name cannot be empty.");
|
"A keyword parameter name cannot be empty.");
|
||||||
}
|
}
|
||||||
|
|
||||||
KeywordParameter parameter{
|
KeywordParameter parameter{UppercaseAscii(parameter_name),
|
||||||
uppercaseAscii(parameterName), std::nullopt};
|
std::nullopt};
|
||||||
if (equals != std::string::npos) {
|
if (equals != std::string::npos) {
|
||||||
parameter.value = trim(
|
parameter.value = Trim(std::string_view{field}.substr(equals + 1U));
|
||||||
std::string_view{field}.substr(equals + 1U));
|
|
||||||
}
|
}
|
||||||
block.parameters.push_back(std::move(parameter));
|
block.parameters.push_back(std::move(parameter));
|
||||||
}
|
}
|
||||||
parsed.blocks.push_back(std::move(block));
|
parsed.blocks.push_back(std::move(block));
|
||||||
} else {
|
} else {
|
||||||
if (parsed.blocks.empty()) {
|
if (parsed.blocks.empty()) {
|
||||||
return failure(
|
return Failure(source_path, line_number, "orphan-data-line", "",
|
||||||
sourcePath,
|
|
||||||
lineNumber,
|
|
||||||
"orphan-data-line",
|
|
||||||
"",
|
|
||||||
"A data line must follow a keyword line.");
|
"A data line must follow a keyword line.");
|
||||||
}
|
}
|
||||||
parsed.blocks.back().data.push_back(
|
parsed.blocks.back().data.push_back(
|
||||||
{splitFields(originalLine), {sourcePath, lineNumber}});
|
{SplitFields(original_line), {source_path, line_number}});
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (newline == std::string::npos) {
|
if (newline == std::string::npos) {
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
lineStart = newline + 1U;
|
line_start = newline + 1U;
|
||||||
++lineNumber;
|
++line_number;
|
||||||
}
|
}
|
||||||
|
|
||||||
return Result<ParsedInput>::Success(std::move(parsed));
|
return Result<ParsedInput>::Success(std::move(parsed));
|
||||||
|
|||||||
+1199
-1474
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
#include "fesa/app/fesa_application.hpp"
|
#include "fesa/app/fesa_application.h"
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
#include <hdf5.h>
|
#include <hdf5.h>
|
||||||
@@ -19,15 +19,13 @@ namespace {
|
|||||||
constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
|
constexpr const char* kStepRoot = "/steps/Step-1/frames/0";
|
||||||
|
|
||||||
class TempDirectory {
|
class TempDirectory {
|
||||||
public:
|
public:
|
||||||
explicit TempDirectory(const std::string& label) {
|
explicit TempDirectory(const std::string& label) {
|
||||||
static std::atomic<unsigned long long> sequence{0U};
|
static std::atomic<unsigned long long> sequence{0U};
|
||||||
const auto tick = std::chrono::steady_clock::now()
|
const auto tick =
|
||||||
.time_since_epoch()
|
std::chrono::steady_clock::now().time_since_epoch().count();
|
||||||
.count();
|
|
||||||
path_ = std::filesystem::temp_directory_path() /
|
path_ = std::filesystem::temp_directory_path() /
|
||||||
("fesa-step24-app-" + label + "-" +
|
("fesa-step24-app-" + label + "-" + std::to_string(tick) + "-" +
|
||||||
std::to_string(tick) + "-" +
|
|
||||||
std::to_string(sequence.fetch_add(1U)));
|
std::to_string(sequence.fetch_add(1U)));
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
if (!std::filesystem::create_directory(path_, error) || error) {
|
if (!std::filesystem::create_directory(path_, error) || error) {
|
||||||
@@ -43,14 +41,14 @@ public:
|
|||||||
std::filesystem::remove_all(path_, ignored);
|
std::filesystem::remove_all(path_, ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path& path() const noexcept { return path_; }
|
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::filesystem::path path_;
|
std::filesystem::path path_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class CurrentDirectoryGuard {
|
class CurrentDirectoryGuard {
|
||||||
public:
|
public:
|
||||||
explicit CurrentDirectoryGuard(const std::filesystem::path& replacement)
|
explicit CurrentDirectoryGuard(const std::filesystem::path& replacement)
|
||||||
: original_{std::filesystem::current_path()} {
|
: original_{std::filesystem::current_path()} {
|
||||||
std::filesystem::current_path(replacement);
|
std::filesystem::current_path(replacement);
|
||||||
@@ -64,12 +62,12 @@ public:
|
|||||||
std::filesystem::current_path(original_, ignored);
|
std::filesystem::current_path(original_, ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::filesystem::path original_;
|
std::filesystem::path original_;
|
||||||
};
|
};
|
||||||
|
|
||||||
class Hdf5Handle {
|
class Hdf5Handle {
|
||||||
public:
|
public:
|
||||||
using Closer = herr_t (*)(hid_t);
|
using Closer = herr_t (*)(hid_t);
|
||||||
|
|
||||||
Hdf5Handle(const hid_t value, Closer closer)
|
Hdf5Handle(const hid_t value, Closer closer)
|
||||||
@@ -87,14 +85,14 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hid_t get() const noexcept { return value_; }
|
hid_t Get() const noexcept { return value_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
hid_t value_{-1};
|
hid_t value_{-1};
|
||||||
Closer closer_{nullptr};
|
Closer closer_{nullptr};
|
||||||
};
|
};
|
||||||
|
|
||||||
void writeText(const std::filesystem::path& path, const std::string& text) {
|
void WriteText(const std::filesystem::path& path, const std::string& text) {
|
||||||
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
||||||
stream.write(text.data(), static_cast<std::streamsize>(text.size()));
|
stream.write(text.data(), static_cast<std::streamsize>(text.size()));
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
@@ -102,18 +100,13 @@ void writeText(const std::filesystem::path& path, const std::string& text) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string axialDeck(
|
std::string AxialDeck(const bool constrained, const bool zero_length,
|
||||||
const bool constrained,
|
const bool output_requests) {
|
||||||
const bool zeroLength,
|
const std::string second_node =
|
||||||
const bool outputRequests) {
|
zero_length ? "2, 0., 0., 0.\n" : "2, 2., 0., 0.\n";
|
||||||
const std::string secondNode = zeroLength
|
const std::string boundaries =
|
||||||
? "2, 0., 0., 0.\n"
|
constrained ? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n" : "";
|
||||||
: "2, 2., 0., 0.\n";
|
const std::string outputs = output_requests ? R"inp(*Output, field
|
||||||
const std::string boundaries = constrained
|
|
||||||
? "*Boundary\nRoot, 1, 6\nTip, 2, 6\n"
|
|
||||||
: "";
|
|
||||||
const std::string outputs = outputRequests
|
|
||||||
? R"inp(*Output, field
|
|
||||||
*Node Output
|
*Node Output
|
||||||
U, RF
|
U, RF
|
||||||
*Element Output, directions=YES
|
*Element Output, directions=YES
|
||||||
@@ -126,7 +119,8 @@ S, SF
|
|||||||
return std::string{R"inp(*Part, name=BeamPart
|
return std::string{R"inp(*Part, name=BeamPart
|
||||||
*Node
|
*Node
|
||||||
1, 0., 0., 0.
|
1, 0., 0., 0.
|
||||||
)inp"} + secondNode + R"inp(*Element, type=B33
|
)inp"} + second_node +
|
||||||
|
R"inp(*Element, type=B33
|
||||||
1, 1, 2
|
1, 1, 2
|
||||||
*Elset, elset=BeamSet
|
*Elset, elset=BeamSet
|
||||||
1
|
1
|
||||||
@@ -145,16 +139,18 @@ S, SF
|
|||||||
*Material, name=Steel
|
*Material, name=Steel
|
||||||
*Elastic
|
*Elastic
|
||||||
100., 0.25
|
100., 0.25
|
||||||
)inp" + boundaries + R"inp(*Step, name=Load, nlgeom=NO
|
)inp" + boundaries +
|
||||||
|
R"inp(*Step, name=Load, nlgeom=NO
|
||||||
*Static
|
*Static
|
||||||
0.1, 1., 0.01, 1.
|
0.1, 1., 0.01, 1.
|
||||||
*Cload
|
*Cload
|
||||||
Tip, 1, 10.
|
Tip, 1, 10.
|
||||||
)inp" + outputs + R"inp(*End Step
|
)inp" + outputs +
|
||||||
|
R"inp(*End Step
|
||||||
)inp";
|
)inp";
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string allConstrainedShellDeck() {
|
std::string AllConstrainedShellDeck() {
|
||||||
return R"inp(*Part, name=ShellPart
|
return R"inp(*Part, name=ShellPart
|
||||||
*Node
|
*Node
|
||||||
1, 0., 0., 0.
|
1, 0., 0., 0.
|
||||||
@@ -186,16 +182,16 @@ All, 1, 6
|
|||||||
)inp";
|
)inp";
|
||||||
}
|
}
|
||||||
|
|
||||||
Hdf5Handle openFile(const std::filesystem::path& path) {
|
Hdf5Handle OpenFile(const std::filesystem::path& path) {
|
||||||
const hid_t file = H5Fopen(
|
const hid_t file =
|
||||||
path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
H5Fopen(path.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||||
if (file < 0) {
|
if (file < 0) {
|
||||||
throw std::runtime_error{"Unable to open the CLI HDF5 artifact."};
|
throw std::runtime_error{"Unable to open the CLI HDF5 artifact."};
|
||||||
}
|
}
|
||||||
return Hdf5Handle{file, H5Fclose};
|
return Hdf5Handle{file, H5Fclose};
|
||||||
}
|
}
|
||||||
|
|
||||||
Hdf5Handle openDataset(const hid_t file, const std::string& path) {
|
Hdf5Handle OpenDataset(const hid_t file, const std::string& path) {
|
||||||
const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT);
|
const hid_t dataset = H5Dopen2(file, path.c_str(), H5P_DEFAULT);
|
||||||
if (dataset < 0) {
|
if (dataset < 0) {
|
||||||
throw std::runtime_error{"Unable to open mandatory dataset: " + path};
|
throw std::runtime_error{"Unable to open mandatory dataset: " + path};
|
||||||
@@ -203,49 +199,48 @@ Hdf5Handle openDataset(const hid_t file, const std::string& path) {
|
|||||||
return Hdf5Handle{dataset, H5Dclose};
|
return Hdf5Handle{dataset, H5Dclose};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<hsize_t> datasetDimensions(
|
std::vector<hsize_t> DatasetDimensions(const hid_t file,
|
||||||
const hid_t file, const std::string& path) {
|
const std::string& path) {
|
||||||
const auto dataset = openDataset(file, path);
|
const auto dataset = OpenDataset(file, path);
|
||||||
Hdf5Handle space{H5Dget_space(dataset.get()), H5Sclose};
|
Hdf5Handle space{H5Dget_space(dataset.Get()), H5Sclose};
|
||||||
const int rank = H5Sget_simple_extent_ndims(space.get());
|
const int rank = H5Sget_simple_extent_ndims(space.Get());
|
||||||
if (space.get() < 0 || rank < 0) {
|
if (space.Get() < 0 || rank < 0) {
|
||||||
throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."};
|
throw std::runtime_error{"Unable to inspect mandatory dataset dimensions."};
|
||||||
}
|
}
|
||||||
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
|
std::vector<hsize_t> dimensions(static_cast<std::size_t>(rank));
|
||||||
if (rank > 0 &&
|
if (rank > 0 &&
|
||||||
H5Sget_simple_extent_dims(space.get(), dimensions.data(), nullptr) < 0) {
|
H5Sget_simple_extent_dims(space.Get(), dimensions.data(), nullptr) < 0) {
|
||||||
throw std::runtime_error{"Unable to read mandatory dataset dimensions."};
|
throw std::runtime_error{"Unable to read mandatory dataset dimensions."};
|
||||||
}
|
}
|
||||||
return dimensions;
|
return dimensions;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<double> readDoubleDataset(
|
std::vector<double> ReadDoubleDataset(const hid_t file,
|
||||||
const hid_t file, const std::string& path) {
|
const std::string& path) {
|
||||||
const auto dimensions = datasetDimensions(file, path);
|
const auto dimensions = DatasetDimensions(file, path);
|
||||||
std::size_t count = 1U;
|
std::size_t count = 1U;
|
||||||
for (const hsize_t dimension : dimensions) {
|
for (const hsize_t dimension : dimensions) {
|
||||||
count *= static_cast<std::size_t>(dimension);
|
count *= static_cast<std::size_t>(dimension);
|
||||||
}
|
}
|
||||||
const auto dataset = openDataset(file, path);
|
const auto dataset = OpenDataset(file, path);
|
||||||
std::vector<double> values(count);
|
std::vector<double> values(count);
|
||||||
if (!values.empty() &&
|
if (!values.empty() && H5Dread(dataset.Get(), H5T_NATIVE_DOUBLE, H5S_ALL,
|
||||||
H5Dread(dataset.get(), H5T_NATIVE_DOUBLE, H5S_ALL, H5S_ALL,
|
H5S_ALL, H5P_DEFAULT, values.data()) < 0) {
|
||||||
H5P_DEFAULT, values.data()) < 0) {
|
|
||||||
throw std::runtime_error{"Unable to read mandatory numeric results."};
|
throw std::runtime_error{"Unable to read mandatory numeric results."};
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string readStringAttribute(const hid_t object, const char* name) {
|
std::string ReadStringAttribute(const hid_t object, const char* name) {
|
||||||
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
|
Hdf5Handle attribute{H5Aopen(object, name, H5P_DEFAULT), H5Aclose};
|
||||||
Hdf5Handle type{H5Aget_type(attribute.get()), H5Tclose};
|
Hdf5Handle type{H5Aget_type(attribute.Get()), H5Tclose};
|
||||||
if (attribute.get() < 0 || type.get() < 0 ||
|
if (attribute.Get() < 0 || type.Get() < 0 ||
|
||||||
H5Tget_class(type.get()) != H5T_STRING ||
|
H5Tget_class(type.Get()) != H5T_STRING ||
|
||||||
H5Tis_variable_str(type.get()) <= 0) {
|
H5Tis_variable_str(type.Get()) <= 0) {
|
||||||
throw std::runtime_error{"Expected a variable-length string attribute."};
|
throw std::runtime_error{"Expected a variable-length string attribute."};
|
||||||
}
|
}
|
||||||
char* raw = nullptr;
|
char* raw = nullptr;
|
||||||
if (H5Aread(attribute.get(), type.get(), &raw) < 0 || raw == nullptr) {
|
if (H5Aread(attribute.Get(), type.Get(), &raw) < 0 || raw == nullptr) {
|
||||||
throw std::runtime_error{"Unable to read the HDF5 identity attribute."};
|
throw std::runtime_error{"Unable to read the HDF5 identity attribute."};
|
||||||
}
|
}
|
||||||
const std::string value{raw};
|
const std::string value{raw};
|
||||||
@@ -253,108 +248,92 @@ std::string readStringAttribute(const hid_t object, const char* name) {
|
|||||||
return value;
|
return value;
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectMandatoryInventory(const hid_t file) {
|
void ExpectMandatoryInventory(const hid_t file) {
|
||||||
for (const char* path : {
|
for (const char* path :
|
||||||
"/metadata",
|
{"/metadata", "/model/nodes", "/model/elements",
|
||||||
"/model/nodes",
|
|
||||||
"/model/elements",
|
|
||||||
"/steps/Step-1/frames/0/nodal/displacement",
|
"/steps/Step-1/frames/0/nodal/displacement",
|
||||||
"/steps/Step-1/frames/0/nodal/reaction",
|
"/steps/Step-1/frames/0/nodal/reaction",
|
||||||
"/steps/Step-1/frames/0/element/end_force_local",
|
"/steps/Step-1/frames/0/element/end_force_local",
|
||||||
"/steps/Step-1/frames/0/element/section_resultant",
|
"/steps/Step-1/frames/0/element/section_resultant",
|
||||||
"/steps/Step-1/frames/0/element/generalized_strain",
|
"/steps/Step-1/frames/0/element/generalized_strain",
|
||||||
"/steps/Step-1/frames/0/element/generalized_resultant",
|
"/steps/Step-1/frames/0/element/generalized_resultant",
|
||||||
"/steps/Step-1/frames/0/element/stress_s11",
|
"/steps/Step-1/frames/0/element/stress_s11", "/diagnostics"}) {
|
||||||
"/diagnostics"}) {
|
|
||||||
EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path;
|
EXPECT_GT(H5Lexists(file, path, H5P_DEFAULT), 0) << path;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectFesaHdf5Identity(
|
void ExpectFesaHdf5Identity(const std::filesystem::path& output,
|
||||||
const std::filesystem::path& output,
|
|
||||||
const std::filesystem::path& input) {
|
const std::filesystem::path& input) {
|
||||||
ASSERT_TRUE(std::filesystem::exists(output));
|
ASSERT_TRUE(std::filesystem::exists(output));
|
||||||
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
|
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
|
||||||
const auto file = openFile(output);
|
const auto file = OpenFile(output);
|
||||||
expectMandatoryInventory(file.get());
|
ExpectMandatoryInventory(file.Get());
|
||||||
Hdf5Handle metadata{
|
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||||
H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
ASSERT_GE(metadata.Get(), 0);
|
||||||
ASSERT_GE(metadata.get(), 0);
|
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
|
||||||
EXPECT_EQ(
|
|
||||||
readStringAttribute(metadata.get(), "feature_id"),
|
|
||||||
"linear-static-3d-euler-beam");
|
"linear-static-3d-euler-beam");
|
||||||
const std::string normalizedInput =
|
const std::string normalized_input =
|
||||||
std::filesystem::absolute(input).lexically_normal().generic_u8string();
|
std::filesystem::absolute(input).lexically_normal().generic_u8string();
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity")
|
||||||
readStringAttribute(metadata.get(), "source_input_identity").find(
|
.find("path=" + normalized_input + ";content_identity="),
|
||||||
"path=" + normalizedInput + ";content_identity="),
|
|
||||||
0U);
|
0U);
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectShellHdf5Identity(
|
void ExpectShellHdf5Identity(const std::filesystem::path& output,
|
||||||
const std::filesystem::path& output,
|
|
||||||
const std::filesystem::path& input) {
|
const std::filesystem::path& input) {
|
||||||
ASSERT_TRUE(std::filesystem::exists(output));
|
ASSERT_TRUE(std::filesystem::exists(output));
|
||||||
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
|
ASSERT_GT(H5Fis_hdf5(output.string().c_str()), 0);
|
||||||
const auto file = openFile(output);
|
const auto file = OpenFile(output);
|
||||||
Hdf5Handle metadata{
|
Hdf5Handle metadata{H5Gopen2(file.Get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
||||||
H5Gopen2(file.get(), "/metadata", H5P_DEFAULT), H5Gclose};
|
ASSERT_GE(metadata.Get(), 0);
|
||||||
ASSERT_GE(metadata.get(), 0);
|
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "feature_id"),
|
||||||
EXPECT_EQ(
|
|
||||||
readStringAttribute(metadata.get(), "feature_id"),
|
|
||||||
"linear-static-mitc4-shell");
|
"linear-static-mitc4-shell");
|
||||||
EXPECT_GT(
|
EXPECT_GT(H5Lexists(file.Get(),
|
||||||
H5Lexists(
|
|
||||||
file.get(),
|
|
||||||
"/steps/Step-1/frames/0/element/shell/generalized_strain",
|
"/steps/Step-1/frames/0/element/shell/generalized_strain",
|
||||||
H5P_DEFAULT),
|
H5P_DEFAULT),
|
||||||
0);
|
0);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(DatasetDimensions(file.Get(),
|
||||||
datasetDimensions(
|
|
||||||
file.get(),
|
|
||||||
"/steps/Step-1/frames/0/nodal/displacement"),
|
"/steps/Step-1/frames/0/nodal/displacement"),
|
||||||
(std::vector<hsize_t>{4U, 6U}));
|
(std::vector<hsize_t>{4U, 6U}));
|
||||||
const std::string normalizedInput =
|
const std::string normalized_input =
|
||||||
std::filesystem::absolute(input).lexically_normal().generic_u8string();
|
std::filesystem::absolute(input).lexically_normal().generic_u8string();
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(ReadStringAttribute(metadata.Get(), "source_input_identity")
|
||||||
readStringAttribute(metadata.get(), "source_input_identity").find(
|
.find("path=" + normalized_input + ";content_identity="),
|
||||||
"path=" + normalizedInput + ";content_identity="),
|
|
||||||
0U);
|
0U);
|
||||||
}
|
}
|
||||||
|
|
||||||
struct AppRun {
|
struct AppRun {
|
||||||
int exitCode;
|
int exit_code;
|
||||||
std::string standardError;
|
std::string standard_error;
|
||||||
};
|
};
|
||||||
|
|
||||||
AppRun runApplication(const std::vector<std::string>& arguments) {
|
AppRun RunApplication(const std::vector<std::string>& arguments) {
|
||||||
testing::internal::CaptureStderr();
|
testing::internal::CaptureStderr();
|
||||||
try {
|
try {
|
||||||
const int exitCode = fesa::FesaApplication{}.run(arguments);
|
const int exit_code = fesa::FesaApplication{}.Run(arguments);
|
||||||
return {exitCode, testing::internal::GetCapturedStderr()};
|
return {exit_code, testing::internal::GetCapturedStderr()};
|
||||||
} catch (...) {
|
} catch (...) {
|
||||||
(void)testing::internal::GetCapturedStderr();
|
(void)testing::internal::GetCapturedStderr();
|
||||||
throw;
|
throw;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectDiagnosticFieldOrder(const std::string& text) {
|
void ExpectDiagnosticFieldOrder(const std::string& text) {
|
||||||
ASSERT_FALSE(text.empty());
|
ASSERT_FALSE(text.empty());
|
||||||
std::size_t cursor = 0U;
|
std::size_t cursor = 0U;
|
||||||
for (const char* field : {
|
for (const char* field : {"severity", "code", "file", "line", "keyword",
|
||||||
"severity", "code", "file", "line", "keyword",
|
|
||||||
"entity_identity", "message"}) {
|
"entity_identity", "message"}) {
|
||||||
const auto position = text.find(field, cursor);
|
const auto position = text.find(field, cursor);
|
||||||
ASSERT_NE(position, std::string::npos)
|
ASSERT_NE(position, std::string::npos)
|
||||||
<< "Missing or out-of-order diagnostic field: " << field
|
<< "Missing or out-of-order diagnostic field: " << field
|
||||||
<< "\nstderr:\n" << text;
|
<< "\nstderr:\n"
|
||||||
|
<< text;
|
||||||
cursor = position + std::string{field}.size();
|
cursor = position + std::string{field}.size();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> explicitOutputArguments(
|
std::vector<std::string> ExplicitOutputArguments(
|
||||||
const std::filesystem::path& input,
|
const std::filesystem::path& input, const std::filesystem::path& output) {
|
||||||
const std::filesystem::path& output) {
|
|
||||||
// FesaApplication receives argv[0]-excluded operands and options.
|
// FesaApplication receives argv[0]-excluded operands and options.
|
||||||
return {input.string(), "--output", output.string()};
|
return {input.string(), "--output", output.string()};
|
||||||
}
|
}
|
||||||
@@ -363,122 +342,117 @@ std::vector<std::string> explicitOutputArguments(
|
|||||||
|
|
||||||
TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) {
|
TEST(LinearStaticCli, DefaultAndExplicitOutputProduceFesaHdf5) {
|
||||||
TempDirectory directory{"paths"};
|
TempDirectory directory{"paths"};
|
||||||
const auto input = directory.path() / "model.inp";
|
const auto input = directory.Path() / "model.inp";
|
||||||
writeText(input, axialDeck(true, false, false));
|
WriteText(input, AxialDeck(true, false, false));
|
||||||
|
|
||||||
const auto defaultOutput = directory.path() / "results.h5";
|
const auto default_output = directory.Path() / "results.h5";
|
||||||
{
|
{
|
||||||
CurrentDirectoryGuard currentDirectory{directory.path()};
|
CurrentDirectoryGuard current_directory{directory.Path()};
|
||||||
const auto result = runApplication({input.string()});
|
const auto result = RunApplication({input.string()});
|
||||||
ASSERT_EQ(result.exitCode, 0) << result.standardError;
|
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||||
}
|
}
|
||||||
expectFesaHdf5Identity(defaultOutput, input);
|
ExpectFesaHdf5Identity(default_output, input);
|
||||||
|
|
||||||
const auto explicitOutput = directory.path() / "named-output.h5";
|
const auto explicit_output = directory.Path() / "named-output.h5";
|
||||||
const auto result = runApplication(
|
const auto result =
|
||||||
explicitOutputArguments(input, explicitOutput));
|
RunApplication(ExplicitOutputArguments(input, explicit_output));
|
||||||
ASSERT_EQ(result.exitCode, 0) << result.standardError;
|
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||||
expectFesaHdf5Identity(explicitOutput, input);
|
ExpectFesaHdf5Identity(explicit_output, input);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
|
TEST(LinearStaticCli, ReturnsEveryExactExitCodeAndOrderedDiagnostic) {
|
||||||
TempDirectory directory{"exit-codes"};
|
TempDirectory directory{"exit-codes"};
|
||||||
const auto validInput = directory.path() / "valid.inp";
|
const auto valid_input = directory.Path() / "valid.inp";
|
||||||
const auto modelInput = directory.path() / "invalid-model.inp";
|
const auto model_input = directory.Path() / "invalid-model.inp";
|
||||||
const auto solverInput = directory.path() / "singular.inp";
|
const auto solver_input = directory.Path() / "singular.inp";
|
||||||
writeText(validInput, axialDeck(true, false, false));
|
WriteText(valid_input, AxialDeck(true, false, false));
|
||||||
writeText(modelInput, axialDeck(true, true, false));
|
WriteText(model_input, AxialDeck(true, true, false));
|
||||||
writeText(solverInput, axialDeck(false, false, false));
|
WriteText(solver_input, AxialDeck(false, false, false));
|
||||||
|
|
||||||
const auto success = runApplication(explicitOutputArguments(
|
const auto success = RunApplication(
|
||||||
validInput, directory.path() / "success.h5"));
|
ExplicitOutputArguments(valid_input, directory.Path() / "success.h5"));
|
||||||
const auto usage = runApplication({});
|
const auto usage = RunApplication({});
|
||||||
const auto missingInput = directory.path() / "missing.inp";
|
const auto missing_input = directory.Path() / "missing.inp";
|
||||||
const auto input = runApplication({missingInput.string()});
|
const auto input = RunApplication({missing_input.string()});
|
||||||
const auto repeatedOutput = runApplication(
|
const auto repeated_output =
|
||||||
{missingInput.string(), "--output", "--output"});
|
RunApplication({missing_input.string(), "--output", "--output"});
|
||||||
const auto unknownOutputOption = runApplication(
|
const auto unknown_output_option =
|
||||||
{missingInput.string(), "--output", "--bogus"});
|
RunApplication({missing_input.string(), "--output", "--bogus"});
|
||||||
const auto model = runApplication(explicitOutputArguments(
|
const auto model = RunApplication(ExplicitOutputArguments(
|
||||||
modelInput, directory.path() / "model-failure.h5"));
|
model_input, directory.Path() / "model-failure.h5"));
|
||||||
const auto solver = runApplication(explicitOutputArguments(
|
const auto solver = RunApplication(ExplicitOutputArguments(
|
||||||
solverInput, directory.path() / "solver-failure.h5"));
|
solver_input, directory.Path() / "solver-failure.h5"));
|
||||||
const auto output = runApplication(explicitOutputArguments(
|
const auto output = RunApplication(ExplicitOutputArguments(
|
||||||
validInput,
|
valid_input, directory.Path() / "nonexistent-parent" / "results.h5"));
|
||||||
directory.path() / "nonexistent-parent" / "results.h5"));
|
|
||||||
|
|
||||||
EXPECT_EQ(success.exitCode, 0) << success.standardError;
|
EXPECT_EQ(success.exit_code, 0) << success.standard_error;
|
||||||
EXPECT_EQ(usage.exitCode, 2);
|
EXPECT_EQ(usage.exit_code, 2);
|
||||||
EXPECT_EQ(input.exitCode, 3);
|
EXPECT_EQ(input.exit_code, 3);
|
||||||
EXPECT_EQ(model.exitCode, 4);
|
EXPECT_EQ(model.exit_code, 4);
|
||||||
EXPECT_EQ(solver.exitCode, 5);
|
EXPECT_EQ(solver.exit_code, 5);
|
||||||
EXPECT_EQ(output.exitCode, 6);
|
EXPECT_EQ(output.exit_code, 6);
|
||||||
|
|
||||||
expectDiagnosticFieldOrder(usage.standardError);
|
ExpectDiagnosticFieldOrder(usage.standard_error);
|
||||||
expectDiagnosticFieldOrder(input.standardError);
|
ExpectDiagnosticFieldOrder(input.standard_error);
|
||||||
expectDiagnosticFieldOrder(model.standardError);
|
ExpectDiagnosticFieldOrder(model.standard_error);
|
||||||
expectDiagnosticFieldOrder(solver.standardError);
|
ExpectDiagnosticFieldOrder(solver.standard_error);
|
||||||
expectDiagnosticFieldOrder(output.standardError);
|
ExpectDiagnosticFieldOrder(output.standard_error);
|
||||||
EXPECT_EQ(repeatedOutput.exitCode, 2);
|
EXPECT_EQ(repeated_output.exit_code, 2);
|
||||||
expectDiagnosticFieldOrder(repeatedOutput.standardError);
|
ExpectDiagnosticFieldOrder(repeated_output.standard_error);
|
||||||
EXPECT_NE(repeatedOutput.standardError.find("code=cli-usage"),
|
EXPECT_NE(repeated_output.standard_error.find("code=cli-usage"),
|
||||||
std::string::npos);
|
std::string::npos);
|
||||||
EXPECT_EQ(unknownOutputOption.exitCode, 2);
|
EXPECT_EQ(unknown_output_option.exit_code, 2);
|
||||||
expectDiagnosticFieldOrder(unknownOutputOption.standardError);
|
ExpectDiagnosticFieldOrder(unknown_output_option.standard_error);
|
||||||
EXPECT_NE(unknownOutputOption.standardError.find("code=cli-usage"),
|
EXPECT_NE(unknown_output_option.standard_error.find("code=cli-usage"),
|
||||||
std::string::npos);
|
std::string::npos);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) {
|
TEST(LinearStaticCli, OutputRequestsDoNotFilterMandatoryResults) {
|
||||||
TempDirectory directory{"output-requests"};
|
TempDirectory directory{"output-requests"};
|
||||||
const auto plainInput = directory.path() / "plain.inp";
|
const auto plain_input = directory.Path() / "plain.inp";
|
||||||
const auto requestedInput = directory.path() / "requested.inp";
|
const auto requested_input = directory.Path() / "requested.inp";
|
||||||
const auto plainOutput = directory.path() / "plain.h5";
|
const auto plain_output = directory.Path() / "plain.h5";
|
||||||
const auto requestedOutput = directory.path() / "requested.h5";
|
const auto requested_output = directory.Path() / "requested.h5";
|
||||||
writeText(plainInput, axialDeck(true, false, false));
|
WriteText(plain_input, AxialDeck(true, false, false));
|
||||||
writeText(requestedInput, axialDeck(true, false, true));
|
WriteText(requested_input, AxialDeck(true, false, true));
|
||||||
|
|
||||||
const auto plain = runApplication(
|
const auto plain =
|
||||||
explicitOutputArguments(plainInput, plainOutput));
|
RunApplication(ExplicitOutputArguments(plain_input, plain_output));
|
||||||
const auto requested = runApplication(
|
const auto requested = RunApplication(
|
||||||
explicitOutputArguments(requestedInput, requestedOutput));
|
ExplicitOutputArguments(requested_input, requested_output));
|
||||||
ASSERT_EQ(plain.exitCode, 0) << plain.standardError;
|
ASSERT_EQ(plain.exit_code, 0) << plain.standard_error;
|
||||||
ASSERT_EQ(requested.exitCode, 0) << requested.standardError;
|
ASSERT_EQ(requested.exit_code, 0) << requested.standard_error;
|
||||||
|
|
||||||
const auto plainFile = openFile(plainOutput);
|
const auto plain_file = OpenFile(plain_output);
|
||||||
const auto requestedFile = openFile(requestedOutput);
|
const auto requested_file = OpenFile(requested_output);
|
||||||
expectMandatoryInventory(plainFile.get());
|
ExpectMandatoryInventory(plain_file.Get());
|
||||||
expectMandatoryInventory(requestedFile.get());
|
ExpectMandatoryInventory(requested_file.Get());
|
||||||
|
|
||||||
for (const char* suffix : {
|
for (const char* suffix :
|
||||||
"/nodal/displacement",
|
{"/nodal/displacement", "/nodal/reaction", "/element/end_force_local",
|
||||||
"/nodal/reaction",
|
"/element/section_resultant", "/element/generalized_strain",
|
||||||
"/element/end_force_local",
|
|
||||||
"/element/section_resultant",
|
|
||||||
"/element/generalized_strain",
|
|
||||||
"/element/generalized_resultant"}) {
|
"/element/generalized_resultant"}) {
|
||||||
const std::string path = std::string{kStepRoot} + suffix;
|
const std::string path = std::string{kStepRoot} + suffix;
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(ReadDoubleDataset(requested_file.Get(), path),
|
||||||
readDoubleDataset(requestedFile.get(), path),
|
ReadDoubleDataset(plain_file.Get(), path))
|
||||||
readDoubleDataset(plainFile.get(), path))
|
|
||||||
<< path;
|
<< path;
|
||||||
}
|
}
|
||||||
EXPECT_EQ(datasetDimensions(plainFile.get(), "/diagnostics"),
|
EXPECT_EQ(DatasetDimensions(plain_file.Get(), "/diagnostics"),
|
||||||
std::vector<hsize_t>({0U}));
|
std::vector<hsize_t>({0U}));
|
||||||
const auto requestedDiagnostics =
|
const auto requested_diagnostics =
|
||||||
datasetDimensions(requestedFile.get(), "/diagnostics");
|
DatasetDimensions(requested_file.Get(), "/diagnostics");
|
||||||
ASSERT_EQ(requestedDiagnostics.size(), 1U);
|
ASSERT_EQ(requested_diagnostics.size(), 1U);
|
||||||
EXPECT_GT(requestedDiagnostics[0U], 0U);
|
EXPECT_GT(requested_diagnostics[0U], 0U);
|
||||||
}
|
}
|
||||||
|
|
||||||
// MITC4-FLOW-001: shell input uses the unchanged application route and syntax.
|
// MITC4-FLOW-001: shell input uses the unchanged application route and syntax.
|
||||||
TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) {
|
TEST(Mitc4ShellCli, WritesShellHdf5ThroughTheExistingApplicationRoute) {
|
||||||
TempDirectory directory{"shell-route"};
|
TempDirectory directory{"shell-route"};
|
||||||
const auto input = directory.path() / "shell.inp";
|
const auto input = directory.Path() / "shell.inp";
|
||||||
const auto output = directory.path() / "shell-results.h5";
|
const auto output = directory.Path() / "shell-results.h5";
|
||||||
writeText(input, allConstrainedShellDeck());
|
WriteText(input, AllConstrainedShellDeck());
|
||||||
|
|
||||||
const auto result = runApplication(explicitOutputArguments(input, output));
|
const auto result = RunApplication(ExplicitOutputArguments(input, output));
|
||||||
ASSERT_EQ(result.exitCode, 0) << result.standardError;
|
ASSERT_EQ(result.exit_code, 0) << result.standard_error;
|
||||||
expectShellHdf5Identity(output, input);
|
ExpectShellHdf5Identity(output, input);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +1,5 @@
|
|||||||
#include "reference_comparison.hpp"
|
|
||||||
|
|
||||||
#include "fesa/app/fesa_application.hpp"
|
|
||||||
|
|
||||||
#include <hdf5.h>
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
#include <hdf5.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
#include <array>
|
#include <array>
|
||||||
@@ -17,6 +12,9 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/app/fesa_application.h"
|
||||||
|
#include "reference_comparison.h"
|
||||||
|
|
||||||
#ifndef FESA_TEST_SOURCE_DIR
|
#ifndef FESA_TEST_SOURCE_DIR
|
||||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||||
#endif
|
#endif
|
||||||
@@ -27,13 +25,12 @@
|
|||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
constexpr const char* kStressPath =
|
constexpr const char* kStressPath = "/steps/Step-1/frames/0/element/stress_s11";
|
||||||
"/steps/Step-1/frames/0/element/stress_s11";
|
|
||||||
constexpr std::size_t kExpectedRowCount = 176U;
|
constexpr std::size_t kExpectedRowCount = 176U;
|
||||||
constexpr std::size_t kExpectedMetricCount = 16U;
|
constexpr std::size_t kExpectedMetricCount = 16U;
|
||||||
|
|
||||||
class Hdf5Handle {
|
class Hdf5Handle {
|
||||||
public:
|
public:
|
||||||
using Closer = herr_t (*)(hid_t);
|
using Closer = herr_t (*)(hid_t);
|
||||||
|
|
||||||
Hdf5Handle(const hid_t value, Closer closer)
|
Hdf5Handle(const hid_t value, Closer closer)
|
||||||
@@ -46,21 +43,21 @@ public:
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
hid_t get() const noexcept { return value_; }
|
hid_t Get() const noexcept { return value_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
hid_t value_;
|
hid_t value_;
|
||||||
Closer closer_;
|
Closer closer_;
|
||||||
};
|
};
|
||||||
|
|
||||||
struct ReferenceSnapshotEntry {
|
struct ReferenceSnapshotEntry {
|
||||||
std::filesystem::path relativePath;
|
std::filesystem::path relative_path;
|
||||||
bool isDirectory;
|
bool is_directory;
|
||||||
std::string bytes;
|
std::string bytes;
|
||||||
std::filesystem::file_time_type lastWriteTime;
|
std::filesystem::file_time_type last_write_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string readBytes(const std::filesystem::path& path) {
|
std::string ReadBytes(const std::filesystem::path& path) {
|
||||||
std::ifstream stream{path, std::ios::binary};
|
std::ifstream stream{path, std::ios::binary};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw std::runtime_error{"Unable to read reference evidence: " +
|
throw std::runtime_error{"Unable to read reference evidence: " +
|
||||||
@@ -70,85 +67,77 @@ std::string readBytes(const std::filesystem::path& path) {
|
|||||||
std::istreambuf_iterator<char>{}};
|
std::istreambuf_iterator<char>{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<ReferenceSnapshotEntry> snapshotTree(
|
std::vector<ReferenceSnapshotEntry> SnapshotTree(
|
||||||
const std::filesystem::path& root) {
|
const std::filesystem::path& root) {
|
||||||
std::vector<ReferenceSnapshotEntry> entries;
|
std::vector<ReferenceSnapshotEntry> entries;
|
||||||
for (const auto& entry : std::filesystem::recursive_directory_iterator{root}) {
|
for (const auto& entry :
|
||||||
const bool isDirectory = entry.is_directory();
|
std::filesystem::recursive_directory_iterator{root}) {
|
||||||
if (!isDirectory && !entry.is_regular_file()) {
|
const bool is_directory = entry.is_directory();
|
||||||
|
if (!is_directory && !entry.is_regular_file()) {
|
||||||
throw std::runtime_error{"Unexpected reference-tree entry type."};
|
throw std::runtime_error{"Unexpected reference-tree entry type."};
|
||||||
}
|
}
|
||||||
entries.push_back({
|
entries.push_back({std::filesystem::relative(entry.path(), root),
|
||||||
std::filesystem::relative(entry.path(), root),
|
is_directory,
|
||||||
isDirectory,
|
is_directory ? std::string{} : ReadBytes(entry.path()),
|
||||||
isDirectory ? std::string{} : readBytes(entry.path()),
|
|
||||||
entry.last_write_time()});
|
entry.last_write_time()});
|
||||||
}
|
}
|
||||||
std::sort(
|
std::sort(entries.begin(), entries.end(),
|
||||||
entries.begin(),
|
|
||||||
entries.end(),
|
|
||||||
[](const ReferenceSnapshotEntry& left,
|
[](const ReferenceSnapshotEntry& left,
|
||||||
const ReferenceSnapshotEntry& right) {
|
const ReferenceSnapshotEntry& right) {
|
||||||
return left.relativePath.generic_string() <
|
return left.relative_path.generic_string() <
|
||||||
right.relativePath.generic_string();
|
right.relative_path.generic_string();
|
||||||
});
|
});
|
||||||
return entries;
|
return entries;
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectTreeUnchanged(
|
void ExpectTreeUnchanged(const std::vector<ReferenceSnapshotEntry>& before,
|
||||||
const std::vector<ReferenceSnapshotEntry>& before,
|
|
||||||
const std::vector<ReferenceSnapshotEntry>& after) {
|
const std::vector<ReferenceSnapshotEntry>& after) {
|
||||||
ASSERT_EQ(after.size(), before.size());
|
ASSERT_EQ(after.size(), before.size());
|
||||||
for (std::size_t index = 0U; index < before.size(); ++index) {
|
for (std::size_t index = 0U; index < before.size(); ++index) {
|
||||||
EXPECT_EQ(after[index].relativePath, before[index].relativePath);
|
EXPECT_EQ(after[index].relative_path, before[index].relative_path);
|
||||||
EXPECT_EQ(after[index].isDirectory, before[index].isDirectory);
|
EXPECT_EQ(after[index].is_directory, before[index].is_directory);
|
||||||
EXPECT_EQ(after[index].bytes, before[index].bytes)
|
EXPECT_EQ(after[index].bytes, before[index].bytes)
|
||||||
<< before[index].relativePath.string();
|
<< before[index].relative_path.string();
|
||||||
EXPECT_EQ(after[index].lastWriteTime, before[index].lastWriteTime)
|
EXPECT_EQ(after[index].last_write_time, before[index].last_write_time)
|
||||||
<< before[index].relativePath.string();
|
<< before[index].relative_path.string();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
double norm(const std::array<double, 3>& value) {
|
double Norm(const std::array<double, 3>& value) {
|
||||||
return std::sqrt(
|
return std::sqrt(value[0U] * value[0U] + value[1U] * value[1U] +
|
||||||
value[0U] * value[0U] + value[1U] * value[1U] +
|
|
||||||
value[2U] * value[2U]);
|
value[2U] * value[2U]);
|
||||||
}
|
}
|
||||||
|
|
||||||
std::array<double, 3> sum(
|
std::array<double, 3> Sum(const std::array<double, 3>& left,
|
||||||
const std::array<double, 3>& left,
|
|
||||||
const std::array<double, 3>& right) {
|
const std::array<double, 3>& right) {
|
||||||
return {
|
return {left[0U] + right[0U], left[1U] + right[1U], left[2U] + right[2U]};
|
||||||
left[0U] + right[0U],
|
|
||||||
left[1U] + right[1U],
|
|
||||||
left[2U] + right[2U]};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
std::size_t stressRowCount(const std::filesystem::path& results) {
|
std::size_t StressRowCount(const std::filesystem::path& results) {
|
||||||
const hid_t fileId =
|
const hid_t file_id =
|
||||||
H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
H5Fopen(results.string().c_str(), H5F_ACC_RDONLY, H5P_DEFAULT);
|
||||||
if (fileId < 0) {
|
if (file_id < 0) {
|
||||||
throw std::runtime_error{"Unable to open authoritative HDF5 output."};
|
throw std::runtime_error{"Unable to open authoritative HDF5 output."};
|
||||||
}
|
}
|
||||||
const Hdf5Handle file{fileId, H5Fclose};
|
const Hdf5Handle file{file_id, H5Fclose};
|
||||||
if (H5Lexists(file.get(), kStressPath, H5P_DEFAULT) <= 0) {
|
if (H5Lexists(file.Get(), kStressPath, H5P_DEFAULT) <= 0) {
|
||||||
throw std::runtime_error{"Mandatory stress_s11 dataset is missing."};
|
throw std::runtime_error{"Mandatory stress_s11 dataset is missing."};
|
||||||
}
|
}
|
||||||
const hid_t datasetId = H5Dopen2(file.get(), kStressPath, H5P_DEFAULT);
|
const hid_t dataset_id = H5Dopen2(file.Get(), kStressPath, H5P_DEFAULT);
|
||||||
if (datasetId < 0) {
|
if (dataset_id < 0) {
|
||||||
throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."};
|
throw std::runtime_error{"Unable to open mandatory stress_s11 dataset."};
|
||||||
}
|
}
|
||||||
const Hdf5Handle dataset{datasetId, H5Dclose};
|
const Hdf5Handle dataset{dataset_id, H5Dclose};
|
||||||
const hid_t spaceId = H5Dget_space(dataset.get());
|
const hid_t space_id = H5Dget_space(dataset.Get());
|
||||||
if (spaceId < 0) {
|
if (space_id < 0) {
|
||||||
throw std::runtime_error{"Unable to inspect stress_s11 dataspace."};
|
throw std::runtime_error{"Unable to inspect stress_s11 dataspace."};
|
||||||
}
|
}
|
||||||
const Hdf5Handle space{spaceId, H5Sclose};
|
const Hdf5Handle space{space_id, H5Sclose};
|
||||||
if (H5Sget_simple_extent_ndims(space.get()) != 1) {
|
if (H5Sget_simple_extent_ndims(space.Get()) != 1) {
|
||||||
throw std::runtime_error{"stress_s11 must be a flat row dataset."};
|
throw std::runtime_error{"stress_s11 must be a flat row dataset."};
|
||||||
}
|
}
|
||||||
hsize_t count = 0U;
|
hsize_t count = 0U;
|
||||||
if (H5Sget_simple_extent_dims(space.get(), &count, nullptr) < 0) {
|
if (H5Sget_simple_extent_dims(space.Get(), &count, nullptr) < 0) {
|
||||||
throw std::runtime_error{"Unable to read stress_s11 extent."};
|
throw std::runtime_error{"Unable to read stress_s11 extent."};
|
||||||
}
|
}
|
||||||
return static_cast<std::size_t>(count);
|
return static_cast<std::size_t>(count);
|
||||||
@@ -156,121 +145,104 @@ std::size_t stressRowCount(const std::filesystem::path& results) {
|
|||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(B33ReferenceComparison,
|
TEST(B33ReferenceComparison, GeneratesAuthoritativeHdf5AndComparisonEvidence) {
|
||||||
GeneratesAuthoritativeHdf5AndComparisonEvidence) {
|
const std::filesystem::path source_root{FESA_TEST_SOURCE_DIR};
|
||||||
const std::filesystem::path sourceRoot{FESA_TEST_SOURCE_DIR};
|
const std::filesystem::path binary_root{FESA_TEST_BINARY_DIR};
|
||||||
const std::filesystem::path binaryRoot{FESA_TEST_BINARY_DIR};
|
const auto reference_directory =
|
||||||
const auto referenceDirectory =
|
source_root / "reference" / "cantilever beam";
|
||||||
sourceRoot / "reference" / "cantilever beam";
|
const auto input = reference_directory / "cantilever beam.inp";
|
||||||
const auto input = referenceDirectory / "cantilever beam.inp";
|
const auto output_directory =
|
||||||
const auto outputDirectory =
|
binary_root / "reference" / "cantilever-beam-b33";
|
||||||
binaryRoot / "reference" / "cantilever-beam-b33";
|
const auto results = output_directory / "results.h5";
|
||||||
const auto results = outputDirectory / "results.h5";
|
const auto comparison = output_directory / "comparison.json";
|
||||||
const auto comparison = outputDirectory / "comparison.json";
|
|
||||||
|
|
||||||
// Only the exact build-local evidence directory is reset; the approved
|
// Only the exact build-local evidence directory is reset; the approved
|
||||||
// reference tree is snapshotted and subsequently opened read-only.
|
// reference tree is snapshotted and subsequently opened read-only.
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
std::filesystem::remove_all(outputDirectory, error);
|
std::filesystem::remove_all(output_directory, error);
|
||||||
error.clear();
|
error.clear();
|
||||||
ASSERT_TRUE(std::filesystem::create_directories(outputDirectory, error));
|
ASSERT_TRUE(std::filesystem::create_directories(output_directory, error));
|
||||||
ASSERT_FALSE(error);
|
ASSERT_FALSE(error);
|
||||||
const auto referenceBefore = snapshotTree(referenceDirectory);
|
const auto reference_before = SnapshotTree(reference_directory);
|
||||||
ASSERT_EQ(referenceBefore.size(), 4U);
|
ASSERT_EQ(reference_before.size(), 4U);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(reference_before[0U].relative_path,
|
||||||
referenceBefore[0U].relativePath,
|
|
||||||
std::filesystem::path{"cantilever beam displacements.csv"});
|
std::filesystem::path{"cantilever beam displacements.csv"});
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(reference_before[1U].relative_path,
|
||||||
referenceBefore[1U].relativePath,
|
|
||||||
std::filesystem::path{"cantilever beam elemental forces.csv"});
|
std::filesystem::path{"cantilever beam elemental forces.csv"});
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(reference_before[2U].relative_path,
|
||||||
referenceBefore[2U].relativePath,
|
|
||||||
std::filesystem::path{"cantilever beam reactions.csv"});
|
std::filesystem::path{"cantilever beam reactions.csv"});
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(reference_before[3U].relative_path,
|
||||||
referenceBefore[3U].relativePath,
|
|
||||||
std::filesystem::path{"cantilever beam.inp"});
|
std::filesystem::path{"cantilever beam.inp"});
|
||||||
|
|
||||||
fesa::FesaApplication application;
|
fesa::FesaApplication application;
|
||||||
// FesaApplication receives application operands/options; main strips argv[0].
|
// FesaApplication receives application operands/options; main strips argv[0].
|
||||||
ASSERT_EQ(
|
ASSERT_EQ(application.Run({input.string(), "--output", results.string()}), 0);
|
||||||
application.run(
|
|
||||||
{input.string(), "--output", results.string()}),
|
|
||||||
0);
|
|
||||||
ASSERT_TRUE(std::filesystem::is_regular_file(results));
|
ASSERT_TRUE(std::filesystem::is_regular_file(results));
|
||||||
ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0);
|
ASSERT_GT(H5Fis_hdf5(results.string().c_str()), 0);
|
||||||
EXPECT_GT(stressRowCount(results), 0U);
|
EXPECT_GT(StressRowCount(results), 0U);
|
||||||
|
|
||||||
auto comparisonResult = fesa::test::ReferenceComparison::compare(
|
auto comparison_result =
|
||||||
results, referenceDirectory);
|
fesa::test::ReferenceComparison::Compare(results, reference_directory);
|
||||||
ASSERT_TRUE(comparisonResult.HasValue());
|
ASSERT_TRUE(comparison_result.HasValue());
|
||||||
const auto& report = comparisonResult.Value();
|
const auto& report = comparison_result.Value();
|
||||||
ASSERT_TRUE(report.passed);
|
ASSERT_TRUE(report.passed);
|
||||||
ASSERT_EQ(report.rows.size(), kExpectedRowCount);
|
ASSERT_EQ(report.rows.size(), kExpectedRowCount);
|
||||||
ASSERT_EQ(report.metrics.size(), kExpectedMetricCount);
|
ASSERT_EQ(report.metrics.size(), kExpectedMetricCount);
|
||||||
EXPECT_TRUE(std::all_of(
|
EXPECT_TRUE(std::all_of(
|
||||||
report.rows.begin(),
|
report.rows.begin(), report.rows.end(),
|
||||||
report.rows.end(),
|
|
||||||
[](const fesa::test::RowDecision& row) {
|
[](const fesa::test::RowDecision& row) {
|
||||||
return row.passed && std::isfinite(row.absoluteError) &&
|
return row.passed && std::isfinite(row.absolute_error) &&
|
||||||
std::isfinite(row.tolerance) && row.tolerance > 0.0 &&
|
std::isfinite(row.tolerance) && row.tolerance > 0.0 &&
|
||||||
row.fesa.modelId == "cantilever-beam-b33" &&
|
row.fesa.model_id == "cantilever-beam-b33" &&
|
||||||
row.reference.modelId == "cantilever-beam-b33" &&
|
row.reference.model_id == "cantilever-beam-b33" &&
|
||||||
row.fesa.stepName == "Step-1" &&
|
row.fesa.step_name == "Step-1" &&
|
||||||
row.reference.stepName == "Step-1" &&
|
row.reference.step_name == "Step-1" &&
|
||||||
row.fesa.frameIndex == 0U &&
|
row.fesa.frame_index == 0U && row.reference.frame_index == 0U &&
|
||||||
row.reference.frameIndex == 0U &&
|
row.fesa.instance_name == "PART-1_1-1" &&
|
||||||
row.fesa.instanceName == "PART-1_1-1" &&
|
row.reference.instance_name == "PART-1_1-1" &&
|
||||||
row.reference.instanceName == "PART-1_1-1" &&
|
!row.fesa.hdf5_dataset_path.empty();
|
||||||
!row.fesa.hdf5DatasetPath.empty();
|
|
||||||
}));
|
}));
|
||||||
EXPECT_TRUE(std::all_of(
|
EXPECT_TRUE(
|
||||||
report.metrics.begin(),
|
std::all_of(report.metrics.begin(), report.metrics.end(),
|
||||||
report.metrics.end(),
|
|
||||||
[](const fesa::test::ComponentMetrics& metric) {
|
[](const fesa::test::ComponentMetrics& metric) {
|
||||||
return std::isfinite(metric.referenceScale) &&
|
return std::isfinite(metric.reference_scale) &&
|
||||||
std::isfinite(metric.maximumAbsoluteError) &&
|
std::isfinite(metric.maximum_absolute_error) &&
|
||||||
std::isfinite(metric.maximumNormalizedError) &&
|
std::isfinite(metric.maximum_normalized_error) &&
|
||||||
std::isfinite(metric.rmsError) &&
|
std::isfinite(metric.rms_error) &&
|
||||||
std::isfinite(metric.normError) &&
|
std::isfinite(metric.norm_error) &&
|
||||||
metric.maximumNormalizedError <= 1.0;
|
metric.maximum_normalized_error <= 1.0;
|
||||||
}));
|
}));
|
||||||
|
|
||||||
EXPECT_FALSE(report.stressComparisonApplicable);
|
EXPECT_FALSE(report.stress_comparison_applicable);
|
||||||
EXPECT_NE(report.stressComparisonReason.find("N/A"), std::string::npos);
|
EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos);
|
||||||
EXPECT_NE(report.stressComparisonReason.find("HDF5"), std::string::npos);
|
EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos);
|
||||||
EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed);
|
EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
|
||||||
EXPECT_TRUE(std::isfinite(report.physicsEvidence.freeResidualNorm));
|
EXPECT_TRUE(std::isfinite(report.physics_evidence.free_residual_norm));
|
||||||
EXPECT_LE(report.physicsEvidence.freeResidualNorm, 1.0e-3);
|
EXPECT_LE(report.physics_evidence.free_residual_norm, 1.0e-3);
|
||||||
EXPECT_LE(
|
EXPECT_LE(Norm(Sum(report.physics_evidence.applied_force,
|
||||||
norm(sum(
|
report.physics_evidence.reaction_force)),
|
||||||
report.physicsEvidence.appliedForce,
|
|
||||||
report.physicsEvidence.reactionForce)),
|
|
||||||
1.0e-3);
|
1.0e-3);
|
||||||
EXPECT_LE(
|
EXPECT_LE(Norm(Sum(report.physics_evidence.applied_moment_about_origin,
|
||||||
norm(sum(
|
report.physics_evidence.reaction_moment_about_origin)),
|
||||||
report.physicsEvidence.appliedMomentAboutOrigin,
|
|
||||||
report.physicsEvidence.reactionMomentAboutOrigin)),
|
|
||||||
1.0e-2);
|
1.0e-2);
|
||||||
|
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(fesa::test::ReferenceComparison::WriteDeterministicJson(
|
||||||
fesa::test::ReferenceComparison::writeDeterministicJson(
|
|
||||||
report, comparison)
|
report, comparison)
|
||||||
.IsOk());
|
.IsOk());
|
||||||
ASSERT_TRUE(std::filesystem::is_regular_file(comparison));
|
ASSERT_TRUE(std::filesystem::is_regular_file(comparison));
|
||||||
const std::string json = readBytes(comparison);
|
const std::string json = ReadBytes(comparison);
|
||||||
EXPECT_NE(json.find("\"stress_comparison_applicable\":false"),
|
EXPECT_NE(json.find("\"stress_comparison_applicable\":false"),
|
||||||
std::string::npos);
|
std::string::npos);
|
||||||
EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos);
|
EXPECT_NE(json.find("\"physics_evidence\""), std::string::npos);
|
||||||
|
|
||||||
std::vector<std::string> generatedNames;
|
std::vector<std::string> generated_names;
|
||||||
for (const auto& entry :
|
for (const auto& entry :
|
||||||
std::filesystem::directory_iterator{outputDirectory}) {
|
std::filesystem::directory_iterator{output_directory}) {
|
||||||
generatedNames.push_back(entry.path().filename().string());
|
generated_names.push_back(entry.path().filename().string());
|
||||||
}
|
}
|
||||||
std::sort(generatedNames.begin(), generatedNames.end());
|
std::sort(generated_names.begin(), generated_names.end());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(generated_names,
|
||||||
generatedNames,
|
|
||||||
(std::vector<std::string>{"comparison.json", "results.h5"}));
|
(std::vector<std::string>{"comparison.json", "results.h5"}));
|
||||||
|
|
||||||
expectTreeUnchanged(referenceBefore, snapshotTree(referenceDirectory));
|
ExpectTreeUnchanged(reference_before, SnapshotTree(reference_directory));
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,3 @@
|
|||||||
#include "mitc4_reference_comparison.hpp"
|
|
||||||
|
|
||||||
#include "fesa/app/fesa_application.hpp"
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <algorithm>
|
#include <algorithm>
|
||||||
@@ -14,6 +10,9 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/app/fesa_application.h"
|
||||||
|
#include "mitc4_reference_comparison.h"
|
||||||
|
|
||||||
#ifndef FESA_TEST_SOURCE_DIR
|
#ifndef FESA_TEST_SOURCE_DIR
|
||||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||||
#endif
|
#endif
|
||||||
@@ -30,7 +29,7 @@ constexpr const char* kIntegrationRule =
|
|||||||
constexpr std::size_t kNodeCount = 49U;
|
constexpr std::size_t kNodeCount = 49U;
|
||||||
constexpr std::size_t kComponentCount = 6U;
|
constexpr std::size_t kComponentCount = 6U;
|
||||||
|
|
||||||
std::string readBytes(const std::filesystem::path& path) {
|
std::string ReadBytes(const std::filesystem::path& path) {
|
||||||
std::ifstream stream{path, std::ios::binary};
|
std::ifstream stream{path, std::ios::binary};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw std::runtime_error{"Unable to read declared reference artifact."};
|
throw std::runtime_error{"Unable to read declared reference artifact."};
|
||||||
@@ -41,117 +40,106 @@ std::string readBytes(const std::filesystem::path& path) {
|
|||||||
|
|
||||||
struct ArtifactSnapshot {
|
struct ArtifactSnapshot {
|
||||||
std::string bytes;
|
std::string bytes;
|
||||||
std::filesystem::file_time_type lastWriteTime;
|
std::filesystem::file_time_type last_write_time;
|
||||||
};
|
};
|
||||||
|
|
||||||
ArtifactSnapshot snapshot(const std::filesystem::path& path) {
|
ArtifactSnapshot Snapshot(const std::filesystem::path& path) {
|
||||||
return {readBytes(path), std::filesystem::last_write_time(path)};
|
return {ReadBytes(path), std::filesystem::last_write_time(path)};
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectUnchanged(
|
void ExpectUnchanged(const std::filesystem::path& path,
|
||||||
const std::filesystem::path& path, const ArtifactSnapshot& before) {
|
const ArtifactSnapshot& before) {
|
||||||
EXPECT_EQ(readBytes(path), before.bytes) << path.string();
|
EXPECT_EQ(ReadBytes(path), before.bytes) << path.string();
|
||||||
EXPECT_EQ(std::filesystem::last_write_time(path), before.lastWriteTime)
|
EXPECT_EQ(std::filesystem::last_write_time(path), before.last_write_time)
|
||||||
<< path.string();
|
<< path.string();
|
||||||
}
|
}
|
||||||
|
|
||||||
struct CaseEvidence {
|
struct CaseEvidence {
|
||||||
fesa::test::Mitc4ComparisonReport report;
|
fesa::test::Mitc4ComparisonReport report;
|
||||||
std::filesystem::path comparisonJson;
|
std::filesystem::path comparison_json;
|
||||||
};
|
};
|
||||||
|
|
||||||
CaseEvidence runCase(
|
CaseEvidence RunCase(const std::string& case_id,
|
||||||
const std::string& caseId,
|
const std::string& source_element_type,
|
||||||
const std::string& sourceElementType,
|
const std::filesystem::path& reference_directory,
|
||||||
const std::filesystem::path& referenceDirectory,
|
|
||||||
const std::filesystem::path& input,
|
const std::filesystem::path& input,
|
||||||
const std::filesystem::path& csv,
|
const std::filesystem::path& csv,
|
||||||
const std::string& outputName) {
|
const std::string& output_name) {
|
||||||
const auto inputBefore = snapshot(input);
|
const auto input_before = Snapshot(input);
|
||||||
const auto csvBefore = snapshot(csv);
|
const auto csv_before = Snapshot(csv);
|
||||||
const std::filesystem::path outputDirectory =
|
const std::filesystem::path output_directory =
|
||||||
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / outputName;
|
std::filesystem::path{FESA_TEST_BINARY_DIR} / "reference" / output_name;
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
std::filesystem::remove_all(outputDirectory, error);
|
std::filesystem::remove_all(output_directory, error);
|
||||||
error.clear();
|
error.clear();
|
||||||
if (!std::filesystem::create_directories(outputDirectory, error) || error) {
|
if (!std::filesystem::create_directories(output_directory, error) || error) {
|
||||||
throw std::runtime_error{"Unable to create MITC4 evidence directory."};
|
throw std::runtime_error{"Unable to create MITC4 evidence directory."};
|
||||||
}
|
}
|
||||||
const auto results = outputDirectory / "results.h5";
|
const auto results = output_directory / "results.h5";
|
||||||
const auto comparison = outputDirectory / "comparison.json";
|
const auto comparison = output_directory / "comparison.json";
|
||||||
|
|
||||||
fesa::FesaApplication application;
|
fesa::FesaApplication application;
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(application.Run({input.string(), "--output", results.string()}), 0);
|
||||||
application.run({input.string(), "--output", results.string()}), 0);
|
|
||||||
EXPECT_TRUE(std::filesystem::is_regular_file(results));
|
EXPECT_TRUE(std::filesystem::is_regular_file(results));
|
||||||
auto comparisonResult = fesa::test::Mitc4ReferenceComparison::compare(
|
auto comparison_result = fesa::test::Mitc4ReferenceComparison::Compare(
|
||||||
{caseId, sourceElementType, input, csv, results});
|
{case_id, source_element_type, input, csv, results});
|
||||||
if (!comparisonResult.HasValue()) {
|
if (!comparison_result.HasValue()) {
|
||||||
std::string diagnostics;
|
std::string diagnostics;
|
||||||
for (const auto& diagnostic :
|
for (const auto& diagnostic : comparison_result.GetStatus().Diagnostics()) {
|
||||||
comparisonResult.GetStatus().Diagnostics()) {
|
|
||||||
diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message;
|
diagnostics += "\n" + diagnostic.code + ": " + diagnostic.message;
|
||||||
}
|
}
|
||||||
ADD_FAILURE() << "MITC4 comparison precheck failed for " << caseId
|
ADD_FAILURE() << "MITC4 comparison precheck failed for " << case_id
|
||||||
<< diagnostics;
|
<< diagnostics;
|
||||||
return {{}, comparison};
|
return {{}, comparison};
|
||||||
}
|
}
|
||||||
EXPECT_TRUE(
|
EXPECT_TRUE(fesa::test::Mitc4ReferenceComparison::WriteDeterministicJson(
|
||||||
fesa::test::Mitc4ReferenceComparison::writeDeterministicJson(
|
comparison_result.Value(), comparison)
|
||||||
comparisonResult.Value(), comparison)
|
|
||||||
.IsOk());
|
.IsOk());
|
||||||
EXPECT_TRUE(std::filesystem::is_regular_file(comparison));
|
EXPECT_TRUE(std::filesystem::is_regular_file(comparison));
|
||||||
|
|
||||||
std::vector<std::string> generated;
|
std::vector<std::string> generated;
|
||||||
for (const auto& entry :
|
for (const auto& entry :
|
||||||
std::filesystem::directory_iterator{outputDirectory}) {
|
std::filesystem::directory_iterator{output_directory}) {
|
||||||
generated.push_back(entry.path().filename().string());
|
generated.push_back(entry.path().filename().string());
|
||||||
}
|
}
|
||||||
std::sort(generated.begin(), generated.end());
|
std::sort(generated.begin(), generated.end());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(generated,
|
||||||
generated,
|
|
||||||
(std::vector<std::string>{"comparison.json", "results.h5"}));
|
(std::vector<std::string>{"comparison.json", "results.h5"}));
|
||||||
EXPECT_TRUE(std::filesystem::is_directory(referenceDirectory));
|
EXPECT_TRUE(std::filesystem::is_directory(reference_directory));
|
||||||
expectUnchanged(input, inputBefore);
|
ExpectUnchanged(input, input_before);
|
||||||
expectUnchanged(csv, csvBefore);
|
ExpectUnchanged(csv, csv_before);
|
||||||
return {std::move(comparisonResult.Value()), comparison};
|
return {std::move(comparison_result.Value()), comparison};
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectCommonMetadata(
|
void ExpectCommonMetadata(const fesa::test::Mitc4ComparisonReport& report,
|
||||||
const fesa::test::Mitc4ComparisonReport& report,
|
const std::string& case_id,
|
||||||
const std::string& caseId,
|
const std::string& source_element_type) {
|
||||||
const std::string& sourceElementType) {
|
EXPECT_EQ(report.case_id, case_id);
|
||||||
EXPECT_EQ(report.caseId, caseId);
|
EXPECT_EQ(report.source_element_type, source_element_type);
|
||||||
EXPECT_EQ(report.sourceElementType, sourceElementType);
|
EXPECT_EQ(report.internal_formulation, kInternalFormulation);
|
||||||
EXPECT_EQ(report.internalFormulation, kInternalFormulation);
|
EXPECT_EQ(report.integration_rule, kIntegrationRule);
|
||||||
EXPECT_EQ(report.integrationRule, kIntegrationRule);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectComparisonCoverage(
|
void ExpectComparisonCoverage(const fesa::test::Mitc4ComparisonReport& report) {
|
||||||
const fesa::test::Mitc4ComparisonReport& report) {
|
|
||||||
ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount);
|
ASSERT_EQ(report.rows.size(), kNodeCount * kComponentCount);
|
||||||
ASSERT_EQ(report.metrics.size(), kComponentCount);
|
ASSERT_EQ(report.metrics.size(), kComponentCount);
|
||||||
ASSERT_EQ(report.vectorMetrics.size(), kNodeCount);
|
ASSERT_EQ(report.vector_metrics.size(), kNodeCount);
|
||||||
EXPECT_TRUE(report.passed);
|
EXPECT_TRUE(report.passed);
|
||||||
const std::size_t blockingRows = static_cast<std::size_t>(std::count_if(
|
const std::size_t blocking_rows = static_cast<std::size_t>(std::count_if(
|
||||||
report.rows.begin(), report.rows.end(),
|
report.rows.begin(), report.rows.end(),
|
||||||
|
[](const fesa::test::Mitc4RowDecision& row) { return row.blocking; }));
|
||||||
|
const std::size_t rotation_rows = report.rows.size() - blocking_rows;
|
||||||
|
EXPECT_EQ(blocking_rows, kNodeCount * 3U);
|
||||||
|
EXPECT_EQ(rotation_rows, kNodeCount * 3U);
|
||||||
|
EXPECT_TRUE(std::all_of(report.rows.begin(), report.rows.end(),
|
||||||
[](const fesa::test::Mitc4RowDecision& row) {
|
[](const fesa::test::Mitc4RowDecision& row) {
|
||||||
return row.blocking;
|
return !row.blocking || row.within_tolerance;
|
||||||
}));
|
}));
|
||||||
const std::size_t rotationRows = report.rows.size() - blockingRows;
|
EXPECT_EQ(report.warnings.size(),
|
||||||
EXPECT_EQ(blockingRows, kNodeCount * 3U);
|
static_cast<std::size_t>(
|
||||||
EXPECT_EQ(rotationRows, kNodeCount * 3U);
|
std::count_if(report.rows.begin(), report.rows.end(),
|
||||||
EXPECT_TRUE(std::all_of(
|
|
||||||
report.rows.begin(), report.rows.end(),
|
|
||||||
[](const fesa::test::Mitc4RowDecision& row) {
|
[](const fesa::test::Mitc4RowDecision& row) {
|
||||||
return !row.blocking || row.withinTolerance;
|
return !row.blocking && !row.within_tolerance;
|
||||||
}));
|
|
||||||
EXPECT_EQ(
|
|
||||||
report.warnings.size(),
|
|
||||||
static_cast<std::size_t>(std::count_if(
|
|
||||||
report.rows.begin(), report.rows.end(),
|
|
||||||
[](const fesa::test::Mitc4RowDecision& row) {
|
|
||||||
return !row.blocking && !row.withinTolerance;
|
|
||||||
})));
|
})));
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -159,21 +147,21 @@ void expectComparisonCoverage(
|
|||||||
TEST(Mitc4S4Reference, PreservesS4AndWritesCommonMitc4Metadata) {
|
TEST(Mitc4S4Reference, PreservesS4AndWritesCommonMitc4Metadata) {
|
||||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
||||||
const auto directory = root / "reference" / "shell";
|
const auto directory = root / "reference" / "shell";
|
||||||
const auto evidence = runCase(
|
const auto evidence =
|
||||||
"shell-s4", "S4", directory, directory / "shell.inp",
|
RunCase("shell-s4", "S4", directory, directory / "shell.inp",
|
||||||
directory / "shell displacements.csv", "mitc4-shell-s4-metadata");
|
directory / "shell displacements.csv", "mitc4-shell-s4-metadata");
|
||||||
expectCommonMetadata(evidence.report, "shell-s4", "S4");
|
ExpectCommonMetadata(evidence.report, "shell-s4", "S4");
|
||||||
}
|
}
|
||||||
|
|
||||||
// MITC4-E2E-S4-002
|
// MITC4-E2E-S4-002
|
||||||
TEST(Mitc4S4Reference, PassesBlockingUAndReportsEveryUrRow) {
|
TEST(Mitc4S4Reference, PassesBlockingUAndReportsEveryUrRow) {
|
||||||
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
||||||
const auto directory = root / "reference" / "shell";
|
const auto directory = root / "reference" / "shell";
|
||||||
const auto evidence = runCase(
|
const auto evidence = RunCase(
|
||||||
"shell-s4", "S4", directory, directory / "shell.inp",
|
"shell-s4", "S4", directory, directory / "shell.inp",
|
||||||
directory / "shell displacements.csv", "mitc4-shell-s4-comparison");
|
directory / "shell displacements.csv", "mitc4-shell-s4-comparison");
|
||||||
expectCommonMetadata(evidence.report, "shell-s4", "S4");
|
ExpectCommonMetadata(evidence.report, "shell-s4", "S4");
|
||||||
expectComparisonCoverage(evidence.report);
|
ExpectComparisonCoverage(evidence.report);
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,84 @@
|
|||||||
|
#ifndef FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_
|
||||||
|
#define FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
|
||||||
|
namespace fesa::test {
|
||||||
|
|
||||||
|
struct Mitc4ReferenceCase {
|
||||||
|
std::string case_id;
|
||||||
|
std::string expected_source_element_type;
|
||||||
|
std::filesystem::path input_path;
|
||||||
|
std::filesystem::path displacement_csv_path;
|
||||||
|
std::filesystem::path results_hdf5_path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mitc4RowDecision {
|
||||||
|
std::string case_id;
|
||||||
|
std::string instance_name;
|
||||||
|
std::int64_t source_node_label;
|
||||||
|
std::string component;
|
||||||
|
double fesa_value;
|
||||||
|
double reference_value;
|
||||||
|
double absolute_error;
|
||||||
|
double tolerance;
|
||||||
|
double normalized_error;
|
||||||
|
bool blocking;
|
||||||
|
bool within_tolerance;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mitc4ComponentMetrics {
|
||||||
|
std::string component;
|
||||||
|
double reference_scale;
|
||||||
|
double tolerance;
|
||||||
|
double maximum_absolute_error;
|
||||||
|
double maximum_normalized_error;
|
||||||
|
double rms_error;
|
||||||
|
double vector_norm_error;
|
||||||
|
std::size_t worst_row;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mitc4VectorMetrics {
|
||||||
|
std::string instance_name;
|
||||||
|
std::int64_t source_node_label;
|
||||||
|
double displacement_norm_error;
|
||||||
|
double rotation_norm_error;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mitc4Warning {
|
||||||
|
std::string code;
|
||||||
|
std::size_t row;
|
||||||
|
std::string message;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct Mitc4ComparisonReport {
|
||||||
|
std::string case_id;
|
||||||
|
std::string source_element_type;
|
||||||
|
std::string internal_formulation;
|
||||||
|
std::string integration_rule;
|
||||||
|
std::vector<Mitc4RowDecision> rows;
|
||||||
|
std::vector<Mitc4ComponentMetrics> metrics;
|
||||||
|
std::vector<Mitc4VectorMetrics> vector_metrics;
|
||||||
|
std::vector<Mitc4Warning> warnings;
|
||||||
|
std::size_t worst_row;
|
||||||
|
bool passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
class Mitc4ReferenceComparison {
|
||||||
|
public:
|
||||||
|
static Result<Mitc4ComparisonReport> Compare(
|
||||||
|
const Mitc4ReferenceCase& reference_case);
|
||||||
|
static Status WriteDeterministicJson(
|
||||||
|
const Mitc4ComparisonReport& report,
|
||||||
|
const std::filesystem::path& output_json);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::test
|
||||||
|
|
||||||
|
#endif // FESA_TESTS_REFERENCE_MITC4_REFERENCE_COMPARISON_H_
|
||||||
@@ -1,81 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "fesa/core/status.h"
|
|
||||||
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace fesa::test {
|
|
||||||
|
|
||||||
struct Mitc4ReferenceCase {
|
|
||||||
std::string caseId;
|
|
||||||
std::string expectedSourceElementType;
|
|
||||||
std::filesystem::path inputPath;
|
|
||||||
std::filesystem::path displacementCsvPath;
|
|
||||||
std::filesystem::path resultsHdf5Path;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Mitc4RowDecision {
|
|
||||||
std::string caseId;
|
|
||||||
std::string instanceName;
|
|
||||||
std::int64_t sourceNodeLabel;
|
|
||||||
std::string component;
|
|
||||||
double fesaValue;
|
|
||||||
double referenceValue;
|
|
||||||
double absoluteError;
|
|
||||||
double tolerance;
|
|
||||||
double normalizedError;
|
|
||||||
bool blocking;
|
|
||||||
bool withinTolerance;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Mitc4ComponentMetrics {
|
|
||||||
std::string component;
|
|
||||||
double referenceScale;
|
|
||||||
double tolerance;
|
|
||||||
double maximumAbsoluteError;
|
|
||||||
double maximumNormalizedError;
|
|
||||||
double rmsError;
|
|
||||||
double vectorNormError;
|
|
||||||
std::size_t worstRow;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Mitc4VectorMetrics {
|
|
||||||
std::string instanceName;
|
|
||||||
std::int64_t sourceNodeLabel;
|
|
||||||
double displacementNormError;
|
|
||||||
double rotationNormError;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Mitc4Warning {
|
|
||||||
std::string code;
|
|
||||||
std::size_t row;
|
|
||||||
std::string message;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct Mitc4ComparisonReport {
|
|
||||||
std::string caseId;
|
|
||||||
std::string sourceElementType;
|
|
||||||
std::string internalFormulation;
|
|
||||||
std::string integrationRule;
|
|
||||||
std::vector<Mitc4RowDecision> rows;
|
|
||||||
std::vector<Mitc4ComponentMetrics> metrics;
|
|
||||||
std::vector<Mitc4VectorMetrics> vectorMetrics;
|
|
||||||
std::vector<Mitc4Warning> warnings;
|
|
||||||
std::size_t worstRow;
|
|
||||||
bool passed;
|
|
||||||
};
|
|
||||||
|
|
||||||
class Mitc4ReferenceComparison {
|
|
||||||
public:
|
|
||||||
static Result<Mitc4ComparisonReport> compare(
|
|
||||||
const Mitc4ReferenceCase& referenceCase);
|
|
||||||
static Status writeDeterministicJson(
|
|
||||||
const Mitc4ComparisonReport& report,
|
|
||||||
const std::filesystem::path& outputJson);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa::test
|
|
||||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,81 @@
|
|||||||
|
#ifndef FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_
|
||||||
|
#define FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_
|
||||||
|
|
||||||
|
#include <array>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/core/status.h"
|
||||||
|
|
||||||
|
namespace fesa::test {
|
||||||
|
|
||||||
|
enum class ComparisonQuantity { kDisplacement, kReaction, kSectionResultant };
|
||||||
|
|
||||||
|
struct CanonicalComparisonRow {
|
||||||
|
std::string model_id;
|
||||||
|
std::string step_name;
|
||||||
|
std::size_t frame_index;
|
||||||
|
std::string instance_name;
|
||||||
|
std::int64_t source_node_label;
|
||||||
|
ComparisonQuantity quantity;
|
||||||
|
std::string component;
|
||||||
|
double value;
|
||||||
|
std::string unit_dimension;
|
||||||
|
std::string coordinate_system;
|
||||||
|
std::string hdf5_dataset_path;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct RowDecision {
|
||||||
|
CanonicalComparisonRow fesa;
|
||||||
|
CanonicalComparisonRow reference;
|
||||||
|
double absolute_error;
|
||||||
|
double tolerance;
|
||||||
|
bool passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComponentMetrics {
|
||||||
|
ComparisonQuantity quantity;
|
||||||
|
std::string component;
|
||||||
|
double reference_scale;
|
||||||
|
double maximum_absolute_error;
|
||||||
|
double maximum_normalized_error;
|
||||||
|
double rms_error;
|
||||||
|
double norm_error;
|
||||||
|
std::size_t worst_row;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct PhysicsEvidence {
|
||||||
|
double free_residual_norm;
|
||||||
|
std::array<double, 3> applied_force;
|
||||||
|
std::array<double, 3> reaction_force;
|
||||||
|
std::array<double, 3> applied_moment_about_origin;
|
||||||
|
std::array<double, 3> reaction_moment_about_origin;
|
||||||
|
bool endpoint_consistency_passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ComparisonReport {
|
||||||
|
std::vector<RowDecision> rows;
|
||||||
|
std::vector<ComponentMetrics> metrics;
|
||||||
|
PhysicsEvidence physics_evidence;
|
||||||
|
bool stress_comparison_applicable;
|
||||||
|
std::string stress_comparison_reason;
|
||||||
|
bool passed;
|
||||||
|
};
|
||||||
|
|
||||||
|
// Test-only comparison support keeps Abaqus artifacts read-only and exposes no
|
||||||
|
// backend handles to the implementation or downstream verification Steps.
|
||||||
|
class ReferenceComparison {
|
||||||
|
public:
|
||||||
|
static Result<ComparisonReport> Compare(
|
||||||
|
const std::filesystem::path& results_hdf5,
|
||||||
|
const std::filesystem::path& legacy_reference_directory);
|
||||||
|
static Status WriteDeterministicJson(
|
||||||
|
const ComparisonReport& report, const std::filesystem::path& output_json);
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa::test
|
||||||
|
|
||||||
|
#endif // FESA_TESTS_REFERENCE_REFERENCE_COMPARISON_H_
|
||||||
@@ -1,83 +0,0 @@
|
|||||||
#pragma once
|
|
||||||
|
|
||||||
#include "fesa/core/status.h"
|
|
||||||
|
|
||||||
#include <array>
|
|
||||||
#include <cstddef>
|
|
||||||
#include <cstdint>
|
|
||||||
#include <filesystem>
|
|
||||||
#include <string>
|
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
namespace fesa::test {
|
|
||||||
|
|
||||||
enum class ComparisonQuantity {
|
|
||||||
displacement,
|
|
||||||
reaction,
|
|
||||||
sectionResultant
|
|
||||||
};
|
|
||||||
|
|
||||||
struct CanonicalComparisonRow {
|
|
||||||
std::string modelId;
|
|
||||||
std::string stepName;
|
|
||||||
std::size_t frameIndex;
|
|
||||||
std::string instanceName;
|
|
||||||
std::int64_t sourceNodeLabel;
|
|
||||||
ComparisonQuantity quantity;
|
|
||||||
std::string component;
|
|
||||||
double value;
|
|
||||||
std::string unitDimension;
|
|
||||||
std::string coordinateSystem;
|
|
||||||
std::string hdf5DatasetPath;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct RowDecision {
|
|
||||||
CanonicalComparisonRow fesa;
|
|
||||||
CanonicalComparisonRow reference;
|
|
||||||
double absoluteError;
|
|
||||||
double tolerance;
|
|
||||||
bool passed;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ComponentMetrics {
|
|
||||||
ComparisonQuantity quantity;
|
|
||||||
std::string component;
|
|
||||||
double referenceScale;
|
|
||||||
double maximumAbsoluteError;
|
|
||||||
double maximumNormalizedError;
|
|
||||||
double rmsError;
|
|
||||||
double normError;
|
|
||||||
std::size_t worstRow;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct PhysicsEvidence {
|
|
||||||
double freeResidualNorm;
|
|
||||||
std::array<double, 3> appliedForce;
|
|
||||||
std::array<double, 3> reactionForce;
|
|
||||||
std::array<double, 3> appliedMomentAboutOrigin;
|
|
||||||
std::array<double, 3> reactionMomentAboutOrigin;
|
|
||||||
bool endpointConsistencyPassed;
|
|
||||||
};
|
|
||||||
|
|
||||||
struct ComparisonReport {
|
|
||||||
std::vector<RowDecision> rows;
|
|
||||||
std::vector<ComponentMetrics> metrics;
|
|
||||||
PhysicsEvidence physicsEvidence;
|
|
||||||
bool stressComparisonApplicable;
|
|
||||||
std::string stressComparisonReason;
|
|
||||||
bool passed;
|
|
||||||
};
|
|
||||||
|
|
||||||
// Test-only comparison support keeps Abaqus artifacts read-only and exposes no
|
|
||||||
// backend handles to the implementation or downstream verification Steps.
|
|
||||||
class ReferenceComparison {
|
|
||||||
public:
|
|
||||||
static Result<ComparisonReport> compare(
|
|
||||||
const std::filesystem::path& resultsHdf5,
|
|
||||||
const std::filesystem::path& legacyReferenceDirectory);
|
|
||||||
static Status writeDeterministicJson(
|
|
||||||
const ComparisonReport& report,
|
|
||||||
const std::filesystem::path& outputJson);
|
|
||||||
};
|
|
||||||
|
|
||||||
} // namespace fesa::test
|
|
||||||
@@ -1,11 +1,4 @@
|
|||||||
#include "reference_comparison.hpp"
|
#include "reference_comparison.h"
|
||||||
|
|
||||||
#include "fesa/analysis/analysis_model.h"
|
|
||||||
#include "fesa/analysis/analysis_state.h"
|
|
||||||
#include "fesa/fem/dof_manager.h"
|
|
||||||
#include "fesa/io/abaqus/input_reader.hpp"
|
|
||||||
#include "fesa/io/hdf5/hdf5_results_writer.hpp"
|
|
||||||
#include "fesa/model/domain.h"
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
@@ -24,6 +17,13 @@
|
|||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/analysis/analysis_model.h"
|
||||||
|
#include "fesa/analysis/analysis_state.h"
|
||||||
|
#include "fesa/fem/dof_manager.h"
|
||||||
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
#include "fesa/io/hdf5/hdf5_results_writer.h"
|
||||||
|
#include "fesa/model/domain.h"
|
||||||
|
|
||||||
#ifndef FESA_TEST_SOURCE_DIR
|
#ifndef FESA_TEST_SOURCE_DIR
|
||||||
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
#error FESA_TEST_SOURCE_DIR must identify the repository root.
|
||||||
#endif
|
#endif
|
||||||
@@ -51,20 +51,20 @@ using EndpointValues =
|
|||||||
struct ComparisonValues {
|
struct ComparisonValues {
|
||||||
NodalValues displacement{};
|
NodalValues displacement{};
|
||||||
NodalValues reaction{};
|
NodalValues reaction{};
|
||||||
EndpointValues sectionResultants{};
|
EndpointValues section_resultants{};
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::filesystem::path& sourceRoot() {
|
const std::filesystem::path& SourceRoot() {
|
||||||
static const std::filesystem::path root{FESA_TEST_SOURCE_DIR};
|
static const std::filesystem::path kRoot{FESA_TEST_SOURCE_DIR};
|
||||||
return root;
|
return kRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path& binaryRoot() {
|
const std::filesystem::path& BinaryRoot() {
|
||||||
static const std::filesystem::path root{FESA_TEST_BINARY_DIR};
|
static const std::filesystem::path kRoot{FESA_TEST_BINARY_DIR};
|
||||||
return root;
|
return kRoot;
|
||||||
}
|
}
|
||||||
|
|
||||||
std::string readBytes(const std::filesystem::path& path) {
|
std::string ReadBytes(const std::filesystem::path& path) {
|
||||||
std::ifstream stream{path, std::ios::binary};
|
std::ifstream stream{path, std::ios::binary};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw std::runtime_error{"Unable to read fixture: " + path.string()};
|
throw std::runtime_error{"Unable to read fixture: " + path.string()};
|
||||||
@@ -73,7 +73,8 @@ std::string readBytes(const std::filesystem::path& path) {
|
|||||||
std::istreambuf_iterator<char>{}};
|
std::istreambuf_iterator<char>{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeBytes(const std::filesystem::path& path, const std::string& contents) {
|
void WriteBytes(const std::filesystem::path& path,
|
||||||
|
const std::string& contents) {
|
||||||
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
std::ofstream stream{path, std::ios::binary | std::ios::trunc};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw std::runtime_error{"Unable to write build-local fixture: " +
|
throw std::runtime_error{"Unable to write build-local fixture: " +
|
||||||
@@ -85,7 +86,7 @@ void writeBytes(const std::filesystem::path& path, const std::string& contents)
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
std::vector<std::string> readLines(const std::filesystem::path& path) {
|
std::vector<std::string> ReadLines(const std::filesystem::path& path) {
|
||||||
std::ifstream stream{path};
|
std::ifstream stream{path};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw std::runtime_error{"Unable to read fixture lines."};
|
throw std::runtime_error{"Unable to read fixture lines."};
|
||||||
@@ -100,8 +101,7 @@ std::vector<std::string> readLines(const std::filesystem::path& path) {
|
|||||||
return lines;
|
return lines;
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeLines(
|
void WriteLines(const std::filesystem::path& path,
|
||||||
const std::filesystem::path& path,
|
|
||||||
const std::vector<std::string>& lines) {
|
const std::vector<std::string>& lines) {
|
||||||
std::ofstream stream{path, std::ios::trunc};
|
std::ofstream stream{path, std::ios::trunc};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
@@ -115,9 +115,7 @@ void writeLines(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
void replaceFirst(
|
void ReplaceFirst(std::string& contents, const std::string& from,
|
||||||
std::string& contents,
|
|
||||||
const std::string& from,
|
|
||||||
const std::string& to) {
|
const std::string& to) {
|
||||||
const std::size_t position = contents.find(from);
|
const std::size_t position = contents.find(from);
|
||||||
if (position == std::string::npos) {
|
if (position == std::string::npos) {
|
||||||
@@ -126,32 +124,16 @@ void replaceFirst(
|
|||||||
contents.replace(position, from.size(), to);
|
contents.replace(position, from.size(), to);
|
||||||
}
|
}
|
||||||
|
|
||||||
ComparisonValues referenceValues() {
|
ComparisonValues ReferenceValues() {
|
||||||
ComparisonValues values{};
|
ComparisonValues values{};
|
||||||
const std::array<double, kNodeCount> uz = {
|
const std::array<double, kNodeCount> uz = {
|
||||||
-1.0e-30,
|
-1.0e-30, -2.761905780e-4, -1.066667140e-3, -2.314286540e-3,
|
||||||
-2.761905780e-4,
|
-3.961906300e-3, -5.952383390e-3, -8.228574880e-3, -1.073333810e-2,
|
||||||
-1.066667140e-3,
|
-1.340952890e-2, -1.620000600e-2, -1.904762720e-2};
|
||||||
-2.314286540e-3,
|
|
||||||
-3.961906300e-3,
|
|
||||||
-5.952383390e-3,
|
|
||||||
-8.228574880e-3,
|
|
||||||
-1.073333810e-2,
|
|
||||||
-1.340952890e-2,
|
|
||||||
-1.620000600e-2,
|
|
||||||
-1.904762720e-2};
|
|
||||||
const std::array<double, kNodeCount> ury = {
|
const std::array<double, kNodeCount> ury = {
|
||||||
1.0e-29,
|
1.0e-29, 5.428573350e-4, 1.028571860e-3, 1.457143460e-3,
|
||||||
5.428573350e-4,
|
1.828572130e-3, 2.142857990e-3, 2.400001050e-3, 2.600000940e-3,
|
||||||
1.028571860e-3,
|
2.742858140e-3, 2.828572640e-3, 2.857143990e-3};
|
||||||
1.457143460e-3,
|
|
||||||
1.828572130e-3,
|
|
||||||
2.142857990e-3,
|
|
||||||
2.400001050e-3,
|
|
||||||
2.600000940e-3,
|
|
||||||
2.742858140e-3,
|
|
||||||
2.828572640e-3,
|
|
||||||
2.857143990e-3};
|
|
||||||
std::array<std::array<double, 4>, kNodeCount> stations{};
|
std::array<std::array<double, 4>, kNodeCount> stations{};
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
values.displacement[node][2U] = uz[node];
|
values.displacement[node][2U] = uz[node];
|
||||||
@@ -163,29 +145,25 @@ ComparisonValues referenceValues() {
|
|||||||
values.reaction[0U][2U] = 1.0e6;
|
values.reaction[0U][2U] = 1.0e6;
|
||||||
values.reaction[0U][4U] = -1.0e7;
|
values.reaction[0U][4U] = -1.0e7;
|
||||||
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
||||||
values.sectionResultants[element][0U] = stations[element];
|
values.section_resultants[element][0U] = stations[element];
|
||||||
values.sectionResultants[element][1U] = stations[element + 1U];
|
values.section_resultants[element][1U] = stations[element + 1U];
|
||||||
}
|
}
|
||||||
return values;
|
return values;
|
||||||
}
|
}
|
||||||
|
|
||||||
fesa::ModelDefinition makeDefinition(
|
fesa::ModelDefinition MakeDefinition(const std::filesystem::path& input,
|
||||||
const std::filesystem::path& input,
|
std::string source_content_identity) {
|
||||||
std::string sourceContentIdentity) {
|
|
||||||
fesa::ModelDefinition definition{};
|
fesa::ModelDefinition definition{};
|
||||||
definition.source_path = input;
|
definition.source_path = input;
|
||||||
definition.source_content_identity = std::move(sourceContentIdentity);
|
definition.source_content_identity = std::move(source_content_identity);
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
const auto label = static_cast<std::int64_t>(node + 1U);
|
const auto label = static_cast<std::int64_t>(node + 1U);
|
||||||
definition.nodes.push_back({
|
definition.nodes.push_back({{kInstanceName, label, std::to_string(label)},
|
||||||
{kInstanceName, label, std::to_string(label)},
|
|
||||||
{static_cast<double>(node), 0.0, 0.0},
|
{static_cast<double>(node), 0.0, 0.0},
|
||||||
{input, node + 1U}});
|
{input, node + 1U}});
|
||||||
}
|
}
|
||||||
definition.materials.push_back(
|
definition.materials.push_back({"Material-1", 2.1e11, 0.3, {input, 20U}});
|
||||||
{"Material-1", 2.1e11, 0.3, {input, 20U}});
|
definition.sections.push_back({"Section-1",
|
||||||
definition.sections.push_back({
|
|
||||||
"Section-1",
|
|
||||||
1.0,
|
1.0,
|
||||||
0.0833333,
|
0.0833333,
|
||||||
0.0,
|
0.0,
|
||||||
@@ -196,8 +174,8 @@ fesa::ModelDefinition makeDefinition(
|
|||||||
{input, 30U}});
|
{input, 30U}});
|
||||||
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
||||||
const auto label = static_cast<std::int64_t>(element + 1U);
|
const auto label = static_cast<std::int64_t>(element + 1U);
|
||||||
definition.elements.push_back({
|
definition.elements.push_back(
|
||||||
{kInstanceName, label, std::to_string(label)},
|
{{kInstanceName, label, std::to_string(label)},
|
||||||
{static_cast<fesa::EntityIndex>(element),
|
{static_cast<fesa::EntityIndex>(element),
|
||||||
static_cast<fesa::EntityIndex>(element + 1U)},
|
static_cast<fesa::EntityIndex>(element + 1U)},
|
||||||
0U,
|
0U,
|
||||||
@@ -209,32 +187,32 @@ fesa::ModelDefinition makeDefinition(
|
|||||||
return definition;
|
return definition;
|
||||||
}
|
}
|
||||||
|
|
||||||
void writeResultsFixture(
|
void WriteResultsFixture(const std::filesystem::path& output,
|
||||||
const std::filesystem::path& output,
|
|
||||||
const std::filesystem::path& input,
|
const std::filesystem::path& input,
|
||||||
const ComparisonValues& values) {
|
const ComparisonValues& values) {
|
||||||
auto parsedInput = fesa::AbaqusInputReader{}.read(input);
|
auto parsed_input = fesa::AbaqusInputReader{}.Read(input);
|
||||||
if (!parsedInput.HasValue()) {
|
if (!parsed_input.HasValue()) {
|
||||||
throw std::runtime_error{"Reference fixture input identity read failed."};
|
throw std::runtime_error{"Reference fixture input identity read failed."};
|
||||||
}
|
}
|
||||||
auto domainResult = fesa::Domain::Create(
|
auto domain_result = fesa::Domain::Create(
|
||||||
makeDefinition(input, parsedInput.Value().sourceContentIdentity));
|
MakeDefinition(input, parsed_input.Value().source_content_identity));
|
||||||
if (!domainResult.HasValue()) {
|
if (!domain_result.HasValue()) {
|
||||||
throw std::runtime_error{"Reference fixture Domain construction failed."};
|
throw std::runtime_error{"Reference fixture Domain construction failed."};
|
||||||
}
|
}
|
||||||
fesa::Domain domain = std::move(domainResult.Value());
|
fesa::Domain domain = std::move(domain_result.Value());
|
||||||
auto modelResult = fesa::AnalysisModel::Create(domain);
|
auto model_result = fesa::AnalysisModel::Create(domain);
|
||||||
if (!modelResult.HasValue()) {
|
if (!model_result.HasValue()) {
|
||||||
throw std::runtime_error{"Reference fixture AnalysisModel construction failed."};
|
throw std::runtime_error{
|
||||||
|
"Reference fixture AnalysisModel construction failed."};
|
||||||
}
|
}
|
||||||
fesa::AnalysisModel model = std::move(modelResult.Value());
|
fesa::AnalysisModel model = std::move(model_result.Value());
|
||||||
auto dofsResult = fesa::DofManager::Create(model);
|
auto dofs_result = fesa::DofManager::Create(model);
|
||||||
if (!dofsResult.HasValue()) {
|
if (!dofs_result.HasValue()) {
|
||||||
throw std::runtime_error{"Reference fixture DofManager construction failed."};
|
throw std::runtime_error{
|
||||||
|
"Reference fixture DofManager construction failed."};
|
||||||
}
|
}
|
||||||
fesa::DofManager dofs = std::move(dofsResult.Value());
|
fesa::DofManager dofs = std::move(dofs_result.Value());
|
||||||
fesa::AnalysisState state =
|
fesa::AnalysisState state = fesa::AnalysisState::Create(dofs, {"Step-1", 0U});
|
||||||
fesa::AnalysisState::Create(dofs, {"Step-1", 0U});
|
|
||||||
|
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
for (std::size_t component = 0U; component < 6U; ++component) {
|
for (std::size_t component = 0U; component < 6U; ++component) {
|
||||||
@@ -247,33 +225,21 @@ void writeResultsFixture(
|
|||||||
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
for (std::size_t element = 0U; element < kElementCount; ++element) {
|
||||||
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
for (std::size_t endpoint = 0U; endpoint < 2U; ++endpoint) {
|
||||||
const std::size_t node = element + endpoint;
|
const std::size_t node = element + endpoint;
|
||||||
state.EndpointResults().push_back({
|
state.EndpointResults().push_back(
|
||||||
static_cast<fesa::EntityIndex>(element),
|
{static_cast<fesa::EntityIndex>(element),
|
||||||
static_cast<int>(endpoint),
|
static_cast<int>(endpoint),
|
||||||
domain.Nodes()[node].source_id,
|
domain.Nodes()[node].source_id,
|
||||||
{},
|
{},
|
||||||
values.sectionResultants[element][endpoint]});
|
values.section_resultants[element][endpoint]});
|
||||||
}
|
}
|
||||||
state.GaussResults().push_back(
|
state.GaussResults().push_back(
|
||||||
{static_cast<fesa::EntityIndex>(element), 1, {}, {}});
|
{static_cast<fesa::EntityIndex>(element), 1, {}, {}});
|
||||||
state.GaussResults().push_back(
|
state.GaussResults().push_back(
|
||||||
{static_cast<fesa::EntityIndex>(element), 2, {}, {}});
|
{static_cast<fesa::EntityIndex>(element), 2, {}, {}});
|
||||||
state.StressResults().push_back({
|
state.StressResults().push_back({static_cast<fesa::EntityIndex>(element), 1,
|
||||||
static_cast<fesa::EntityIndex>(element),
|
0U, 0.0, 0.0, 0.0, "fesa-default"});
|
||||||
1,
|
state.StressResults().push_back({static_cast<fesa::EntityIndex>(element), 2,
|
||||||
0U,
|
0U, 0.0, 0.0, 0.0, "fesa-default"});
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
"fesa-default"});
|
|
||||||
state.StressResults().push_back({
|
|
||||||
static_cast<fesa::EntityIndex>(element),
|
|
||||||
2,
|
|
||||||
0U,
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
0.0,
|
|
||||||
"fesa-default"});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
fesa::Hdf5ResultsWriter writer;
|
fesa::Hdf5ResultsWriter writer;
|
||||||
@@ -284,12 +250,11 @@ void writeResultsFixture(
|
|||||||
}
|
}
|
||||||
|
|
||||||
class ContractFixture {
|
class ContractFixture {
|
||||||
public:
|
public:
|
||||||
ContractFixture(std::string label, const ComparisonValues& values) {
|
ContractFixture(std::string label, const ComparisonValues& values) {
|
||||||
static std::atomic<std::uint64_t> sequence{0U};
|
static std::atomic<std::uint64_t> sequence{0U};
|
||||||
root_ = binaryRoot() / "reference" / "contract-fixtures" /
|
root_ = BinaryRoot() / "reference" / "contract-fixtures" /
|
||||||
(std::move(label) + "-" +
|
(std::move(label) + "-" + std::to_string(sequence.fetch_add(1U)));
|
||||||
std::to_string(sequence.fetch_add(1U)));
|
|
||||||
legacy_ = root_ / "cantilever beam";
|
legacy_ = root_ / "cantilever beam";
|
||||||
std::error_code error;
|
std::error_code error;
|
||||||
std::filesystem::remove_all(root_, error);
|
std::filesystem::remove_all(root_, error);
|
||||||
@@ -297,16 +262,15 @@ public:
|
|||||||
if (!std::filesystem::create_directories(legacy_, error) || error) {
|
if (!std::filesystem::create_directories(legacy_, error) || error) {
|
||||||
throw std::runtime_error{"Unable to create contract fixture directory."};
|
throw std::runtime_error{"Unable to create contract fixture directory."};
|
||||||
}
|
}
|
||||||
const auto approved = sourceRoot() / "reference" / "cantilever beam";
|
const auto approved = SourceRoot() / "reference" / "cantilever beam";
|
||||||
for (const char* name :
|
for (const char* name :
|
||||||
{kInputName, kDisplacementName, kReactionName, kSectionName}) {
|
{kInputName, kDisplacementName, kReactionName, kSectionName}) {
|
||||||
std::filesystem::copy_file(
|
std::filesystem::copy_file(
|
||||||
approved / name,
|
approved / name, legacy_ / name,
|
||||||
legacy_ / name,
|
|
||||||
std::filesystem::copy_options::overwrite_existing);
|
std::filesystem::copy_options::overwrite_existing);
|
||||||
}
|
}
|
||||||
results_ = root_ / "results.h5";
|
results_ = root_ / "results.h5";
|
||||||
writeResultsFixture(results_, legacy_ / kInputName, values);
|
WriteResultsFixture(results_, legacy_ / kInputName, values);
|
||||||
}
|
}
|
||||||
|
|
||||||
ContractFixture(const ContractFixture&) = delete;
|
ContractFixture(const ContractFixture&) = delete;
|
||||||
@@ -317,392 +281,345 @@ public:
|
|||||||
std::filesystem::remove_all(root_, ignored);
|
std::filesystem::remove_all(root_, ignored);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path& root() const noexcept { return root_; }
|
const std::filesystem::path& Root() const noexcept { return root_; }
|
||||||
const std::filesystem::path& legacy() const noexcept { return legacy_; }
|
const std::filesystem::path& Legacy() const noexcept { return legacy_; }
|
||||||
const std::filesystem::path& results() const noexcept { return results_; }
|
const std::filesystem::path& Results() const noexcept { return results_; }
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::filesystem::path root_;
|
std::filesystem::path root_;
|
||||||
std::filesystem::path legacy_;
|
std::filesystem::path legacy_;
|
||||||
std::filesystem::path results_;
|
std::filesystem::path results_;
|
||||||
};
|
};
|
||||||
|
|
||||||
void expectFailureCode(
|
void ExpectFailureCode(const fesa::Result<fesa::test::ComparisonReport>& result,
|
||||||
const fesa::Result<fesa::test::ComparisonReport>& result,
|
const std::string& expected_code) {
|
||||||
const std::string& expectedCode) {
|
|
||||||
ASSERT_FALSE(result.HasValue());
|
ASSERT_FALSE(result.HasValue());
|
||||||
ASSERT_FALSE(result.GetStatus().IsOk());
|
ASSERT_FALSE(result.GetStatus().IsOk());
|
||||||
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
|
||||||
EXPECT_EQ(result.GetStatus().Diagnostics().front().code, expectedCode);
|
EXPECT_EQ(result.GetStatus().Diagnostics().front().code, expected_code);
|
||||||
}
|
}
|
||||||
|
|
||||||
const fesa::test::RowDecision* findRow(
|
const fesa::test::RowDecision* FindRow(
|
||||||
const fesa::test::ComparisonReport& report,
|
const fesa::test::ComparisonReport& report,
|
||||||
const fesa::test::ComparisonQuantity quantity,
|
const fesa::test::ComparisonQuantity quantity,
|
||||||
const std::int64_t sourceNodeLabel,
|
const std::int64_t source_node_label, const std::string& component) {
|
||||||
const std::string& component) {
|
const auto found = std::find_if(report.rows.begin(), report.rows.end(),
|
||||||
const auto found = std::find_if(
|
|
||||||
report.rows.begin(),
|
|
||||||
report.rows.end(),
|
|
||||||
[&](const fesa::test::RowDecision& row) {
|
[&](const fesa::test::RowDecision& row) {
|
||||||
return row.reference.quantity == quantity &&
|
return row.reference.quantity == quantity &&
|
||||||
row.reference.sourceNodeLabel == sourceNodeLabel &&
|
row.reference.source_node_label ==
|
||||||
|
source_node_label &&
|
||||||
row.reference.component == component;
|
row.reference.component == component;
|
||||||
});
|
});
|
||||||
return found == report.rows.end() ? nullptr : &*found;
|
return found == report.rows.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
const fesa::test::ComponentMetrics* findMetric(
|
const fesa::test::ComponentMetrics* FindMetric(
|
||||||
const fesa::test::ComparisonReport& report,
|
const fesa::test::ComparisonReport& report,
|
||||||
const fesa::test::ComparisonQuantity quantity,
|
const fesa::test::ComparisonQuantity quantity,
|
||||||
const std::string& component) {
|
const std::string& component) {
|
||||||
const auto found = std::find_if(
|
const auto found = std::find_if(
|
||||||
report.metrics.begin(),
|
report.metrics.begin(), report.metrics.end(),
|
||||||
report.metrics.end(),
|
|
||||||
[&](const fesa::test::ComponentMetrics& metric) {
|
[&](const fesa::test::ComponentMetrics& metric) {
|
||||||
return metric.quantity == quantity && metric.component == component;
|
return metric.quantity == quantity && metric.component == component;
|
||||||
});
|
});
|
||||||
return found == report.metrics.end() ? nullptr : &*found;
|
return found == report.metrics.end() ? nullptr : &*found;
|
||||||
}
|
}
|
||||||
|
|
||||||
void expectExactRowInventory(const fesa::test::ComparisonReport& report) {
|
void ExpectExactRowInventory(const fesa::test::ComparisonReport& report) {
|
||||||
ASSERT_EQ(report.rows.size(), kExpectedRowCount);
|
ASSERT_EQ(report.rows.size(), kExpectedRowCount);
|
||||||
std::size_t rowIndex = 0U;
|
std::size_t row_index = 0U;
|
||||||
const auto expectRow = [&](const fesa::test::ComparisonQuantity quantity,
|
const auto expect_row = [&](const fesa::test::ComparisonQuantity quantity,
|
||||||
const std::size_t node,
|
const std::size_t node,
|
||||||
const std::string& component,
|
const std::string& component,
|
||||||
const std::string& unit,
|
const std::string& unit,
|
||||||
const std::string& coordinateSystem,
|
const std::string& coordinate_system,
|
||||||
const std::string& datasetPath) {
|
const std::string& dataset_path) {
|
||||||
ASSERT_LT(rowIndex, report.rows.size());
|
ASSERT_LT(row_index, report.rows.size());
|
||||||
const auto& row = report.rows[rowIndex++];
|
const auto& row = report.rows[row_index++];
|
||||||
for (const auto* side : {&row.fesa, &row.reference}) {
|
for (const auto* side : {&row.fesa, &row.reference}) {
|
||||||
EXPECT_EQ(side->modelId, "cantilever-beam-b33");
|
EXPECT_EQ(side->model_id, "cantilever-beam-b33");
|
||||||
EXPECT_EQ(side->stepName, "Step-1");
|
EXPECT_EQ(side->step_name, "Step-1");
|
||||||
EXPECT_EQ(side->frameIndex, 0U);
|
EXPECT_EQ(side->frame_index, 0U);
|
||||||
EXPECT_EQ(side->instanceName, kInstanceName);
|
EXPECT_EQ(side->instance_name, kInstanceName);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(side->source_node_label, static_cast<std::int64_t>(node + 1U));
|
||||||
side->sourceNodeLabel,
|
|
||||||
static_cast<std::int64_t>(node + 1U));
|
|
||||||
EXPECT_EQ(side->quantity, quantity);
|
EXPECT_EQ(side->quantity, quantity);
|
||||||
EXPECT_EQ(side->component, component);
|
EXPECT_EQ(side->component, component);
|
||||||
EXPECT_EQ(side->unitDimension, unit);
|
EXPECT_EQ(side->unit_dimension, unit);
|
||||||
EXPECT_EQ(side->coordinateSystem, coordinateSystem);
|
EXPECT_EQ(side->coordinate_system, coordinate_system);
|
||||||
EXPECT_EQ(side->hdf5DatasetPath, datasetPath);
|
EXPECT_EQ(side->hdf5_dataset_path, dataset_path);
|
||||||
}
|
}
|
||||||
};
|
};
|
||||||
|
|
||||||
const std::array<std::string, 6> displacementComponents = {
|
const std::array<std::string, 6> displacement_components = {
|
||||||
"UX", "UY", "UZ", "URX", "URY", "URZ"};
|
"UX", "UY", "UZ", "URX", "URY", "URZ"};
|
||||||
const std::array<std::string, 6> displacementUnits = {
|
const std::array<std::string, 6> displacement_units = {
|
||||||
"length", "length", "length", "radian", "radian", "radian"};
|
"length", "length", "length", "radian", "radian", "radian"};
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
for (std::size_t component = 0U;
|
for (std::size_t component = 0U; component < displacement_components.size();
|
||||||
component < displacementComponents.size();
|
|
||||||
++component) {
|
++component) {
|
||||||
expectRow(
|
expect_row(fesa::test::ComparisonQuantity::kDisplacement, node,
|
||||||
fesa::test::ComparisonQuantity::displacement,
|
displacement_components[component],
|
||||||
node,
|
displacement_units[component], "global-cartesian",
|
||||||
displacementComponents[component],
|
|
||||||
displacementUnits[component],
|
|
||||||
"global-cartesian",
|
|
||||||
"/steps/Step-1/frames/0/nodal/displacement");
|
"/steps/Step-1/frames/0/nodal/displacement");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::array<std::string, 6> reactionComponents = {
|
const std::array<std::string, 6> reaction_components = {"RF1", "RF2", "RF3",
|
||||||
"RF1", "RF2", "RF3", "RM1", "RM2", "RM3"};
|
"RM1", "RM2", "RM3"};
|
||||||
const std::array<std::string, 6> reactionUnits = {
|
const std::array<std::string, 6> reaction_units = {
|
||||||
"force",
|
"force", "force", "force",
|
||||||
"force",
|
"force*length", "force*length", "force*length"};
|
||||||
"force",
|
|
||||||
"force*length",
|
|
||||||
"force*length",
|
|
||||||
"force*length"};
|
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
for (std::size_t component = 0U;
|
for (std::size_t component = 0U; component < reaction_components.size();
|
||||||
component < reactionComponents.size();
|
|
||||||
++component) {
|
++component) {
|
||||||
expectRow(
|
expect_row(fesa::test::ComparisonQuantity::kReaction, node,
|
||||||
fesa::test::ComparisonQuantity::reaction,
|
reaction_components[component], reaction_units[component],
|
||||||
node,
|
"global-cartesian", "/steps/Step-1/frames/0/nodal/reaction");
|
||||||
reactionComponents[component],
|
|
||||||
reactionUnits[component],
|
|
||||||
"global-cartesian",
|
|
||||||
"/steps/Step-1/frames/0/nodal/reaction");
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::array<std::string, 4> sectionComponents = {
|
const std::array<std::string, 4> section_components = {"N", "T", "My", "Mz"};
|
||||||
"N", "T", "My", "Mz"};
|
const std::array<std::string, 4> section_units = {
|
||||||
const std::array<std::string, 4> sectionUnits = {
|
|
||||||
"force", "force*length", "force*length", "force*length"};
|
"force", "force*length", "force*length", "force*length"};
|
||||||
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
for (std::size_t node = 0U; node < kNodeCount; ++node) {
|
||||||
for (std::size_t component = 0U;
|
for (std::size_t component = 0U; component < section_components.size();
|
||||||
component < sectionComponents.size();
|
|
||||||
++component) {
|
++component) {
|
||||||
expectRow(
|
expect_row(fesa::test::ComparisonQuantity::kSectionResultant, node,
|
||||||
fesa::test::ComparisonQuantity::sectionResultant,
|
section_components[component], section_units[component],
|
||||||
node,
|
|
||||||
sectionComponents[component],
|
|
||||||
sectionUnits[component],
|
|
||||||
"beam-local",
|
"beam-local",
|
||||||
"/steps/Step-1/frames/0/element/section_resultant");
|
"/steps/Step-1/frames/0/element/section_resultant");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
EXPECT_EQ(rowIndex, report.rows.size());
|
EXPECT_EQ(row_index, report.rows.size());
|
||||||
}
|
}
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(ReferenceComparisonContract,
|
TEST(ReferenceComparisonContract,
|
||||||
PrecheckRejectsMissingSchemaDuplicateAndNonfiniteRows) {
|
PrecheckRejectsMissingSchemaDuplicateAndNonfiniteRows) {
|
||||||
auto mismatchedValues = referenceValues();
|
auto mismatched_values = ReferenceValues();
|
||||||
mismatchedValues.displacement[0U][0U] = 1.0;
|
mismatched_values.displacement[0U][0U] = 1.0;
|
||||||
|
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"missing-file", mismatchedValues};
|
ContractFixture fixture{"missing-file", mismatched_values};
|
||||||
ASSERT_TRUE(std::filesystem::remove(
|
ASSERT_TRUE(std::filesystem::remove(fixture.Legacy() / kDisplacementName));
|
||||||
fixture.legacy() / kDisplacementName));
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
expectFailureCode(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fesa::test::ReferenceComparison::compare(
|
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"needs-reference-artifacts");
|
"needs-reference-artifacts");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"b31", mismatchedValues};
|
ContractFixture fixture{"b31", mismatched_values};
|
||||||
auto input = readBytes(fixture.legacy() / kInputName);
|
auto input = ReadBytes(fixture.Legacy() / kInputName);
|
||||||
replaceFirst(input, "type=B33", "type=B31");
|
ReplaceFirst(input, "type=B33", "type=B31");
|
||||||
writeBytes(fixture.legacy() / kInputName, input);
|
WriteBytes(fixture.Legacy() / kInputName, input);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"needs-reference-artifacts");
|
"needs-reference-artifacts");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"header", mismatchedValues};
|
ContractFixture fixture{"header", mismatched_values};
|
||||||
auto csv = readBytes(fixture.legacy() / kDisplacementName);
|
auto csv = ReadBytes(fixture.Legacy() / kDisplacementName);
|
||||||
replaceFirst(csv, "U-U1", "U1");
|
ReplaceFirst(csv, "U-U1", "U1");
|
||||||
writeBytes(fixture.legacy() / kDisplacementName, csv);
|
WriteBytes(fixture.Legacy() / kDisplacementName, csv);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"schema-mismatch");
|
"schema-mismatch");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"missing-row", mismatchedValues};
|
ContractFixture fixture{"missing-row", mismatched_values};
|
||||||
auto lines = readLines(fixture.legacy() / kDisplacementName);
|
auto lines = ReadLines(fixture.Legacy() / kDisplacementName);
|
||||||
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
||||||
lines.pop_back();
|
lines.pop_back();
|
||||||
writeLines(fixture.legacy() / kDisplacementName, lines);
|
WriteLines(fixture.Legacy() / kDisplacementName, lines);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"schema-mismatch");
|
"schema-mismatch");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"extra-row", mismatchedValues};
|
ContractFixture fixture{"extra-row", mismatched_values};
|
||||||
auto lines = readLines(fixture.legacy() / kDisplacementName);
|
auto lines = ReadLines(fixture.Legacy() / kDisplacementName);
|
||||||
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
||||||
std::string extra = lines.back();
|
std::string extra = lines.back();
|
||||||
replaceFirst(extra, ",PART-1_1-1,11,", ",PART-1_1-1,12,");
|
ReplaceFirst(extra, ",PART-1_1-1,11,", ",PART-1_1-1,12,");
|
||||||
lines.push_back(std::move(extra));
|
lines.push_back(std::move(extra));
|
||||||
writeLines(fixture.legacy() / kDisplacementName, lines);
|
WriteLines(fixture.Legacy() / kDisplacementName, lines);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"schema-mismatch");
|
"schema-mismatch");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"duplicate-row", mismatchedValues};
|
ContractFixture fixture{"duplicate-row", mismatched_values};
|
||||||
auto lines = readLines(fixture.legacy() / kReactionName);
|
auto lines = ReadLines(fixture.Legacy() / kReactionName);
|
||||||
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
ASSERT_EQ(lines.size(), kNodeCount + 1U);
|
||||||
lines.push_back(lines[1U]);
|
lines.push_back(lines[1U]);
|
||||||
writeLines(fixture.legacy() / kReactionName, lines);
|
WriteLines(fixture.Legacy() / kReactionName, lines);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"schema-mismatch");
|
"schema-mismatch");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"nonfinite-row", mismatchedValues};
|
ContractFixture fixture{"nonfinite-row", mismatched_values};
|
||||||
auto csv = readBytes(fixture.legacy() / kReactionName);
|
auto csv = ReadBytes(fixture.Legacy() / kReactionName);
|
||||||
replaceFirst(csv, "0.000000000E+00", "NaN");
|
ReplaceFirst(csv, "0.000000000E+00", "NaN");
|
||||||
writeBytes(fixture.legacy() / kReactionName, csv);
|
WriteBytes(fixture.Legacy() / kReactionName, csv);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"schema-mismatch");
|
"schema-mismatch");
|
||||||
}
|
}
|
||||||
{
|
{
|
||||||
ContractFixture fixture{"identity", mismatchedValues};
|
ContractFixture fixture{"identity", mismatched_values};
|
||||||
auto csv = readBytes(fixture.legacy() / kSectionName);
|
auto csv = ReadBytes(fixture.Legacy() / kSectionName);
|
||||||
replaceFirst(csv, "PART-1_1-1", "WRONG-INSTANCE");
|
ReplaceFirst(csv, "PART-1_1-1", "WRONG-INSTANCE");
|
||||||
writeBytes(fixture.legacy() / kSectionName, csv);
|
WriteBytes(fixture.Legacy() / kSectionName, csv);
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(
|
||||||
fesa::test::ReferenceComparison::compare(
|
fixture.Results(), fixture.Legacy()),
|
||||||
fixture.results(), fixture.legacy()),
|
|
||||||
"schema-mismatch");
|
"schema-mismatch");
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ReferenceComparisonContract,
|
TEST(ReferenceComparisonContract,
|
||||||
AppliesAbaqusOnlyComponentScaleWithoutClampOrDrop) {
|
AppliesAbaqusOnlyComponentScaleWithoutClampOrDrop) {
|
||||||
auto values = referenceValues();
|
auto values = ReferenceValues();
|
||||||
values.displacement[1U][0U] = 0.999e-9;
|
values.displacement[1U][0U] = 0.999e-9;
|
||||||
values.displacement[2U][0U] = 1.001e-9;
|
values.displacement[2U][0U] = 1.001e-9;
|
||||||
values.sectionResultants[0U][0U][2U] = 1.0e7 + 9.0;
|
values.section_resultants[0U][0U][2U] = 1.0e7 + 9.0;
|
||||||
values.sectionResultants[9U][1U][2U] = 0.0;
|
values.section_resultants[9U][1U][2U] = 0.0;
|
||||||
ContractFixture fixture{"tolerance", values};
|
ContractFixture fixture{"tolerance", values};
|
||||||
|
|
||||||
auto result = fesa::test::ReferenceComparison::compare(
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
||||||
fixture.results(), fixture.legacy());
|
fixture.Legacy());
|
||||||
ASSERT_TRUE(result.HasValue());
|
ASSERT_TRUE(result.HasValue());
|
||||||
const auto& report = result.Value();
|
const auto& report = result.Value();
|
||||||
EXPECT_FALSE(report.passed);
|
EXPECT_FALSE(report.passed);
|
||||||
EXPECT_EQ(report.rows.size(), kExpectedRowCount);
|
EXPECT_EQ(report.rows.size(), kExpectedRowCount);
|
||||||
EXPECT_EQ(report.metrics.size(), kExpectedMetricCount);
|
EXPECT_EQ(report.metrics.size(), kExpectedMetricCount);
|
||||||
|
|
||||||
const auto* zero = findRow(
|
const auto* zero =
|
||||||
report, fesa::test::ComparisonQuantity::displacement, 1, "UX");
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 1, "UX");
|
||||||
const auto* nearZero = findRow(
|
const auto* near_zero =
|
||||||
report, fesa::test::ComparisonQuantity::displacement, 2, "UX");
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX");
|
||||||
const auto* deliberateFailure = findRow(
|
const auto* deliberate_failure =
|
||||||
report, fesa::test::ComparisonQuantity::displacement, 3, "UX");
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 3, "UX");
|
||||||
ASSERT_NE(zero, nullptr);
|
ASSERT_NE(zero, nullptr);
|
||||||
ASSERT_NE(nearZero, nullptr);
|
ASSERT_NE(near_zero, nullptr);
|
||||||
ASSERT_NE(deliberateFailure, nullptr);
|
ASSERT_NE(deliberate_failure, nullptr);
|
||||||
EXPECT_DOUBLE_EQ(zero->reference.value, 0.0);
|
EXPECT_DOUBLE_EQ(zero->reference.value, 0.0);
|
||||||
EXPECT_DOUBLE_EQ(zero->fesa.value, 0.0);
|
EXPECT_DOUBLE_EQ(zero->fesa.value, 0.0);
|
||||||
EXPECT_DOUBLE_EQ(zero->tolerance, 1.0e-9);
|
EXPECT_DOUBLE_EQ(zero->tolerance, 1.0e-9);
|
||||||
EXPECT_TRUE(zero->passed);
|
EXPECT_TRUE(zero->passed);
|
||||||
EXPECT_TRUE(nearZero->passed);
|
EXPECT_TRUE(near_zero->passed);
|
||||||
EXPECT_FALSE(deliberateFailure->passed);
|
EXPECT_FALSE(deliberate_failure->passed);
|
||||||
|
|
||||||
const auto* myMetric = findMetric(
|
const auto* my_metric = FindMetric(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, "My");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, "My");
|
||||||
ASSERT_NE(myMetric, nullptr);
|
ASSERT_NE(my_metric, nullptr);
|
||||||
EXPECT_DOUBLE_EQ(myMetric->referenceScale, 1.0e7);
|
EXPECT_DOUBLE_EQ(my_metric->reference_scale, 1.0e7);
|
||||||
const auto* scaled = findRow(
|
const auto* scaled = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 1, "My");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "My");
|
||||||
const auto* residue = findRow(
|
const auto* residue = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 11, "My");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 11, "My");
|
||||||
ASSERT_NE(scaled, nullptr);
|
ASSERT_NE(scaled, nullptr);
|
||||||
ASSERT_NE(residue, nullptr);
|
ASSERT_NE(residue, nullptr);
|
||||||
EXPECT_DOUBLE_EQ(scaled->tolerance, 10.001);
|
EXPECT_DOUBLE_EQ(scaled->tolerance, 10.001);
|
||||||
EXPECT_DOUBLE_EQ(scaled->absoluteError, 9.0);
|
EXPECT_DOUBLE_EQ(scaled->absolute_error, 9.0);
|
||||||
EXPECT_TRUE(scaled->passed);
|
EXPECT_TRUE(scaled->passed);
|
||||||
EXPECT_DOUBLE_EQ(residue->reference.value, -1.56e-2);
|
EXPECT_DOUBLE_EQ(residue->reference.value, -1.56e-2);
|
||||||
EXPECT_DOUBLE_EQ(residue->fesa.value, 0.0);
|
EXPECT_DOUBLE_EQ(residue->fesa.value, 0.0);
|
||||||
EXPECT_DOUBLE_EQ(residue->absoluteError, 1.56e-2);
|
EXPECT_DOUBLE_EQ(residue->absolute_error, 1.56e-2);
|
||||||
EXPECT_DOUBLE_EQ(residue->tolerance, 10.001);
|
EXPECT_DOUBLE_EQ(residue->tolerance, 10.001);
|
||||||
EXPECT_TRUE(residue->passed);
|
EXPECT_TRUE(residue->passed);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ReferenceComparisonContract,
|
TEST(ReferenceComparisonContract,
|
||||||
ReportsEveryRowAndAggregateMetricDeterministically) {
|
ReportsEveryRowAndAggregateMetricDeterministically) {
|
||||||
auto values = referenceValues();
|
auto values = ReferenceValues();
|
||||||
values.displacement[0U][0U] = 0.5e-9;
|
values.displacement[0U][0U] = 0.5e-9;
|
||||||
values.displacement[1U][0U] = -1.0e-9;
|
values.displacement[1U][0U] = -1.0e-9;
|
||||||
ContractFixture fixture{"metrics", values};
|
ContractFixture fixture{"metrics", values};
|
||||||
|
|
||||||
auto result = fesa::test::ReferenceComparison::compare(
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
||||||
fixture.results(), fixture.legacy());
|
fixture.Legacy());
|
||||||
ASSERT_TRUE(result.HasValue());
|
ASSERT_TRUE(result.HasValue());
|
||||||
const auto& report = result.Value();
|
const auto& report = result.Value();
|
||||||
ASSERT_TRUE(report.passed);
|
ASSERT_TRUE(report.passed);
|
||||||
expectExactRowInventory(report);
|
ExpectExactRowInventory(report);
|
||||||
ASSERT_EQ(report.metrics.size(), kExpectedMetricCount);
|
ASSERT_EQ(report.metrics.size(), kExpectedMetricCount);
|
||||||
EXPECT_TRUE(std::all_of(
|
EXPECT_TRUE(std::all_of(
|
||||||
report.rows.begin(),
|
report.rows.begin(), report.rows.end(),
|
||||||
report.rows.end(),
|
|
||||||
[](const fesa::test::RowDecision& row) { return row.passed; }));
|
[](const fesa::test::RowDecision& row) { return row.passed; }));
|
||||||
|
|
||||||
const auto* metric = findMetric(
|
const auto* metric =
|
||||||
report, fesa::test::ComparisonQuantity::displacement, "UX");
|
FindMetric(report, fesa::test::ComparisonQuantity::kDisplacement, "UX");
|
||||||
const auto* worst = findRow(
|
const auto* worst =
|
||||||
report, fesa::test::ComparisonQuantity::displacement, 2, "UX");
|
FindRow(report, fesa::test::ComparisonQuantity::kDisplacement, 2, "UX");
|
||||||
ASSERT_NE(metric, nullptr);
|
ASSERT_NE(metric, nullptr);
|
||||||
ASSERT_NE(worst, nullptr);
|
ASSERT_NE(worst, nullptr);
|
||||||
EXPECT_DOUBLE_EQ(metric->referenceScale, 0.0);
|
EXPECT_DOUBLE_EQ(metric->reference_scale, 0.0);
|
||||||
EXPECT_DOUBLE_EQ(metric->maximumAbsoluteError, 1.0e-9);
|
EXPECT_DOUBLE_EQ(metric->maximum_absolute_error, 1.0e-9);
|
||||||
EXPECT_DOUBLE_EQ(metric->maximumNormalizedError, 1.0);
|
EXPECT_DOUBLE_EQ(metric->maximum_normalized_error, 1.0);
|
||||||
EXPECT_NEAR(
|
EXPECT_NEAR(metric->rms_error,
|
||||||
metric->rmsError,
|
|
||||||
std::sqrt(1.25 / static_cast<double>(kNodeCount)) * 1.0e-9,
|
std::sqrt(1.25 / static_cast<double>(kNodeCount)) * 1.0e-9,
|
||||||
1.0e-21);
|
1.0e-21);
|
||||||
EXPECT_NEAR(metric->normError, std::sqrt(1.25) * 1.0e-9, 1.0e-21);
|
EXPECT_NEAR(metric->norm_error, std::sqrt(1.25) * 1.0e-9, 1.0e-21);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(metric->worst_row,
|
||||||
metric->worstRow,
|
|
||||||
static_cast<std::size_t>(worst - report.rows.data()));
|
static_cast<std::size_t>(worst - report.rows.data()));
|
||||||
|
|
||||||
EXPECT_FALSE(report.stressComparisonApplicable);
|
EXPECT_FALSE(report.stress_comparison_applicable);
|
||||||
EXPECT_NE(report.stressComparisonReason.find("N/A"), std::string::npos);
|
EXPECT_NE(report.stress_comparison_reason.find("N/A"), std::string::npos);
|
||||||
EXPECT_NE(report.stressComparisonReason.find("HDF5"), std::string::npos);
|
EXPECT_NE(report.stress_comparison_reason.find("HDF5"), std::string::npos);
|
||||||
EXPECT_DOUBLE_EQ(report.physicsEvidence.freeResidualNorm, 0.0);
|
EXPECT_DOUBLE_EQ(report.physics_evidence.free_residual_norm, 0.0);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(report.physics_evidence.applied_force,
|
||||||
report.physicsEvidence.appliedForce,
|
|
||||||
(std::array<double, 3>{0.0, 0.0, -1.0e6}));
|
(std::array<double, 3>{0.0, 0.0, -1.0e6}));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(report.physics_evidence.reaction_force,
|
||||||
report.physicsEvidence.reactionForce,
|
|
||||||
(std::array<double, 3>{0.0, 0.0, 1.0e6}));
|
(std::array<double, 3>{0.0, 0.0, 1.0e6}));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(report.physics_evidence.applied_moment_about_origin,
|
||||||
report.physicsEvidence.appliedMomentAboutOrigin,
|
|
||||||
(std::array<double, 3>{0.0, 1.0e7, 0.0}));
|
(std::array<double, 3>{0.0, 1.0e7, 0.0}));
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(report.physics_evidence.reaction_moment_about_origin,
|
||||||
report.physicsEvidence.reactionMomentAboutOrigin,
|
|
||||||
(std::array<double, 3>{0.0, -1.0e7, 0.0}));
|
(std::array<double, 3>{0.0, -1.0e7, 0.0}));
|
||||||
EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed);
|
EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
|
||||||
|
|
||||||
const auto jsonA = fixture.root() / "comparison-a.json";
|
const auto json_a = fixture.Root() / "comparison-a.json";
|
||||||
const auto jsonB = fixture.root() / "comparison-b.json";
|
const auto json_b = fixture.Root() / "comparison-b.json";
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(
|
||||||
fesa::test::ReferenceComparison::writeDeterministicJson(report, jsonA)
|
fesa::test::ReferenceComparison::WriteDeterministicJson(report, json_a)
|
||||||
.IsOk());
|
.IsOk());
|
||||||
ASSERT_TRUE(
|
ASSERT_TRUE(
|
||||||
fesa::test::ReferenceComparison::writeDeterministicJson(report, jsonB)
|
fesa::test::ReferenceComparison::WriteDeterministicJson(report, json_b)
|
||||||
.IsOk());
|
.IsOk());
|
||||||
const std::string first = readBytes(jsonA);
|
const std::string first = ReadBytes(json_a);
|
||||||
EXPECT_EQ(first, readBytes(jsonB));
|
EXPECT_EQ(first, ReadBytes(json_b));
|
||||||
for (const char* required : {
|
for (const char* required :
|
||||||
"\"rows\"",
|
{"\"rows\"", "\"metrics\"", "\"stress_comparison_applicable\":false",
|
||||||
"\"metrics\"",
|
"\"stress_comparison_reason\"", "\"physics_evidence\"",
|
||||||
"\"stress_comparison_applicable\":false",
|
"\"free_residual_norm\"", "\"applied_force\"", "\"reaction_force\"",
|
||||||
"\"stress_comparison_reason\"",
|
"\"applied_moment_about_origin\"", "\"reaction_moment_about_origin\"",
|
||||||
"\"physics_evidence\"",
|
|
||||||
"\"free_residual_norm\"",
|
|
||||||
"\"applied_force\"",
|
|
||||||
"\"reaction_force\"",
|
|
||||||
"\"applied_moment_about_origin\"",
|
|
||||||
"\"reaction_moment_about_origin\"",
|
|
||||||
"\"endpoint_consistency_passed\""}) {
|
"\"endpoint_consistency_passed\""}) {
|
||||||
EXPECT_NE(first.find(required), std::string::npos) << required;
|
EXPECT_NE(first.find(required), std::string::npos) << required;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(ReferenceComparisonContract,
|
TEST(ReferenceComparisonContract, NormalizesEligibleStationsWithoutAveraging) {
|
||||||
NormalizesEligibleStationsWithoutAveraging) {
|
auto values = ReferenceValues();
|
||||||
auto values = referenceValues();
|
values.section_resultants[0U][0U] = {2.0e-4, 3.0e-4, 1.0e7, 4.0e-4};
|
||||||
values.sectionResultants[0U][0U] = {2.0e-4, 3.0e-4, 1.0e7, 4.0e-4};
|
values.section_resultants[0U][1U][2U] = 9.0e6 - 5.0;
|
||||||
values.sectionResultants[0U][1U][2U] = 9.0e6 - 5.0;
|
values.section_resultants[1U][0U][2U] = 9.0e6 + 5.0;
|
||||||
values.sectionResultants[1U][0U][2U] = 9.0e6 + 5.0;
|
|
||||||
ContractFixture fixture{"stations", values};
|
ContractFixture fixture{"stations", values};
|
||||||
|
|
||||||
auto result = fesa::test::ReferenceComparison::compare(
|
auto result = fesa::test::ReferenceComparison::Compare(fixture.Results(),
|
||||||
fixture.results(), fixture.legacy());
|
fixture.Legacy());
|
||||||
ASSERT_TRUE(result.HasValue());
|
ASSERT_TRUE(result.HasValue());
|
||||||
const auto& report = result.Value();
|
const auto& report = result.Value();
|
||||||
ASSERT_TRUE(report.passed);
|
ASSERT_TRUE(report.passed);
|
||||||
EXPECT_TRUE(report.physicsEvidence.endpointConsistencyPassed);
|
EXPECT_TRUE(report.physics_evidence.endpoint_consistency_passed);
|
||||||
|
|
||||||
const auto* n = findRow(
|
const auto* n = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 1, "N");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "N");
|
||||||
const auto* t = findRow(
|
const auto* t = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 1, "T");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "T");
|
||||||
const auto* my = findRow(
|
const auto* my = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 1, "My");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "My");
|
||||||
const auto* mz = findRow(
|
const auto* mz = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 1, "Mz");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 1, "Mz");
|
||||||
ASSERT_NE(n, nullptr);
|
ASSERT_NE(n, nullptr);
|
||||||
ASSERT_NE(t, nullptr);
|
ASSERT_NE(t, nullptr);
|
||||||
ASSERT_NE(my, nullptr);
|
ASSERT_NE(my, nullptr);
|
||||||
@@ -712,19 +629,18 @@ TEST(ReferenceComparisonContract,
|
|||||||
EXPECT_DOUBLE_EQ(my->fesa.value, 1.0e7);
|
EXPECT_DOUBLE_EQ(my->fesa.value, 1.0e7);
|
||||||
EXPECT_DOUBLE_EQ(mz->fesa.value, 4.0e-4);
|
EXPECT_DOUBLE_EQ(mz->fesa.value, 4.0e-4);
|
||||||
|
|
||||||
const auto* interior = findRow(
|
const auto* interior = FindRow(
|
||||||
report, fesa::test::ComparisonQuantity::sectionResultant, 2, "My");
|
report, fesa::test::ComparisonQuantity::kSectionResultant, 2, "My");
|
||||||
ASSERT_NE(interior, nullptr);
|
ASSERT_NE(interior, nullptr);
|
||||||
EXPECT_DOUBLE_EQ(interior->fesa.value, 9.0e6 - 5.0);
|
EXPECT_DOUBLE_EQ(interior->fesa.value, 9.0e6 - 5.0);
|
||||||
EXPECT_DOUBLE_EQ(interior->reference.value, 9.0e6);
|
EXPECT_DOUBLE_EQ(interior->reference.value, 9.0e6);
|
||||||
EXPECT_DOUBLE_EQ(interior->absoluteError, 5.0);
|
EXPECT_DOUBLE_EQ(interior->absolute_error, 5.0);
|
||||||
|
|
||||||
auto mismatchValues = referenceValues();
|
auto mismatch_values = ReferenceValues();
|
||||||
mismatchValues.sectionResultants[0U][1U][2U] = 9.0e6 - 6.0;
|
mismatch_values.section_resultants[0U][1U][2U] = 9.0e6 - 6.0;
|
||||||
mismatchValues.sectionResultants[1U][0U][2U] = 9.0e6 + 6.0;
|
mismatch_values.section_resultants[1U][0U][2U] = 9.0e6 + 6.0;
|
||||||
ContractFixture mismatch{"station-mismatch", mismatchValues};
|
ContractFixture mismatch{"station-mismatch", mismatch_values};
|
||||||
expectFailureCode(
|
ExpectFailureCode(fesa::test::ReferenceComparison::Compare(mismatch.Results(),
|
||||||
fesa::test::ReferenceComparison::compare(
|
mismatch.Legacy()),
|
||||||
mismatch.results(), mismatch.legacy()),
|
|
||||||
"tolerance-failure");
|
"tolerance-failure");
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,4 @@
|
|||||||
#include "fesa/io/abaqus/input_reader.hpp"
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
@@ -12,7 +12,7 @@
|
|||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class TemporaryInputFile {
|
class TemporaryInputFile {
|
||||||
public:
|
public:
|
||||||
TemporaryInputFile(const std::string& stem, const std::string& content)
|
TemporaryInputFile(const std::string& stem, const std::string& content)
|
||||||
: path_{std::filesystem::temp_directory_path() /
|
: path_{std::filesystem::temp_directory_path() /
|
||||||
("fesa-" + stem + ".inp")} {
|
("fesa-" + stem + ".inp")} {
|
||||||
@@ -28,25 +28,22 @@ public:
|
|||||||
std::filesystem::remove(path_, error);
|
std::filesystem::remove(path_, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path& path() const noexcept {
|
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||||
return path_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::filesystem::path path_;
|
std::filesystem::path path_;
|
||||||
};
|
};
|
||||||
|
|
||||||
std::string readExactBytes(const std::filesystem::path& path) {
|
std::string ReadExactBytes(const std::filesystem::path& path) {
|
||||||
std::ifstream stream{path, std::ios::binary};
|
std::ifstream stream{path, std::ios::binary};
|
||||||
if (!stream) {
|
if (!stream) {
|
||||||
throw std::runtime_error{"Unable to read legacy INP fixture."};
|
throw std::runtime_error{"Unable to read legacy INP fixture."};
|
||||||
}
|
}
|
||||||
return std::string{
|
return std::string{std::istreambuf_iterator<char>{stream},
|
||||||
std::istreambuf_iterator<char>{stream},
|
|
||||||
std::istreambuf_iterator<char>{}};
|
std::istreambuf_iterator<char>{}};
|
||||||
}
|
}
|
||||||
|
|
||||||
std::filesystem::path repositoryRoot() {
|
std::filesystem::path RepositoryRoot() {
|
||||||
auto path = std::filesystem::path{__FILE__}.parent_path();
|
auto path = std::filesystem::path{__FILE__}.parent_path();
|
||||||
for (int parent = 0; parent < 4; ++parent) {
|
for (int parent = 0; parent < 4; ++parent) {
|
||||||
path = path.parent_path();
|
path = path.parent_path();
|
||||||
@@ -57,69 +54,62 @@ std::filesystem::path repositoryRoot() {
|
|||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(InpSyntax, RejectsMalformedOrOrphanData) {
|
TEST(InpSyntax, RejectsMalformedOrOrphanData) {
|
||||||
const auto missingPath =
|
const auto missing_path =
|
||||||
std::filesystem::temp_directory_path() / "fesa-missing-input.inp";
|
std::filesystem::temp_directory_path() / "fesa-missing-input.inp";
|
||||||
std::error_code removeError;
|
std::error_code remove_error;
|
||||||
std::filesystem::remove(missingPath, removeError);
|
std::filesystem::remove(missing_path, remove_error);
|
||||||
|
|
||||||
const auto unreadable = fesa::AbaqusInputReader{}.read(missingPath);
|
const auto unreadable = fesa::AbaqusInputReader{}.Read(missing_path);
|
||||||
ASSERT_FALSE(unreadable.HasValue());
|
ASSERT_FALSE(unreadable.HasValue());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(unreadable.GetStatus().Category(), fesa::FailureCategory::kInput);
|
||||||
unreadable.GetStatus().Category(),
|
|
||||||
fesa::FailureCategory::kInput);
|
|
||||||
ASSERT_EQ(unreadable.GetStatus().Diagnostics().size(), 1U);
|
ASSERT_EQ(unreadable.GetStatus().Diagnostics().size(), 1U);
|
||||||
EXPECT_EQ(unreadable.GetStatus().Diagnostics()[0].code,
|
EXPECT_EQ(unreadable.GetStatus().Diagnostics()[0].code,
|
||||||
"input-file-unreadable");
|
"input-file-unreadable");
|
||||||
|
|
||||||
const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"};
|
const TemporaryInputFile malformed{"malformed-keyword", "*, name=value\n"};
|
||||||
const auto malformedResult =
|
const auto malformed_result =
|
||||||
fesa::AbaqusInputReader{}.read(malformed.path());
|
fesa::AbaqusInputReader{}.Read(malformed.Path());
|
||||||
ASSERT_FALSE(malformedResult.HasValue());
|
ASSERT_FALSE(malformed_result.HasValue());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(malformed_result.GetStatus().Category(),
|
||||||
malformedResult.GetStatus().Category(),
|
|
||||||
fesa::FailureCategory::kInput);
|
fesa::FailureCategory::kInput);
|
||||||
ASSERT_EQ(malformedResult.GetStatus().Diagnostics().size(), 1U);
|
ASSERT_EQ(malformed_result.GetStatus().Diagnostics().size(), 1U);
|
||||||
EXPECT_EQ(malformedResult.GetStatus().Diagnostics()[0].code,
|
EXPECT_EQ(malformed_result.GetStatus().Diagnostics()[0].code,
|
||||||
"malformed-keyword");
|
"malformed-keyword");
|
||||||
EXPECT_EQ(malformedResult.GetStatus().Diagnostics()[0].location.line, 1U);
|
EXPECT_EQ(malformed_result.GetStatus().Diagnostics()[0].location.line, 1U);
|
||||||
|
|
||||||
const TemporaryInputFile orphan{
|
const TemporaryInputFile orphan{"orphan-data",
|
||||||
"orphan-data", "** comment\n\norphan, data\n"};
|
"** comment\n\norphan, data\n"};
|
||||||
const auto orphanResult = fesa::AbaqusInputReader{}.read(orphan.path());
|
const auto orphan_result = fesa::AbaqusInputReader{}.Read(orphan.Path());
|
||||||
ASSERT_FALSE(orphanResult.HasValue());
|
ASSERT_FALSE(orphan_result.HasValue());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(orphan_result.GetStatus().Category(),
|
||||||
orphanResult.GetStatus().Category(),
|
|
||||||
fesa::FailureCategory::kInput);
|
fesa::FailureCategory::kInput);
|
||||||
ASSERT_EQ(orphanResult.GetStatus().Diagnostics().size(), 1U);
|
ASSERT_EQ(orphan_result.GetStatus().Diagnostics().size(), 1U);
|
||||||
EXPECT_EQ(orphanResult.GetStatus().Diagnostics()[0].code,
|
EXPECT_EQ(orphan_result.GetStatus().Diagnostics()[0].code,
|
||||||
"orphan-data-line");
|
"orphan-data-line");
|
||||||
EXPECT_EQ(orphanResult.GetStatus().Diagnostics()[0].location.line, 3U);
|
EXPECT_EQ(orphan_result.GetStatus().Diagnostics()[0].location.line, 3U);
|
||||||
}
|
}
|
||||||
|
|
||||||
TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
|
TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
|
||||||
const auto inputPath =
|
const auto input_path = RepositoryRoot() / "reference" / "cantilever beam" /
|
||||||
repositoryRoot() / "reference" / "cantilever beam" /
|
|
||||||
"cantilever beam.inp";
|
"cantilever beam.inp";
|
||||||
const auto bytesBefore = readExactBytes(inputPath);
|
const auto bytes_before = ReadExactBytes(input_path);
|
||||||
const auto timestampBefore = std::filesystem::last_write_time(inputPath);
|
const auto timestamp_before = std::filesystem::last_write_time(input_path);
|
||||||
|
|
||||||
const auto result = fesa::AbaqusInputReader{}.read(inputPath);
|
const auto result = fesa::AbaqusInputReader{}.Read(input_path);
|
||||||
|
|
||||||
ASSERT_TRUE(result.HasValue());
|
ASSERT_TRUE(result.HasValue());
|
||||||
EXPECT_EQ(result.Value().sourceContentIdentity,
|
EXPECT_EQ(result.Value().source_content_identity, "fnv1a64:04543464cc970405");
|
||||||
"fnv1a64:04543464cc970405");
|
EXPECT_EQ(result.Value().source_path,
|
||||||
EXPECT_EQ(result.Value().sourcePath,
|
std::filesystem::absolute(input_path).lexically_normal());
|
||||||
std::filesystem::absolute(inputPath).lexically_normal());
|
|
||||||
ASSERT_EQ(result.Value().blocks.size(), 30U);
|
ASSERT_EQ(result.Value().blocks.size(), 30U);
|
||||||
EXPECT_EQ(result.Value().blocks.front().canonicalName, "HEADING");
|
EXPECT_EQ(result.Value().blocks.front().canonical_name, "HEADING");
|
||||||
EXPECT_EQ(result.Value().blocks.front().location.line, 1U);
|
EXPECT_EQ(result.Value().blocks.front().location.line, 1U);
|
||||||
EXPECT_EQ(result.Value().blocks.back().canonicalName, "END STEP");
|
EXPECT_EQ(result.Value().blocks.back().canonical_name, "END STEP");
|
||||||
|
|
||||||
const auto element = std::find_if(
|
const auto element =
|
||||||
result.Value().blocks.begin(),
|
std::find_if(result.Value().blocks.begin(), result.Value().blocks.end(),
|
||||||
result.Value().blocks.end(),
|
|
||||||
[](const fesa::KeywordBlock& block) {
|
[](const fesa::KeywordBlock& block) {
|
||||||
return block.canonicalName == "ELEMENT";
|
return block.canonical_name == "ELEMENT";
|
||||||
});
|
});
|
||||||
ASSERT_NE(element, result.Value().blocks.end());
|
ASSERT_NE(element, result.Value().blocks.end());
|
||||||
ASSERT_EQ(element->parameters.size(), 1U);
|
ASSERT_EQ(element->parameters.size(), 1U);
|
||||||
@@ -128,6 +118,6 @@ TEST(InpSyntax, ReadsLegacyCantileverWithoutMutation) {
|
|||||||
EXPECT_EQ(*element->parameters[0].value, "B33");
|
EXPECT_EQ(*element->parameters[0].value, "B33");
|
||||||
EXPECT_EQ(element->data.size(), 10U);
|
EXPECT_EQ(element->data.size(), 10U);
|
||||||
|
|
||||||
EXPECT_EQ(readExactBytes(inputPath), bytesBefore);
|
EXPECT_EQ(ReadExactBytes(input_path), bytes_before);
|
||||||
EXPECT_EQ(std::filesystem::last_write_time(inputPath), timestampBefore);
|
EXPECT_EQ(std::filesystem::last_write_time(input_path), timestamp_before);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,5 +1,3 @@
|
|||||||
#include "fesa/io/abaqus/input_reader.hpp"
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
#include <filesystem>
|
#include <filesystem>
|
||||||
@@ -8,10 +6,12 @@
|
|||||||
#include <string>
|
#include <string>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
|
#include "fesa/io/abaqus/input_reader.h"
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
|
|
||||||
class TemporaryInputFile {
|
class TemporaryInputFile {
|
||||||
public:
|
public:
|
||||||
TemporaryInputFile(const std::string& stem, const std::string& content)
|
TemporaryInputFile(const std::string& stem, const std::string& content)
|
||||||
: path_{std::filesystem::temp_directory_path() /
|
: path_{std::filesystem::temp_directory_path() /
|
||||||
("fesa-" + stem + ".inp")} {
|
("fesa-" + stem + ".inp")} {
|
||||||
@@ -27,29 +27,26 @@ public:
|
|||||||
std::filesystem::remove(path_, error);
|
std::filesystem::remove(path_, error);
|
||||||
}
|
}
|
||||||
|
|
||||||
const std::filesystem::path& path() const noexcept {
|
const std::filesystem::path& Path() const noexcept { return path_; }
|
||||||
return path_;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
std::filesystem::path path_;
|
std::filesystem::path path_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace
|
} // namespace
|
||||||
|
|
||||||
TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) {
|
TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) {
|
||||||
const std::string originalLine =
|
const std::string original_line =
|
||||||
" *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set ";
|
" *eLeMeNt, TyPe= b33 , generate, ELSET=Beam_Set ";
|
||||||
const TemporaryInputFile input{
|
const TemporaryInputFile input{"canonical-names", original_line + "\n"};
|
||||||
"canonical-names", originalLine + "\n"};
|
|
||||||
|
|
||||||
const auto result = fesa::AbaqusInputReader{}.read(input.path());
|
const auto result = fesa::AbaqusInputReader{}.Read(input.Path());
|
||||||
|
|
||||||
ASSERT_TRUE(result.HasValue());
|
ASSERT_TRUE(result.HasValue());
|
||||||
ASSERT_EQ(result.Value().blocks.size(), 1U);
|
ASSERT_EQ(result.Value().blocks.size(), 1U);
|
||||||
const auto& block = result.Value().blocks[0];
|
const auto& block = result.Value().blocks[0];
|
||||||
EXPECT_EQ(block.canonicalName, "ELEMENT");
|
EXPECT_EQ(block.canonical_name, "ELEMENT");
|
||||||
EXPECT_EQ(block.originalLine, originalLine);
|
EXPECT_EQ(block.original_line, original_line);
|
||||||
EXPECT_EQ(block.location.line, 1U);
|
EXPECT_EQ(block.location.line, 1U);
|
||||||
|
|
||||||
ASSERT_EQ(block.parameters.size(), 3U);
|
ASSERT_EQ(block.parameters.size(), 3U);
|
||||||
@@ -64,32 +61,29 @@ TEST(InpSyntax, CanonicalizesKeywordAndParameterNamesOnly) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
TEST(InpSyntax, PreservesDataAndSourceLocations) {
|
TEST(InpSyntax, PreservesDataAndSourceLocations) {
|
||||||
const std::string exactBytes =
|
const std::string exact_bytes =
|
||||||
"** retained only in line accounting\r\n"
|
"** retained only in line accounting\r\n"
|
||||||
"\r\n"
|
"\r\n"
|
||||||
"*NoDe\r\n"
|
"*NoDe\r\n"
|
||||||
" 0007, Label_A, ,\r\n";
|
" 0007, Label_A, ,\r\n";
|
||||||
const TemporaryInputFile input{"data-and-locations", exactBytes};
|
const TemporaryInputFile input{"data-and-locations", exact_bytes};
|
||||||
|
|
||||||
const auto result = fesa::AbaqusInputReader{}.read(input.path());
|
const auto result = fesa::AbaqusInputReader{}.Read(input.Path());
|
||||||
|
|
||||||
ASSERT_TRUE(result.HasValue());
|
ASSERT_TRUE(result.HasValue());
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(result.Value().source_path,
|
||||||
result.Value().sourcePath,
|
std::filesystem::absolute(input.Path()).lexically_normal());
|
||||||
std::filesystem::absolute(input.path()).lexically_normal());
|
EXPECT_EQ(result.Value().source_content_identity, "fnv1a64:c120b6ed2445be46");
|
||||||
EXPECT_EQ(result.Value().sourceContentIdentity,
|
|
||||||
"fnv1a64:c120b6ed2445be46");
|
|
||||||
ASSERT_EQ(result.Value().blocks.size(), 1U);
|
ASSERT_EQ(result.Value().blocks.size(), 1U);
|
||||||
const auto& block = result.Value().blocks[0];
|
const auto& block = result.Value().blocks[0];
|
||||||
EXPECT_EQ(block.canonicalName, "NODE");
|
EXPECT_EQ(block.canonical_name, "NODE");
|
||||||
EXPECT_EQ(block.originalLine, "*NoDe");
|
EXPECT_EQ(block.original_line, "*NoDe");
|
||||||
EXPECT_EQ(block.location.file, result.Value().sourcePath);
|
EXPECT_EQ(block.location.file, result.Value().source_path);
|
||||||
EXPECT_EQ(block.location.line, 3U);
|
EXPECT_EQ(block.location.line, 3U);
|
||||||
|
|
||||||
ASSERT_EQ(block.data.size(), 1U);
|
ASSERT_EQ(block.data.size(), 1U);
|
||||||
EXPECT_EQ(
|
EXPECT_EQ(block.data[0].fields,
|
||||||
block.data[0].fields,
|
|
||||||
(std::vector<std::string>{"0007", "Label_A", "", ""}));
|
(std::vector<std::string>{"0007", "Label_A", "", ""}));
|
||||||
EXPECT_EQ(block.data[0].location.file, result.Value().sourcePath);
|
EXPECT_EQ(block.data[0].location.file, result.Value().source_path);
|
||||||
EXPECT_EQ(block.data[0].location.line, 4U);
|
EXPECT_EQ(block.data[0].location.line, 4U);
|
||||||
}
|
}
|
||||||
|
|||||||
File diff suppressed because it is too large
Load Diff
Reference in New Issue
Block a user