Files
FESA/src/fesa/validation/reference_csv.cpp
T

455 lines
14 KiB
C++

#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;
}
void remove_trailing_empty_fields(std::vector<std::string>& fields) {
while (!fields.empty() && fields.back().empty()) {
fields.pop_back();
}
}
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-SF3", "SF-SF2", "SM-SM3", "SM-SM1", "SM-SM2"};
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);
}
std::vector<std::string> headers = split_fields(line);
remove_trailing_empty_fields(headers);
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;
}
std::vector<std::string> fields = split_fields(line);
remove_trailing_empty_fields(fields);
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