feat(domain-and-input-skeleton): step 1 — domain-validation
This commit is contained in:
@@ -27,6 +27,8 @@ include(cmake/FesaDependencies.cmake)
|
|||||||
|
|
||||||
add_library(fesa_core STATIC
|
add_library(fesa_core STATIC
|
||||||
src/fesa/core/version.cpp
|
src/fesa/core/version.cpp
|
||||||
|
src/fesa/model/domain.cpp
|
||||||
|
src/fesa/model/domain_builder.cpp
|
||||||
)
|
)
|
||||||
|
|
||||||
target_include_directories(fesa_core
|
target_include_directories(fesa_core
|
||||||
|
|||||||
@@ -1,8 +1,11 @@
|
|||||||
#pragma once
|
#pragma once
|
||||||
|
|
||||||
#include <algorithm>
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <map>
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <stdexcept>
|
#include <string>
|
||||||
|
#include <unordered_map>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
#include <vector>
|
||||||
|
|
||||||
@@ -15,23 +18,14 @@
|
|||||||
|
|
||||||
namespace fesa {
|
namespace fesa {
|
||||||
|
|
||||||
|
class DomainBuilder;
|
||||||
|
|
||||||
class Domain final {
|
class Domain final {
|
||||||
public:
|
public:
|
||||||
Domain(
|
Domain(const Domain&) = default;
|
||||||
std::vector<Node> nodes,
|
Domain(Domain&&) noexcept = default;
|
||||||
std::vector<BeamElement> beam_elements,
|
Domain& operator=(const Domain&) = default;
|
||||||
std::vector<IsotropicElastic> materials,
|
Domain& operator=(Domain&&) noexcept = default;
|
||||||
std::vector<BeamSection> sections,
|
|
||||||
std::vector<NodeSet> node_sets,
|
|
||||||
std::vector<ElementSet> element_sets,
|
|
||||||
StepDefinition step)
|
|
||||||
: nodes_{std::move(nodes)},
|
|
||||||
beam_elements_{std::move(beam_elements)},
|
|
||||||
materials_{std::move(materials)},
|
|
||||||
sections_{std::move(sections)},
|
|
||||||
node_sets_{std::move(node_sets)},
|
|
||||||
element_sets_{std::move(element_sets)},
|
|
||||||
step_{std::move(step)} {}
|
|
||||||
|
|
||||||
[[nodiscard]] std::span<const Node> nodes() const noexcept {
|
[[nodiscard]] std::span<const Node> nodes() const noexcept {
|
||||||
return nodes_;
|
return nodes_;
|
||||||
@@ -61,29 +55,25 @@ public:
|
|||||||
return step_;
|
return step_;
|
||||||
}
|
}
|
||||||
|
|
||||||
[[nodiscard]] const Node& node(const NodeId id) const {
|
[[nodiscard]] const Node& node(NodeId id) const;
|
||||||
const auto found = std::find_if(
|
[[nodiscard]] const Node& node(const EntityOrigin& origin) const;
|
||||||
nodes_.begin(), nodes_.end(),
|
|
||||||
[id](const Node& candidate) { return candidate.id == id; });
|
|
||||||
if (found == nodes_.end()) {
|
|
||||||
throw std::out_of_range{"Node ID is not present in the Domain."};
|
|
||||||
}
|
|
||||||
return *found;
|
|
||||||
}
|
|
||||||
|
|
||||||
[[nodiscard]] const Node& node(const EntityOrigin& origin) const {
|
|
||||||
const auto found = std::find_if(
|
|
||||||
nodes_.begin(), nodes_.end(), [&origin](const Node& candidate) {
|
|
||||||
return candidate.origin == origin;
|
|
||||||
});
|
|
||||||
if (found == nodes_.end()) {
|
|
||||||
throw std::out_of_range{
|
|
||||||
"Node origin is not present in the Domain."};
|
|
||||||
}
|
|
||||||
return *found;
|
|
||||||
}
|
|
||||||
|
|
||||||
private:
|
private:
|
||||||
|
friend class DomainBuilder;
|
||||||
|
|
||||||
|
using OriginKey = std::pair<std::string, std::int64_t>;
|
||||||
|
|
||||||
|
Domain(
|
||||||
|
std::vector<Node> nodes,
|
||||||
|
std::vector<BeamElement> beam_elements,
|
||||||
|
std::vector<IsotropicElastic> materials,
|
||||||
|
std::vector<BeamSection> sections,
|
||||||
|
std::vector<NodeSet> node_sets,
|
||||||
|
std::vector<ElementSet> element_sets,
|
||||||
|
StepDefinition step);
|
||||||
|
|
||||||
|
[[nodiscard]] static OriginKey origin_key(const EntityOrigin& origin);
|
||||||
|
|
||||||
std::vector<Node> nodes_;
|
std::vector<Node> nodes_;
|
||||||
std::vector<BeamElement> beam_elements_;
|
std::vector<BeamElement> beam_elements_;
|
||||||
std::vector<IsotropicElastic> materials_;
|
std::vector<IsotropicElastic> materials_;
|
||||||
@@ -91,6 +81,12 @@ private:
|
|||||||
std::vector<NodeSet> node_sets_;
|
std::vector<NodeSet> node_sets_;
|
||||||
std::vector<ElementSet> element_sets_;
|
std::vector<ElementSet> element_sets_;
|
||||||
StepDefinition step_;
|
StepDefinition step_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> node_indices_;
|
||||||
|
std::map<OriginKey, std::size_t> node_origin_indices_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> beam_element_indices_;
|
||||||
|
std::map<OriginKey, std::size_t> beam_element_origin_indices_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> material_indices_;
|
||||||
|
std::unordered_map<std::int64_t, std::size_t> section_indices_;
|
||||||
};
|
};
|
||||||
|
|
||||||
} // namespace fesa
|
} // namespace fesa
|
||||||
|
|||||||
@@ -0,0 +1,37 @@
|
|||||||
|
#pragma once
|
||||||
|
|
||||||
|
#include <optional>
|
||||||
|
#include <vector>
|
||||||
|
|
||||||
|
#include <fesa/core/diagnostic.hpp>
|
||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
struct DomainBuildResult final {
|
||||||
|
std::optional<Domain> domain;
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DomainBuilder final {
|
||||||
|
public:
|
||||||
|
void add_node(Node value);
|
||||||
|
void add_material(IsotropicElastic value);
|
||||||
|
void add_section(BeamSection value);
|
||||||
|
void add_beam_element(BeamElement value);
|
||||||
|
void add_node_set(NodeSet value);
|
||||||
|
void add_element_set(ElementSet value);
|
||||||
|
void set_step(StepDefinition value);
|
||||||
|
[[nodiscard]] DomainBuildResult build() &&;
|
||||||
|
|
||||||
|
private:
|
||||||
|
std::vector<Node> nodes_;
|
||||||
|
std::vector<BeamElement> beam_elements_;
|
||||||
|
std::vector<IsotropicElastic> materials_;
|
||||||
|
std::vector<BeamSection> sections_;
|
||||||
|
std::vector<NodeSet> node_sets_;
|
||||||
|
std::vector<ElementSet> element_sets_;
|
||||||
|
std::optional<StepDefinition> step_;
|
||||||
|
};
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,62 @@
|
|||||||
|
#include <fesa/model/domain.hpp>
|
||||||
|
|
||||||
|
#include <cstddef>
|
||||||
|
#include <stdexcept>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
|
||||||
|
Domain::Domain(
|
||||||
|
std::vector<Node> nodes,
|
||||||
|
std::vector<BeamElement> beam_elements,
|
||||||
|
std::vector<IsotropicElastic> materials,
|
||||||
|
std::vector<BeamSection> sections,
|
||||||
|
std::vector<NodeSet> node_sets,
|
||||||
|
std::vector<ElementSet> element_sets,
|
||||||
|
StepDefinition step)
|
||||||
|
: nodes_{std::move(nodes)},
|
||||||
|
beam_elements_{std::move(beam_elements)},
|
||||||
|
materials_{std::move(materials)},
|
||||||
|
sections_{std::move(sections)},
|
||||||
|
node_sets_{std::move(node_sets)},
|
||||||
|
element_sets_{std::move(element_sets)},
|
||||||
|
step_{std::move(step)} {
|
||||||
|
for (std::size_t index = 0; index < nodes_.size(); ++index) {
|
||||||
|
node_indices_.emplace(nodes_[index].id.value(), index);
|
||||||
|
node_origin_indices_.emplace(origin_key(nodes_[index].origin), index);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < beam_elements_.size(); ++index) {
|
||||||
|
beam_element_indices_.emplace(
|
||||||
|
beam_elements_[index].id.value(), index);
|
||||||
|
beam_element_origin_indices_.emplace(
|
||||||
|
origin_key(beam_elements_[index].origin), index);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < materials_.size(); ++index) {
|
||||||
|
material_indices_.emplace(materials_[index].id.value(), index);
|
||||||
|
}
|
||||||
|
for (std::size_t index = 0; index < sections_.size(); ++index) {
|
||||||
|
section_indices_.emplace(sections_[index].id.value(), index);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node& Domain::node(const NodeId id) const {
|
||||||
|
const auto found = node_indices_.find(id.value());
|
||||||
|
if (found == node_indices_.end()) {
|
||||||
|
throw std::out_of_range{"Node ID is not present in the Domain."};
|
||||||
|
}
|
||||||
|
return nodes_[found->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node& Domain::node(const EntityOrigin& origin) const {
|
||||||
|
const auto found = node_origin_indices_.find(origin_key(origin));
|
||||||
|
if (found == node_origin_indices_.end()) {
|
||||||
|
throw std::out_of_range{
|
||||||
|
"Node origin is not present in the Domain."};
|
||||||
|
}
|
||||||
|
return nodes_[found->second];
|
||||||
|
}
|
||||||
|
|
||||||
|
Domain::OriginKey Domain::origin_key(const EntityOrigin& origin) {
|
||||||
|
return {origin.instance_name, origin.local_label};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -0,0 +1,477 @@
|
|||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <cmath>
|
||||||
|
#include <cstddef>
|
||||||
|
#include <cstdint>
|
||||||
|
#include <limits>
|
||||||
|
#include <map>
|
||||||
|
#include <string>
|
||||||
|
#include <string_view>
|
||||||
|
#include <unordered_map>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
namespace fesa {
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
using IndexLookup = std::unordered_map<std::int64_t, std::size_t>;
|
||||||
|
using OriginKey = std::pair<std::string, std::int64_t>;
|
||||||
|
|
||||||
|
void add_error(
|
||||||
|
std::vector<Diagnostic>& diagnostics,
|
||||||
|
std::string code,
|
||||||
|
std::string message) {
|
||||||
|
diagnostics.push_back({
|
||||||
|
DiagnosticStage::model,
|
||||||
|
Severity::error,
|
||||||
|
std::move(code),
|
||||||
|
std::move(message),
|
||||||
|
std::nullopt,
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
OriginKey origin_key(const EntityOrigin& origin) {
|
||||||
|
return {origin.instance_name, origin.local_label};
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entity, class IdAccessor>
|
||||||
|
IndexLookup collect_ids(
|
||||||
|
const std::vector<Entity>& entities,
|
||||||
|
IdAccessor id_of,
|
||||||
|
const std::string_view duplicate_code,
|
||||||
|
const std::string_view entity_name,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
IndexLookup indices;
|
||||||
|
for (std::size_t index = 0; index < entities.size(); ++index) {
|
||||||
|
const std::int64_t id = id_of(entities[index]).value();
|
||||||
|
if (!indices.emplace(id, index).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
std::string{duplicate_code},
|
||||||
|
"Duplicate " + std::string{entity_name} +
|
||||||
|
" internal ID " + std::to_string(id) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return indices;
|
||||||
|
}
|
||||||
|
|
||||||
|
template <class Entity>
|
||||||
|
void collect_duplicate_origins(
|
||||||
|
const std::vector<Entity>& entities,
|
||||||
|
const std::string_view duplicate_code,
|
||||||
|
const std::string_view entity_name,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
std::map<OriginKey, std::size_t> origins;
|
||||||
|
for (std::size_t index = 0; index < entities.size(); ++index) {
|
||||||
|
const bool inserted =
|
||||||
|
origins.emplace(origin_key(entities[index].origin), index).second;
|
||||||
|
if (!inserted) {
|
||||||
|
const EntityOrigin& origin = entities[index].origin;
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
std::string{duplicate_code},
|
||||||
|
"Duplicate " + std::string{entity_name} + " origin (" +
|
||||||
|
origin.instance_name + ", " +
|
||||||
|
std::to_string(origin.local_label) + ").");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_id(const IndexLookup& indices, const std::int64_t id) {
|
||||||
|
return indices.contains(id);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_finite(const std::array<double, 2>& value) {
|
||||||
|
return std::isfinite(value[0]) && std::isfinite(value[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_finite(const std::array<double, 6>& value) {
|
||||||
|
return std::ranges::all_of(
|
||||||
|
value, [](const double component) {
|
||||||
|
return std::isfinite(component);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
void validate_section_property(
|
||||||
|
const BeamSection& section,
|
||||||
|
const double value,
|
||||||
|
const std::string_view property_name,
|
||||||
|
std::vector<Diagnostic>& diagnostics) {
|
||||||
|
if (!std::isfinite(value)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Section " + std::to_string(section.id.value()) + " has a "
|
||||||
|
"nonfinite " +
|
||||||
|
std::string{property_name} + ".");
|
||||||
|
} else if (value <= 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_section",
|
||||||
|
"Section " + std::to_string(section.id.value()) + " requires " +
|
||||||
|
std::string{property_name} + " > 0.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
double length(const Vec3 value) {
|
||||||
|
return std::hypot(value.x, value.y, value.z);
|
||||||
|
}
|
||||||
|
|
||||||
|
bool is_parallel(const Vec3 first, const Vec3 second) {
|
||||||
|
const double first_length = length(first);
|
||||||
|
const double second_length = length(second);
|
||||||
|
if (first_length == 0.0 || second_length == 0.0) {
|
||||||
|
return false;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3 first_unit{
|
||||||
|
first.x / first_length,
|
||||||
|
first.y / first_length,
|
||||||
|
first.z / first_length,
|
||||||
|
};
|
||||||
|
const Vec3 second_unit{
|
||||||
|
second.x / second_length,
|
||||||
|
second.y / second_length,
|
||||||
|
second.z / second_length,
|
||||||
|
};
|
||||||
|
const Vec3 cross{
|
||||||
|
first_unit.y * second_unit.z -
|
||||||
|
first_unit.z * second_unit.y,
|
||||||
|
first_unit.z * second_unit.x -
|
||||||
|
first_unit.x * second_unit.z,
|
||||||
|
first_unit.x * second_unit.y -
|
||||||
|
first_unit.y * second_unit.x,
|
||||||
|
};
|
||||||
|
return length(cross) <=
|
||||||
|
64.0 * std::numeric_limits<double>::epsilon();
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
|
|
||||||
|
void DomainBuilder::add_node(Node value) {
|
||||||
|
nodes_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_material(IsotropicElastic value) {
|
||||||
|
materials_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_section(BeamSection value) {
|
||||||
|
sections_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_beam_element(BeamElement value) {
|
||||||
|
beam_elements_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_node_set(NodeSet value) {
|
||||||
|
node_sets_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::add_element_set(ElementSet value) {
|
||||||
|
element_sets_.push_back(std::move(value));
|
||||||
|
}
|
||||||
|
|
||||||
|
void DomainBuilder::set_step(StepDefinition value) {
|
||||||
|
step_ = std::move(value);
|
||||||
|
}
|
||||||
|
|
||||||
|
DomainBuildResult DomainBuilder::build() && {
|
||||||
|
std::vector<Diagnostic> diagnostics;
|
||||||
|
|
||||||
|
const IndexLookup node_indices = collect_ids(
|
||||||
|
nodes_,
|
||||||
|
[](const Node& node) { return node.id; },
|
||||||
|
"model.duplicate_node_id",
|
||||||
|
"node",
|
||||||
|
diagnostics);
|
||||||
|
const IndexLookup material_indices = collect_ids(
|
||||||
|
materials_,
|
||||||
|
[](const IsotropicElastic& material) { return material.id; },
|
||||||
|
"model.duplicate_material_id",
|
||||||
|
"material",
|
||||||
|
diagnostics);
|
||||||
|
const IndexLookup section_indices = collect_ids(
|
||||||
|
sections_,
|
||||||
|
[](const BeamSection& section) { return section.id; },
|
||||||
|
"model.duplicate_section_id",
|
||||||
|
"section",
|
||||||
|
diagnostics);
|
||||||
|
const IndexLookup element_indices = collect_ids(
|
||||||
|
beam_elements_,
|
||||||
|
[](const BeamElement& element) { return element.id; },
|
||||||
|
"model.duplicate_element_id",
|
||||||
|
"Beam element",
|
||||||
|
diagnostics);
|
||||||
|
|
||||||
|
collect_duplicate_origins(
|
||||||
|
nodes_,
|
||||||
|
"model.duplicate_node_origin",
|
||||||
|
"node",
|
||||||
|
diagnostics);
|
||||||
|
collect_duplicate_origins(
|
||||||
|
beam_elements_,
|
||||||
|
"model.duplicate_element_origin",
|
||||||
|
"Beam element",
|
||||||
|
diagnostics);
|
||||||
|
|
||||||
|
for (const Node& node : nodes_) {
|
||||||
|
if (!is_finite(node.position)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Node " + std::to_string(node.id.value()) +
|
||||||
|
" has a nonfinite coordinate.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const IsotropicElastic& material : materials_) {
|
||||||
|
if (!std::isfinite(material.young)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" has a nonfinite Young's modulus.");
|
||||||
|
} else if (material.young <= 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_material",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" requires E > 0.");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!std::isfinite(material.poisson)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" has a nonfinite Poisson ratio.");
|
||||||
|
} else if (
|
||||||
|
material.poisson <= -1.0 || material.poisson >= 0.5) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_material",
|
||||||
|
"Material " + std::to_string(material.id.value()) +
|
||||||
|
" requires -1 < nu < 0.5.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const BeamSection& section : sections_) {
|
||||||
|
validate_section_property(
|
||||||
|
section, section.area, "A", diagnostics);
|
||||||
|
validate_section_property(section, section.iy, "Iy", diagnostics);
|
||||||
|
validate_section_property(section, section.iz, "Iz", diagnostics);
|
||||||
|
validate_section_property(
|
||||||
|
section, section.torsion_j, "J", diagnostics);
|
||||||
|
validate_section_property(
|
||||||
|
section, section.shear_area_y, "Asy", diagnostics);
|
||||||
|
validate_section_property(
|
||||||
|
section, section.shear_area_z, "Asz", diagnostics);
|
||||||
|
|
||||||
|
if (!is_finite(section.orientation)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Section " + std::to_string(section.id.value()) +
|
||||||
|
" has a nonfinite orientation.");
|
||||||
|
} else if (length(section.orientation) == 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_orientation",
|
||||||
|
"Section " + std::to_string(section.id.value()) +
|
||||||
|
" has a zero orientation vector.");
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const auto& recovery_point : section.recovery_points) {
|
||||||
|
if (!is_finite(recovery_point)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Section " + std::to_string(section.id.value()) +
|
||||||
|
" has a nonfinite recovery point.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const BeamElement& element : beam_elements_) {
|
||||||
|
const bool has_first_node =
|
||||||
|
has_id(node_indices, element.nodes[0].value());
|
||||||
|
const bool has_second_node =
|
||||||
|
has_id(node_indices, element.nodes[1].value());
|
||||||
|
if (!has_first_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing node " +
|
||||||
|
std::to_string(element.nodes[0].value()) + ".");
|
||||||
|
}
|
||||||
|
if (!has_second_node) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing node " +
|
||||||
|
std::to_string(element.nodes[1].value()) + ".");
|
||||||
|
}
|
||||||
|
if (!has_id(material_indices, element.material.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_material_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing material " +
|
||||||
|
std::to_string(element.material.value()) + ".");
|
||||||
|
}
|
||||||
|
if (!has_id(section_indices, element.section.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_section_reference",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" references missing section " +
|
||||||
|
std::to_string(element.section.value()) + ".");
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!has_first_node || !has_second_node) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Node& first = nodes_[node_indices.at(element.nodes[0].value())];
|
||||||
|
const Node& second =
|
||||||
|
nodes_[node_indices.at(element.nodes[1].value())];
|
||||||
|
if (!is_finite(first.position) || !is_finite(second.position)) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const Vec3 axis{
|
||||||
|
second.position.x - first.position.x,
|
||||||
|
second.position.y - first.position.y,
|
||||||
|
second.position.z - first.position.z,
|
||||||
|
};
|
||||||
|
if (length(axis) == 0.0) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.zero_length_element",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" has zero length.");
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto section_found =
|
||||||
|
section_indices.find(element.section.value());
|
||||||
|
if (section_found == section_indices.end()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const Vec3 orientation =
|
||||||
|
sections_[section_found->second].orientation;
|
||||||
|
if (is_finite(orientation) && length(orientation) > 0.0 &&
|
||||||
|
is_parallel(axis, orientation)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_orientation",
|
||||||
|
"Beam element " + std::to_string(element.id.value()) +
|
||||||
|
" has an orientation parallel to its axis.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const NodeSet& node_set : node_sets_) {
|
||||||
|
for (const NodeId member : node_set.members) {
|
||||||
|
if (!has_id(node_indices, member.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Node set " + node_set.name +
|
||||||
|
" references missing node " +
|
||||||
|
std::to_string(member.value()) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const ElementSet& element_set : element_sets_) {
|
||||||
|
for (const ElementId member : element_set.members) {
|
||||||
|
if (!has_id(element_indices, member.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_element_reference",
|
||||||
|
"Element set " + element_set.name +
|
||||||
|
" references missing element " +
|
||||||
|
std::to_string(member.value()) + ".");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!step_.has_value()) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_step",
|
||||||
|
"The Domain requires one linear static step.");
|
||||||
|
} else {
|
||||||
|
std::map<std::pair<std::int64_t, std::uint8_t>, double>
|
||||||
|
prescribed_values;
|
||||||
|
for (const PrescribedDof& prescribed : step_->prescribed_dofs) {
|
||||||
|
if (!has_id(node_indices, prescribed.node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Boundary condition references missing node " +
|
||||||
|
std::to_string(prescribed.node.value()) + ".");
|
||||||
|
}
|
||||||
|
if (prescribed.dof < 1 || prescribed.dof > 6) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.invalid_dof",
|
||||||
|
"Boundary condition DOF must be in [1, 6].");
|
||||||
|
}
|
||||||
|
if (!std::isfinite(prescribed.value)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Boundary condition has a nonfinite value.");
|
||||||
|
}
|
||||||
|
|
||||||
|
const auto key =
|
||||||
|
std::pair{prescribed.node.value(), prescribed.dof};
|
||||||
|
if (!prescribed_values.emplace(key, prescribed.value).second) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.conflicting_boundary_condition",
|
||||||
|
"A node DOF has more than one prescribed value.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for (const NodalLoad& load : step_->nodal_loads) {
|
||||||
|
if (!has_id(node_indices, load.node.value())) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.missing_node_reference",
|
||||||
|
"Concentrated load references missing node " +
|
||||||
|
std::to_string(load.node.value()) + ".");
|
||||||
|
}
|
||||||
|
if (!is_finite(load.values)) {
|
||||||
|
add_error(
|
||||||
|
diagnostics,
|
||||||
|
"model.nonfinite_value",
|
||||||
|
"Concentrated load has a nonfinite component.");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!diagnostics.empty()) {
|
||||||
|
return {std::nullopt, std::move(diagnostics)};
|
||||||
|
}
|
||||||
|
|
||||||
|
return {
|
||||||
|
Domain{
|
||||||
|
std::move(nodes_),
|
||||||
|
std::move(beam_elements_),
|
||||||
|
std::move(materials_),
|
||||||
|
std::move(sections_),
|
||||||
|
std::move(node_sets_),
|
||||||
|
std::move(element_sets_),
|
||||||
|
std::move(*step_),
|
||||||
|
},
|
||||||
|
{},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace fesa
|
||||||
@@ -77,6 +77,7 @@ add_test(
|
|||||||
)
|
)
|
||||||
|
|
||||||
add_executable(fesa_model_value_tests
|
add_executable(fesa_model_value_tests
|
||||||
|
unit/model/domain_builder_test.cpp
|
||||||
unit/model/entity_origin_test.cpp
|
unit/model/entity_origin_test.cpp
|
||||||
unit/model/model_types_test.cpp
|
unit/model/model_types_test.cpp
|
||||||
)
|
)
|
||||||
@@ -99,3 +100,13 @@ add_test(
|
|||||||
NAME EntityOrigin
|
NAME EntityOrigin
|
||||||
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=EntityOrigin.*
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=EntityOrigin.*
|
||||||
)
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME DomainBuilder
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=DomainBuilder.*
|
||||||
|
)
|
||||||
|
|
||||||
|
add_test(
|
||||||
|
NAME DomainValidation
|
||||||
|
COMMAND "$<TARGET_FILE:fesa_model_value_tests>" --gtest_filter=*DomainValidation*
|
||||||
|
)
|
||||||
|
|||||||
@@ -0,0 +1,572 @@
|
|||||||
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
|
#include <algorithm>
|
||||||
|
#include <array>
|
||||||
|
#include <limits>
|
||||||
|
#include <string_view>
|
||||||
|
#include <utility>
|
||||||
|
|
||||||
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
|
namespace {
|
||||||
|
|
||||||
|
fesa::Node first_node() {
|
||||||
|
return {
|
||||||
|
fesa::NodeId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
fesa::Vec3{0.0, 0.0, 0.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::Node second_node() {
|
||||||
|
return {
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
|
||||||
|
fesa::Vec3{2.0, 0.0, 0.0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::IsotropicElastic valid_material() {
|
||||||
|
return {
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
"Steel",
|
||||||
|
210.0e9,
|
||||||
|
0.3,
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::BeamSection valid_section() {
|
||||||
|
return {
|
||||||
|
fesa::SectionId{0},
|
||||||
|
"General",
|
||||||
|
0.04,
|
||||||
|
1.2e-4,
|
||||||
|
1.4e-4,
|
||||||
|
2.0e-4,
|
||||||
|
0.03,
|
||||||
|
0.031,
|
||||||
|
fesa::ShearPropertySource::input,
|
||||||
|
fesa::Vec3{0.0, 1.0, 0.0},
|
||||||
|
{{-0.1, 0.0}, {0.1, 0.0}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::BeamElement valid_element() {
|
||||||
|
return {
|
||||||
|
fesa::ElementId{0},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 1},
|
||||||
|
{fesa::NodeId{0}, fesa::NodeId{1}},
|
||||||
|
fesa::MaterialId{0},
|
||||||
|
fesa::SectionId{0},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::StepDefinition valid_step() {
|
||||||
|
return {
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{0}, 1, 0.0}},
|
||||||
|
{{fesa::NodeId{1}, {0.0, -100.0, 0.0, 0.0, 0.0, 0.0}}},
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::DomainBuilder make_builder(
|
||||||
|
fesa::IsotropicElastic material,
|
||||||
|
fesa::BeamSection section,
|
||||||
|
fesa::BeamElement element,
|
||||||
|
fesa::StepDefinition step) {
|
||||||
|
fesa::DomainBuilder builder;
|
||||||
|
builder.add_node(first_node());
|
||||||
|
builder.add_node(second_node());
|
||||||
|
builder.add_material(std::move(material));
|
||||||
|
builder.add_section(std::move(section));
|
||||||
|
builder.add_beam_element(std::move(element));
|
||||||
|
builder.add_node_set({"Fixed", {fesa::NodeId{0}}});
|
||||||
|
builder.add_element_set({"Beam", {fesa::ElementId{0}}});
|
||||||
|
builder.set_step(std::move(step));
|
||||||
|
return builder;
|
||||||
|
}
|
||||||
|
|
||||||
|
fesa::DomainBuilder make_valid_builder() {
|
||||||
|
return make_builder(
|
||||||
|
valid_material(), valid_section(), valid_element(), valid_step());
|
||||||
|
}
|
||||||
|
|
||||||
|
bool has_diagnostic(
|
||||||
|
const fesa::DomainBuildResult& result,
|
||||||
|
const std::string_view code) {
|
||||||
|
return std::ranges::any_of(
|
||||||
|
result.diagnostics,
|
||||||
|
[code](const fesa::Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.code == code;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
std::size_t diagnostic_count(
|
||||||
|
const fesa::DomainBuildResult& result,
|
||||||
|
const std::string_view code) {
|
||||||
|
return static_cast<std::size_t>(std::ranges::count_if(
|
||||||
|
result.diagnostics,
|
||||||
|
[code](const fesa::Diagnostic& diagnostic) {
|
||||||
|
return diagnostic.code == code;
|
||||||
|
}));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainBuilder, BuildsImmutableDomainAndDenseNodeLookups) {
|
||||||
|
auto result = std::move(make_valid_builder()).build();
|
||||||
|
|
||||||
|
ASSERT_TRUE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(result.diagnostics.empty());
|
||||||
|
|
||||||
|
const fesa::Domain& domain = *result.domain;
|
||||||
|
ASSERT_EQ(domain.nodes().size(), 2);
|
||||||
|
ASSERT_EQ(domain.beam_elements().size(), 1);
|
||||||
|
ASSERT_EQ(domain.materials().size(), 1);
|
||||||
|
ASSERT_EQ(domain.sections().size(), 1);
|
||||||
|
ASSERT_EQ(domain.node_sets().size(), 1);
|
||||||
|
ASSERT_EQ(domain.element_sets().size(), 1);
|
||||||
|
EXPECT_EQ(domain.step().name, "Load");
|
||||||
|
EXPECT_EQ(&domain.node(fesa::NodeId{1}), &domain.nodes()[1]);
|
||||||
|
EXPECT_EQ(
|
||||||
|
&domain.node(fesa::EntityOrigin{"BeamPart", "Beam-1", 2}),
|
||||||
|
&domain.nodes()[1]);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateInternalId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 3},
|
||||||
|
fesa::Vec3{3.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateMaterialId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto material = valid_material();
|
||||||
|
material.name = "Duplicate";
|
||||||
|
builder.add_material(std::move(material));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_material_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateSectionId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto section = valid_section();
|
||||||
|
section.name = "Duplicate";
|
||||||
|
builder.add_section(std::move(section));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_section_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateElementId) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto element = valid_element();
|
||||||
|
element.origin.local_label = 2;
|
||||||
|
builder.add_beam_element(std::move(element));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_id"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateOriginWithinEntityKind) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{2},
|
||||||
|
fesa::EntityOrigin{"OtherPart", "Beam-1", 2},
|
||||||
|
fesa::Vec3{3.0, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_node_origin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsDuplicateElementOriginWithinEntityKind) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
auto element = valid_element();
|
||||||
|
element.id = fesa::ElementId{1};
|
||||||
|
element.origin.part_name = "OtherPart";
|
||||||
|
builder.add_beam_element(std::move(element));
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.duplicate_element_origin"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsMissingReferencesAcrossSemanticEntities) {
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_beam_element({
|
||||||
|
fesa::ElementId{1},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 2},
|
||||||
|
{fesa::NodeId{90}, fesa::NodeId{91}},
|
||||||
|
fesa::MaterialId{92},
|
||||||
|
fesa::SectionId{93},
|
||||||
|
});
|
||||||
|
builder.add_node_set({"MissingNodes", {fesa::NodeId{94}}});
|
||||||
|
builder.add_element_set({"MissingElements", {fesa::ElementId{95}}});
|
||||||
|
builder.set_step({
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{96}, 1, 0.0}},
|
||||||
|
{{fesa::NodeId{97}, {1.0, 0.0, 0.0, 0.0, 0.0, 0.0}}},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_node_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_element_reference"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonpositiveYoungsModulus) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = 0.0;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsPoissonRatioOutsideOpenPhysicalRange) {
|
||||||
|
for (const double poisson : {-1.0, 0.5}) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.poisson = poisson;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value()) << "poisson=" << poisson;
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"))
|
||||||
|
<< "poisson=" << poisson;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
struct InvalidSectionProperty final {
|
||||||
|
const char* name;
|
||||||
|
double fesa::BeamSection::*member;
|
||||||
|
};
|
||||||
|
|
||||||
|
class DomainValidationInvalidSection
|
||||||
|
: public testing::TestWithParam<InvalidSectionProperty> {};
|
||||||
|
|
||||||
|
TEST_P(DomainValidationInvalidSection, RejectsNonpositiveProperty) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.*(GetParam().member) = 0.0;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
|
||||||
|
}
|
||||||
|
|
||||||
|
INSTANTIATE_TEST_SUITE_P(
|
||||||
|
SectionProperties,
|
||||||
|
DomainValidationInvalidSection,
|
||||||
|
testing::Values(
|
||||||
|
InvalidSectionProperty{"Area", &fesa::BeamSection::area},
|
||||||
|
InvalidSectionProperty{"Iy", &fesa::BeamSection::iy},
|
||||||
|
InvalidSectionProperty{"Iz", &fesa::BeamSection::iz},
|
||||||
|
InvalidSectionProperty{"TorsionJ", &fesa::BeamSection::torsion_j},
|
||||||
|
InvalidSectionProperty{
|
||||||
|
"ShearAreaY", &fesa::BeamSection::shear_area_y},
|
||||||
|
InvalidSectionProperty{
|
||||||
|
"ShearAreaZ", &fesa::BeamSection::shear_area_z}),
|
||||||
|
[](const testing::TestParamInfo<InvalidSectionProperty>& info) {
|
||||||
|
return info.param.name;
|
||||||
|
});
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteNodeCoordinate) {
|
||||||
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
auto builder = make_valid_builder();
|
||||||
|
builder.add_node({
|
||||||
|
fesa::NodeId{2},
|
||||||
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 3},
|
||||||
|
fesa::Vec3{nan, 0.0, 0.0},
|
||||||
|
});
|
||||||
|
|
||||||
|
const auto result = std::move(builder).build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteMaterialConstant) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfinitePoissonRatio) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.poisson = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteSectionProperty) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.area = std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteSectionOrientation) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.orientation.y = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteRecoveryPoint) {
|
||||||
|
auto section = valid_section();
|
||||||
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
section.recovery_points = {{nan, 0.0}};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfinitePrescribedValue) {
|
||||||
|
auto step = valid_step();
|
||||||
|
step.prescribed_dofs[0].value =
|
||||||
|
std::numeric_limits<double>::infinity();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsNonfiniteLoadComponent) {
|
||||||
|
auto step = valid_step();
|
||||||
|
step.nodal_loads[0].values[0] =
|
||||||
|
std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsIndependentMaterialPropertyDiagnostics) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
material.poisson = 0.5;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsIndependentSectionPropertyDiagnostics) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.area = -1.0;
|
||||||
|
section.iy = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.nonfinite_value"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsNonfiniteStepValuesIndependently) {
|
||||||
|
const double nan = std::numeric_limits<double>::quiet_NaN();
|
||||||
|
const double infinity = std::numeric_limits<double>::infinity();
|
||||||
|
auto step = fesa::StepDefinition{
|
||||||
|
"Load",
|
||||||
|
{{fesa::NodeId{0}, 1, infinity}},
|
||||||
|
{{fesa::NodeId{1}, {nan, 0.0, 0.0, 0.0, 0.0, 0.0}}},
|
||||||
|
};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_EQ(diagnostic_count(result, "model.nonfinite_value"), 2);
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsZeroLengthElement) {
|
||||||
|
auto element = valid_element();
|
||||||
|
element.nodes[1] = fesa::NodeId{0};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
element,
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.zero_length_element"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsZeroAndElementParallelOrientation) {
|
||||||
|
for (const fesa::Vec3 orientation :
|
||||||
|
{fesa::Vec3{0.0, 0.0, 0.0}, fesa::Vec3{1.0, 0.0, 0.0}}) {
|
||||||
|
auto section = valid_section();
|
||||||
|
section.orientation = orientation;
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsElementWithoutMaterialAndSectionAssignments) {
|
||||||
|
auto element = valid_element();
|
||||||
|
element.material = fesa::MaterialId{8};
|
||||||
|
element.section = fesa::SectionId{9};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
element,
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_material_reference"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.missing_section_reference"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, RejectsConflictingBoundaryConditions) {
|
||||||
|
auto step = valid_step();
|
||||||
|
step.prescribed_dofs.push_back({fesa::NodeId{0}, 1, 0.25});
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
valid_material(),
|
||||||
|
valid_section(),
|
||||||
|
valid_element(),
|
||||||
|
step))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(
|
||||||
|
has_diagnostic(result, "model.conflicting_boundary_condition"));
|
||||||
|
}
|
||||||
|
|
||||||
|
TEST(DomainValidation, CollectsIndependentDiagnosticsWithoutEarlyExit) {
|
||||||
|
auto material = valid_material();
|
||||||
|
material.young = -1.0;
|
||||||
|
auto section = valid_section();
|
||||||
|
section.area = -1.0;
|
||||||
|
section.orientation = fesa::Vec3{1.0, 0.0, 0.0};
|
||||||
|
|
||||||
|
const auto result = std::move(make_builder(
|
||||||
|
material,
|
||||||
|
section,
|
||||||
|
valid_element(),
|
||||||
|
valid_step()))
|
||||||
|
.build();
|
||||||
|
|
||||||
|
EXPECT_FALSE(result.domain.has_value());
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_material"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_section"));
|
||||||
|
EXPECT_TRUE(has_diagnostic(result, "model.invalid_orientation"));
|
||||||
|
}
|
||||||
|
|
||||||
|
} // namespace
|
||||||
@@ -1,10 +1,8 @@
|
|||||||
#include <fesa/model/domain.hpp>
|
#include <fesa/model/domain_builder.hpp>
|
||||||
|
|
||||||
#include <span>
|
#include <span>
|
||||||
#include <type_traits>
|
#include <type_traits>
|
||||||
#include <utility>
|
#include <utility>
|
||||||
#include <vector>
|
|
||||||
|
|
||||||
#include <gtest/gtest.h>
|
#include <gtest/gtest.h>
|
||||||
|
|
||||||
namespace {
|
namespace {
|
||||||
@@ -31,15 +29,12 @@ TEST(EntityOrigin, FindsHierarchicalNodeByCompositeOrigin) {
|
|||||||
fesa::EntityOrigin{"BeamPart", "Beam-1", 101},
|
fesa::EntityOrigin{"BeamPart", "Beam-1", 101},
|
||||||
fesa::Vec3{1.0, 2.0, 3.0},
|
fesa::Vec3{1.0, 2.0, 3.0},
|
||||||
};
|
};
|
||||||
const fesa::Domain domain{
|
fesa::DomainBuilder builder;
|
||||||
std::vector<fesa::Node>{node},
|
builder.add_node(node);
|
||||||
{},
|
builder.set_step(fesa::StepDefinition{"Load", {}, {}});
|
||||||
{},
|
auto result = std::move(builder).build();
|
||||||
{},
|
ASSERT_TRUE(result.domain.has_value());
|
||||||
{},
|
const fesa::Domain& domain = *result.domain;
|
||||||
{},
|
|
||||||
fesa::StepDefinition{"Load", {}, {}},
|
|
||||||
};
|
|
||||||
|
|
||||||
const fesa::EntityOrigin lookup{"BeamPart", "Beam-1", 101};
|
const fesa::EntityOrigin lookup{"BeamPart", "Beam-1", 101};
|
||||||
const fesa::Node& found_by_origin = domain.node(lookup);
|
const fesa::Node& found_by_origin = domain.node(lookup);
|
||||||
|
|||||||
Reference in New Issue
Block a user