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)