feat(solid): add Hex8 input and three-DOF semantics

This commit is contained in:
KOKO\Mimi
2026-10-09 01:12:30 +09:00
parent df0388ae2f
commit 9acf18db4b
16 changed files with 1468 additions and 98 deletions
+244 -11
View File
@@ -1,10 +1,13 @@
#include "fesa/model/domain.h"
#include <cstddef>
#include <algorithm>
#include <array>
#include <cmath>
#include <cstddef>
#include <functional>
#include <map>
#include <memory>
#include <set>
#include <string>
#include <utility>
@@ -48,12 +51,14 @@ Status ValidateThermalTargets(
Status ValidateThermalDefinition(const ModelDefinition& definition) {
const bool has_structural = !definition.elements.empty() ||
!definition.shell_elements.empty();
const bool has_thermal_data = !definition.thermal_materials.empty() ||
!definition.thermal_sections.empty() ||
definition.thermal_step.has_value() ||
!definition.thermal_initial_conditions.empty() ||
!definition.thermal_model_boundaries.empty();
!definition.shell_elements.empty() ||
!definition.solid_elements.empty();
const bool has_thermal_data =
!definition.thermal_materials.empty() ||
!definition.thermal_sections.empty() ||
definition.thermal_step.has_value() ||
!definition.thermal_initial_conditions.empty() ||
!definition.thermal_model_boundaries.empty();
if (has_structural && !definition.thermal_elements.empty()) {
return InvalidThermalDefinition({}, "DS4",
"Structural and DS4 elements cannot mix.");
@@ -161,6 +166,180 @@ Status ValidateThermalDefinition(const ModelDefinition& definition) {
return Status::Ok();
}
/// @brief Checks Solid semantic candidates before immutable ownership starts.
Status ValidateSolidDefinition(ModelDefinition& definition) {
const auto fail = [](const SourceLocation& location,
const std::string& identity,
const std::string& message) {
return Status::Failure(FailureCategory::kModel,
{{Severity::kError, "invalid-solid-definition",
location, "SOLID", identity, message}});
};
if (definition.solid_elements.empty()) {
if (!definition.solid_sections.empty() || !definition.amplitudes.empty())
return fail({}, "C3D8", "Solid definitions require a C3D8 model.");
return Status::Ok();
}
if (!definition.elements.empty() || !definition.shell_elements.empty() ||
!definition.thermal_elements.empty() || definition.thermal_step ||
!definition.sections.empty() || !definition.shell_sections.empty() ||
!definition.element_surfaces.empty()) {
return fail({}, "C3D8",
"Solid models cannot mix element or section families.");
}
if (definition.steps.size() + (definition.frequency_step ? 1U : 0U) +
(definition.dynamic_step ? 1U : 0U) !=
1U) {
return fail({}, "STEP", "Solid models require exactly one procedure.");
}
std::vector<bool> active(definition.nodes.size(), false);
std::set<std::pair<std::string, std::int64_t>> node_ids;
for (const auto& node : definition.nodes) {
if (!node_ids
.emplace(node.source_id.instance_name, node.source_id.source_label)
.second)
return fail(node.location, node.source_id.source_label_text,
"Node identity is duplicated.");
for (const double coordinate : node.coordinates)
if (!std::isfinite(coordinate))
return fail(node.location, node.source_id.source_label_text,
"Solid coordinates must be finite.");
}
std::set<std::pair<std::string, std::int64_t>> element_ids;
std::set<EntityIndex> active_materials;
for (const auto& element : definition.solid_elements) {
if (!element_ids
.emplace(element.source_id.instance_name,
element.source_id.source_label)
.second ||
element.section_index >= definition.solid_sections.size() ||
element.material_index >= definition.materials.size()) {
return fail(
element.location, element.source_id.source_label_text,
"Solid identity, section, and material must resolve uniquely.");
}
if (definition.solid_sections[element.section_index].MaterialIndex() !=
element.material_index)
return fail(element.location, element.source_id.source_label_text,
"Solid material assignment conflicts.");
active_materials.insert(element.material_index);
std::set<EntityIndex> distinct;
for (const auto node : element.node_indices) {
if (node >= active.size() || !distinct.insert(node).second)
return fail(element.location, element.source_id.source_label_text,
"Solid connectivity requires eight distinct active nodes.");
active[node] = true;
}
}
for (std::size_t node = 0U; node < active.size(); ++node)
if (!active[node])
return fail(definition.nodes[node].location,
definition.nodes[node].source_id.source_label_text,
"Solid nodes must be connected.");
for (const auto& section : definition.solid_sections)
if (section.MaterialIndex() >= definition.materials.size() ||
section.Name().empty())
return fail(section.Location(), section.Name(),
"Solid material assignment is invalid.");
for (const auto index : active_materials) {
const auto& material = definition.materials[index];
const double young = material.YoungsModulus();
const double nu = material.PoissonsRatio();
const double mu = young / (2.0 * (1.0 + nu));
const double lambda = young * nu / ((1.0 + nu) * (1.0 - 2.0 * nu));
if (!std::isfinite(young) || !(young > 0.0) || !std::isfinite(nu) ||
!(nu > -1.0 && nu < 0.5) || !std::isfinite(mu) ||
!std::isfinite(lambda))
return fail(material.Location(), material.Name(),
"Solid isotropic constants are invalid.");
}
std::set<EntityIndex> density_materials;
for (const auto& density : definition.densities) {
if (density.material_index >= definition.materials.size() ||
!std::isfinite(density.rho) || !(density.rho > 0.0) ||
!density_materials.insert(density.material_index).second)
return fail(density.location, "DENSITY",
"Solid density must be finite positive and unique.");
}
if (definition.dynamic_step || definition.frequency_step)
for (const auto material : active_materials)
if (density_materials.count(material) == 0U)
return fail({}, "DENSITY",
"Solid frequency and dynamic materials require density.");
std::set<std::string> amplitude_names;
for (const auto& amplitude : definition.amplitudes) {
if (!definition.dynamic_step || amplitude.name_raw.empty() ||
amplitude.name_canonical.empty() ||
!amplitude_names.insert(amplitude.name_canonical).second ||
amplitude.points.empty())
return fail(amplitude.location, amplitude.name_raw,
"Solid amplitude identity is invalid.");
double previous = -1.0;
for (const auto& point : amplitude.points) {
if (!std::isfinite(point[0]) || !std::isfinite(point[1]) ||
point[0] < 0.0 || !(point[0] > previous))
return fail(amplitude.location, amplitude.name_raw,
"Solid amplitude pairs are invalid.");
previous = point[0];
}
}
const auto validate_boundaries = [&fail](const auto& boundaries,
bool homogeneous) {
for (const auto& boundary : boundaries) {
if (boundary.first_dof < 1 || boundary.last_dof < boundary.first_dof ||
boundary.last_dof > 3 || !std::isfinite(boundary.value) ||
(homogeneous && boundary.value != 0.0))
return fail(boundary.location, boundary.target,
"Solid physical boundaries require DOF1..3.");
}
return Status::Ok();
};
const auto validate_loads = [&fail, &definition](const auto& loads,
bool dynamic) {
for (const auto& load : loads) {
if (load.dof < 1 || load.dof > 3 || !std::isfinite(load.magnitude) ||
(load.amplitude_index &&
(!dynamic || *load.amplitude_index >= definition.amplitudes.size())))
return fail(load.location, load.target,
"Solid load component or amplitude is invalid.");
}
return Status::Ok();
};
if (definition.frequency_step)
return validate_boundaries(definition.frequency_step->boundaries, true);
if (definition.dynamic_step) {
const auto& dynamic = *definition.dynamic_step;
if (!std::isfinite(dynamic.initial_increment) ||
!(dynamic.initial_increment > 0.0) ||
!std::isfinite(dynamic.time_period) || !(dynamic.time_period > 0.0) ||
dynamic.increment_count == 0U ||
dynamic.increment_count > dynamic.maximum_increments ||
dynamic.initial_increment *
static_cast<double>(dynamic.increment_count) !=
dynamic.time_period ||
dynamic.alpha != -0.05 || dynamic.beta != 0.275625 ||
dynamic.gamma != 0.55)
return fail(dynamic.data_location, dynamic.source_step_name_raw,
"Solid dynamic controls are invalid.");
const auto status = validate_boundaries(dynamic.boundaries, true);
if (!status.IsOk()) return status;
return validate_loads(dynamic.loads, true);
}
const auto& step = definition.steps[0];
if (step.direct &&
(!std::isfinite(step.initial_increment) ||
!(step.initial_increment > 0.0) || !std::isfinite(step.time_period) ||
!(step.time_period > 0.0) || step.increment_count == 0U ||
step.increment_count > step.maximum_increments ||
step.initial_increment * static_cast<double>(step.increment_count) !=
step.time_period))
return fail(step.location, step.name,
"Solid static fixed schedule is invalid.");
const auto status = validate_boundaries(step.boundaries, false);
if (!status.IsOk()) return status;
return validate_loads(step.loads, false);
}
} // namespace
StepDefinition::StepDefinition(StaticStepDefinition definition)
@@ -192,12 +371,17 @@ StepDefinition::StepDefinition(StaticStepDefinition definition)
auto& load = definition.loads[source_order];
auto owned = std::make_unique<ConcentratedNodalLoad>(
SourceTargetQuery{SourceEntityKind::kNode, "", load.target}, load.dof,
load.magnitude, source_order, std::move(load.location));
load.magnitude, source_order, std::move(load.location),
load.amplitude_index, std::move(load.amplitude_name_raw),
std::move(load.amplitude_location));
loads_view_.push_back(std::cref(*owned));
concentrated_loads_view_.Add(*owned);
loads_.push_back(std::move(owned));
}
surface_pressure_loads_ = std::move(definition.surface_pressure_loads);
direct_static_ = definition.direct;
increment_count_ = definition.increment_count;
maximum_increments_ = definition.maximum_increments;
}
StepDefinition::StepDefinition(FrequencyStepDefinition definition)
@@ -227,8 +411,10 @@ StepDefinition::StepDefinition(FrequencyStepDefinition definition)
StepDefinition::StepDefinition(DynamicStepDefinition definition)
: name_{definition.source_step_name_raw},
procedure_kind_{StaticProcedureKind::kNonlinearFullNewton},
nlgeom_{true},
procedure_kind_{definition.nlgeom
? StaticProcedureKind::kNonlinearFullNewton
: StaticProcedureKind::kLinearSmallDisplacement},
nlgeom_{definition.nlgeom},
initial_increment_{definition.initial_increment},
time_period_{definition.time_period},
minimum_increment_{definition.initial_increment},
@@ -255,7 +441,9 @@ StepDefinition::StepDefinition(DynamicStepDefinition definition)
auto& load = dynamic_->loads[source_order];
auto owned = std::make_unique<ConcentratedNodalLoad>(
SourceTargetQuery{SourceEntityKind::kNode, "", load.target}, load.dof,
load.magnitude, source_order, std::move(load.location));
load.magnitude, source_order, std::move(load.location),
load.amplitude_index, std::move(load.amplitude_name_raw),
std::move(load.amplitude_location));
loads_view_.push_back(std::cref(*owned));
concentrated_loads_view_.Add(*owned);
loads_.push_back(std::move(owned));
@@ -325,11 +513,21 @@ double StepDefinition::MaximumIncrement() const noexcept {
return maximum_increment_;
}
bool StepDefinition::IsDirectStatic() const noexcept { return direct_static_; }
std::uint32_t StepDefinition::IncrementCount() const noexcept {
return increment_count_;
}
std::uint32_t StepDefinition::MaximumIncrements() const noexcept {
return maximum_increments_;
}
const SourceLocation& StepDefinition::Location() const noexcept {
return location_;
}
Result<Domain> Domain::Create(ModelDefinition definition) {
const Status solid_status = ValidateSolidDefinition(definition);
if (!solid_status.IsOk()) return Result<Domain>::Failure(solid_status);
const Status thermal_status = ValidateThermalDefinition(definition);
if (!thermal_status.IsOk()) return Result<Domain>::Failure(thermal_status);
return Result<Domain>::Success(Domain{std::move(definition)});
@@ -354,6 +552,23 @@ const DomainCollectionView<Mitc4ShellDefinition>& Domain::ShellElements()
return shell_elements_view_;
}
const DomainCollectionView<Hex8Definition>& Domain::SolidElements()
const noexcept {
return solid_elements_view_;
}
const DomainCollectionView<SolidSection>& Domain::SolidSections()
const noexcept {
return solid_sections_view_;
}
bool Domain::IsSolid() const noexcept {
return !solid_elements_view_.Empty() &&
solid_elements_view_.Size() == elements_view_.Size();
}
const std::vector<TabularAmplitudeDefinition>& Domain::Amplitudes()
const noexcept {
return definition_.amplitudes;
}
const DomainCollectionView<Material>& Domain::Materials() const noexcept {
return materials_view_;
}
@@ -496,6 +711,14 @@ Domain::Domain(ModelDefinition definition)
element_properties_.push_back(std::move(owned));
}
definition_.shell_sections.clear();
const std::size_t solid_property_offset = element_properties_.size();
for (auto& section : definition_.solid_sections) {
auto owned = std::make_unique<SolidSection>(std::move(section));
properties_view_.Add(*owned);
solid_sections_view_.Add(*owned);
element_properties_.push_back(std::move(owned));
}
definition_.solid_sections.clear();
element_definitions_.reserve(definition_.elements.size() +
definition_.shell_elements.size());
@@ -517,6 +740,16 @@ Domain::Domain(ModelDefinition definition)
element_definitions_.push_back(std::move(owned));
}
definition_.shell_elements.clear();
for (auto& element : definition_.solid_elements) {
element.SynchronizeNodeIndices();
element.SetPropertyIndex(static_cast<EntityIndex>(solid_property_offset +
element.section_index));
auto owned = std::make_unique<Hex8Definition>(std::move(element));
elements_view_.Add(*owned);
solid_elements_view_.Add(*owned);
element_definitions_.push_back(std::move(owned));
}
definition_.solid_elements.clear();
for (auto& element : definition_.thermal_elements) {
const EntityIndex material_index = static_cast<EntityIndex>(
structural_material_count +