feat(beam-reference-qualification): step 1 — reference-csv-adapters
This commit is contained in:
@@ -48,6 +48,7 @@ add_library(fesa_core STATIC
|
|||||||
src/fesa/results/result_database.cpp
|
src/fesa/results/result_database.cpp
|
||||||
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
src/fesa/solvers/linear/pardiso_linear_solver.cpp
|
||||||
src/fesa/validation/comparison.cpp
|
src/fesa/validation/comparison.cpp
|
||||||
|
src/fesa/validation/reference_csv.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(fesa_core
|
target_include_directories(fesa_core
|
||||||
|
|||||||
@@ -0,0 +1,28 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <filesystem>
|
||||||
|
#include <string_view>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/validation/comparison.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct ReferenceRow final {
|
||||||
|
ReferenceQuantity quantity;
|
||||||
|
ResultPosition position;
|
||||||
|
std::vector<double> values;
|
||||||
|
};
|
||||||
|
|
||||||
|
struct ReferenceCsvReadResult final {
|
||||||
|
std::vector<ReferenceRow> rows;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
[[nodiscard]] ReferenceCsvReadResult read_reference_csv(
|
||||||
|
ReferenceQuantity quantity,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
std::string_view single_instance_name);
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,446 @@
|
|||||||
|
#include <fesa/validation/reference_csv.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <charconv>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <fstream>
|
||||||
|
#include <iterator>
|
||||||
|
#include <map>
|
||||||
|
#include <optional>
|
||||||
|
#include <set>
|
||||||
|
#include <sstream>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <tuple>
|
||||||
|
#include <utility>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
constexpr std::string_view instance_column{"Part Instance Name"};
|
||||||
|
constexpr std::string_view node_column{"Node Label"};
|
||||||
|
constexpr std::string_view element_column{"Element Label"};
|
||||||
|
|
||||||
|
using PositionKey =
|
||||||
|
std::tuple<std::string, std::int64_t, std::optional<std::int64_t>>;
|
||||||
|
using ColumnIndices =
|
||||||
|
std::map<std::string, std::size_t, std::less<>>;
|
||||||
|
|
||||||
|
std::size_t column_index(
|
||||||
|
const ColumnIndices& column_indices,
|
||||||
|
const std::string_view name) {
|
||||||
|
return column_indices.find(name)->second;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string_view trim(const std::string_view value) {
|
||||||
|
constexpr std::string_view whitespace{" \t\f\v\r\n"};
|
||||||
|
const std::size_t first = value.find_first_not_of(whitespace);
|
||||||
|
if (first == std::string_view::npos) {
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
const std::size_t last = value.find_last_not_of(whitespace);
|
||||||
|
return value.substr(first, last - first + 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string> split_fields(const std::string_view line) {
|
||||||
|
std::vector<std::string> fields;
|
||||||
|
std::size_t first = 0U;
|
||||||
|
while (true) {
|
||||||
|
const std::size_t comma = line.find(',', first);
|
||||||
|
const std::string_view field =
|
||||||
|
comma == std::string_view::npos
|
||||||
|
? line.substr(first)
|
||||||
|
: line.substr(first, comma - first);
|
||||||
|
fields.emplace_back(trim(field));
|
||||||
|
if (comma == std::string_view::npos) {
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
first = comma + 1U;
|
||||||
|
}
|
||||||
|
return fields;
|
||||||
|
}
|
||||||
|
|
||||||
|
ReferenceCsvReadResult failure(
|
||||||
|
const DiagnosticStage stage,
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::size_t line) {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
diagnostics.push_back({
|
||||||
|
stage,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
SourceLocation{path, line, line == 0U ? 0U : 1U},
|
||||||
|
});
|
||||||
|
return {{}, std::move(diagnostics)};
|
||||||
|
}
|
||||||
|
|
||||||
|
ReferenceCsvReadResult validation_failure(
|
||||||
|
std::string code,
|
||||||
|
std::string message,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::size_t line) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::validation,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
path,
|
||||||
|
line);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_valid_utf8(const std::string_view text) {
|
||||||
|
std::size_t index = 0U;
|
||||||
|
while (index < text.size()) {
|
||||||
|
const auto first = static_cast<unsigned char>(text[index]);
|
||||||
|
if (first <= 0x7FU) {
|
||||||
|
++index;
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t continuation_count = 0U;
|
||||||
|
std::uint32_t code_point = 0U;
|
||||||
|
std::uint32_t minimum = 0U;
|
||||||
|
if (first >= 0xC2U && first <= 0xDFU) {
|
||||||
|
continuation_count = 1U;
|
||||||
|
code_point = first & 0x1FU;
|
||||||
|
minimum = 0x80U;
|
||||||
|
} else if (first >= 0xE0U && first <= 0xEFU) {
|
||||||
|
continuation_count = 2U;
|
||||||
|
code_point = first & 0x0FU;
|
||||||
|
minimum = 0x800U;
|
||||||
|
} else if (first >= 0xF0U && first <= 0xF4U) {
|
||||||
|
continuation_count = 3U;
|
||||||
|
code_point = first & 0x07U;
|
||||||
|
minimum = 0x10000U;
|
||||||
|
} else {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
if (index + continuation_count >= text.size()) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
for (std::size_t offset = 1U; offset <= continuation_count; ++offset) {
|
||||||
|
const auto continuation =
|
||||||
|
static_cast<unsigned char>(text[index + offset]);
|
||||||
|
if ((continuation & 0xC0U) != 0x80U) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
code_point = (code_point << 6U) | (continuation & 0x3FU);
|
||||||
|
}
|
||||||
|
if (code_point < minimum || code_point > 0x10FFFFU ||
|
||||||
|
(code_point >= 0xD800U && code_point <= 0xDFFFU)) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
index += continuation_count + 1U;
|
||||||
|
}
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t line_at_offset(
|
||||||
|
const std::string_view text,
|
||||||
|
const std::size_t offset) {
|
||||||
|
return 1U + static_cast<std::size_t>(std::ranges::count(
|
||||||
|
text.substr(0U, offset), '\n'));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> required_columns(
|
||||||
|
const ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
return {
|
||||||
|
node_column,
|
||||||
|
"U-U1",
|
||||||
|
"U-U2",
|
||||||
|
"U-U3",
|
||||||
|
"UR-UR1",
|
||||||
|
"UR-UR2",
|
||||||
|
"UR-UR3",
|
||||||
|
};
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
return {
|
||||||
|
node_column,
|
||||||
|
"RF-RF1",
|
||||||
|
"RF-RF2",
|
||||||
|
"RF-RF3",
|
||||||
|
"RM-RM1",
|
||||||
|
"RM-RM2",
|
||||||
|
"RM-RM3",
|
||||||
|
};
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return {
|
||||||
|
element_column,
|
||||||
|
node_column,
|
||||||
|
"SF-SF1",
|
||||||
|
"SF-SF2",
|
||||||
|
"SF-SF3",
|
||||||
|
"SM-SM1",
|
||||||
|
"SM-SM2",
|
||||||
|
"SM-SM3",
|
||||||
|
};
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return {element_column, node_column, "Sxx"};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<std::string_view> value_columns(
|
||||||
|
const ReferenceQuantity quantity) {
|
||||||
|
switch (quantity) {
|
||||||
|
case ReferenceQuantity::displacement:
|
||||||
|
return {"U-U1", "U-U2", "U-U3", "UR-UR1", "UR-UR2", "UR-UR3"};
|
||||||
|
case ReferenceQuantity::reaction:
|
||||||
|
return {"RF-RF1", "RF-RF2", "RF-RF3", "RM-RM1", "RM-RM2", "RM-RM3"};
|
||||||
|
case ReferenceQuantity::internal_force:
|
||||||
|
return {"SF-SF1", "SF-SF2", "SF-SF3", "SM-SM1", "SM-SM2", "SM-SM3"};
|
||||||
|
case ReferenceQuantity::centroid_stress:
|
||||||
|
return {"Sxx"};
|
||||||
|
}
|
||||||
|
return {};
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_positive_label(
|
||||||
|
std::string_view text,
|
||||||
|
std::int64_t& value) {
|
||||||
|
if (text.starts_with('+')) {
|
||||||
|
text.remove_prefix(1U);
|
||||||
|
}
|
||||||
|
const auto parsed = std::from_chars(
|
||||||
|
text.data(), text.data() + text.size(), value);
|
||||||
|
return !text.empty() && parsed.ec == std::errc{} &&
|
||||||
|
parsed.ptr == text.data() + text.size() && value > 0;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool parse_finite_double(std::string_view text, double& value) {
|
||||||
|
if (text.starts_with('+')) {
|
||||||
|
text.remove_prefix(1U);
|
||||||
|
}
|
||||||
|
const auto parsed = std::from_chars(
|
||||||
|
text.data(),
|
||||||
|
text.data() + text.size(),
|
||||||
|
value,
|
||||||
|
std::chars_format::general);
|
||||||
|
return !text.empty() && parsed.ec == std::errc{} &&
|
||||||
|
parsed.ptr == text.data() + text.size() &&
|
||||||
|
std::isfinite(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
ReferenceCsvReadResult read_reference_csv(
|
||||||
|
const ReferenceQuantity quantity,
|
||||||
|
const std::filesystem::path& path,
|
||||||
|
const std::string_view single_instance_name) {
|
||||||
|
std::ifstream file{path, std::ios::binary};
|
||||||
|
if (!file) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"validation.reference_csv_open_failed",
|
||||||
|
"Unable to open reference CSV file.",
|
||||||
|
path,
|
||||||
|
0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string bytes{
|
||||||
|
std::istreambuf_iterator<char>{file},
|
||||||
|
std::istreambuf_iterator<char>{},
|
||||||
|
};
|
||||||
|
if (file.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"validation.reference_csv_read_failed",
|
||||||
|
"Failed while reading reference CSV file.",
|
||||||
|
path,
|
||||||
|
0U);
|
||||||
|
}
|
||||||
|
|
||||||
|
constexpr std::string_view bom{"\xEF\xBB\xBF"};
|
||||||
|
if (bytes.starts_with(bom)) {
|
||||||
|
bytes.erase(0U, bom.size());
|
||||||
|
}
|
||||||
|
const std::size_t misplaced_bom = bytes.find(bom);
|
||||||
|
if (misplaced_bom != std::string::npos || !is_valid_utf8(bytes)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_encoding",
|
||||||
|
"Reference CSV must be UTF-8 with an optional BOM only at the file start.",
|
||||||
|
path,
|
||||||
|
misplaced_bom == std::string::npos
|
||||||
|
? 1U
|
||||||
|
: line_at_offset(bytes, misplaced_bom));
|
||||||
|
}
|
||||||
|
|
||||||
|
std::istringstream input{bytes};
|
||||||
|
std::string line;
|
||||||
|
if (!std::getline(input, line)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_header",
|
||||||
|
"Reference CSV is missing its header row.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string> headers = split_fields(line);
|
||||||
|
ColumnIndices column_indices;
|
||||||
|
for (std::size_t index = 0U; index < headers.size(); ++index) {
|
||||||
|
if (headers[index].empty() ||
|
||||||
|
!column_indices.emplace(headers[index], index).second) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_duplicate_column",
|
||||||
|
"Reference CSV contains an empty or duplicate column name.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string_view> required = required_columns(quantity);
|
||||||
|
for (const std::string_view name : required) {
|
||||||
|
if (!column_indices.contains(name)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_column",
|
||||||
|
"Reference CSV is missing required column '" +
|
||||||
|
std::string{name} + "'.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const bool has_instance = column_indices.contains(instance_column);
|
||||||
|
if (!has_instance && single_instance_name.empty()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_column",
|
||||||
|
"Reference CSV omits 'Part Instance Name' without a single-Instance name.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::set<std::string, std::less<>> allowed_columns;
|
||||||
|
allowed_columns.emplace(instance_column);
|
||||||
|
for (const std::string_view name : required) {
|
||||||
|
allowed_columns.emplace(name);
|
||||||
|
}
|
||||||
|
for (const std::string& header : headers) {
|
||||||
|
if (!allowed_columns.contains(header)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_unsupported_column",
|
||||||
|
"Reference CSV contains unsupported column '" + header + "'.",
|
||||||
|
path,
|
||||||
|
1U);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const std::vector<std::string_view> components = value_columns(quantity);
|
||||||
|
std::vector<ReferenceRow> rows;
|
||||||
|
std::set<PositionKey> positions;
|
||||||
|
std::size_t line_number = 1U;
|
||||||
|
while (std::getline(input, line)) {
|
||||||
|
++line_number;
|
||||||
|
if (trim(line).empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const std::vector<std::string> fields = split_fields(line);
|
||||||
|
if (fields.size() != headers.size()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_row",
|
||||||
|
"Reference CSV row field count does not match the header.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::string instance_name = has_instance
|
||||||
|
? fields[column_index(
|
||||||
|
column_indices,
|
||||||
|
instance_column)]
|
||||||
|
: std::string{single_instance_name};
|
||||||
|
if (instance_name.empty()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_row",
|
||||||
|
"Reference CSV row has an empty Instance name.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::int64_t entity_label = 0;
|
||||||
|
const std::string_view entity_column =
|
||||||
|
quantity == ReferenceQuantity::displacement ||
|
||||||
|
quantity == ReferenceQuantity::reaction
|
||||||
|
? node_column
|
||||||
|
: element_column;
|
||||||
|
if (!parse_positive_label(
|
||||||
|
fields[column_index(column_indices, entity_column)],
|
||||||
|
entity_label)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_number",
|
||||||
|
"Reference CSV entity label must be a positive integer.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
|
||||||
|
std::optional<std::int64_t> end_node_label;
|
||||||
|
if (quantity == ReferenceQuantity::internal_force ||
|
||||||
|
quantity == ReferenceQuantity::centroid_stress) {
|
||||||
|
std::int64_t parsed_end_node = 0;
|
||||||
|
if (!parse_positive_label(
|
||||||
|
fields[column_index(column_indices, node_column)],
|
||||||
|
parsed_end_node)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_number",
|
||||||
|
"Reference CSV end-node label must be a positive integer.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
end_node_label = parsed_end_node;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::vector<double> values;
|
||||||
|
values.reserve(components.size());
|
||||||
|
for (const std::string_view component : components) {
|
||||||
|
double value = 0.0;
|
||||||
|
if (!parse_finite_double(
|
||||||
|
fields[column_index(column_indices, component)],
|
||||||
|
value)) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_invalid_number",
|
||||||
|
"Reference CSV component '" + std::string{component} +
|
||||||
|
"' must be a finite number.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
values.push_back(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
PositionKey key{instance_name, entity_label, end_node_label};
|
||||||
|
if (!positions.insert(key).second) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_duplicate_row",
|
||||||
|
"Reference CSV contains a duplicate result position.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
rows.push_back({
|
||||||
|
quantity,
|
||||||
|
{std::move(instance_name), entity_label, end_node_label},
|
||||||
|
std::move(values),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
if (input.bad()) {
|
||||||
|
return failure(
|
||||||
|
DiagnosticStage::io,
|
||||||
|
"validation.reference_csv_read_failed",
|
||||||
|
"Failed while reading reference CSV file.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
if (rows.empty()) {
|
||||||
|
return validation_failure(
|
||||||
|
"validation.reference_csv_missing_rows",
|
||||||
|
"Reference CSV contains no result rows.",
|
||||||
|
path,
|
||||||
|
line_number);
|
||||||
|
}
|
||||||
|
return {std::move(rows), {}};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -746,6 +746,7 @@ set_property(
|
|||||||
|
|
||||||
add_executable(fesa_validation_comparison_tests
|
add_executable(fesa_validation_comparison_tests
|
||||||
unit/validation/comparison_test.cpp
|
unit/validation/comparison_test.cpp
|
||||||
|
unit/validation/reference_csv_test.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_compile_features(
|
target_compile_features(
|
||||||
@@ -754,6 +755,11 @@ target_compile_features(
|
|||||||
target_compile_options(
|
target_compile_options(
|
||||||
fesa_validation_comparison_tests PRIVATE /W4 /permissive- /EHsc
|
fesa_validation_comparison_tests PRIVATE /W4 /permissive- /EHsc
|
||||||
)
|
)
|
||||||
|
target_compile_definitions(
|
||||||
|
fesa_validation_comparison_tests
|
||||||
|
PRIVATE
|
||||||
|
FESA_TEST_SOURCE_DIR="${CMAKE_CURRENT_SOURCE_DIR}"
|
||||||
|
)
|
||||||
target_link_libraries(
|
target_link_libraries(
|
||||||
fesa_validation_comparison_tests
|
fesa_validation_comparison_tests
|
||||||
PRIVATE
|
PRIVATE
|
||||||
@@ -772,3 +778,21 @@ add_test(
|
|||||||
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
--gtest_filter=EntityMatching.*
|
--gtest_filter=EntityMatching.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME ReferenceCsv
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=ReferenceCsv.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME InternalForceCsv
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=InternalForceCsv.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME StressCsv
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_validation_comparison_tests>"
|
||||||
|
--gtest_filter=StressCsv.*
|
||||||
|
)
|
||||||
|
|||||||
+3
@@ -0,0 +1,3 @@
|
|||||||
|
Part Instance Name, Element Label, Node Label, SF-SF1, SF-SF2, SF-SF3, SM-SM1, SM-SM2, SM-SM3
|
||||||
|
Part-1-1 , 501 , 101 , 1.25 , -2.5 , 3.75 , -4.0 , 5.5 , -6.25
|
||||||
|
Part-1-1 , 501 , 102 , 7.0 , 8.0 , 9.0 , 10.0 , 11.0 , 12.0
|
||||||
|
+3
@@ -0,0 +1,3 @@
|
|||||||
|
Element Label, Node Label, Sxx
|
||||||
|
501, 101, 42.5
|
||||||
|
501, 102, -17.25
|
||||||
|
@@ -0,0 +1,235 @@
|
|||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <filesystem>
|
||||||
|
#include <fstream>
|
||||||
|
#include <stdexcept>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <system_error>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/validation/reference_csv.hpp>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
class TemporaryCsv final {
|
||||||
|
public:
|
||||||
|
TemporaryCsv(std::string_view name, std::string_view contents)
|
||||||
|
: path_{std::filesystem::path{testing::TempDir()} / name} {
|
||||||
|
std::ofstream output{path_, std::ios::binary};
|
||||||
|
output.write(
|
||||||
|
contents.data(), static_cast<std::streamsize>(contents.size()));
|
||||||
|
if (!output) {
|
||||||
|
throw std::runtime_error{"Failed to write temporary reference CSV."};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
~TemporaryCsv() {
|
||||||
|
std::error_code error;
|
||||||
|
std::filesystem::remove(path_, error);
|
||||||
|
}
|
||||||
|
|
||||||
|
TemporaryCsv(const TemporaryCsv&) = delete;
|
||||||
|
TemporaryCsv& operator=(const TemporaryCsv&) = delete;
|
||||||
|
|
||||||
|
[[nodiscard]] const std::filesystem::path& path() const noexcept {
|
||||||
|
return path_;
|
||||||
|
}
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::filesystem::path path_;
|
||||||
|
};
|
||||||
|
|
||||||
|
std::filesystem::path fixture_path(const std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_TEST_SOURCE_DIR} / "fixtures" /
|
||||||
|
"reference" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
std::filesystem::path supplied_reference_path(const std::string_view name) {
|
||||||
|
return std::filesystem::path{FESA_TEST_SOURCE_DIR}.parent_path() /
|
||||||
|
"reference" / "cantilever beam" / name;
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_diagnostic(
|
||||||
|
const std::vector<fesa::Diagnostic>& diagnostics,
|
||||||
|
const std::string_view code) {
|
||||||
|
return std::ranges::any_of(
|
||||||
|
diagnostics,
|
||||||
|
[code](const fesa::Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.code == code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, ReadsSuppliedDisplacementsWithWhitespaceHeader) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement,
|
||||||
|
supplied_reference_path("cantilever beam displacements.csv"),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 11U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().quantity,
|
||||||
|
fesa::ReferenceQuantity::displacement);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 1);
|
||||||
|
EXPECT_FALSE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{0.0, 0.0, -1.0e-32, 0.0, 1.0e-31, 0.0}));
|
||||||
|
EXPECT_EQ(result.rows.back().position.entity_label, 11);
|
||||||
|
EXPECT_EQ(result.rows.back().values[2], -1.92e-4);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, ReadsSuppliedReactionsWithWhitespaceHeader) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
supplied_reference_path("cantilever beam reactions.csv"),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 11U);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 1);
|
||||||
|
EXPECT_FALSE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{0.0, 0.0, 1.0e4, 0.0, -1.0e5, 0.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(InternalForceCsv, MapsAllSixComponentsAndElementEndPosition) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::internal_force,
|
||||||
|
fixture_path("internalforces.csv"),
|
||||||
|
"unused-request-name");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 2U);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().quantity,
|
||||||
|
fesa::ReferenceQuantity::internal_force);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 501);
|
||||||
|
ASSERT_TRUE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows.front().values,
|
||||||
|
(std::vector<double>{1.25, -2.5, 3.75, -4.0, 5.5, -6.25}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(StressCsv, FillsOmittedInstanceAndReadsCentroidStress) {
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
fixture_path("stresses.csv"),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 2U);
|
||||||
|
EXPECT_EQ(result.rows.front().position.instance_name, "Part-1-1");
|
||||||
|
EXPECT_EQ(result.rows.front().position.entity_label, 501);
|
||||||
|
ASSERT_TRUE(result.rows.front().position.end_node_label.has_value());
|
||||||
|
EXPECT_EQ(*result.rows.front().position.end_node_label, 101);
|
||||||
|
EXPECT_EQ(result.rows.front().values, (std::vector<double>{42.5}));
|
||||||
|
EXPECT_EQ(result.rows.back().values, (std::vector<double>{-17.25}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, AcceptsUtf8BomOnlyAtTheStartAndTrimsValues) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-bom.csv",
|
||||||
|
"\xEF\xBB\xBF Part Instance Name , Node Label , U-U1 , U-U2 , "
|
||||||
|
"U-U3 , UR-UR1 , UR-UR2 , UR-UR3\n"
|
||||||
|
" Beam-1 , 7 , 1 , 2 , 3 , 4 , 5 , 6 \n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement, input.path(), "unused");
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.diagnostics.empty());
|
||||||
|
ASSERT_EQ(result.rows.size(), 1U);
|
||||||
|
EXPECT_EQ(result.rows[0].position.instance_name, "Beam-1");
|
||||||
|
EXPECT_EQ(result.rows[0].position.entity_label, 7);
|
||||||
|
EXPECT_EQ(
|
||||||
|
result.rows[0].values,
|
||||||
|
(std::vector<double>{1.0, 2.0, 3.0, 4.0, 5.0, 6.0}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, DiagnosesMissingRequiredColumn) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-missing-column.csv",
|
||||||
|
"Node Label,U-U1,U-U2,U-U3,UR-UR1,UR-UR2,Unexpected\n"
|
||||||
|
"1,0,0,0,0,0,0\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.rows.empty());
|
||||||
|
ASSERT_TRUE(has_diagnostic(
|
||||||
|
result.diagnostics, "validation.reference_csv_missing_column"));
|
||||||
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics.front().source->line, 1U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, DiagnosesDuplicateResultPosition) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-duplicate.csv",
|
||||||
|
"Element Label,Node Label,Sxx\n"
|
||||||
|
"8,2,10\n"
|
||||||
|
"8,2,11\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
input.path(),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.rows.empty());
|
||||||
|
ASSERT_TRUE(has_diagnostic(
|
||||||
|
result.diagnostics, "validation.reference_csv_duplicate_row"));
|
||||||
|
ASSERT_TRUE(result.diagnostics.front().source.has_value());
|
||||||
|
EXPECT_EQ(result.diagnostics.front().source->line, 3U);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, DiagnosesInvalidAndNonfiniteNumbers) {
|
||||||
|
const TemporaryCsv invalid{
|
||||||
|
"fesa-reference-invalid-number.csv",
|
||||||
|
"Node Label,RF-RF1,RF-RF2,RF-RF3,RM-RM1,RM-RM2,RM-RM3\n"
|
||||||
|
"1,0,0,not-a-number,0,0,0\n"};
|
||||||
|
const TemporaryCsv nonfinite{
|
||||||
|
"fesa-reference-nonfinite-number.csv",
|
||||||
|
"Element Label,Node Label,Sxx\n"
|
||||||
|
"8,2,nan\n"};
|
||||||
|
|
||||||
|
const auto invalid_result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::reaction,
|
||||||
|
invalid.path(),
|
||||||
|
"Part-1-1");
|
||||||
|
const auto nonfinite_result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::centroid_stress,
|
||||||
|
nonfinite.path(),
|
||||||
|
"Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(invalid_result.rows.empty());
|
||||||
|
EXPECT_TRUE(nonfinite_result.rows.empty());
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
invalid_result.diagnostics,
|
||||||
|
"validation.reference_csv_invalid_number"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
nonfinite_result.diagnostics,
|
||||||
|
"validation.reference_csv_invalid_number"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(ReferenceCsv, RejectsUtf8BomOutsideTheFileStart) {
|
||||||
|
const TemporaryCsv input{
|
||||||
|
"fesa-reference-misplaced-bom.csv",
|
||||||
|
"Node Label,\xEF\xBB\xBF U-U1,U-U2,U-U3,UR-UR1,UR-UR2,UR-UR3\n"
|
||||||
|
"1,0,0,0,0,0,0\n"};
|
||||||
|
|
||||||
|
const auto result = fesa::read_reference_csv(
|
||||||
|
fesa::ReferenceQuantity::displacement, input.path(), "Part-1-1");
|
||||||
|
|
||||||
|
EXPECT_TRUE(result.rows.empty());
|
||||||
|
EXPECT_TRUE(has_diagnostic(
|
||||||
|
result.diagnostics,
|
||||||
|
"validation.reference_csv_invalid_encoding"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
Reference in New Issue
Block a user