#include "fesa/model/domain.h" #include #include #include #include #include #include #include namespace fesa { namespace { Status InvalidThermalDefinition(const SourceLocation& location, const std::string& identity, const std::string& message) { return Status::Failure( FailureCategory::kModel, {{Severity::kError, "invalid-thermal-definition", location, "THERMAL", identity, message}}); } bool IsThermalDof(const int dof) { return dof == 11 || dof == 15; } Status ValidateThermalTargets( const std::vector& boundaries, const std::vector& active_nodes, const char* identity) { std::map, double> values; for (const auto& boundary : boundaries) { if (boundary.node_index >= active_nodes.size() || !active_nodes[boundary.node_index] || !IsThermalDof(boundary.dof) || !std::isfinite(boundary.value)) { return InvalidThermalDefinition(boundary.location, identity, "Thermal target is invalid or inactive."); } const auto inserted = values.emplace( std::make_pair(boundary.node_index, boundary.dof), boundary.value); if (!inserted.second && inserted.first->second != boundary.value) { return InvalidThermalDefinition( boundary.location, identity, "Conflicting thermal boundary values target one node and DOF."); } } return Status::Ok(); } 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(); if (has_structural && !definition.thermal_elements.empty()) { return InvalidThermalDefinition({}, "DS4", "Structural and DS4 elements cannot mix."); } if (definition.thermal_elements.empty()) { if (has_thermal_data) { return InvalidThermalDefinition({}, "DS4", "Thermal definitions require a DS4 element."); } return Status::Ok(); } if (!definition.thermal_step) { return InvalidThermalDefinition({}, "DS4", "A DS4 model requires one thermal step."); } std::map material_identities; for (const auto& material : definition.thermal_materials) { const std::string identity = material.source_id.instance_name + ":" + material.source_id.source_label_text; if (!material_identities.emplace(identity, true).second) { return InvalidThermalDefinition(material.location, identity, "Thermal material identity is duplicated."); } const auto candidate = IsotropicThermalMaterial::Create( material.source_id, material.name, material.conductivity, material.density, material.specific_heat, material.location); if (!candidate.HasValue()) return candidate.GetStatus(); } std::map section_identities; for (const auto& section : definition.thermal_sections) { const std::string identity = section.source_id.instance_name + ":" + section.source_id.source_label_text; if (section.source_id.source_label_text.empty() || section.name.empty() || !std::isfinite(section.thickness) || !(section.thickness > 0.0) || section.material_index >= definition.thermal_materials.size() || !section_identities.emplace(identity, true).second) { return InvalidThermalDefinition(section.location, section.name, "Thermal section is invalid."); } } std::vector active_nodes(definition.nodes.size(), false); std::map identities; for (const auto& element : definition.thermal_elements) { const std::string identity = element.source_id.instance_name + ":" + element.source_id.source_label_text; if (element.source_id.source_label_text.empty() || !identities.emplace(identity, true).second || element.section_index >= definition.thermal_sections.size()) { return InvalidThermalDefinition(element.location, identity, "DS4 section assignment is invalid."); } for (const EntityIndex node : element.node_indices) { if (node >= active_nodes.size()) { return InvalidThermalDefinition(element.location, identity, "DS4 node target is invalid."); } active_nodes[node] = true; } } std::map> initial_values; for (const auto& initial : definition.thermal_initial_conditions) { if (initial.node_index >= active_nodes.size() || !active_nodes[initial.node_index]) { return InvalidThermalDefinition(initial.location, "INITIAL CONDITIONS", "Thermal initial target is inactive."); } for (const double value : initial.station_values) { if (!std::isfinite(value) || value != initial.station_values[0U]) { return InvalidThermalDefinition( initial.location, "INITIAL CONDITIONS", "Initial temperatures must be finite and equal at five stations."); } } const auto inserted = initial_values.emplace(initial.node_index, initial.station_values); if (!inserted.second && inserted.first->second != initial.station_values) { return InvalidThermalDefinition(initial.location, "INITIAL CONDITIONS", "Conflicting initial temperatures target " "one node."); } } Status model_status = ValidateThermalTargets( definition.thermal_model_boundaries, active_nodes, "BOUNDARY"); if (!model_status.IsOk()) return model_status; const auto& step = *definition.thermal_step; if (!std::isfinite(step.nominal_increment) || !(step.nominal_increment > 0.0) || !std::isfinite(step.time_period) || !(step.time_period > 0.0) || step.maximum_increments == 0U || step.nlgeom) { return InvalidThermalDefinition(step.location, step.name, "Thermal step controls are invalid."); } Status step_status = ValidateThermalTargets(step.boundaries, active_nodes, "STEP BOUNDARY"); if (!step_status.IsOk()) return step_status; for (const auto& load : step.loads) { if (load.node_index >= active_nodes.size() || !active_nodes[load.node_index] || !IsThermalDof(load.dof) || !std::isfinite(load.power)) { return InvalidThermalDefinition(load.location, "CFLUX", "Thermal flux target is invalid or inactive."); } } return Status::Ok(); } } // namespace StepDefinition::StepDefinition(StaticStepDefinition definition) : name_{std::move(definition.name)}, procedure_kind_{definition.procedure_kind}, nlgeom_{definition.nlgeom}, initial_increment_{definition.initial_increment}, time_period_{definition.time_period}, minimum_increment_{definition.minimum_increment}, maximum_increment_{definition.maximum_increment}, location_{std::move(definition.location)} { for (auto& boundary : definition.boundaries) { for (int source_dof = boundary.first_dof; source_dof <= boundary.last_dof; ++source_dof) { const std::size_t source_order = boundary_conditions_.size(); auto owned = std::make_unique( SourceTargetQuery{SourceEntityKind::kNode, "", boundary.target}, static_cast(source_dof - 1), boundary.value, source_order, boundary.location); boundary_conditions_view_.push_back(std::cref(*owned)); prescribed_displacements_view_.Add(*owned); boundary_conditions_.push_back(std::move(owned)); } } loads_.reserve(definition.loads.size()); loads_view_.reserve(definition.loads.size()); for (std::size_t source_order = 0U; source_order < definition.loads.size(); ++source_order) { auto& load = definition.loads[source_order]; auto owned = std::make_unique( SourceTargetQuery{SourceEntityKind::kNode, "", load.target}, load.dof, load.magnitude, source_order, std::move(load.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); } StepDefinition::StepDefinition(FrequencyStepDefinition definition) : name_{definition.source_step_name_raw}, procedure_kind_{StaticProcedureKind::kLinearSmallDisplacement}, nlgeom_{false}, initial_increment_{0.0}, time_period_{0.0}, minimum_increment_{0.0}, maximum_increment_{0.0}, location_{definition.step_location}, frequency_{std::move(definition)} { for (const auto& boundary : frequency_->boundaries) { for (int source_dof = boundary.first_dof; source_dof <= boundary.last_dof; ++source_dof) { const std::size_t source_order = boundary_conditions_.size(); auto owned = std::make_unique( SourceTargetQuery{SourceEntityKind::kNode, "", boundary.target}, static_cast(source_dof - 1), boundary.value, source_order, boundary.location); boundary_conditions_view_.push_back(std::cref(*owned)); prescribed_displacements_view_.Add(*owned); boundary_conditions_.push_back(std::move(owned)); } } } StepDefinition::StepDefinition(DynamicStepDefinition definition) : name_{definition.source_step_name_raw}, procedure_kind_{StaticProcedureKind::kNonlinearFullNewton}, nlgeom_{true}, initial_increment_{definition.initial_increment}, time_period_{definition.time_period}, minimum_increment_{definition.initial_increment}, maximum_increment_{definition.initial_increment}, location_{definition.step_location}, dynamic_{std::move(definition)} { for (const auto& boundary : dynamic_->boundaries) { for (int source_dof = boundary.first_dof; source_dof <= boundary.last_dof; ++source_dof) { const std::size_t source_order = boundary_conditions_.size(); auto owned = std::make_unique( SourceTargetQuery{SourceEntityKind::kNode, "", boundary.target}, static_cast(source_dof - 1), boundary.value, source_order, boundary.location); boundary_conditions_view_.push_back(std::cref(*owned)); prescribed_displacements_view_.Add(*owned); boundary_conditions_.push_back(std::move(owned)); } } loads_.reserve(dynamic_->loads.size()); loads_view_.reserve(dynamic_->loads.size()); for (std::size_t source_order = 0U; source_order < dynamic_->loads.size(); ++source_order) { auto& load = dynamic_->loads[source_order]; auto owned = std::make_unique( SourceTargetQuery{SourceEntityKind::kNode, "", load.target}, load.dof, load.magnitude, source_order, std::move(load.location)); loads_view_.push_back(std::cref(*owned)); concentrated_loads_view_.Add(*owned); loads_.push_back(std::move(owned)); } } const std::string& StepDefinition::Name() const noexcept { return name_; } const BoundaryConditionView& StepDefinition::BoundaryConditions() const noexcept { return boundary_conditions_view_; } const DomainCollectionView& StepDefinition::PrescribedDisplacements() const noexcept { return prescribed_displacements_view_; } const LoadView& StepDefinition::Loads() const noexcept { return loads_view_; } const DomainCollectionView& StepDefinition::ConcentratedLoads() const noexcept { return concentrated_loads_view_; } const std::vector& StepDefinition::SurfacePressureLoads() const noexcept { return surface_pressure_loads_; } StaticProcedureKind StepDefinition::ProcedureKind() const noexcept { return procedure_kind_; } bool StepDefinition::IsNonlinearStatic() const noexcept { return !dynamic_ && procedure_kind_ == StaticProcedureKind::kNonlinearFullNewton; } bool StepDefinition::IsFrequency() const noexcept { return frequency_.has_value(); } bool StepDefinition::IsDynamic() const noexcept { return dynamic_.has_value(); } const FrequencyStepDefinition* StepDefinition::Frequency() const noexcept { return frequency_ ? &*frequency_ : nullptr; } const DynamicStepDefinition* StepDefinition::Dynamic() const noexcept { return dynamic_ ? &*dynamic_ : nullptr; } bool StepDefinition::UsesNonlinearGeometry() const noexcept { return nlgeom_; } double StepDefinition::InitialIncrement() const noexcept { return initial_increment_; } double StepDefinition::TimePeriod() const noexcept { return time_period_; } double StepDefinition::MinimumIncrement() const noexcept { return minimum_increment_; } double StepDefinition::MaximumIncrement() const noexcept { return maximum_increment_; } const SourceLocation& StepDefinition::Location() const noexcept { return location_; } Result Domain::Create(ModelDefinition definition) { const Status thermal_status = ValidateThermalDefinition(definition); if (!thermal_status.IsOk()) return Result::Failure(thermal_status); return Result::Success(Domain{std::move(definition)}); } const std::vector& Domain::Nodes() const noexcept { return definition_.nodes; } const DomainCollectionView& Domain::Elements() const noexcept { return elements_view_; } const DomainCollectionView& Domain::BeamElements() const noexcept { return beam_elements_view_; } const DomainCollectionView& Domain::ShellElements() const noexcept { return shell_elements_view_; } const DomainCollectionView& Domain::Materials() const noexcept { return materials_view_; } const DomainCollectionView& Domain::LinearElasticMaterials() const noexcept { return linear_materials_view_; } const DomainCollectionView& Domain::ThermalMaterials() const noexcept { return thermal_materials_view_; } const DomainCollectionView& Domain::ThermalElements() const noexcept { return thermal_elements_view_; } const std::vector& Domain::ThermalSections() const noexcept { return definition_.thermal_sections; } const std::vector& Domain::ThermalInitialConditions() const noexcept { return definition_.thermal_initial_conditions; } const std::vector& Domain::ThermalModelBoundaries() const noexcept { return definition_.thermal_model_boundaries; } const ThermalStepDefinition* Domain::ThermalStep() const noexcept { return definition_.thermal_step ? &*definition_.thermal_step : nullptr; } const DomainCollectionView& Domain::Properties() const noexcept { return properties_view_; } const DomainCollectionView& Domain::Sections() const noexcept { return sections_view_; } const DomainCollectionView& Domain::ShellSections() const noexcept { return shell_sections_view_; } const std::vector& Domain::ShellNodeInitialFrames() const noexcept { return definition_.shell_node_initial_frames; } const std::vector& Domain::NodeSets() const noexcept { return definition_.node_sets; } const std::vector& Domain::ElementSets() const noexcept { return definition_.element_sets; } const std::vector& Domain::ElementSurfaces() const noexcept { return definition_.element_surfaces; } const DomainCollectionView& Domain::Steps() const noexcept { return steps_view_; } const std::vector& Domain::Densities() const noexcept { return definition_.densities; } const FrequencyStepDefinition* Domain::Frequency() const noexcept { return steps_view_.Size() == 1U ? steps_view_[0U].Frequency() : nullptr; } const DynamicStepDefinition* Domain::Dynamic() const noexcept { return steps_view_.Size() == 1U ? steps_view_[0U].Dynamic() : nullptr; } const std::vector& Domain::Warnings() const noexcept { return definition_.warnings; } const std::filesystem::path& Domain::SourcePath() const noexcept { return definition_.source_path; } const std::string& Domain::SourceContentIdentity() const noexcept { return definition_.source_content_identity; } Domain::Domain(ModelDefinition definition) : definition_{std::move(definition)} { materials_.reserve(definition_.materials.size()); for (auto& material : definition_.materials) { auto owned = std::make_unique(std::move(material)); materials_view_.Add(*owned); linear_materials_view_.Add(*owned); materials_.push_back(std::move(owned)); } definition_.materials.clear(); const std::size_t structural_material_count = materials_.size(); materials_.reserve(materials_.size() + definition_.thermal_materials.size()); for (const auto& material : definition_.thermal_materials) { const auto candidate = IsotropicThermalMaterial::Create( material.source_id, material.name, material.conductivity, material.density, material.specific_heat, material.location); auto owned = std::make_unique( std::move(candidate.Value())); materials_view_.Add(*owned); thermal_materials_view_.Add(*owned); materials_.push_back(std::move(owned)); } definition_.thermal_materials.clear(); element_properties_.reserve(definition_.sections.size() + definition_.shell_sections.size()); for (auto& section : definition_.sections) { auto owned = std::make_unique(std::move(section)); properties_view_.Add(*owned); sections_view_.Add(*owned); element_properties_.push_back(std::move(owned)); } const std::size_t beam_property_count = element_properties_.size(); definition_.sections.clear(); for (auto& section : definition_.shell_sections) { auto owned = std::make_unique(std::move(section)); properties_view_.Add(*owned); shell_sections_view_.Add(*owned); element_properties_.push_back(std::move(owned)); } definition_.shell_sections.clear(); element_definitions_.reserve(definition_.elements.size() + definition_.shell_elements.size()); for (auto& element : definition_.elements) { element.SynchronizeNodeIndices(); auto owned = std::make_unique(std::move(element)); elements_view_.Add(*owned); beam_elements_view_.Add(*owned); element_definitions_.push_back(std::move(owned)); } definition_.elements.clear(); for (auto& element : definition_.shell_elements) { element.SynchronizeNodeIndices(); element.SetPropertyIndex( static_cast(beam_property_count + element.section_index)); auto owned = std::make_unique(std::move(element)); elements_view_.Add(*owned); shell_elements_view_.Add(*owned); element_definitions_.push_back(std::move(owned)); } definition_.shell_elements.clear(); for (auto& element : definition_.thermal_elements) { const EntityIndex material_index = static_cast( structural_material_count + definition_.thermal_sections[element.section_index].material_index); auto owned = std::make_unique( std::move(element.source_id), element.node_indices, element.section_index, std::move(element.location)); owned->BindMaterialIndex(material_index); elements_view_.Add(*owned); thermal_elements_view_.Add(*owned); element_definitions_.push_back(std::move(owned)); } definition_.thermal_elements.clear(); if (definition_.thermal_step) { for (std::size_t index = 0U; index < definition_.thermal_step->loads.size(); ++index) { definition_.thermal_step->loads[index].source_order = index; } } step_definitions_.reserve(definition_.steps.size() + (definition_.frequency_step ? 1U : 0U) + (definition_.dynamic_step ? 1U : 0U)); for (auto& step : definition_.steps) { auto owned = std::unique_ptr(new StepDefinition(std::move(step))); steps_view_.Add(*owned); step_definitions_.push_back(std::move(owned)); } definition_.steps.clear(); if (definition_.frequency_step) { auto owned = std::unique_ptr( new StepDefinition(std::move(*definition_.frequency_step))); steps_view_.Add(*owned); step_definitions_.push_back(std::move(owned)); } if (definition_.dynamic_step) { auto owned = std::unique_ptr( new StepDefinition(std::move(*definition_.dynamic_step))); steps_view_.Add(*owned); step_definitions_.push_back(std::move(owned)); } } } // namespace fesa