feat: add thermal semantic domain model

This commit is contained in:
KOKO\Mimi
2026-09-29 18:33:44 +09:00
parent e6fd9e9e6b
commit 254247efc1
11 changed files with 755 additions and 2 deletions
+50
View File
@@ -1,11 +1,22 @@
#include "fesa/analysis/analysis_model.h"
#include <cstddef>
#include <string>
#include <vector>
namespace fesa {
Result<AnalysisModel> AnalysisModel::Create(const Domain& domain) {
if (domain.ThermalStep() != nullptr) {
if (!domain.Steps().Empty()) {
return Result<AnalysisModel>::Failure(Status::Failure(
FailureCategory::kModel,
{{Severity::kError, "mixed-thermal-procedure",
domain.ThermalStep()->location, "STEP", domain.ThermalStep()->name,
"Thermal and structural steps cannot coexist."}}));
}
return Result<AnalysisModel>::Success(AnalysisModel{domain});
}
if (domain.Steps().Empty()) {
return Result<AnalysisModel>::Failure(
Status::Failure(FailureCategory::kInput,
@@ -33,6 +44,12 @@ const StepDefinition& AnalysisModel::Step() const noexcept {
return domain_->Steps()[0U];
}
bool AnalysisModel::IsThermal() const noexcept { return is_thermal_; }
const ThermalStepDefinition* AnalysisModel::ThermalStep() const noexcept {
return is_thermal_ ? domain_->ThermalStep() : nullptr;
}
const std::vector<EntityIndex>& AnalysisModel::ActiveElements() const noexcept {
return active_elements_;
}
@@ -65,12 +82,45 @@ const std::vector<EntityIndex>& AnalysisModel::ActiveLoads() const noexcept {
return active_loads_;
}
const std::vector<EntityIndex>& AnalysisModel::ActiveThermalElements()
const noexcept {
return active_thermal_elements_;
}
const std::vector<EntityIndex>& AnalysisModel::ActiveThermalNodes()
const noexcept {
return active_thermal_nodes_;
}
const std::vector<SurfacePressureLoadDefinition>&
AnalysisModel::ActiveSurfacePressureLoads() const noexcept {
static const std::vector<SurfacePressureLoadDefinition> kNoSurfacePressures;
if (is_thermal_) return kNoSurfacePressures;
return Step().SurfacePressureLoads();
}
AnalysisModel::AnalysisModel(const Domain& domain) : domain_{&domain} {
if (domain.ThermalStep() != nullptr) {
is_thermal_ = true;
std::vector<bool> active_nodes(domain.Nodes().size(), false);
const std::size_t first_element = domain.Elements().Size() -
domain.ThermalElements().Size();
for (std::size_t index = 0U; index < domain.ThermalElements().Size();
++index) {
active_thermal_elements_.push_back(
static_cast<EntityIndex>(first_element + index));
for (const EntityIndex node :
domain.ThermalElements()[index].NodeIndices()) {
active_nodes[node] = true;
}
}
for (std::size_t index = 0U; index < active_nodes.size(); ++index) {
if (active_nodes[index]) {
active_thermal_nodes_.push_back(static_cast<EntityIndex>(index));
}
}
return;
}
std::vector<bool> reachable_materials(domain.Materials().Size(), false);
std::vector<bool> reachable_properties(domain.Properties().Size(), false);
std::vector<bool> reachable_sections(domain.Sections().Size(), false);
@@ -0,0 +1,76 @@
#include "fesa/materials/isotropic_thermal_material.h"
#include <cmath>
#include <string>
#include <utility>
namespace fesa {
namespace {
Status InvalidThermalMaterial(const SourceEntityId& source_id,
const SourceLocation& location,
const std::string& message) {
return Status::Failure(
FailureCategory::kModel,
{{Severity::kError, "invalid-thermal-material", location, "*MATERIAL",
source_id.source_label_text, message}});
}
} // namespace
Result<IsotropicThermalMaterial> IsotropicThermalMaterial::Create(
SourceEntityId source_id, std::string name, double conductivity,
double density, double specific_heat, SourceLocation location) {
if (name.empty() || source_id.source_label_text.empty()) {
return Result<IsotropicThermalMaterial>::Failure(InvalidThermalMaterial(
source_id, location, "Material source identity must not be empty."));
}
if (!std::isfinite(conductivity) || !(conductivity > 0.0) ||
!std::isfinite(density) || !(density > 0.0) ||
!std::isfinite(specific_heat) || !(specific_heat > 0.0)) {
return Result<IsotropicThermalMaterial>::Failure(InvalidThermalMaterial(
source_id, location,
"Conductivity, density, and specific heat must be finite and positive."));
}
return Result<IsotropicThermalMaterial>::Success(IsotropicThermalMaterial{
std::move(source_id), std::move(name), conductivity, density,
specific_heat, std::move(location)});
}
MaterialKind IsotropicThermalMaterial::Kind() const noexcept {
return MaterialKind::kIsotropicThermal;
}
const SourceEntityId& IsotropicThermalMaterial::SourceId() const noexcept {
return source_id_;
}
const SourceLocation& IsotropicThermalMaterial::Location() const noexcept {
return location_;
}
const std::string& IsotropicThermalMaterial::Name() const noexcept {
return name_;
}
double IsotropicThermalMaterial::Conductivity() const noexcept {
return conductivity_;
}
double IsotropicThermalMaterial::Density() const noexcept { return density_; }
double IsotropicThermalMaterial::SpecificHeat() const noexcept {
return specific_heat_;
}
IsotropicThermalMaterial::IsotropicThermalMaterial(
SourceEntityId source_id, std::string name, double conductivity,
double density, double specific_heat, SourceLocation location)
: source_id_{std::move(source_id)},
name_{std::move(name)},
conductivity_{conductivity},
density_{density},
specific_heat_{specific_heat},
location_{std::move(location)} {}
} // namespace fesa
+216
View File
@@ -1,11 +1,163 @@
#include "fesa/model/domain.h"
#include <cstddef>
#include <cmath>
#include <functional>
#include <map>
#include <memory>
#include <string>
#include <utility>
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<ThermalBoundaryConditionDefinition>& boundaries,
const std::vector<bool>& active_nodes, const char* identity) {
std::map<std::pair<EntityIndex, int>, 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<std::string, bool> 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<std::string, bool> 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<bool> active_nodes(definition.nodes.size(), false);
std::map<std::string, bool> 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<EntityIndex, std::array<double, 5>> 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)},
@@ -174,6 +326,8 @@ const SourceLocation& StepDefinition::Location() const noexcept {
}
Result<Domain> Domain::Create(ModelDefinition definition) {
const Status thermal_status = ValidateThermalDefinition(definition);
if (!thermal_status.IsOk()) return Result<Domain>::Failure(thermal_status);
return Result<Domain>::Success(Domain{std::move(definition)});
}
@@ -205,6 +359,35 @@ Domain::LinearElasticMaterials() const noexcept {
return linear_materials_view_;
}
const DomainCollectionView<IsotropicThermalMaterial>& Domain::ThermalMaterials()
const noexcept {
return thermal_materials_view_;
}
const DomainCollectionView<Ds4ThermalDefinition>& Domain::ThermalElements()
const noexcept {
return thermal_elements_view_;
}
const std::vector<ThermalShellSectionDefinition>& Domain::ThermalSections()
const noexcept {
return definition_.thermal_sections;
}
const std::vector<ThermalInitialConditionDefinition>&
Domain::ThermalInitialConditions() const noexcept {
return definition_.thermal_initial_conditions;
}
const std::vector<ThermalBoundaryConditionDefinition>&
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<ElementProperty>& Domain::Properties()
const noexcept {
return properties_view_;
@@ -278,6 +461,20 @@ Domain::Domain(ModelDefinition definition)
}
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<IsotropicThermalMaterial>(
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) {
@@ -316,6 +513,25 @@ Domain::Domain(ModelDefinition definition)
element_definitions_.push_back(std::move(owned));
}
definition_.shell_elements.clear();
for (auto& element : definition_.thermal_elements) {
const EntityIndex material_index = static_cast<EntityIndex>(
structural_material_count +
definition_.thermal_sections[element.section_index].material_index);
auto owned = std::make_unique<Ds4ThermalDefinition>(
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) +