#include "fesa/model/domain.h" #include #include #include #include #include #include #include "fesa/analysis/analysis_model.h" #include "fesa/elements/ds4_thermal.h" #include "fesa/materials/isotropic_thermal_material.h" namespace { fesa::SourceLocation Location() { return {"models/thermal.inp", 10U}; } fesa::ModelDefinition ValidDefinition() { fesa::ModelDefinition definition{}; definition.source_path = "models/thermal.inp"; definition.nodes = { {{"Plate", 1, "1"}, {0.0, 0.0, 0.0}, Location()}, {{"Plate", 2, "2"}, {1.0, 0.0, 0.0}, Location()}, {{"Plate", 3, "3"}, {1.0, 1.0, 0.0}, Location()}, {{"Plate", 4, "4"}, {0.0, 1.0, 0.0}, Location()}, {{"Plate", 5, "5"}, {2.0, 2.0, 0.0}, Location()}}; definition.thermal_materials = { {{"", 1, "Thermal-Steel"}, "Thermal-Steel", 45.0, 7800.0, 500.0, Location()}}; definition.thermal_sections = { {{"", 1, "Section"}, "Section", 0.01, 0U, Location()}}; definition.thermal_elements = {{{"Plate", 8, "8"}, {0U, 1U, 2U, 3U}, 0U, Location()}}; definition.thermal_initial_conditions = {{0U, 12.5, Location()}}; definition.thermal_model_boundaries = {{0U, 11, 10.0, Location()}}; definition.thermal_step = {"Heat", 0.2, 1.0, 100U, false, {{0U, 11, 20.0, Location()}}, {{0U, 15, -5.0, Location()}}, Location()}; return definition; } } // namespace TEST(ThermalDomain, StructuralDefinitionHasNoThermalStep) { fesa::ModelDefinition definition{}; definition.steps = {{"Static", {}, {}, 0.1, 1.0, 0.01, 1.0, Location()}}; const auto result = fesa::Domain::Create(std::move(definition)); ASSERT_TRUE(result.HasValue()); EXPECT_EQ(result.Value().ThermalStep(), nullptr); } TEST(IsotropicThermalMaterial, RequiresFinitePositivePropertiesAndIdentity) { const auto valid = fesa::IsotropicThermalMaterial::Create( {"", 7, "Thermal-Steel"}, "Thermal-Steel", 45.0, 7800.0, 500.0, Location()); ASSERT_TRUE(valid.HasValue()); EXPECT_EQ(valid.Value().Kind(), fesa::MaterialKind::kIsotropicThermal); EXPECT_DOUBLE_EQ(valid.Value().Conductivity(), 45.0); EXPECT_DOUBLE_EQ(valid.Value().Density(), 7800.0); EXPECT_DOUBLE_EQ(valid.Value().SpecificHeat(), 500.0); const auto invalid = fesa::IsotropicThermalMaterial::Create( {"", 7, "Thermal-Steel"}, "Thermal-Steel", std::numeric_limits::infinity(), 7800.0, 500.0, Location()); ASSERT_FALSE(invalid.HasValue()); EXPECT_EQ(invalid.GetStatus().Category(), fesa::FailureCategory::kModel); EXPECT_FALSE(fesa::IsotropicThermalMaterial::Create( {"", 7, "Thermal-Steel"}, "Thermal-Steel", 45.0, 0.0, 500.0, Location()) .HasValue()); EXPECT_FALSE(fesa::IsotropicThermalMaterial::Create( {"", 7, "Thermal-Steel"}, "Thermal-Steel", 45.0, 7800.0, std::numeric_limits::quiet_NaN(), Location()) .HasValue()); } TEST(ThermalDomain, OwnsFiveStationDefinitionsWithoutEquationState) { static_assert(!std::is_copy_constructible_v); static_assert(std::is_move_constructible_v); static_assert(std::has_virtual_destructor_v); const auto result = fesa::Domain::Create(ValidDefinition()); ASSERT_TRUE(result.HasValue()); const fesa::Domain& domain = result.Value(); ASSERT_NE(domain.ThermalStep(), nullptr); EXPECT_EQ(domain.ThermalElements().Size(), 1U); EXPECT_EQ(domain.ThermalElements()[0U].Kind(), fesa::ElementDefinitionKind::kDs4Thermal); EXPECT_EQ(domain.ThermalElements()[0U].PropertyIndex(), fesa::Ds4ThermalDefinition::kNoPropertyIndex); EXPECT_EQ(domain.ThermalElements()[0U].NodeIndices(), (std::vector{0U, 1U, 2U, 3U})); EXPECT_EQ(domain.ThermalInitialConditions()[0U].station_values, (std::array{12.5, 12.5, 12.5, 12.5, 12.5})); EXPECT_EQ(domain.ThermalStep()->Boundaries()[0U].value, 20.0); EXPECT_EQ(domain.ThermalStep()->Loads()[0U].source_order, 0U); EXPECT_EQ(static_cast(fesa::ThermalDofComponent::kBottom), 11); EXPECT_EQ(static_cast(fesa::ThermalDofComponent::kTop), 15); } TEST(ThermalDomain, RejectsInactiveTargetsConflictsAndMixedElements) { auto invalid = ValidDefinition(); invalid.thermal_initial_conditions.push_back({4U, 12.5, Location()}); const auto inactive = fesa::Domain::Create(std::move(invalid)); ASSERT_FALSE(inactive.HasValue()); EXPECT_EQ(inactive.GetStatus().Category(), fesa::FailureCategory::kModel); invalid = ValidDefinition(); invalid.thermal_step->boundaries.push_back({0U, 11, 21.0, Location()}); const auto conflicting = fesa::Domain::Create(std::move(invalid)); ASSERT_FALSE(conflicting.HasValue()); EXPECT_EQ(conflicting.GetStatus().Category(), fesa::FailureCategory::kModel); invalid = ValidDefinition(); invalid.elements.push_back( {{"Plate", 9, "9"}, {0U, 1U}, 0U, 0U, Location()}); const auto mixed = fesa::Domain::Create(std::move(invalid)); ASSERT_FALSE(mixed.HasValue()); EXPECT_EQ(mixed.GetStatus().Category(), fesa::FailureCategory::kModel); invalid = ValidDefinition(); invalid.thermal_sections[0U].thickness = 0.0; const auto invalid_thickness = fesa::Domain::Create(std::move(invalid)); ASSERT_FALSE(invalid_thickness.HasValue()); EXPECT_EQ(invalid_thickness.GetStatus().Category(), fesa::FailureCategory::kModel); } TEST(ThermalDomain, AllowsEqualInitialTargetsAndStepBoundaryOverride) { auto definition = ValidDefinition(); definition.thermal_initial_conditions.push_back({0U, 12.5, Location()}); const auto equal_initial = fesa::Domain::Create(std::move(definition)); EXPECT_TRUE(equal_initial.HasValue()); definition = ValidDefinition(); definition.thermal_initial_conditions.push_back({0U, 13.0, Location()}); const auto conflicting_initial = fesa::Domain::Create(std::move(definition)); ASSERT_FALSE(conflicting_initial.HasValue()); EXPECT_EQ(conflicting_initial.GetStatus().Category(), fesa::FailureCategory::kModel); definition = ValidDefinition(); definition.thermal_step->boundaries.push_back({0U, 11, 20.0, Location()}); const auto overriding_step = fesa::Domain::Create(std::move(definition)); EXPECT_TRUE(overriding_step.HasValue()); } TEST(ThermalDomain, RejectsInteriorThermalDofsAndInactiveFluxTargets) { for (const int dof : {12, 13, 14}) { auto definition = ValidDefinition(); definition.thermal_step->boundaries = {{0U, dof, 1.0, Location()}}; const auto boundary = fesa::Domain::Create(std::move(definition)); EXPECT_FALSE(boundary.HasValue()); definition = ValidDefinition(); definition.thermal_step->loads = {{0U, dof, 1.0, Location()}}; const auto flux = fesa::Domain::Create(std::move(definition)); EXPECT_FALSE(flux.HasValue()); } auto definition = ValidDefinition(); definition.thermal_step->loads = {{4U, 11, 1.0, Location()}}; const auto inactive_flux = fesa::Domain::Create(std::move(definition)); EXPECT_FALSE(inactive_flux.HasValue()); definition = ValidDefinition(); definition.thermal_model_boundaries = {{4U, 11, 1.0, Location()}}; const auto inactive_boundary = fesa::Domain::Create(std::move(definition)); EXPECT_FALSE(inactive_boundary.HasValue()); } TEST(ThermalDomain, PreservesAdditiveFluxSourceOrder) { auto definition = ValidDefinition(); definition.thermal_step->loads = {{0U, 11, -5.0, Location()}, {1U, 11, 3.0, Location()}}; const auto result = fesa::Domain::Create(std::move(definition)); ASSERT_TRUE(result.HasValue()); const auto& loads = result.Value().ThermalStep()->Loads(); ASSERT_EQ(loads.size(), 2U); EXPECT_EQ(loads[0U].source_order, 0U); EXPECT_EQ(loads[1U].source_order, 1U); EXPECT_DOUBLE_EQ(loads[0U].power, -5.0); EXPECT_DOUBLE_EQ(loads[1U].power, 3.0); } TEST(ThermalAnalysisModel, KeepsThermalSectionMaterialIndicesSeparateFromDomainMaterials) { auto definition = ValidDefinition(); definition.materials = {{"Unused-Structural", 210.0e9, 0.3, Location()}}; definition.thermal_materials.push_back( {{"", 2, "Thermal-Aluminum"}, "Thermal-Aluminum", 120.0, 2700.0, 900.0, Location()}); definition.thermal_sections = { {{"", 1, "Section-A"}, "Section-A", 0.01, 1U, Location()}, {{"", 2, "Section-B"}, "Section-B", 0.02, 0U, Location()}}; definition.nodes.push_back( {{"Plate", 6, "6"}, {3.0, 0.0, 0.0}, Location()}); definition.nodes.push_back( {{"Plate", 7, "7"}, {4.0, 0.0, 0.0}, Location()}); definition.nodes.push_back( {{"Plate", 8, "8"}, {4.0, 1.0, 0.0}, Location()}); definition.nodes.push_back( {{"Plate", 9, "9"}, {3.0, 1.0, 0.0}, Location()}); definition.thermal_elements = { {{"Plate", 8, "8"}, {0U, 1U, 2U, 3U}, 0U, Location()}, {{"Plate", 9, "9"}, {5U, 6U, 7U, 8U}, 1U, Location()}}; const auto domain_result = fesa::Domain::Create(std::move(definition)); ASSERT_TRUE(domain_result.HasValue()); const fesa::Domain& domain = domain_result.Value(); ASSERT_EQ(domain.Materials().Size(), 3U); ASSERT_EQ(domain.ThermalMaterials().Size(), 2U); EXPECT_EQ(domain.ThermalSections()[0U].material_index, 1U); EXPECT_EQ(domain.ThermalSections()[1U].material_index, 0U); EXPECT_EQ(domain.ThermalElements()[0U].MaterialIndex(), 2U); EXPECT_EQ(domain.ThermalElements()[1U].MaterialIndex(), 1U); EXPECT_EQ(domain.ThermalElements()[0U].PropertyIndex(), fesa::Ds4ThermalDefinition::kNoPropertyIndex); const auto model_result = fesa::AnalysisModel::Create(domain); ASSERT_TRUE(model_result.HasValue()); EXPECT_EQ(model_result.Value().ActiveThermalMaterials(), (std::vector{0U, 1U})); EXPECT_EQ(model_result.Value().ActiveThermalSections(), (std::vector{0U, 1U})); EXPECT_TRUE(model_result.Value().ActiveProperties().empty()); } TEST(ThermalAnalysisModel, ViewsConnectedDs4EntitiesWithoutOwningDomain) { auto domain_result = fesa::Domain::Create(ValidDefinition()); ASSERT_TRUE(domain_result.HasValue()); const fesa::Domain& domain = domain_result.Value(); const auto model_result = fesa::AnalysisModel::Create(domain); ASSERT_TRUE(model_result.HasValue()); const fesa::AnalysisModel& model = model_result.Value(); EXPECT_TRUE(model.IsThermal()); EXPECT_EQ(model.ThermalStep(), domain.ThermalStep()); EXPECT_THROW(model.Step(), std::logic_error); EXPECT_EQ(model.ActiveThermalElements(), (std::vector{0U})); EXPECT_EQ(model.ActiveThermalNodes(), (std::vector{0U, 1U, 2U, 3U})); EXPECT_EQ(model.ActiveThermalMaterials(), (std::vector{0U})); EXPECT_EQ(model.ActiveThermalSections(), (std::vector{0U})); EXPECT_TRUE(model.ActiveProperties().empty()); EXPECT_EQ(domain.ThermalSections()[0U].material_index, 0U); EXPECT_EQ(domain.ThermalElements()[0U].MaterialIndex(), 0U); EXPECT_EQ(&model.GetDomain(), &domain); fesa::Domain moved_domain = std::move(domain_result.Value()); const auto moved_model = fesa::AnalysisModel::Create(moved_domain); ASSERT_TRUE(moved_model.HasValue()); EXPECT_EQ(&moved_model.Value().GetDomain(), &moved_domain); EXPECT_DOUBLE_EQ(moved_domain.ThermalMaterials()[0U].Conductivity(), 45.0); EXPECT_EQ(moved_domain.ThermalElements()[0U].SourceId().source_label, 8); }