fix(abaqus): enforce thermal input subset and preserve provenance

This commit is contained in:
KOKO\Mimi
2026-09-29 20:38:07 +09:00
parent ba57a469c7
commit 360f8df01d
2 changed files with 890 additions and 57 deletions
+155 -49
View File
@@ -432,6 +432,31 @@ class DomainMappingContext::Impl {
return true;
}
/// @brief Parses finite thermal decimal scalars without strtod underflow
/// flags.
/// @note from_chars preserves representable subnormals and rejects values
/// outside binary64 range; structural scalar parsing remains unchanged.
bool ParseThermalDouble(const std::string& text, double& value,
const SourceLocation& location,
const std::string& keyword) {
if (text.empty() ||
(text.front() == '+' &&
(text.size() == 1U || text[1U] == '+' || text[1U] == '-'))) {
return InputFailure("invalid-numeric-value", location, keyword, text,
"A thermal scalar requires finite decimal syntax.");
}
const char* begin = text.data() + (text.front() == '+' ? 1U : 0U);
const char* end = text.data() + text.size();
const auto parsed =
std::from_chars(begin, end, value, std::chars_format::general);
if (parsed.ec != std::errc{} || parsed.ptr != end ||
!std::isfinite(value)) {
return InputFailure("invalid-numeric-value", location, keyword, text,
"A thermal scalar must be representable and finite.");
}
return true;
}
bool ParseSurfacePressureDouble(const std::string& text, double& value,
const SourceLocation& location,
const std::string& keyword) {
@@ -705,8 +730,12 @@ class DomainMappingContext::Impl {
RequiredParameterValue(block, "ABSOLUTE ZERO");
if (absolute_zero == nullptr) return;
double value = 0.0;
ParseDouble(*absolute_zero, value, block.location, block.canonical_name);
physical_constants_seen_ = !failure_;
if (ParseThermalDouble(*absolute_zero, value, block.location,
block.canonical_name)) {
if (!physical_constants_location_)
physical_constants_location_ = block.location;
AddIgnoredWarning(block);
}
return;
}
if (block.canonical_name == "PART") {
@@ -1107,10 +1136,15 @@ class DomainMappingContext::Impl {
return;
}
double thickness = 0.0;
if (!ParseModelDouble(block.data[0].fields[0], thickness,
block.data[0].location, block.canonical_name,
"invalid-shell-thickness",
"Shell thickness must be finite and positive.")) {
const bool parsed_thickness =
model_element_family_ == ElementFamily::kThermalShell
? ParseThermalDouble(block.data[0].fields[0], thickness,
block.data[0].location, block.canonical_name)
: ParseModelDouble(block.data[0].fields[0], thickness,
block.data[0].location, block.canonical_name,
"invalid-shell-thickness",
"Shell thickness must be finite and positive.");
if (!parsed_thickness) {
return;
}
if (!(thickness > 0.0)) {
@@ -1201,6 +1235,15 @@ class DomainMappingContext::Impl {
}
}
std::set<std::int64_t> unique;
if (model_element_family_ == ElementFamily::kThermalShell) {
// Thermal sets expand each source member once, in first-seen order.
members.erase(std::remove_if(members.begin(), members.end(),
[&unique](const auto member) {
return !unique.insert(member).second;
}),
members.end());
return true;
}
for (const auto member : members) {
if (!unique.insert(member).second) {
return InputFailure("duplicate-entity", block.location,
@@ -1604,6 +1647,8 @@ class DomainMappingContext::Impl {
surface.members.push_back({row.fields[0], side, row.location, ordinal});
}
element_surfaces_.push_back(std::move(surface));
if (model_element_family_ == ElementFamily::kThermalShell)
AddIgnoredWarning(block);
}
void ParseMaterial(const KeywordBlock& block) {
@@ -1811,10 +1856,8 @@ class DomainMappingContext::Impl {
return;
}
double value = 0.0;
if (!ParseModelDouble(
block.data[0].fields[0], value, block.data[0].location,
block.canonical_name, "invalid-thermal-material",
"Thermal material values must be finite and positive.") ||
if (!ParseThermalDouble(block.data[0].fields[0], value,
block.data[0].location, block.canonical_name) ||
!(value > 0.0)) {
if (!failure_) {
ModelFailure("invalid-thermal-material", block.data[0].location,
@@ -1850,8 +1893,8 @@ class DomainMappingContext::Impl {
return;
}
double value = 0.0;
if (!ParseDouble(row.fields[1], value, row.location,
block.canonical_name))
if (!ParseThermalDouble(row.fields[1], value, row.location,
block.canonical_name))
return;
thermal_initial_conditions_.push_back(
{row.fields[0], value, row.location});
@@ -1886,8 +1929,8 @@ class DomainMappingContext::Impl {
}
double value = 0.0;
if (row.fields.size() == 4U &&
!ParseDouble(row.fields[3], value, row.location,
block.canonical_name))
!ParseThermalDouble(row.fields[3], value, row.location,
block.canonical_name))
return;
destination.push_back(
{row.fields[0], static_cast<int>(first), value, row.location});
@@ -2150,6 +2193,15 @@ class DomainMappingContext::Impl {
}
void ParseStepBlock(const KeywordBlock& block) {
if (model_element_family_ == ElementFamily::kThermalShell &&
(block.canonical_name == "STATIC" ||
block.canonical_name == "FREQUENCY" ||
block.canonical_name == "DYNAMIC")) {
InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, step_.name_raw,
"DS4 supports only one HEAT TRANSFER procedure.");
return;
}
if (block.canonical_name == "END STEP") {
active_output_ = false;
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
@@ -2278,7 +2330,7 @@ class DomainMappingContext::Impl {
return;
}
step_no_op_seen_ = true;
if (step_.frequency) {
if (step_.frequency || step_.has_heat_transfer) {
const auto* write = Parameter(block, "WRITE");
const auto* frequency = Parameter(block, "FREQUENCY");
if (!ValidateParameters(block, {"WRITE", "FREQUENCY"}) ||
@@ -2583,10 +2635,10 @@ class DomainMappingContext::Impl {
"HEAT TRANSFER requires increment and time period.");
return;
}
if (!ParseDouble(fields[0], step_.thermal_increment, block.data[0].location,
block.canonical_name) ||
!ParseDouble(fields[1], step_.thermal_period, block.data[0].location,
block.canonical_name) ||
if (!ParseThermalDouble(fields[0], step_.thermal_increment,
block.data[0].location, block.canonical_name) ||
!ParseThermalDouble(fields[1], step_.thermal_period,
block.data[0].location, block.canonical_name) ||
!(step_.thermal_increment > 0.0) || !(step_.thermal_period > 0.0)) {
if (!failure_)
InputFailure(
@@ -2599,9 +2651,13 @@ class DomainMappingContext::Impl {
ThermalTimeGrid::Create(step_.thermal_period, step_.thermal_increment,
step_.maximum_increments);
if (!grid.HasValue()) {
auto diagnostic = grid.GetStatus().Diagnostics().front();
diagnostic.location = block.data[0].location;
diagnostic.keyword = block.canonical_name;
diagnostic.entity_identity = step_.name_raw;
failure_ = MappingFailure{
grid.GetStatus().Category().value_or(FailureCategory::kInput),
grid.GetStatus().Diagnostics().front()};
std::move(diagnostic)};
return;
}
step_.has_heat_transfer = true;
@@ -2624,8 +2680,8 @@ class DomainMappingContext::Impl {
double power = 0.0;
if (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name,
true) ||
!ParseDouble(row.fields[2], power, row.location,
block.canonical_name) ||
!ParseThermalDouble(row.fields[2], power, row.location,
block.canonical_name) ||
(dof != 11 && dof != 15)) {
InputFailure("invalid-dof", row.location, block.canonical_name,
row.fields[0], "CFLUX supports DOF 11 or 15.");
@@ -2691,6 +2747,27 @@ class DomainMappingContext::Impl {
}
void ParseOutputRoot(const KeywordBlock& block) {
if (step_.has_heat_transfer) {
const auto* field = Parameter(block, "FIELD");
const auto* history = Parameter(block, "HISTORY");
const auto* variable = Parameter(block, "VARIABLE");
const auto* frequency = Parameter(block, "FREQUENCY");
const bool valid_field =
field && !field->value && variable && variable->value &&
AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT");
const bool valid_history = history && !history->value && frequency &&
frequency->value && *frequency->value == "0";
if (!RequireNoData(block)) return;
if (block.parameters.size() != 2U || (!valid_field && !valid_history)) {
InputFailure("unsupported-keyword", block.location,
block.canonical_name, "",
"Thermal OUTPUT accepts FIELD, VARIABLE=PRESELECT or "
"HISTORY, FREQUENCY=0.");
return;
}
AddIgnoredWarning(block);
return;
}
if (!ValidateParameters(
block, (step_.dynamic || step_.has_heat_transfer)
? std::vector<std::string_view>{"FIELD", "HISTORY",
@@ -2957,9 +3034,9 @@ class DomainMappingContext::Impl {
}
return;
}
if (physical_constants_seen_) {
InputFailure("unsupported-keyword", step_.location, "PHYSICAL CONSTANTS",
"",
if (physical_constants_location_) {
InputFailure("unsupported-keyword", *physical_constants_location_,
"PHYSICAL CONSTANTS", "",
"PHYSICAL CONSTANTS is supported only by thermal input.");
return;
}
@@ -3556,8 +3633,10 @@ class DomainMappingContext::Impl {
SurfaceMemberKind member_kind, const std::string& elset_name_raw,
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
candidates) {
if (static_cast<std::size_t>(element_index) >=
definition_.shell_elements.size()) {
const bool thermal = model_element_family_ == ElementFamily::kThermalShell;
const auto element_count = thermal ? definition_.thermal_elements.size()
: definition_.shell_elements.size();
if (static_cast<std::size_t>(element_index) >= element_count) {
return InputFailure(
"unsupported-element-formulation", member.location, "SURFACE",
member.target,
@@ -3569,17 +3648,23 @@ class DomainMappingContext::Impl {
"unresolved-reference", member.location, "SURFACE", member.target,
"The surface instance must resolve to its source part.");
}
const auto& element = definition_.shell_elements[element_index];
const auto& source_id =
thermal ? definition_.thermal_elements[element_index].source_id
: definition_.shell_elements[element_index].source_id;
const std::string source_type =
element.source_type == ShellSourceElementType::kS4 ? "S4" : "S4R";
thermal ? "DS4"
: (definition_.shell_elements[element_index].source_type ==
ShellSourceElementType::kS4
? "S4"
: "S4R");
ElementSurfaceFacetDefinition facet{element_index,
0U,
part->name,
CanonicalizeAscii(part->name),
raw_instance.name,
CanonicalizeAscii(raw_instance.name),
element.source_id.source_label,
element.source_id.source_label_text,
source_id.source_label,
source_id.source_label_text,
source_type,
member.side,
member.location,
@@ -3633,23 +3718,25 @@ class DomainMappingContext::Impl {
member.target,
"A direct surface member instance must resolve.");
}
const auto element = std::find_if(
definition_.shell_elements.begin(), definition_.shell_elements.end(),
[&raw_instance, source_label](const Mitc4ShellDefinition& value) {
return AsciiCaseInsensitiveEquals(value.source_id.instance_name,
raw_instance->name) &&
value.source_id.source_label == source_label;
const auto instance = std::find_if(
definition_.instances.begin(), definition_.instances.end(),
[&raw_instance](const InstanceDefinition& value) {
return AsciiCaseInsensitiveEquals(value.name, raw_instance->name);
});
if (element == definition_.shell_elements.end()) {
const auto element = std::find_if(
instance->element_mappings.begin(), instance->element_mappings.end(),
[source_label](const SourceIndexMapping& value) {
return value.source_label == source_label;
});
if (element == instance->element_mappings.end()) {
return InputFailure(
"unresolved-reference", member.location, "SURFACE", member.target,
"A direct surface member must resolve to an S4 or S4R.");
"A direct surface member must resolve to a supported shell element.");
}
return AppendSurfaceFacet(member, *raw_instance,
static_cast<std::size_t>(std::distance(
instances_.begin(), raw_instance)),
static_cast<EntityIndex>(std::distance(
definition_.shell_elements.begin(), element)),
element->internal_index,
SurfaceMemberKind::kDirect, "", candidates);
}
@@ -3718,9 +3805,6 @@ class DomainMappingContext::Impl {
}
void ResolveElementSurfaces() {
if (model_element_family_ == ElementFamily::kThermalShell) {
return;
}
std::vector<ElementSurfaceDefinition> resolved_surfaces;
resolved_surfaces.reserve(element_surfaces_.size());
for (const auto& surface : element_surfaces_) {
@@ -3758,10 +3842,12 @@ class DomainMappingContext::Impl {
"One shell element cannot appear on both surface sides.");
return;
}
definition_.warnings.push_back(
{Severity::kWarning, "duplicate-surface-member-normalized",
candidate.first.first_member_location, "SURFACE", surface.name,
"A repeated same-side surface member was normalized once."});
if (model_element_family_ != ElementFamily::kThermalShell) {
definition_.warnings.push_back(
{Severity::kWarning, "duplicate-surface-member-normalized",
candidate.first.first_member_location, "SURFACE", surface.name,
"A repeated same-side surface member was normalized once."});
}
continue;
}
unique_facets.push_back(std::move(candidate));
@@ -3829,6 +3915,12 @@ class DomainMappingContext::Impl {
std::vector<EntityIndex> indices;
indices.reserve(resolved.Value().size());
for (const auto& entry : resolved.Value()) {
if (model_element_family_ == ElementFamily::kThermalShell &&
!thermal_active_nodes_[entry.entity_index]) {
ModelFailure("inactive-thermal-target", location, keyword, target,
"Thermal targets must connect to an active DS4 element.");
return std::nullopt;
}
indices.push_back(entry.entity_index);
}
return indices;
@@ -3836,6 +3928,10 @@ class DomainMappingContext::Impl {
void FinalizeStep() {
if (step_.has_heat_transfer) {
thermal_active_nodes_.assign(definition_.nodes.size(), false);
for (const auto& element : definition_.thermal_elements)
for (const auto node : element.node_indices)
thermal_active_nodes_[node] = true;
const SourceTargetIndex target_index = BuildSourceTargetIndex();
const SourceTargetResolver resolver{target_index};
const auto append_boundaries =
@@ -3872,11 +3968,20 @@ class DomainMappingContext::Impl {
if (!append_boundaries(step_.thermal_boundaries, &step_values,
&step_boundaries))
return;
std::map<EntityIndex, double> initial_values;
for (const auto& row : thermal_initial_conditions_) {
const auto nodes = ResolveNodeTarget(resolver, row.target, row.location,
"INITIAL CONDITIONS");
if (!nodes) return;
for (const EntityIndex node : *nodes) {
const auto inserted = initial_values.emplace(node, row.value);
if (!inserted.second && inserted.first->second != row.value) {
InputFailure(
"conflicting-initial-temperature", row.location,
"INITIAL CONDITIONS", row.target,
"Expanded initial temperatures prescribe different values.");
return;
}
definition_.thermal_initial_conditions.push_back(
{node, row.value, row.location});
}
@@ -4005,6 +4110,7 @@ class DomainMappingContext::Impl {
std::vector<PrescribedDisplacementDefinition> model_boundaries_;
std::vector<RawThermalBoundary> thermal_model_boundaries_;
std::vector<RawThermalInitial> thermal_initial_conditions_;
std::vector<bool> thermal_active_nodes_;
RawStep step_{};
std::vector<std::map<std::int64_t, std::pair<EntityIndex, EntityIndex>>>
part_section_assignments_;
@@ -4031,7 +4137,7 @@ class DomainMappingContext::Impl {
bool step_no_op_seen_{false};
bool active_output_{false};
bool active_output_is_field_{false};
bool physical_constants_seen_{false};
std::optional<SourceLocation> physical_constants_location_;
};
DomainMappingContext::DomainMappingContext(const ParsedInput& input)
+735 -8
View File
@@ -1,11 +1,23 @@
#include <gtest/gtest.h>
#include <algorithm>
#include <array>
#include <atomic>
#include <cmath>
#include <filesystem>
#include <fstream>
#include <iomanip>
#include <limits>
#include <random>
#include <sstream>
#include <stdexcept>
#include <string>
#include <utility>
#include <vector>
#include "fesa/analysis/analysis_model.h"
#include "fesa/analysis/thermal_time_grid.h"
#include "fesa/fem/dof_manager.h"
#include "fesa/io/abaqus/domain_mapper.h"
#include "fesa/io/abaqus/input_reader.h"
@@ -13,10 +25,15 @@ namespace {
class TemporaryInput final {
public:
explicit TemporaryInput(const std::string& content)
: path_{std::filesystem::temp_directory_path() /
("fesa-thermal-domain-" + std::to_string(next_id_.fetch_add(1U)) +
".inp")} {
explicit TemporaryInput(const std::string& content) {
// Atomically claim a directory: separate CTest processes never share files.
do {
directory_ =
std::filesystem::temp_directory_path() /
("fesa-thermal-domain-" + std::to_string(std::random_device{}()) +
"-" + std::to_string(next_id_.fetch_add(1U)));
} while (!std::filesystem::create_directory(directory_));
path_ = directory_ / "model.inp";
std::ofstream output{path_, std::ios::binary | std::ios::trunc};
output << content;
if (!output) throw std::runtime_error{"Unable to write thermal fixture."};
@@ -24,11 +41,13 @@ class TemporaryInput final {
~TemporaryInput() {
std::error_code error;
std::filesystem::remove(path_, error);
std::filesystem::remove(directory_, error);
}
const std::filesystem::path& Path() const noexcept { return path_; }
private:
static std::atomic<unsigned long> next_id_;
std::filesystem::path directory_;
std::filesystem::path path_;
};
@@ -87,6 +106,44 @@ Bottom, 11, -3.
)inp";
}
std::string Replace(std::string deck, const std::string& from,
const std::string& to) {
const auto position = deck.find(from);
if (position == std::string::npos)
throw std::runtime_error{"Missing fixture token."};
deck.replace(position, from.size(), to);
return deck;
}
std::size_t LineOf(const std::string& deck, const std::string& token) {
const auto position = deck.find(token);
if (position == std::string::npos)
throw std::runtime_error{"Missing source token."};
return 1U + static_cast<std::size_t>(
std::count(deck.begin(), deck.begin() + position, '\n'));
}
void ExpectRejected(const std::string& deck, const std::string& keyword = "") {
const TemporaryInput input{deck};
const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(parsed.HasValue());
const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value());
ASSERT_FALSE(result.HasValue());
ASSERT_FALSE(result.GetStatus().Diagnostics().empty());
const auto& diagnostic = result.GetStatus().Diagnostics().front();
EXPECT_EQ(diagnostic.severity, fesa::Severity::kError);
EXPECT_EQ(diagnostic.location.file, input.Path());
EXPECT_GT(diagnostic.location.line, 0U);
if (!keyword.empty()) EXPECT_EQ(diagnostic.keyword, keyword);
}
std::string Number(double value) {
std::ostringstream stream;
stream << std::setprecision(std::numeric_limits<double>::max_digits10)
<< value;
return stream.str();
}
TEST(ThermalDomainMapping, MapsDs4MaterialsTargetsAndAdditiveFluxes) {
const auto result = MapText(ThermalDeck());
ASSERT_TRUE(result.HasValue())
@@ -203,10 +260,680 @@ TEST(ThermalDomainMapping, MapsApprovedShellThermalReference) {
ASSERT_TRUE(result.HasValue())
<< (result.HasValue() ? ""
: result.GetStatus().Diagnostics().front().message);
EXPECT_EQ(result.Value().Nodes().size(), 729U);
EXPECT_EQ(result.Value().ThermalElements().Size(), 676U);
ASSERT_NE(result.Value().ThermalStep(), nullptr);
EXPECT_EQ(result.Value().ThermalStep()->maximum_increments, 100000U);
const auto& domain = result.Value();
ASSERT_EQ(domain.Nodes().size(), 729U);
ASSERT_EQ(domain.ThermalElements().Size(), 676U);
ASSERT_EQ(domain.ThermalMaterials().Size(), 1U);
ASSERT_EQ(domain.ThermalSections().size(), 1U);
ASSERT_NE(domain.ThermalStep(), nullptr);
const auto& step = *domain.ThermalStep();
EXPECT_EQ(step.name, "Step-1");
EXPECT_EQ(step.maximum_increments, 100000U);
EXPECT_EQ(step.nominal_increment, 1.);
EXPECT_EQ(step.time_period, 100.);
const auto grid = fesa::ThermalTimeGrid::Create(
step.time_period, step.nominal_increment, step.maximum_increments);
ASSERT_TRUE(grid.HasValue());
EXPECT_EQ(grid.Value().Count(), 100U);
EXPECT_EQ(grid.Value().Time(37U), 37.);
EXPECT_EQ(domain.ThermalMaterials()[0].Name(), "Material-1");
EXPECT_EQ(domain.ThermalMaterials()[0].Conductivity(), 54.);
EXPECT_EQ(domain.ThermalMaterials()[0].Density(), 7850.);
EXPECT_EQ(domain.ThermalMaterials()[0].SpecificHeat(), 460.);
EXPECT_EQ(domain.ThermalSections()[0].name, "Set-1");
EXPECT_EQ(domain.ThermalSections()[0].thickness, .5);
EXPECT_EQ(domain.ThermalSections()[0].location.file, path);
EXPECT_EQ(domain.ThermalSections()[0].location.line, 1421U);
for (std::size_t i = 0; i < domain.Nodes().size(); ++i) {
EXPECT_EQ(domain.Nodes()[i].source_id.instance_name, "Part-1-1");
EXPECT_EQ(domain.Nodes()[i].source_id.source_label,
static_cast<std::int64_t>(i + 1U));
}
for (std::size_t i = 0; i < domain.ThermalElements().Size(); ++i) {
EXPECT_EQ(domain.ThermalElements()[i].SourceId().instance_name, "Part-1-1");
EXPECT_EQ(domain.ThermalElements()[i].SourceId().source_label,
static_cast<std::int64_t>(i + 1U));
}
std::vector<std::int64_t> expected_boundary_labels{2, 5, 6, 7};
for (std::int64_t label = 58; label <= 105; ++label)
expected_boundary_labels.push_back(label);
for (std::int64_t label = 298; label <= 441; ++label)
expected_boundary_labels.push_back(label);
ASSERT_EQ(domain.ThermalModelBoundaries().size(),
expected_boundary_labels.size());
ASSERT_EQ(domain.ThermalModelBoundaries().size(), step.Boundaries().size());
for (std::size_t i = 0; i < step.Boundaries().size(); ++i) {
const auto& model_bc = domain.ThermalModelBoundaries()[i];
const auto& step_bc = step.Boundaries()[i];
EXPECT_EQ(model_bc.node_index, step_bc.node_index);
EXPECT_EQ(domain.Nodes()[model_bc.node_index].source_id.source_label,
expected_boundary_labels[i]);
EXPECT_EQ(model_bc.dof, 11);
EXPECT_EQ(step_bc.dof, 11);
EXPECT_EQ(model_bc.value, 0.);
EXPECT_EQ(step_bc.value, 1000.);
EXPECT_EQ(model_bc.location.file, path);
EXPECT_LT(model_bc.location.line, step_bc.location.line);
}
ASSERT_EQ(domain.ElementSurfaces().size(), 1U);
ASSERT_EQ(domain.ElementSurfaces()[0].facets.size(), 169U);
EXPECT_EQ(domain.ElementSurfaces()[0].facets.front().source_element_label,
170);
EXPECT_EQ(domain.ElementSurfaces()[0].facets.back().source_element_label,
338);
EXPECT_EQ(domain.ElementSurfaces()[0].facets[0].source_element_type, "DS4");
ASSERT_EQ(domain.Warnings().size(), 6U);
for (const auto& block : parsed.Value().blocks) {
if (block.canonical_name != "PREPRINT" &&
block.canonical_name != "PHYSICAL CONSTANTS" &&
block.canonical_name != "SURFACE" &&
block.canonical_name != "RESTART" && block.canonical_name != "OUTPUT")
continue;
EXPECT_EQ(std::count_if(domain.Warnings().begin(), domain.Warnings().end(),
[&](const auto& warning) {
return warning.location.file == path &&
warning.location.line ==
block.location.line &&
warning.keyword == block.canonical_name;
}),
1);
}
}
TEST(ThermalDomainMapping, StrictOutputAndRestartNoOps) {
for (const std::string block :
{"*Output, field\n", "*Output, field, variable=ALL\n",
"*Output, field, variable=PRESELECT, frequency=0\n",
"*Output, history\n", "*Output, history, frequency=7\n",
"*Output, history, frequency=0, variable=PRESELECT\n",
"*Output, field, history, variable=PRESELECT\n",
"*Output, field=YES, variable=PRESELECT\n",
"*Output, field, variable=PRESELECT\nNT\n",
"*Output, history, frequency=0\nNT\n", "*Restart\n",
"*Restart, write\n", "*Restart, frequency=0\n",
"*Restart, write=YES, frequency=0\n", "*Restart, write, frequency=1\n",
"*Restart, write, frequency=0, overlay\n",
"*Restart, write, frequency=0\n0\n"}) {
SCOPED_TRACE(block);
ExpectRejected(Replace(ThermalDeck(), "*End Step", block + "*End Step"));
}
}
TEST(ThermalDomainMapping, EveryAcceptedNoOpHasExactlyOneSourceWarning) {
const std::vector<std::pair<std::string, std::string>> blocks{
{"*Preprint, echo=NO, model=NO, history=NO, contact=NO\n", "PREPRINT"},
{"*Physical Constants, absolute zero=-273.15\n", "PHYSICAL CONSTANTS"},
{"*Surface, type=ELEMENT, name=S\nI.1, SPOS\n", "SURFACE"},
{"*Restart, write, frequency=0\n", "RESTART"},
{"*Output, field, variable=PRESELECT\n", "OUTPUT"},
{"*Output, history, frequency=0\n", "OUTPUT"}};
auto deck = ThermalDeck();
deck = blocks[0].first + deck;
deck = Replace(deck, "*Material", blocks[1].first + "*Material");
deck = Replace(deck, "*End Assembly", blocks[2].first + "*End Assembly");
for (std::size_t i = 3U; i < blocks.size(); ++i)
deck = Replace(deck, "*End Step", blocks[i].first + "*End Step");
const TemporaryInput input{deck};
const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(parsed.HasValue());
const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value());
ASSERT_TRUE(result.HasValue());
const auto& warnings = result.Value().Warnings();
ASSERT_EQ(warnings.size(), blocks.size());
for (const auto& block : blocks) {
const auto line = LineOf(deck, block.first);
EXPECT_EQ(std::count_if(warnings.begin(), warnings.end(),
[&](const auto& w) {
return w.severity == fesa::Severity::kWarning &&
w.keyword == block.second &&
w.location.file == input.Path() &&
w.location.line == line;
}),
1);
}
EXPECT_DOUBLE_EQ(
result.Value().ThermalInitialConditions()[0].station_values[0], 20.);
}
TEST(ThermalDomainMapping, SurfaceFacetsAreResolvedAndPreserved) {
for (const std::string side : {"SPOS", "SNEG"}) {
auto deck = Replace(
ThermalDeck(), "*End Assembly",
"*Elset, elset=Faces, instance=I\n1\n"
"*Surface, type=ELEMENT, name=Direct\nI.1, " +
side + "\n*Surface, type=ELEMENT, name=SetSurface\nFaces, " + side +
"\n*End Assembly");
const auto result = MapText(deck);
ASSERT_TRUE(result.HasValue());
const auto& surfaces = result.Value().ElementSurfaces();
ASSERT_EQ(surfaces.size(), 2U);
for (std::size_t i = 0; i < surfaces.size(); ++i) {
ASSERT_EQ(surfaces[i].facets.size(), 1U);
const auto& facet = surfaces[i].facets[0];
EXPECT_EQ(facet.element_index, 0U);
EXPECT_EQ(facet.source_element_label, 1);
EXPECT_EQ(facet.instance_name_raw, "I");
EXPECT_EQ(facet.part_name_raw, "P");
EXPECT_EQ(facet.source_element_type, "DS4");
EXPECT_EQ(facet.side, side == "SPOS" ? fesa::ShellSurfaceSide::kSpos
: fesa::ShellSurfaceSide::kSneg);
EXPECT_EQ(facet.member_kind, i == 0U ? fesa::SurfaceMemberKind::kDirect
: fesa::SurfaceMemberKind::kElset);
EXPECT_EQ(facet.first_member_location.line,
LineOf(deck, (i == 0U ? "I.1, " : "Faces, ") + side));
}
}
}
TEST(ThermalDomainMapping, RejectsUnresolvedOrInvalidSurfaces) {
for (const std::string row :
{"Missing.1, SPOS", "I.99, SPOS", "Missing, SPOS", "E, SPOS",
"P.1, SPOS", "I.E, SPOS", "I.1, S1", "I.1", "I.1, SPOS, 1"}) {
SCOPED_TRACE(row);
ExpectRejected(
Replace(ThermalDeck(), "*End Assembly",
"*Surface, type=ELEMENT, name=S\n" + row + "\n*End Assembly"),
"SURFACE");
}
for (const std::string header :
{"*Surface, name=S", "*Surface, type=ELEMENT",
"*Surface, type=NODE, name=S",
"*Surface, type=ELEMENT, name=S, internal"}) {
ExpectRejected(Replace(ThermalDeck(), "*End Assembly",
header + "\nI.1, SPOS\n*End Assembly"),
"SURFACE");
}
ExpectRejected(Replace(ThermalDeck(), "*End Assembly",
"*Surface, type=ELEMENT, name=S\n*End Assembly"),
"SURFACE");
}
TEST(ThermalDomainMapping,
PreprintAndPhysicalConstantsAreStrictAndSourceAware) {
for (const std::string block :
{"*Preprint\n", "*Preprint, echo=NO, model=NO, history=NO\n",
"*Preprint, echo=NO, model=NO, history=NO, contact=YES\n",
"*Preprint, echo=NO, model=NO, history=NO, contact\n",
"*Preprint, echo=NO, model=NO, history=NO, contact=NO, unknown=NO\n",
"*Preprint, echo=NO, model=NO, history=NO, contact=NO\ndata\n",
"*Physical Constants\n", "*Physical Constants, absolute zero=nan\n",
"*Physical Constants, absolute zero=inf\n",
"*Physical Constants, absolute zero=\n",
"*Physical Constants, absolute zero=-273.15, stefan boltzmann=1\n",
"*Physical Constants, absolute zero=-273.15\ndata\n"}) {
SCOPED_TRACE(block);
ExpectRejected(block + ThermalDeck());
}
auto structural = Replace(ThermalDeck(), "type=DS4", "type=S4");
structural =
Replace(structural,
"*Density\n7850.,\n*Conductivity\n54.,\n*Specific Heat\n460.,\n",
"*Elastic\n210000., .3\n");
structural = Replace(
structural, "*Initial Conditions, type=TEMPERATURE\nBottom, 20.\n", "");
structural = Replace(structural, "Bottom, 11, 11", "Bottom, 1, 1");
structural = Replace(structural, "*Heat Transfer\n1., 100.",
"*Static\n1., 1., 1., 1.");
structural = Replace(structural,
"*Boundary\nBottom, 15, 15, 30.\n*Cflux\nBottom, 11, "
"-2.\n*Cflux\nBottom, 11, -3.\n",
"");
const std::string block = "*Physical Constants, absolute zero=-273.15\n";
structural = block + structural;
const auto result = MapText(structural);
ASSERT_FALSE(result.HasValue());
EXPECT_EQ(result.GetStatus().Diagnostics()[0].keyword, "PHYSICAL CONSTANTS");
EXPECT_EQ(result.GetStatus().Diagnostics()[0].location.line, 1U);
}
TEST(ThermalDomainMapping, RejectsChildOutputUnknownAndWrongNoOpScopes) {
const std::vector<std::string> blocks{
"*Preprint, echo=NO, model=NO, history=NO, contact=NO\n",
"*Physical Constants, absolute zero=-273.15\n",
"*Restart, write, frequency=0\n", "*Output, field, variable=PRESELECT\n",
"*Surface, type=ELEMENT, name=S\nI.1, SPOS\n"};
for (std::size_t i = 0; i < blocks.size(); ++i) {
ExpectRejected(
Replace(ThermalDeck(), "*End Part", blocks[i] + "*End Part"));
const auto target = i < 2U || i == 4U ? "*End Step" : "*Material";
ExpectRejected(Replace(ThermalDeck(), target, blocks[i] + target));
}
for (const std::string child :
{"NODE OUTPUT", "ELEMENT OUTPUT", "CONTACT OUTPUT", "ENERGY OUTPUT"}) {
ExpectRejected(Replace(ThermalDeck(), "*End Step",
"*Output, field, variable=PRESELECT\n*" + child +
"\n*End Step"),
child);
}
ExpectRejected(Replace(ThermalDeck(), "*End Step",
"*Unknown Thermal Keyword\n*End Step"));
}
TEST(ThermalDomainMapping, MaterialConstantsAndSectionLinksAreStrict) {
for (const auto& property : std::vector<std::pair<std::string, std::string>>{
{"Density", "7850."},
{"Conductivity", "54."},
{"Specific Heat", "460."}}) {
const std::string original =
"*" + property.first + "\n" + property.second + ",\n";
for (const std::string tail : {"\n", ",\n"}) {
const auto result = MapText(
Replace(ThermalDeck(), original,
"*" + property.first + "\n" + property.second + tail));
ASSERT_TRUE(result.HasValue()) << property.first << tail;
}
for (const std::string value :
{"0", "-1", "nan", "inf", "1e400", "", "1, 20", "1,,", "1\n2"}) {
SCOPED_TRACE(property.first + ":" + value);
ExpectRejected(Replace(ThermalDeck(), original,
"*" + property.first + "\n" + value + "\n"));
}
for (const std::string option : {"dependencies=1", "type=ANISOTROPIC",
"type=ORTHOTROPIC", "unknown=1"})
ExpectRejected(Replace(ThermalDeck(), "*" + property.first + "\n",
"*" + property.first + ", " + option + "\n"));
ExpectRejected(Replace(ThermalDeck(), original, ""), "MATERIAL");
ExpectRejected(Replace(ThermalDeck(), original, original + original));
}
for (const std::string row :
{"0", "-1", "nan", "inf", ".5,", ".5, 5.0", ".5, 3", ".5,5,", ".5\n.5"})
ExpectRejected(Replace(ThermalDeck(), ".5, 5\n", row + "\n"),
"SHELL SECTION");
for (const std::string option :
{"offset=0", "offset=SPOS", "composite", "nodal thickness"})
ExpectRejected(Replace(ThermalDeck(), "*Shell Section, elset=E, material=M",
"*Shell Section, elset=E, material=M, " + option),
"SHELL SECTION");
ExpectRejected(Replace(ThermalDeck(), "material=M", "material=Missing"),
"SHELL SECTION");
ExpectRejected(Replace(ThermalDeck(), "*Shell Section, elset=E",
"*Shell Section, elset=Missing"),
"SHELL SECTION");
ExpectRejected(Replace(ThermalDeck(),
"*Shell Section, elset=E, material=M\n.5, 5\n", ""),
"ELEMENT");
ExpectRejected(Replace(ThermalDeck(), "*End Part",
"*Shell Section, elset=E, material=M\n.5\n*End Part"),
"SHELL SECTION");
}
TEST(ThermalDomainMapping, InitialTemperatureDefaultsAndOverlaps) {
const auto unspecified = MapText(
Replace(ThermalDeck(),
"*Initial Conditions, type=TEMPERATURE\nBottom, 20.\n", ""));
ASSERT_TRUE(unspecified.HasValue());
EXPECT_TRUE(unspecified.Value().ThermalInitialConditions().empty());
for (const std::string row : {"1, 20.", "Bottom, 20."}) {
const auto result = MapText(
Replace(ThermalDeck(), "Bottom, 20.\n", "Bottom, 20.\n" + row + "\n"));
ASSERT_TRUE(result.HasValue());
for (const auto& initial : result.Value().ThermalInitialConditions())
for (const auto temperature : initial.station_values)
EXPECT_DOUBLE_EQ(temperature, 20.);
}
ExpectRejected(
Replace(ThermalDeck(), "Bottom, 20.\n", "Bottom, 20.\n1, -20.\n"),
"INITIAL CONDITIONS");
for (const std::string row :
{"Missing, 20.", "99, 20.", "1", "1, 20., 30.", "1, nan", ", 20."})
ExpectRejected(Replace(ThermalDeck(), "Bottom, 20.\n", row + "\n"),
"INITIAL CONDITIONS");
for (const std::string header :
{"*Initial Conditions", "*Initial Conditions, type=STRESS",
"*Initial Conditions, type=TEMPERATURE, amplitude=A"})
ExpectRejected(
Replace(ThermalDeck(), "*Initial Conditions, type=TEMPERATURE", header),
"INITIAL CONDITIONS");
ExpectRejected(
Replace(ThermalDeck(), "*End Step",
"*Initial Conditions, type=TEMPERATURE\n1, 1\n*End Step"),
"INITIAL CONDITIONS");
}
TEST(ThermalDomainMapping, InactiveTargetsFailWithOriginalKeywordAndIdentity) {
const auto inactive = Replace(ThermalDeck(), "*Element, type=DS4",
"99, 2., 2., 0.\n*Element, type=DS4");
const auto accepted = MapText(inactive);
ASSERT_TRUE(accepted.HasValue());
EXPECT_EQ(accepted.Value().Nodes().size(), 5U);
const auto view = fesa::AnalysisModel::Create(accepted.Value());
ASSERT_TRUE(view.HasValue());
EXPECT_EQ(view.Value().ActiveThermalNodes().size(), 4U);
for (const auto& replacement :
std::vector<std::pair<std::string, std::string>>{
{"Bottom, 20.", "99, 20."},
{"Bottom, 11, 11", "99, 11, 11"},
{"Bottom, 15, 15, 30.", "99, 15, 15, 30."},
{"Bottom, 11, -2.", "99, 11, -2."}}) {
const auto deck = Replace(inactive, replacement.first, replacement.second);
const auto result = MapText(deck);
ASSERT_FALSE(result.HasValue());
const auto& diagnostic = result.GetStatus().Diagnostics()[0];
EXPECT_EQ(diagnostic.entity_identity, "99");
EXPECT_EQ(diagnostic.location.line, LineOf(deck, replacement.second));
EXPECT_NE(diagnostic.keyword, "THERMAL");
}
}
TEST(ThermalDomainMapping,
ModelBoundaryInheritanceAndStepOverrideReachDofManager) {
auto deck = Replace(ThermalDeck(), "Bottom, 11, 11\n",
"Bottom, 11, 11, 7.\n4, 15, 15\n1, 11, 11, 7.\n");
deck = Replace(deck, "Bottom, 15, 15, 30.",
"1, 11, 11, -30.\n1, 11, 11, -30.\n2, 15, 15");
const auto result = MapText(deck);
ASSERT_TRUE(result.HasValue());
const auto& domain = result.Value();
ASSERT_EQ(domain.ThermalModelBoundaries().size(), 4U);
ASSERT_EQ(domain.ThermalStep()->Boundaries().size(), 3U);
EXPECT_DOUBLE_EQ(domain.ThermalModelBoundaries()[0].value, 7.);
EXPECT_DOUBLE_EQ(domain.ThermalStep()->Boundaries()[0].value, -30.);
const auto model = fesa::AnalysisModel::Create(domain);
ASSERT_TRUE(model.HasValue());
const auto dofs = fesa::DofManager::CreateThermal(model.Value());
ASSERT_TRUE(dofs.HasValue());
const std::vector<std::size_t> expected_dofs{0U, 5U, 9U, 19U};
EXPECT_EQ(dofs.Value().ConstrainedDofs(), expected_dofs);
const std::array<double, 4> expected_values{-30., 7., 0., 0.};
for (std::size_t i = 0; i < expected_values.size(); ++i)
EXPECT_DOUBLE_EQ(dofs.Value().PrescribedValues()[i], expected_values[i]);
}
TEST(ThermalDomainMapping,
BoundariesRejectConflictsAndUnsupportedRowsAtBothLevels) {
for (const std::string original :
{"Bottom, 11, 11\n", "Bottom, 15, 15, 30.\n"}) {
const std::string equal = original.find("15") == std::string::npos
? "1, 11, 11, 0.\n"
: "1, 15, 15, 30.\n";
EXPECT_TRUE(
MapText(Replace(ThermalDeck(), original, original + equal)).HasValue());
const std::string conflict = original.find("15") == std::string::npos
? "1, 11, 11, 2.\n"
: "1, 15, 15, 2.\n";
ExpectRejected(Replace(ThermalDeck(), original, original + conflict),
"BOUNDARY");
for (const std::string row :
{"Bottom, 12, 12", "Bottom, 13, 13", "Bottom, 14, 14",
"Bottom, 11, 15", "Bottom, 11", "Bottom, 11, 11,",
"Bottom, 11, 11, 1, 2", "Bottom, 15, 15, inf", "Missing, 11, 11"})
ExpectRejected(Replace(ThermalDeck(), original, row + "\n"), "BOUNDARY");
for (const std::string option : {"OP=NEW", "AMPLITUDE=A"})
ExpectRejected(Replace(ThermalDeck(), "*Boundary\n" + original,
"*Boundary, " + option + "\n" + original),
"BOUNDARY");
}
ExpectRejected(
Replace(ThermalDeck(), "*End Part", "*Boundary\n1, 11, 11\n*End Part"),
"BOUNDARY");
ExpectRejected(Replace(ThermalDeck(), "*Heat Transfer",
"*Boundary\n1, 11, 11\n*Heat Transfer"),
"BOUNDARY");
}
TEST(ThermalDomainMapping,
SignedFluxRetainsRowOrderDuplicatesAndConstrainedTargets) {
auto deck =
Replace(ThermalDeck(), "1\n2\n*End Assembly", "2, 1, 2\n*End Assembly");
deck = Replace(
deck, "*Cflux\nBottom, 11, -2.\n*Cflux\nBottom, 11, -3.",
"*Cflux\n2, 15, 9.\nBottom, 11, -2.\n1, 11, 3.\n*Cflux\n1, 11, 3.");
const auto result = MapText(deck);
ASSERT_TRUE(result.HasValue());
const auto& loads = result.Value().ThermalStep()->Loads();
ASSERT_EQ(loads.size(), 5U);
const std::array<unsigned, 5> nodes{1, 1, 0, 0, 0};
const std::array<int, 5> dofs{15, 11, 11, 11, 11};
const std::array<double, 5> powers{9., -2., -2., 3., 3.};
for (std::size_t i = 0; i < loads.size(); ++i) {
EXPECT_EQ(loads[i].node_index, nodes[i]);
EXPECT_EQ(loads[i].dof, dofs[i]);
EXPECT_DOUBLE_EQ(loads[i].power, powers[i]);
EXPECT_EQ(loads[i].source_order, i);
}
EXPECT_EQ(loads[1].location.line, loads[2].location.line);
EXPECT_LT(loads[3].location.line, loads[4].location.line);
}
TEST(ThermalDomainMapping, FluxRejectsOptionsRowsAndWrongScopes) {
for (const std::string row :
{"Bottom, 12, 1", "Bottom, 13, 1", "Bottom, 14, 1", "Bottom, 11",
"Bottom, 11, 1,", "Bottom, 11, inf", "Bottom, 11, nan",
"Missing, 11, 1", ", 11, 1"})
ExpectRejected(Replace(ThermalDeck(), "Bottom, 11, -2.", row), "CFLUX");
for (const std::string option : {"OP=NEW", "AMPLITUDE=A", "unknown=1"})
ExpectRejected(Replace(ThermalDeck(), "*Cflux", "*Cflux, " + option),
"CFLUX");
for (const std::string target :
{"*Part", "*End Part", "*End Assembly", "*Step", "*Heat Transfer"})
ExpectRejected(
Replace(ThermalDeck(), target, "*Cflux\n1, 11, 1\n" + target), "CFLUX");
}
TEST(ThermalDomainMapping,
TimeControlsUseSharedGridIncludingInputCellBoundaries) {
struct Case {
double increment;
double period;
unsigned count;
double last;
};
const double tiny = std::numeric_limits<double>::denorm_min();
const std::vector<Case> cases{
{1., 100., 100, 1.},
{3., 10., 4, 1.},
{3., 2., 1, 2.},
{2., 2., 1, 2.},
{.3, .9, 3, .3},
{.3, std::nextafter(.9, 1.), 4, 1.6653345369377348e-16},
{6 * tiny, 20 * tiny, 3, 6 * tiny},
{5 * tiny, 17 * tiny, 4, 2 * tiny}};
for (const auto& item : cases) {
SCOPED_TRACE(Number(item.increment) + ", " + Number(item.period));
auto deck = Replace(ThermalDeck(), "1., 100.",
Number(item.increment) + ", " + Number(item.period));
deck = Replace(deck, "inc=100", "inc=" + std::to_string(item.count));
const auto result = MapText(deck);
ASSERT_TRUE(result.HasValue());
const auto& step = *result.Value().ThermalStep();
EXPECT_EQ(step.nominal_increment, item.increment);
EXPECT_EQ(step.time_period, item.period);
const auto grid = fesa::ThermalTimeGrid::Create(
step.time_period, step.nominal_increment, step.maximum_increments);
ASSERT_TRUE(grid.HasValue());
EXPECT_EQ(grid.Value().Count(), item.count);
EXPECT_DOUBLE_EQ(grid.Value().Time(item.count), item.period);
EXPECT_EQ(grid.Value().Delta(item.count), item.last);
if (item.count > 1U)
ExpectRejected(Replace(deck, "inc=" + std::to_string(item.count),
"inc=" + std::to_string(item.count - 1U)),
"HEAT TRANSFER");
}
auto largest_grid = Replace(ThermalDeck(), "1., 100.", "1., 4294967295.");
largest_grid = Replace(largest_grid, "inc=100", "inc=4294967295");
const auto upper = MapText(largest_grid);
ASSERT_TRUE(upper.HasValue());
EXPECT_EQ(upper.Value().ThermalStep()->maximum_increments, 4294967295U);
ExpectRejected(Replace(largest_grid, "1., 4294967295.", "1., 4294967296."),
"HEAT TRANSFER");
const auto defaults =
MapText(Replace(ThermalDeck(), ", inc=100", ", nlgeom=NO"));
ASSERT_TRUE(defaults.HasValue());
EXPECT_EQ(defaults.Value().ThermalStep()->maximum_increments, 100U);
for (const std::string option : {"", ", end=PERIOD"}) {
for (int empty = 0; empty <= 3; ++empty) {
auto deck = Replace(
ThermalDeck(), "*Heat Transfer\n1., 100.",
"*Heat Transfer" + option + "\n1., 100." + std::string(empty, ','));
EXPECT_TRUE(MapText(deck).HasValue()) << deck;
}
}
}
TEST(ThermalDomainMapping, RejectsInvalidTimeAndMixedProceduresInEitherOrder) {
for (const std::string row :
{"1, 100,,,,", "1, 100, 0", "1, 100,, 2", "1", "1,", "0, 1", "-1, 1",
"1, 0", "1, -1", "nan, 1", "1, inf", "1e-300, 1e300", "1, 100\n1, 100"})
ExpectRejected(Replace(ThermalDeck(), "1., 100.", row), "HEAT TRANSFER");
for (const std::string option :
{"steady state", "deltmx=1", "end=SS", "unknown=1"})
ExpectRejected(
Replace(ThermalDeck(), "*Heat Transfer", "*Heat Transfer, " + option),
"HEAT TRANSFER");
for (const std::string option :
{"inc=0", "inc=-1", "inc=4294967296", "inc=1.5", "inc="})
ExpectRejected(Replace(ThermalDeck(), "inc=100", option), "STEP");
for (const std::string procedure :
{"*Static\n1, 1, 1, 1\n", "*Frequency\n1\n",
"*Dynamic, direct\n1, 100\n", "*Heat Transfer\n1, 100\n"}) {
ExpectRejected(Replace(ThermalDeck(), "*Heat Transfer\n",
procedure + "*Heat Transfer\n"));
ExpectRejected(
Replace(ThermalDeck(), "1., 100.\n", "1., 100.\n" + procedure));
}
ExpectRejected(ThermalDeck() + "*Step\n*Heat Transfer\n1,1\n*End Step\n",
"STEP");
}
TEST(ThermalDomainMapping, ThermalMaterialAndSourceProvenanceArePreserved) {
const auto deck = Replace(
ThermalDeck(), "*Material, name=M",
"*Material, name=Unused\n*Density\n2\n*Conductivity\n3\n*Specific "
"Heat\n4\n*Material, name=M");
const TemporaryInput input{deck};
const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(parsed.HasValue());
const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value());
ASSERT_TRUE(result.HasValue());
const auto& domain = result.Value();
ASSERT_EQ(domain.ThermalMaterials().Size(), 2U);
ASSERT_EQ(domain.ThermalSections().size(), 1U);
const auto& section = domain.ThermalSections()[0];
EXPECT_EQ(section.material_index, 1U);
EXPECT_EQ(section.name, "E");
EXPECT_EQ(section.source_id.source_label_text, "E");
EXPECT_EQ(section.location.file, input.Path());
EXPECT_EQ(section.location.line, LineOf(deck, "*Shell Section"));
EXPECT_DOUBLE_EQ(section.thickness, .5);
const auto& material = domain.ThermalMaterials()[section.material_index];
EXPECT_EQ(material.Name(), "M");
EXPECT_DOUBLE_EQ(material.Conductivity(), 54.);
EXPECT_DOUBLE_EQ(material.Density(), 7850.);
EXPECT_DOUBLE_EQ(material.SpecificHeat(), 460.);
EXPECT_EQ(material.Location().line, LineOf(deck, "*Material, name=M"));
EXPECT_EQ(domain.ThermalElements()[0].SourceId().instance_name, "I");
EXPECT_EQ(domain.ThermalElements()[0].SourceId().source_label, 1);
EXPECT_EQ(domain.ThermalStep()->name, "Heat");
EXPECT_EQ(domain.ThermalStep()->location.file, input.Path());
EXPECT_EQ(domain.ThermalStep()->location.line, LineOf(deck, "*Step"));
}
TEST(ThermalDomainMapping,
FiniteThermalScalarsPreserveSubnormalAndSignedValues) {
const double tiny = std::numeric_limits<double>::denorm_min();
const std::string text = Number(tiny);
for (const auto& item : std::vector<std::pair<std::string, std::string>>{
{"54.,", text},
{"7850.,", text},
{"460.,", text},
{".5, 5", text + ",5"},
{"Bottom, 20.", "Bottom, -" + text},
{"Bottom, 15, 15, 30.", "Bottom, 15, 15, -" + text},
{"Bottom, 11, -2.", "Bottom, 11, -" + text}}) {
SCOPED_TRACE(item.first);
const auto result =
MapText(Replace(ThermalDeck(), item.first, item.second));
ASSERT_TRUE(result.HasValue());
const auto& domain = result.Value();
if (item.first == "54.,")
EXPECT_EQ(domain.ThermalMaterials()[0].Conductivity(), tiny);
if (item.first == "7850.,")
EXPECT_EQ(domain.ThermalMaterials()[0].Density(), tiny);
if (item.first == "460.,")
EXPECT_EQ(domain.ThermalMaterials()[0].SpecificHeat(), tiny);
if (item.first == ".5, 5")
EXPECT_EQ(domain.ThermalSections()[0].thickness, tiny);
if (item.first == "Bottom, 20.")
EXPECT_EQ(domain.ThermalInitialConditions()[0].station_values[4], -tiny);
if (item.first == "Bottom, 15, 15, 30.")
EXPECT_EQ(domain.ThermalStep()->Boundaries()[0].value, -tiny);
if (item.first == "Bottom, 11, -2.")
EXPECT_EQ(domain.ThermalStep()->Loads()[0].power, -tiny);
}
for (const std::string value :
{"+2.5", "-0", "0", "-20.25", "1.7976931348623157e308"}) {
const auto result =
MapText(Replace(ThermalDeck(), "Bottom, 20.", "Bottom, " + value));
ASSERT_TRUE(result.HasValue());
}
for (const std::string value : {"1e-999", "1e999", "0x1p0", "+-1", "1D2"}) {
for (const std::string original :
{"54.,", "460.,", ".5, 5", "Bottom, 20.", "Bottom, 11, -2."}) {
const auto replacement =
original == "Bottom, 20."
? "Bottom, " + value
: (original == "Bottom, 11, -2." ? "Bottom, 11, " + value
: value);
ExpectRejected(Replace(ThermalDeck(), original, replacement));
}
}
}
TEST(ThermalDomainMapping, ThermalSetAndSurfaceDuplicatesExpandOnce) {
auto deck =
Replace(ThermalDeck(), "*Elset, elset=E\n1\n", "*Elset, elset=E\n1,1\n");
deck = Replace(
deck, "*End Assembly",
"*Surface, type=ELEMENT, name=S\nI.1, SPOS\nI.1, SPOS\n*End Assembly");
const auto result = MapText(deck);
ASSERT_TRUE(result.HasValue());
EXPECT_EQ(result.Value().ThermalElements().Size(), 1U);
ASSERT_EQ(result.Value().ElementSurfaces().size(), 1U);
EXPECT_EQ(result.Value().ElementSurfaces()[0].facets.size(), 1U);
EXPECT_EQ(result.Value().Warnings().size(), 1U);
ExpectRejected(Replace(deck, "I.1, SPOS\nI.1, SPOS", "I.1, SPOS\nI.1, SNEG"),
"SURFACE");
}
TEST(ThermalDomainMapping,
DeferredFailuresRetainExactInputLocationAndIdentity) {
auto deck = Replace(ThermalDeck(), "inc=100", "inc=99");
const TemporaryInput input{deck};
const auto parsed = fesa::AbaqusInputReader{}.Read(input.Path());
ASSERT_TRUE(parsed.HasValue());
const auto result = fesa::AbaqusDomainMapper{}.Map(parsed.Value());
ASSERT_FALSE(result.HasValue());
const auto& diagnostic = result.GetStatus().Diagnostics()[0];
EXPECT_EQ(diagnostic.location.file, input.Path());
EXPECT_EQ(diagnostic.location.line, LineOf(deck, "1., 100."));
EXPECT_EQ(diagnostic.keyword, "HEAT TRANSFER");
EXPECT_EQ(diagnostic.entity_identity, "Heat");
auto preprint =
std::string{"*Preprint, echo=NO, model=NO, history=NO\n"} + ThermalDeck();
const auto missing_contact = MapText(preprint);
ASSERT_FALSE(missing_contact.HasValue());
EXPECT_EQ(missing_contact.GetStatus().Diagnostics()[0].location.line, 1U);
EXPECT_EQ(missing_contact.GetStatus().Diagnostics()[0].keyword, "PREPRINT");
}
TEST(ThermalDomainMapping, RejectsMixedElementsAndWrongThermalMaterialScopes) {
ExpectRejected(Replace(ThermalDeck(), "*Elset, elset=E",
"*Element, type=S4\n2,1,2,3,4\n*Elset, elset=E"),
"ELEMENT");
for (const std::string block :
{"*Conductivity\n54\n", "*Specific Heat\n460\n",
"*Initial Conditions, type=TEMPERATURE\n1, 1\n"}) {
ExpectRejected(Replace(ThermalDeck(), "*End Part", block + "*End Part"));
ExpectRejected(
Replace(ThermalDeck(), "*End Assembly", block + "*End Assembly"));
ExpectRejected(Replace(ThermalDeck(), "*End Step", block + "*End Step"));
}
for (const std::string property :
{"*Conductivity\n54.,\n", "*Specific Heat\n460.,\n"}) {
auto deck = Replace(ThermalDeck(), property, "");
deck = Replace(deck, "*Material", property + "*Material");
ExpectRejected(deck);
}
}
} // namespace