fix(abaqus): enforce thermal input subset and preserve provenance
This commit is contained in:
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user