feat(domain-and-input-skeleton): step 1 — domain-validation

This commit is contained in:
KOKO\Mimi
2026-07-30 17:08:07 +09:00
parent 96f3f3230e
commit 5a5af9c64b
8 changed files with 1202 additions and 50 deletions
+62
View File
@@ -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
+477
View File
@@ -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