4634 lines
187 KiB
C++
4634 lines
187 KiB
C++
#include "io/abaqus/domain_mapping_context.h"
|
|
|
|
#include <algorithm>
|
|
#include <array>
|
|
#include <cerrno>
|
|
#include <charconv>
|
|
#include <cmath>
|
|
#include <cstdlib>
|
|
#include <limits>
|
|
#include <map>
|
|
#include <optional>
|
|
#include <set>
|
|
#include <string>
|
|
#include <string_view>
|
|
#include <system_error>
|
|
#include <tuple>
|
|
#include <utility>
|
|
#include <vector>
|
|
|
|
#include "fesa/analysis/thermal_time_grid.h"
|
|
#include "fesa/core/ascii.h"
|
|
#include "fesa/math/vector3.h"
|
|
#include "fesa/model/shell_geometry.h"
|
|
#include "fesa/model/source_target_resolver.h"
|
|
|
|
namespace fesa::abaqus_internal {
|
|
namespace {
|
|
|
|
std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields) {
|
|
while (!fields.empty() && fields.back().empty()) {
|
|
fields.pop_back();
|
|
}
|
|
return fields;
|
|
}
|
|
|
|
std::string CanonicalizeAscii(std::string value) {
|
|
for (char& character : value) {
|
|
if (character >= 'a' && character <= 'z') {
|
|
character = static_cast<char>(character - 'a' + 'A');
|
|
}
|
|
}
|
|
return value;
|
|
}
|
|
|
|
struct RawNode {
|
|
std::int64_t label;
|
|
std::string label_text;
|
|
std::array<double, 3> coordinates;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawElement {
|
|
enum class Type { kB33, kS4, kS4r, kDs4, kC3d8 };
|
|
|
|
std::int64_t label;
|
|
std::string label_text;
|
|
Type type;
|
|
std::vector<std::int64_t> node_labels;
|
|
SourceLocation location;
|
|
};
|
|
|
|
enum class ElementFamily { kBeam, kShell, kThermalShell, kSolid };
|
|
|
|
struct RawSet {
|
|
std::string name;
|
|
std::vector<std::int64_t> members;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawSection {
|
|
std::string element_set_name;
|
|
std::string material_name;
|
|
std::array<double, 5> properties;
|
|
std::array<double, 3> first_axis;
|
|
std::vector<std::array<double, 2>> section_points;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawShellSection {
|
|
std::string element_set_name;
|
|
std::string material_name;
|
|
double thickness;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawSolidSection {
|
|
std::string element_set_name;
|
|
std::string material_name;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawPart {
|
|
std::string name;
|
|
SourceLocation location;
|
|
std::vector<RawNode> nodes;
|
|
std::vector<RawElement> elements;
|
|
std::vector<RawSet> node_sets;
|
|
std::vector<RawSet> element_sets;
|
|
std::vector<RawSection> sections;
|
|
std::vector<RawShellSection> shell_sections;
|
|
std::vector<RawSolidSection> solid_sections;
|
|
};
|
|
|
|
struct RawInstance {
|
|
std::string name;
|
|
std::string part_name;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawAssemblySet {
|
|
bool is_node_set;
|
|
std::string name;
|
|
std::string instance_name;
|
|
std::vector<std::int64_t> members;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawSurfaceMember {
|
|
std::string target;
|
|
ShellSurfaceSide side;
|
|
SourceLocation location;
|
|
std::size_t ordinal;
|
|
};
|
|
|
|
struct RawElementSurface {
|
|
std::string name;
|
|
SourceLocation location;
|
|
std::size_t declaration_ordinal;
|
|
std::vector<RawSurfaceMember> members;
|
|
};
|
|
|
|
struct RawSurfacePressureLoad {
|
|
std::string surface_name;
|
|
double magnitude;
|
|
std::string magnitude_raw;
|
|
SourceLocation keyword_location;
|
|
SourceLocation data_location;
|
|
std::size_t data_ordinal;
|
|
std::size_t load_ordinal;
|
|
};
|
|
|
|
struct RawMaterial {
|
|
std::string name;
|
|
double youngs_modulus{0.0};
|
|
double poisson_ratio{0.0};
|
|
bool has_elastic{false};
|
|
std::optional<double> density;
|
|
std::optional<double> conductivity;
|
|
std::optional<double> specific_heat;
|
|
SourceLocation location;
|
|
SourceLocation elastic_location;
|
|
SourceLocation density_location;
|
|
};
|
|
|
|
struct RawThermalBoundary {
|
|
std::string target;
|
|
int dof;
|
|
double value;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawThermalInitial {
|
|
std::string target;
|
|
double value;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawThermalFlux {
|
|
std::string target;
|
|
int dof;
|
|
double power;
|
|
SourceLocation location;
|
|
};
|
|
|
|
struct RawStep {
|
|
SourceLocation location;
|
|
std::string name_raw;
|
|
std::string name_canonical;
|
|
bool has_static{false};
|
|
bool static_direct{false};
|
|
std::uint32_t static_increment_count{0U};
|
|
std::optional<DynamicStepDefinition> dynamic;
|
|
bool perturbation_explicit{false};
|
|
bool nlgeom_explicit{false};
|
|
StaticProcedureKind procedure_kind{
|
|
StaticProcedureKind::kLinearSmallDisplacement};
|
|
bool nlgeom{false};
|
|
std::uint32_t maximum_increments{0U};
|
|
std::array<double, 4> static_values{};
|
|
std::optional<FrequencyStepDefinition> frequency;
|
|
bool has_heat_transfer{false};
|
|
double thermal_increment{0.0};
|
|
double thermal_period{0.0};
|
|
std::vector<RawThermalBoundary> thermal_boundaries;
|
|
std::vector<RawThermalFlux> thermal_fluxes;
|
|
std::vector<PrescribedDisplacementDefinition> boundaries;
|
|
std::vector<NodalLoad> loads;
|
|
std::vector<RawSurfacePressureLoad> surface_pressure_loads;
|
|
std::size_t next_load_ordinal{0U};
|
|
};
|
|
|
|
struct MappingFailure {
|
|
FailureCategory category;
|
|
Diagnostic diagnostic;
|
|
};
|
|
|
|
} // namespace
|
|
|
|
/// @brief Builds one complete Domain candidate while retaining source identity.
|
|
/// @note The candidate is committed only after all blocks and cross-references
|
|
/// have been validated.
|
|
class DomainMappingContext::Impl {
|
|
public:
|
|
explicit Impl(const ParsedInput& input) : input_{input} {}
|
|
|
|
Status ParseSyntax() {
|
|
ParseBlocks();
|
|
return CurrentStatus();
|
|
}
|
|
|
|
Status FinalizeMaterialProperties() {
|
|
if (failure_) {
|
|
return CurrentStatus();
|
|
}
|
|
ValidateCardinality();
|
|
if (!failure_) {
|
|
FinalizeMaterials();
|
|
}
|
|
if (!failure_) {
|
|
FinalizePartsAndSections();
|
|
}
|
|
if (!failure_) {
|
|
FinalizeDensities();
|
|
}
|
|
return CurrentStatus();
|
|
}
|
|
|
|
Status FinalizeTopology() {
|
|
if (failure_) {
|
|
return CurrentStatus();
|
|
}
|
|
ExpandInstances();
|
|
if (!failure_ && !definition_.shell_elements.empty()) {
|
|
auto geometry =
|
|
PreprocessShellGeometry(definition_.nodes, definition_.shell_elements,
|
|
definition_.shell_sections);
|
|
if (!geometry.HasValue()) {
|
|
const auto& status = geometry.GetStatus();
|
|
failure_ =
|
|
MappingFailure{status.Category().value_or(FailureCategory::kModel),
|
|
status.Diagnostics().front()};
|
|
} else {
|
|
definition_.shell_node_initial_frames =
|
|
std::move(geometry.Value().nodal_frames);
|
|
}
|
|
}
|
|
if (!failure_) {
|
|
ExpandAssemblySets();
|
|
}
|
|
return CurrentStatus();
|
|
}
|
|
|
|
Status FinalizeElementSurfaces() {
|
|
if (!failure_) {
|
|
ResolveElementSurfaces();
|
|
}
|
|
return CurrentStatus();
|
|
}
|
|
|
|
Status FinalizeStepCandidate() {
|
|
if (!failure_) {
|
|
FinalizeStep();
|
|
}
|
|
return CurrentStatus();
|
|
}
|
|
|
|
Result<Domain> Commit() {
|
|
if (failure_) {
|
|
return Result<Domain>::Failure(Status::Failure(
|
|
failure_->category, {std::move(failure_->diagnostic)}));
|
|
}
|
|
SortDiagnostics(definition_.warnings);
|
|
return Domain::Create(std::move(definition_));
|
|
}
|
|
|
|
DomainMappingCandidateSummary CandidateSummary() const noexcept {
|
|
DomainMappingCandidateSummary summary{};
|
|
summary.part_count = parts_.size();
|
|
summary.instance_count = instances_.size();
|
|
summary.material_count = materials_.size();
|
|
summary.model_boundary_count = model_boundaries_.size();
|
|
summary.step_boundary_count = step_.boundaries.size();
|
|
summary.step_load_count = step_.loads.size();
|
|
summary.has_static_step = step_.has_static;
|
|
for (const auto& part : parts_) {
|
|
summary.node_count += part.nodes.size();
|
|
summary.element_count += part.elements.size();
|
|
summary.node_set_count += part.node_sets.size();
|
|
summary.element_set_count += part.element_sets.size();
|
|
summary.beam_section_count += part.sections.size();
|
|
summary.shell_section_count += part.shell_sections.size();
|
|
}
|
|
for (const auto& set : assembly_sets_) {
|
|
if (set.is_node_set) {
|
|
++summary.node_set_count;
|
|
} else {
|
|
++summary.element_set_count;
|
|
}
|
|
}
|
|
return summary;
|
|
}
|
|
|
|
private:
|
|
Status CurrentStatus() const {
|
|
if (!failure_) {
|
|
return Status::Ok();
|
|
}
|
|
return Status::Failure(failure_->category, {failure_->diagnostic});
|
|
}
|
|
|
|
const KeywordParameter* Parameter(const KeywordBlock& block,
|
|
std::string_view name) const {
|
|
const auto found =
|
|
std::find_if(block.parameters.begin(), block.parameters.end(),
|
|
[name](const KeywordParameter& candidate) {
|
|
return candidate.name == name;
|
|
});
|
|
return found == block.parameters.end() ? nullptr : &*found;
|
|
}
|
|
|
|
bool Fail(FailureCategory category, std::string code,
|
|
const SourceLocation& location, std::string keyword,
|
|
std::string entity_identity, std::string message) {
|
|
if (!failure_) {
|
|
failure_ = MappingFailure{
|
|
category,
|
|
{Severity::kError, std::move(code), location, std::move(keyword),
|
|
std::move(entity_identity), std::move(message)}};
|
|
}
|
|
return false;
|
|
}
|
|
|
|
bool InputFailure(std::string code, const SourceLocation& location,
|
|
std::string keyword, std::string entity_identity,
|
|
std::string message) {
|
|
return Fail(FailureCategory::kInput, std::move(code), location,
|
|
std::move(keyword), std::move(entity_identity),
|
|
std::move(message));
|
|
}
|
|
|
|
bool ModelFailure(std::string code, const SourceLocation& location,
|
|
std::string keyword, std::string entity_identity,
|
|
std::string message) {
|
|
return Fail(FailureCategory::kModel, std::move(code), location,
|
|
std::move(keyword), std::move(entity_identity),
|
|
std::move(message));
|
|
}
|
|
|
|
bool InvalidKeywordLocation(const KeywordBlock& block, std::string message) {
|
|
return InputFailure("invalid-keyword-location", block.location,
|
|
block.canonical_name, "", std::move(message));
|
|
}
|
|
|
|
bool ValidateParameters(
|
|
const KeywordBlock& block, const std::vector<std::string_view>& allowed,
|
|
std::string_view unsupported_code = "invalid-keyword-parameter") {
|
|
std::set<std::string> seen;
|
|
for (const auto& candidate : block.parameters) {
|
|
if (!seen.insert(candidate.name).second) {
|
|
return InputFailure("duplicate-entity", block.location,
|
|
block.canonical_name, candidate.name,
|
|
"A keyword parameter may be declared only once.");
|
|
}
|
|
if (std::find(allowed.begin(), allowed.end(), candidate.name) ==
|
|
allowed.end()) {
|
|
return InputFailure(std::string{unsupported_code}, block.location,
|
|
block.canonical_name, candidate.name,
|
|
"The keyword parameter is outside the approved "
|
|
"subset.");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
const std::string* RequiredParameterValue(const KeywordBlock& block,
|
|
std::string_view name) {
|
|
const auto* found = Parameter(block, name);
|
|
if (found == nullptr || !found->value || found->value->empty()) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, std::string{name},
|
|
"The keyword requires a non-empty parameter value.");
|
|
return nullptr;
|
|
}
|
|
return &*found->value;
|
|
}
|
|
|
|
bool RequireNoData(const KeywordBlock& block) {
|
|
if (!block.data.empty()) {
|
|
return InputFailure("invalid-data-arity", block.data.front().location,
|
|
block.canonical_name, "",
|
|
"This keyword does not accept data rows.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseInteger(const std::string& text, std::int64_t& value,
|
|
const SourceLocation& location, const std::string& keyword,
|
|
bool positive) {
|
|
if (text.empty()) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"A numeric field cannot be empty.");
|
|
}
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const long long parsed = std::strtoll(text.c_str(), &end, 10);
|
|
if (errno == ERANGE || end != text.c_str() + text.size() ||
|
|
(positive && parsed <= 0)) {
|
|
return InputFailure(
|
|
"invalid-numeric-value", location, keyword, text,
|
|
"The field must be a valid positive base-10 integer.");
|
|
}
|
|
value = static_cast<std::int64_t>(parsed);
|
|
return true;
|
|
}
|
|
|
|
bool ParseDouble(const std::string& text, double& value,
|
|
const SourceLocation& location, const std::string& keyword) {
|
|
if (text.empty()) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"A numeric field cannot be empty.");
|
|
}
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const double parsed = std::strtod(text.c_str(), &end);
|
|
if (errno == ERANGE || end != text.c_str() + text.size() ||
|
|
!std::isfinite(parsed)) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"The field must be a finite floating-point value.");
|
|
}
|
|
value = parsed;
|
|
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) {
|
|
std::size_t cursor = 0U;
|
|
if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) {
|
|
++cursor;
|
|
}
|
|
const std::size_t integer_start = cursor;
|
|
while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') {
|
|
++cursor;
|
|
}
|
|
const bool has_integer_digits = cursor != integer_start;
|
|
bool has_fraction_digits = false;
|
|
if (cursor < text.size() && text[cursor] == '.') {
|
|
++cursor;
|
|
const std::size_t fraction_start = cursor;
|
|
while (cursor < text.size() && text[cursor] >= '0' &&
|
|
text[cursor] <= '9') {
|
|
++cursor;
|
|
}
|
|
has_fraction_digits = cursor != fraction_start;
|
|
}
|
|
if (!has_integer_digits && !has_fraction_digits) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"Pressure values require C-locale decimal syntax.");
|
|
}
|
|
if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) {
|
|
++cursor;
|
|
if (cursor < text.size() &&
|
|
(text[cursor] == '+' || text[cursor] == '-')) {
|
|
++cursor;
|
|
}
|
|
const std::size_t exponent_start = cursor;
|
|
while (cursor < text.size() && text[cursor] >= '0' &&
|
|
text[cursor] <= '9') {
|
|
++cursor;
|
|
}
|
|
if (cursor == exponent_start) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"Pressure exponents require one or more digits.");
|
|
}
|
|
}
|
|
if (cursor != text.size()) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"Pressure values require C-locale decimal syntax.");
|
|
}
|
|
const char* const begin = text.data();
|
|
const char* const end = begin + text.size();
|
|
const char* const numeric_begin = *begin == '+' ? begin + 1U : begin;
|
|
const auto parsed =
|
|
std::from_chars(numeric_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,
|
|
"Pressure values must be finite C-locale values.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
bool ParseNonlinearStaticDouble(const std::string& text, double& value,
|
|
const SourceLocation& location,
|
|
const std::string& keyword) {
|
|
if (text.empty()) {
|
|
return InputFailure("invalid-nonlinear-static-data", location, keyword,
|
|
text, "A nonlinear STATIC field cannot be empty.");
|
|
}
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const double parsed = std::strtod(text.c_str(), &end);
|
|
if (errno == ERANGE || end != text.c_str() + text.size() ||
|
|
!std::isfinite(parsed)) {
|
|
return InputFailure("invalid-nonlinear-static-data", location, keyword,
|
|
text,
|
|
"All four nonlinear STATIC fields must be finite.");
|
|
}
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
bool ParseDynamicDouble(const std::string& text, double& value,
|
|
const SourceLocation& location,
|
|
const std::string& keyword) {
|
|
if (text.empty()) {
|
|
return InputFailure("invalid-dynamic-data", location, keyword, text,
|
|
"Dynamic data fields cannot be empty.");
|
|
}
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const double parsed = std::strtod(text.c_str(), &end);
|
|
if (errno == ERANGE || end != text.c_str() + text.size() ||
|
|
!std::isfinite(parsed)) {
|
|
return InputFailure("invalid-dynamic-data", location, keyword, text,
|
|
"Dynamic data fields must be finite.");
|
|
}
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
bool ParseModelDouble(const std::string& text, double& value,
|
|
const SourceLocation& location,
|
|
const std::string& keyword,
|
|
const std::string& diagnostic_code,
|
|
const std::string& message) {
|
|
if (text.empty()) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"A numeric field cannot be empty.");
|
|
}
|
|
char* end = nullptr;
|
|
errno = 0;
|
|
const double parsed = std::strtod(text.c_str(), &end);
|
|
if (end != text.c_str() + text.size()) {
|
|
return InputFailure("invalid-numeric-value", location, keyword, text,
|
|
"The field must use floating-point syntax.");
|
|
}
|
|
if (errno == ERANGE || !std::isfinite(parsed)) {
|
|
return ModelFailure(diagnostic_code, location, keyword, text, message);
|
|
}
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
bool ContainsName(const std::vector<RawPart>& values,
|
|
const std::string& name) const {
|
|
return std::any_of(values.begin(), values.end(),
|
|
[&name](const RawPart& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, name);
|
|
});
|
|
}
|
|
|
|
bool ContainsName(const std::vector<RawInstance>& values,
|
|
const std::string& name) const {
|
|
return std::any_of(values.begin(), values.end(),
|
|
[&name](const RawInstance& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, name);
|
|
});
|
|
}
|
|
|
|
bool ContainsName(const std::vector<RawMaterial>& values,
|
|
const std::string& name) const {
|
|
return std::any_of(values.begin(), values.end(),
|
|
[&name](const RawMaterial& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, name);
|
|
});
|
|
}
|
|
|
|
bool ContainsSetName(const std::vector<RawSet>& sets,
|
|
const std::string& name) const {
|
|
const auto matches = [&name](const RawSet& set) {
|
|
return AsciiCaseInsensitiveEquals(set.name, name);
|
|
};
|
|
return std::any_of(sets.begin(), sets.end(), matches);
|
|
}
|
|
|
|
bool ContainsAssemblySetName(bool node_set, const std::string& name) const {
|
|
return std::any_of(assembly_sets_.begin(), assembly_sets_.end(),
|
|
[node_set, &name](const RawAssemblySet& set) {
|
|
return set.is_node_set == node_set &&
|
|
AsciiCaseInsensitiveEquals(set.name, name);
|
|
});
|
|
}
|
|
|
|
bool ContainsSurfaceName(const std::string& name) const {
|
|
return std::any_of(element_surfaces_.begin(), element_surfaces_.end(),
|
|
[&name](const RawElementSurface& surface) {
|
|
return AsciiCaseInsensitiveEquals(surface.name, name);
|
|
});
|
|
}
|
|
|
|
bool IsPressureNameToken(const std::string_view text) const noexcept {
|
|
if (text.empty() || text.size() > 80U) {
|
|
return false;
|
|
}
|
|
const auto is_initial = [](const char value) {
|
|
return (value >= 'A' && value <= 'Z') || (value >= 'a' && value <= 'z') ||
|
|
value == '_';
|
|
};
|
|
const auto is_subsequent = [is_initial](const char value) {
|
|
return is_initial(value) || (value >= '0' && value <= '9') ||
|
|
value == '-' || value == '_';
|
|
};
|
|
return is_initial(text.front()) &&
|
|
std::all_of(text.begin() + 1, text.end(), is_subsequent);
|
|
}
|
|
|
|
void ParseBlocks() {
|
|
definition_.source_path = input_.source_path;
|
|
definition_.source_content_identity = input_.source_content_identity;
|
|
|
|
for (std::size_t index = 0U; index < input_.blocks.size() && !failure_;
|
|
++index) {
|
|
const auto& block = input_.blocks[index];
|
|
if (in_instance_) {
|
|
ParseInstanceBlock(block);
|
|
} else if (current_part_) {
|
|
ParsePartBlock(block);
|
|
} else if (in_assembly_) {
|
|
ParseAssemblyBlock(block);
|
|
} else if (in_step_) {
|
|
ParseStepBlock(block);
|
|
} else {
|
|
ParseTopLevelBlock(block, index);
|
|
}
|
|
}
|
|
|
|
if (!failure_ &&
|
|
(current_part_ || in_assembly_ || in_instance_ || in_step_)) {
|
|
const auto location = input_.blocks.empty()
|
|
? SourceLocation{input_.source_path, 0U}
|
|
: input_.blocks.back().location;
|
|
InputFailure("unclosed-keyword-block", location, "", "",
|
|
"A part, assembly, instance, or step block was not closed.");
|
|
}
|
|
}
|
|
|
|
void ParseTopLevelBlock(const KeywordBlock& block, std::size_t index) {
|
|
if (step_seen_) {
|
|
if (block.canonical_name == "STEP") {
|
|
ParseStepStart(block);
|
|
} else {
|
|
InvalidKeywordLocation(
|
|
block, "The sole analysis step must be the final top-level block.");
|
|
}
|
|
return;
|
|
}
|
|
|
|
if (block.canonical_name != "ELASTIC" &&
|
|
block.canonical_name != "DENSITY" &&
|
|
block.canonical_name != "CONDUCTIVITY" &&
|
|
block.canonical_name != "SPECIFIC HEAT") {
|
|
material_eligible_.reset();
|
|
}
|
|
pending_section_.reset();
|
|
active_output_ = false;
|
|
|
|
if (block.canonical_name == "HEADING") {
|
|
if (index != 0U || heading_seen_ || !ValidateParameters(block, {})) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-keyword-location", block.location,
|
|
block.canonical_name, "",
|
|
"HEADING is optional only as the first keyword.");
|
|
}
|
|
return;
|
|
}
|
|
heading_seen_ = true;
|
|
for (std::size_t row = 0U; row < block.data.size(); ++row) {
|
|
if (row != 0U) {
|
|
definition_.heading.push_back('\n');
|
|
}
|
|
for (std::size_t field = 0U; field < block.data[row].fields.size();
|
|
++field) {
|
|
if (field != 0U) {
|
|
definition_.heading.push_back(',');
|
|
}
|
|
definition_.heading += block.data[row].fields[field];
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (block.canonical_name == "PREPRINT") {
|
|
if (!RequireNoData(block)) {
|
|
return;
|
|
}
|
|
AddIgnoredWarning(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "PHYSICAL CONSTANTS") {
|
|
if (!ValidateParameters(block, {"ABSOLUTE ZERO"}) ||
|
|
!RequireNoData(block))
|
|
return;
|
|
const auto* absolute_zero =
|
|
RequiredParameterValue(block, "ABSOLUTE ZERO");
|
|
if (absolute_zero == nullptr) return;
|
|
double value = 0.0;
|
|
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") {
|
|
ParsePartStart(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "ASSEMBLY") {
|
|
ParseAssemblyStart(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "AMPLITUDE") {
|
|
ParseAmplitude(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "MATERIAL") {
|
|
ParseMaterial(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "ELASTIC") {
|
|
ParseElastic(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "DENSITY") {
|
|
if (!material_eligible_) {
|
|
RejectUnknown(block);
|
|
return;
|
|
}
|
|
ParseDensity(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "CONDUCTIVITY") {
|
|
ParseThermalProperty(block, true);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "SPECIFIC HEAT") {
|
|
ParseThermalProperty(block, false);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "INITIAL CONDITIONS") {
|
|
ParseInitialConditions(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "BOUNDARY") {
|
|
if (!assembly_seen_ || materials_.empty()) {
|
|
InvalidKeywordLocation(
|
|
block, "Model boundary data follows the assembly and materials.");
|
|
return;
|
|
}
|
|
model_boundary_seen_ = true;
|
|
if (model_element_family_ == ElementFamily::kThermalShell) {
|
|
ParseThermalBoundary(block, thermal_model_boundaries_);
|
|
} else {
|
|
ParseBoundary(block, model_boundaries_);
|
|
}
|
|
return;
|
|
}
|
|
if (block.canonical_name == "STEP") {
|
|
ParseStepStart(block);
|
|
return;
|
|
}
|
|
if (block.canonical_name == "END PART" ||
|
|
block.canonical_name == "END ASSEMBLY" ||
|
|
block.canonical_name == "END INSTANCE" ||
|
|
block.canonical_name == "END STEP") {
|
|
InputFailure("invalid-keyword-location", block.location,
|
|
block.canonical_name, "",
|
|
"The closing keyword has no matching open block.");
|
|
return;
|
|
}
|
|
RejectUnknown(block);
|
|
}
|
|
|
|
void ParsePartStart(const KeywordBlock& block) {
|
|
if (assembly_seen_ || !materials_.empty() || model_boundary_seen_) {
|
|
InvalidKeywordLocation(
|
|
block, "All part blocks must precede the sole assembly block.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) {
|
|
return;
|
|
}
|
|
const auto* name = RequiredParameterValue(block, "NAME");
|
|
if (name == nullptr) {
|
|
return;
|
|
}
|
|
if (ContainsName(parts_, *name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name,
|
|
"Part names are unique under case-insensitive lookup.");
|
|
return;
|
|
}
|
|
parts_.push_back({*name, block.location, {}, {}, {}, {}, {}, {}});
|
|
current_part_ = parts_.size() - 1U;
|
|
part_elements_seen_ = false;
|
|
part_sets_seen_ = false;
|
|
part_sections_seen_ = false;
|
|
beam_section_context_active_ = false;
|
|
}
|
|
|
|
void ParsePartBlock(const KeywordBlock& block) {
|
|
if (block.canonical_name == "END PART") {
|
|
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
|
|
return;
|
|
}
|
|
const RawPart& part = parts_[*current_part_];
|
|
const bool shell_part =
|
|
model_element_family_ == ElementFamily::kShell ||
|
|
model_element_family_ == ElementFamily::kThermalShell;
|
|
if (part.nodes.empty() || part.elements.empty() ||
|
|
(!shell_part && model_element_family_ != ElementFamily::kSolid &&
|
|
part.sections.empty())) {
|
|
InvalidKeywordLocation(block,
|
|
"A part closes only after node, element, "
|
|
"and matching section blocks.");
|
|
return;
|
|
}
|
|
current_part_.reset();
|
|
pending_section_.reset();
|
|
beam_section_context_active_ = false;
|
|
return;
|
|
}
|
|
if (block.canonical_name == "PART") {
|
|
InputFailure("invalid-keyword-location", block.location,
|
|
block.canonical_name, "",
|
|
"Nested part blocks are not supported.");
|
|
return;
|
|
}
|
|
if (block.canonical_name == "ASSEMBLY") {
|
|
InputFailure("unsupported-nested-assembly", block.location,
|
|
block.canonical_name, "",
|
|
"An assembly cannot be nested in a part.");
|
|
return;
|
|
}
|
|
|
|
RawPart& part = parts_[*current_part_];
|
|
if (block.canonical_name == "NODE") {
|
|
beam_section_context_active_ = false;
|
|
if (part_elements_seen_ || part_sets_seen_ || part_sections_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"NODE blocks must precede element, set, and section blocks.");
|
|
return;
|
|
}
|
|
pending_section_.reset();
|
|
ParseNodes(block, part);
|
|
} else if (block.canonical_name == "ELEMENT") {
|
|
beam_section_context_active_ = false;
|
|
if (part.nodes.empty() || part_sets_seen_ || part_sections_seen_) {
|
|
InvalidKeywordLocation(block,
|
|
"ELEMENT blocks follow at least one NODE block "
|
|
"and precede sets and sections.");
|
|
return;
|
|
}
|
|
pending_section_.reset();
|
|
ParseElements(block, part);
|
|
part_elements_seen_ = !failure_;
|
|
} else if (block.canonical_name == "NSET") {
|
|
beam_section_context_active_ = false;
|
|
if (!part_elements_seen_ || part_sections_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"Part sets follow element blocks and precede beam sections.");
|
|
return;
|
|
}
|
|
pending_section_.reset();
|
|
ParseSet(block, true, part);
|
|
part_sets_seen_ = !failure_;
|
|
} else if (block.canonical_name == "ELSET") {
|
|
beam_section_context_active_ = false;
|
|
if (!part_elements_seen_ || part_sections_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"Part sets follow element blocks and precede beam sections.");
|
|
return;
|
|
}
|
|
pending_section_.reset();
|
|
ParseSet(block, false, part);
|
|
part_sets_seen_ = !failure_;
|
|
} else if (block.canonical_name == "BEAM GENERAL SECTION") {
|
|
if (!part_elements_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"BEAM GENERAL SECTION follows the part mesh and optional sets.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kShell ||
|
|
model_element_family_ == ElementFamily::kThermalShell ||
|
|
model_element_family_ == ElementFamily::kSolid) {
|
|
InputFailure(
|
|
"unsupported-mixed-element-model", block.location,
|
|
block.canonical_name, "",
|
|
"Beam-section semantics cannot be mixed with shell elements.");
|
|
return;
|
|
}
|
|
ParseBeamSection(block, part);
|
|
part_sections_seen_ = !failure_;
|
|
beam_section_context_active_ = !failure_;
|
|
} else if (block.canonical_name == "SHELL SECTION") {
|
|
beam_section_context_active_ = false;
|
|
if (!part_elements_seen_) {
|
|
InvalidKeywordLocation(
|
|
block, "SHELL SECTION follows the part mesh and optional sets.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kBeam ||
|
|
model_element_family_ == ElementFamily::kSolid) {
|
|
InputFailure(
|
|
"unsupported-mixed-element-model", block.location,
|
|
block.canonical_name, "",
|
|
"Shell-section semantics cannot be mixed with beam elements.");
|
|
return;
|
|
}
|
|
ParseShellSection(block, part);
|
|
part_sections_seen_ = !failure_;
|
|
} else if (block.canonical_name == "SOLID SECTION") {
|
|
beam_section_context_active_ = false;
|
|
if (!part_elements_seen_ ||
|
|
model_element_family_ != ElementFamily::kSolid) {
|
|
InputFailure("invalid-solid-section", block.location,
|
|
block.canonical_name, "",
|
|
"SOLID SECTION requires an exclusive C3D8 mesh.");
|
|
return;
|
|
}
|
|
ParseSolidSection(block, part);
|
|
part_sections_seen_ = !failure_;
|
|
} else if (block.canonical_name == "SECTION POINTS") {
|
|
ParseSectionPoints(block, part);
|
|
} else if (block.canonical_name == "TRANSVERSE SHEAR STIFFNESS") {
|
|
pending_section_.reset();
|
|
if (!beam_section_context_active_) {
|
|
InputFailure(
|
|
"invalid-keyword-location", block.location, block.canonical_name,
|
|
"",
|
|
"The ignored shear keyword still requires beam-section context.");
|
|
return;
|
|
}
|
|
AddIgnoredWarning(block);
|
|
} else if (block.canonical_name == "SURFACE") {
|
|
InputFailure("unsupported-surface-scope", block.location,
|
|
block.canonical_name, "",
|
|
"Pressure surfaces are supported only at assembly scope.");
|
|
} else {
|
|
pending_section_.reset();
|
|
beam_section_context_active_ = false;
|
|
RejectUnknown(block);
|
|
}
|
|
}
|
|
|
|
void ParseNodes(const KeywordBlock& block, RawPart& part) {
|
|
if (!ValidateParameters(block, {}) || block.data.empty()) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "NODE requires at least one four-field data row.");
|
|
}
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != 4U) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"", "NODE rows require label, x, y, z.");
|
|
return;
|
|
}
|
|
RawNode node{};
|
|
node.label_text = row.fields[0];
|
|
node.location = row.location;
|
|
if (!ParseInteger(row.fields[0], node.label, row.location,
|
|
block.canonical_name, true)) {
|
|
return;
|
|
}
|
|
if (std::any_of(part.nodes.begin(), part.nodes.end(),
|
|
[&node](const RawNode& existing) {
|
|
return existing.label == node.label;
|
|
})) {
|
|
InputFailure("duplicate-entity", row.location, block.canonical_name,
|
|
node.label_text, "Node labels are unique within a part.");
|
|
return;
|
|
}
|
|
for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
|
|
if (!ParseDouble(row.fields[coordinate + 1U],
|
|
node.coordinates[coordinate], row.location,
|
|
block.canonical_name)) {
|
|
return;
|
|
}
|
|
}
|
|
part.nodes.push_back(std::move(node));
|
|
}
|
|
}
|
|
|
|
void ParseElements(const KeywordBlock& block, RawPart& part) {
|
|
const auto* type_parameter = Parameter(block, "TYPE");
|
|
const bool thermal_block =
|
|
type_parameter && type_parameter->value &&
|
|
AsciiCaseInsensitiveEquals(*type_parameter->value, "DS4");
|
|
if (!ValidateParameters(
|
|
block, (thermal_block || (type_parameter && type_parameter->value &&
|
|
AsciiCaseInsensitiveEquals(
|
|
*type_parameter->value, "C3D8")))
|
|
? std::vector<std::string_view>{"TYPE", "ELSET"}
|
|
: std::vector<std::string_view>{"TYPE"})) {
|
|
return;
|
|
}
|
|
const auto* type = RequiredParameterValue(block, "TYPE");
|
|
if (type == nullptr) {
|
|
return;
|
|
}
|
|
RawElement::Type element_type{};
|
|
ElementFamily element_family{};
|
|
std::size_t expected_field_count = 0U;
|
|
if (AsciiCaseInsensitiveEquals(*type, "B33")) {
|
|
element_type = RawElement::Type::kB33;
|
|
element_family = ElementFamily::kBeam;
|
|
expected_field_count = 3U;
|
|
} else if (AsciiCaseInsensitiveEquals(*type, "S4")) {
|
|
element_type = RawElement::Type::kS4;
|
|
element_family = ElementFamily::kShell;
|
|
expected_field_count = 5U;
|
|
} else if (AsciiCaseInsensitiveEquals(*type, "S4R")) {
|
|
element_type = RawElement::Type::kS4r;
|
|
element_family = ElementFamily::kShell;
|
|
expected_field_count = 5U;
|
|
} else if (AsciiCaseInsensitiveEquals(*type, "C3D8")) {
|
|
element_type = RawElement::Type::kC3d8;
|
|
element_family = ElementFamily::kSolid;
|
|
expected_field_count = 9U;
|
|
} else if (AsciiCaseInsensitiveEquals(*type, "DS4")) {
|
|
element_type = RawElement::Type::kDs4;
|
|
element_family = ElementFamily::kThermalShell;
|
|
expected_field_count = 5U;
|
|
} else {
|
|
InputFailure("unsupported-element-formulation", block.location,
|
|
block.canonical_name, *type,
|
|
"Only B33, S4, S4R, DS4, and C3D8 belong to "
|
|
"the approved "
|
|
"element subsets.");
|
|
return;
|
|
}
|
|
if (model_element_family_ && *model_element_family_ != element_family) {
|
|
InputFailure(
|
|
"unsupported-mixed-element-model", block.location,
|
|
block.canonical_name, *type,
|
|
"Beam, Shell, thermal, and Solid families cannot mix in one model.");
|
|
return;
|
|
}
|
|
model_element_family_ = element_family;
|
|
std::optional<RawSet> inline_set;
|
|
if (Parameter(block, "ELSET")) {
|
|
const auto* name = RequiredParameterValue(block, "ELSET");
|
|
if (name == nullptr) return;
|
|
if (ContainsSetName(part.element_sets, *name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name, "Element-set names are unique within their part.");
|
|
return;
|
|
}
|
|
inline_set = RawSet{*name, {}, block.location};
|
|
}
|
|
if (block.data.empty()) {
|
|
InputFailure(element_family == ElementFamily::kSolid
|
|
? "invalid-solid-connectivity"
|
|
: (element_family == ElementFamily::kShell
|
|
? "invalid-shell-connectivity"
|
|
: "invalid-data-arity"),
|
|
block.location, block.canonical_name, "",
|
|
"ELEMENT requires at least one row with the approved "
|
|
"connectivity arity.");
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != expected_field_count) {
|
|
InputFailure(
|
|
element_family == ElementFamily::kSolid
|
|
? "invalid-solid-connectivity"
|
|
: (element_family == ElementFamily::kShell
|
|
? "invalid-shell-connectivity"
|
|
: "invalid-data-arity"),
|
|
row.location, block.canonical_name, "",
|
|
element_family == ElementFamily::kShell
|
|
? "S4 and S4R rows require a label and exactly four nodes."
|
|
: "B33 rows require label, node 1, node 2.");
|
|
return;
|
|
}
|
|
RawElement element{};
|
|
element.label_text = row.fields[0];
|
|
element.type = element_type;
|
|
element.location = row.location;
|
|
if (!ParseInteger(row.fields[0], element.label, row.location,
|
|
block.canonical_name, true)) {
|
|
return;
|
|
}
|
|
element.node_labels.reserve(expected_field_count - 1U);
|
|
for (std::size_t field = 1U; field < expected_field_count; ++field) {
|
|
std::int64_t node_label = 0;
|
|
if (!ParseInteger(row.fields[field], node_label, row.location,
|
|
block.canonical_name, true)) {
|
|
return;
|
|
}
|
|
element.node_labels.push_back(node_label);
|
|
}
|
|
if (element_family != ElementFamily::kBeam) {
|
|
const std::set<std::int64_t> distinct_nodes{element.node_labels.begin(),
|
|
element.node_labels.end()};
|
|
if (distinct_nodes.size() != element.node_labels.size()) {
|
|
InputFailure(element_family == ElementFamily::kSolid
|
|
? "invalid-solid-connectivity"
|
|
: "invalid-shell-connectivity",
|
|
row.location, block.canonical_name, element.label_text,
|
|
"Connectivity requires the specified number of distinct "
|
|
"source nodes.");
|
|
return;
|
|
}
|
|
}
|
|
if (std::any_of(part.elements.begin(), part.elements.end(),
|
|
[&element](const RawElement& existing) {
|
|
return existing.label == element.label;
|
|
})) {
|
|
InputFailure("duplicate-entity", row.location, block.canonical_name,
|
|
element.label_text,
|
|
"Element labels are unique within a part.");
|
|
return;
|
|
}
|
|
if (inline_set) inline_set->members.push_back(element.label);
|
|
part.elements.push_back(std::move(element));
|
|
}
|
|
// Inline membership follows the block's source rows and uses the same
|
|
// part-set namespace and section resolution as an explicit ELSET.
|
|
if (inline_set) part.element_sets.push_back(std::move(*inline_set));
|
|
}
|
|
|
|
/// @brief Parses material-only Solid assignment without thickness defaults.
|
|
void ParseSolidSection(const KeywordBlock& block, RawPart& part) {
|
|
if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) return;
|
|
const auto* elset = RequiredParameterValue(block, "ELSET");
|
|
const auto* material = RequiredParameterValue(block, "MATERIAL");
|
|
if (!elset || !material) return;
|
|
if (block.data.size() > 1U ||
|
|
(!block.data.empty() &&
|
|
std::any_of(block.data[0].fields.begin(), block.data[0].fields.end(),
|
|
[](const auto& field) { return !field.empty(); }))) {
|
|
InputFailure("invalid-solid-section", block.location,
|
|
block.canonical_name, *elset,
|
|
"SOLID SECTION permits omitted or blank data only.");
|
|
return;
|
|
}
|
|
part.solid_sections.push_back({*elset, *material, block.location});
|
|
}
|
|
|
|
/// @brief Validates and owns one model-level supported load amplitude.
|
|
void ParseAmplitude(const KeywordBlock& block) {
|
|
const bool dynamic_source = std::any_of(
|
|
input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) {
|
|
return candidate.canonical_name == "DYNAMIC";
|
|
});
|
|
if (model_element_family_ != ElementFamily::kSolid || !dynamic_source) {
|
|
InputFailure("unsupported-load-amplitude", block.location,
|
|
block.canonical_name, "",
|
|
"Amplitude is supported only in Solid dynamic models.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"NAME", "DEFINITION", "TIME", "VALUE"}))
|
|
return;
|
|
const auto* name = RequiredParameterValue(block, "NAME");
|
|
if (!name) return;
|
|
for (const auto& option :
|
|
std::array<std::pair<const char*, const char*>, 3>{
|
|
{{"DEFINITION", "TABULAR"},
|
|
{"TIME", "STEP TIME"},
|
|
{"VALUE", "RELATIVE"}}}) {
|
|
const auto* parameter = Parameter(block, option.first);
|
|
if (parameter &&
|
|
(!parameter->value ||
|
|
!AsciiCaseInsensitiveEquals(*parameter->value, option.second))) {
|
|
InputFailure("unsupported-amplitude-option", block.location,
|
|
block.canonical_name, option.first,
|
|
"Only TABULAR STEP TIME RELATIVE amplitude is supported.");
|
|
return;
|
|
}
|
|
}
|
|
const std::string canonical = CanonicalizeAscii(*name);
|
|
if (std::any_of(definition_.amplitudes.begin(),
|
|
definition_.amplitudes.end(), [&canonical](const auto& a) {
|
|
return a.name_canonical == canonical;
|
|
})) {
|
|
InputFailure("duplicate-amplitude", block.location, block.canonical_name,
|
|
*name,
|
|
"Amplitude names must be unique ignoring ASCII case.");
|
|
return;
|
|
}
|
|
TabularAmplitudeDefinition amplitude{*name, canonical, block.location, {}};
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.empty() || row.fields.size() % 2U != 0U) {
|
|
InputFailure("invalid-amplitude-data", row.location,
|
|
block.canonical_name, *name,
|
|
"Amplitude rows require complete time/value pairs.");
|
|
return;
|
|
}
|
|
for (std::size_t field = 0U; field < row.fields.size(); field += 2U) {
|
|
double time = 0.0;
|
|
double value = 0.0;
|
|
if (!ParseDouble(row.fields[field], time, row.location,
|
|
block.canonical_name) ||
|
|
!ParseDouble(row.fields[field + 1U], value, row.location,
|
|
block.canonical_name))
|
|
return;
|
|
if (time < 0.0 || (!amplitude.points.empty() &&
|
|
!(time > amplitude.points.back()[0]))) {
|
|
InputFailure(
|
|
"invalid-amplitude-data", row.location, block.canonical_name,
|
|
*name,
|
|
"Amplitude times must be nonnegative and strictly increasing.");
|
|
return;
|
|
}
|
|
amplitude.points.push_back({time, value});
|
|
}
|
|
}
|
|
if (amplitude.points.empty()) {
|
|
InputFailure("invalid-amplitude-data", block.location,
|
|
block.canonical_name, *name,
|
|
"Amplitude requires at least one finite time/value pair.");
|
|
return;
|
|
}
|
|
definition_.amplitudes.push_back(std::move(amplitude));
|
|
}
|
|
|
|
void ParseShellSection(const KeywordBlock& block, RawPart& part) {
|
|
for (const auto& candidate : block.parameters) {
|
|
if (candidate.name != "ELSET" && candidate.name != "MATERIAL") {
|
|
InputFailure("unsupported-shell-section-option", block.location,
|
|
block.canonical_name, candidate.name,
|
|
"The shell-section option is outside the centered "
|
|
"single-layer subset.");
|
|
return;
|
|
}
|
|
}
|
|
if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) {
|
|
return;
|
|
}
|
|
const auto* element_set = RequiredParameterValue(block, "ELSET");
|
|
const auto* material = RequiredParameterValue(block, "MATERIAL");
|
|
if (element_set == nullptr || material == nullptr) {
|
|
return;
|
|
}
|
|
if (block.data.size() != 1U || block.data[0].fields.empty() ||
|
|
block.data[0].fields.size() > 2U) {
|
|
InputFailure("unsupported-shell-section-option", block.location,
|
|
block.canonical_name, *element_set,
|
|
"SHELL SECTION requires one thickness row with one optional "
|
|
"integration-point field.");
|
|
return;
|
|
}
|
|
double thickness = 0.0;
|
|
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)) {
|
|
ModelFailure("invalid-shell-thickness", block.data[0].location,
|
|
block.canonical_name, *element_set,
|
|
"Shell thickness must be finite and positive.");
|
|
return;
|
|
}
|
|
if (block.data[0].fields.size() == 2U) {
|
|
const auto integration_points =
|
|
ParsePositiveSourceLabel(block.data[0].fields[1]);
|
|
if (!integration_points.HasValue()) {
|
|
InputFailure("unsupported-shell-section-option", block.data[0].location,
|
|
block.canonical_name, block.data[0].fields[1],
|
|
"The optional shell integration-point field must be a "
|
|
"positive integer.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kThermalShell &&
|
|
integration_points.Value() != 5) {
|
|
InputFailure(
|
|
"unsupported-shell-section-option", block.data[0].location,
|
|
block.canonical_name, block.data[0].fields[1],
|
|
"Thermal DS4 sections require exactly five integration points.");
|
|
return;
|
|
}
|
|
}
|
|
part.shell_sections.push_back(
|
|
{*element_set, *material, thickness, block.location});
|
|
}
|
|
|
|
bool ParseSetMembers(const KeywordBlock& block, bool generate,
|
|
std::vector<std::int64_t>& members) {
|
|
if (block.data.empty()) {
|
|
return InputFailure("invalid-data-arity", block.location,
|
|
block.canonical_name, "",
|
|
"A set requires at least one member row.");
|
|
}
|
|
for (const auto& data : block.data) {
|
|
auto fields = WithoutTrailingEmpty(data.fields);
|
|
if (generate) {
|
|
if (fields.size() != 3U) {
|
|
return InputFailure("invalid-data-arity", data.location,
|
|
block.canonical_name, "",
|
|
"GENERATE rows require first, last, increment.");
|
|
}
|
|
std::int64_t first = 0;
|
|
std::int64_t last = 0;
|
|
std::int64_t increment = 0;
|
|
if (!ParseInteger(fields[0], first, data.location, block.canonical_name,
|
|
true) ||
|
|
!ParseInteger(fields[1], last, data.location, block.canonical_name,
|
|
true)) {
|
|
return false;
|
|
}
|
|
if (!ParseInteger(fields[2], increment, data.location,
|
|
block.canonical_name, false) ||
|
|
increment <= 0 || first > last || (last - first) % increment != 0) {
|
|
return InputFailure("invalid-set-range", data.location,
|
|
block.canonical_name, "",
|
|
"GENERATE requires an inclusive range reached by "
|
|
"a positive increment.");
|
|
}
|
|
for (std::int64_t label = first; label <= last;) {
|
|
members.push_back(label);
|
|
if (label > last - increment) {
|
|
break;
|
|
}
|
|
label += increment;
|
|
}
|
|
} else {
|
|
if (fields.empty() || std::any_of(fields.begin(), fields.end(),
|
|
[](const std::string& field) {
|
|
return field.empty();
|
|
})) {
|
|
return InputFailure(
|
|
"invalid-data-arity", data.location, block.canonical_name, "",
|
|
"Explicit set rows require non-empty member labels.");
|
|
}
|
|
for (const auto& field : fields) {
|
|
std::int64_t label = 0;
|
|
if (!ParseInteger(field, label, data.location, block.canonical_name,
|
|
true)) {
|
|
return false;
|
|
}
|
|
members.push_back(label);
|
|
}
|
|
}
|
|
}
|
|
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,
|
|
block.canonical_name, std::to_string(member),
|
|
"A set cannot repeat the same source member.");
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ParseSet(const KeywordBlock& block, bool node_set, RawPart& part) {
|
|
const std::string_view name_parameter = node_set ? "NSET" : "ELSET";
|
|
if (!ValidateParameters(block, {name_parameter, "GENERATE"})) {
|
|
return;
|
|
}
|
|
const auto* name = RequiredParameterValue(block, name_parameter);
|
|
if (name == nullptr) {
|
|
return;
|
|
}
|
|
const auto* generate_parameter = Parameter(block, "GENERATE");
|
|
if (generate_parameter != nullptr && generate_parameter->value) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, "GENERATE",
|
|
"GENERATE is a valueless flag.");
|
|
return;
|
|
}
|
|
const auto& sets = node_set ? part.node_sets : part.element_sets;
|
|
if (ContainsSetName(sets, *name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name,
|
|
"Set names are unique within their node-set or element-set "
|
|
"namespace.");
|
|
return;
|
|
}
|
|
RawSet set{*name, {}, block.location};
|
|
if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) {
|
|
return;
|
|
}
|
|
if (node_set) {
|
|
part.node_sets.push_back(std::move(set));
|
|
} else {
|
|
part.element_sets.push_back(std::move(set));
|
|
}
|
|
}
|
|
|
|
void ParseBeamSection(const KeywordBlock& block, RawPart& part) {
|
|
pending_section_.reset();
|
|
if (!ValidateParameters(block, {"ELSET", "MATERIAL", "SECTION"})) {
|
|
return;
|
|
}
|
|
const auto* element_set = RequiredParameterValue(block, "ELSET");
|
|
const auto* material = RequiredParameterValue(block, "MATERIAL");
|
|
const auto* section = RequiredParameterValue(block, "SECTION");
|
|
if (element_set == nullptr || material == nullptr || section == nullptr) {
|
|
return;
|
|
}
|
|
if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) {
|
|
InputFailure("unsupported-section-formulation", block.location,
|
|
block.canonical_name, *section,
|
|
"Only SECTION=GENERAL belongs to the approved subset.");
|
|
return;
|
|
}
|
|
if (std::any_of(part.sections.begin(), part.sections.end(),
|
|
[element_set](const RawSection& existing) {
|
|
return AsciiCaseInsensitiveEquals(
|
|
existing.element_set_name, *element_set);
|
|
})) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*element_set,
|
|
"An element set can receive only one beam section.");
|
|
return;
|
|
}
|
|
if (block.data.size() != 2U || block.data[0].fields.size() != 5U ||
|
|
block.data[1].fields.size() != 3U) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
*element_set,
|
|
"A general section requires one five-field property row and "
|
|
"one three-field axis row.");
|
|
return;
|
|
}
|
|
RawSection raw{*element_set, *material, {}, {}, {}, block.location};
|
|
for (std::size_t property = 0U; property < 5U; ++property) {
|
|
if (!ParseModelDouble(block.data[0].fields[property],
|
|
raw.properties[property], block.data[0].location,
|
|
block.canonical_name, "invalid-beam-property",
|
|
"Beam section properties must be finite.")) {
|
|
return;
|
|
}
|
|
}
|
|
for (std::size_t coordinate = 0U; coordinate < 3U; ++coordinate) {
|
|
if (!ParseModelDouble(block.data[1].fields[coordinate],
|
|
raw.first_axis[coordinate], block.data[1].location,
|
|
block.canonical_name, "invalid-beam-guide-vector",
|
|
"The beam guide vector must be finite.")) {
|
|
return;
|
|
}
|
|
}
|
|
part.sections.push_back(std::move(raw));
|
|
pending_section_ = part.sections.size() - 1U;
|
|
}
|
|
|
|
void ParseSectionPoints(const KeywordBlock& block, RawPart& part) {
|
|
if (!pending_section_ || !ValidateParameters(block, {}) ||
|
|
block.data.empty()) {
|
|
if (!failure_) {
|
|
InputFailure(
|
|
"invalid-keyword-location", block.location, block.canonical_name,
|
|
"", "SECTION POINTS must immediately follow a general section.");
|
|
}
|
|
return;
|
|
}
|
|
RawSection& section = part.sections[*pending_section_];
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != 2U) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
section.element_set_name,
|
|
"Section-point rows require x1 and x2.");
|
|
return;
|
|
}
|
|
std::array<double, 2> point{};
|
|
if (!ParseDouble(row.fields[0], point[0], row.location,
|
|
block.canonical_name) ||
|
|
!ParseDouble(row.fields[1], point[1], row.location,
|
|
block.canonical_name)) {
|
|
return;
|
|
}
|
|
if (std::find(section.section_points.begin(),
|
|
section.section_points.end(),
|
|
point) != section.section_points.end()) {
|
|
InputFailure("duplicate-entity", row.location, block.canonical_name,
|
|
section.element_set_name,
|
|
"Section points must be unique within a section.");
|
|
return;
|
|
}
|
|
section.section_points.push_back(point);
|
|
}
|
|
pending_section_.reset();
|
|
}
|
|
|
|
void ParseAssemblyStart(const KeywordBlock& block) {
|
|
if (assembly_seen_) {
|
|
InputFailure("unsupported-nested-assembly", block.location,
|
|
block.canonical_name, "",
|
|
"V0 accepts exactly one non-nested assembly.");
|
|
return;
|
|
}
|
|
if (parts_.empty() || !materials_.empty() || model_boundary_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"The sole assembly follows all part blocks and precedes model data.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) {
|
|
return;
|
|
}
|
|
if (RequiredParameterValue(block, "NAME") == nullptr) {
|
|
return;
|
|
}
|
|
assembly_seen_ = true;
|
|
in_assembly_ = true;
|
|
assembly_set_seen_ = false;
|
|
}
|
|
|
|
void ParseAssemblyBlock(const KeywordBlock& block) {
|
|
if (block.canonical_name == "END ASSEMBLY") {
|
|
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
|
|
return;
|
|
}
|
|
if (instances_.empty()) {
|
|
InvalidKeywordLocation(
|
|
block, "The assembly requires at least one identity instance.");
|
|
return;
|
|
}
|
|
in_assembly_ = false;
|
|
return;
|
|
}
|
|
if (block.canonical_name == "ASSEMBLY") {
|
|
InputFailure("unsupported-nested-assembly", block.location,
|
|
block.canonical_name, "",
|
|
"Nested or duplicate assembly blocks are unsupported.");
|
|
} else if (block.canonical_name == "INSTANCE") {
|
|
if (assembly_set_seen_) {
|
|
InvalidKeywordLocation(
|
|
block, "All identity instances must precede assembly-level sets.");
|
|
return;
|
|
}
|
|
ParseInstanceStart(block);
|
|
} else if (block.canonical_name == "NSET") {
|
|
if (instances_.empty()) {
|
|
InvalidKeywordLocation(
|
|
block, "Assembly sets follow at least one identity instance.");
|
|
return;
|
|
}
|
|
assembly_set_seen_ = true;
|
|
ParseAssemblySet(block, true);
|
|
} else if (block.canonical_name == "ELSET") {
|
|
if (instances_.empty()) {
|
|
InvalidKeywordLocation(
|
|
block, "Assembly sets follow at least one identity instance.");
|
|
return;
|
|
}
|
|
assembly_set_seen_ = true;
|
|
ParseAssemblySet(block, false);
|
|
} else if (block.canonical_name == "SURFACE") {
|
|
if (instances_.empty()) {
|
|
InvalidKeywordLocation(
|
|
block, "Assembly surfaces follow at least one identity instance.");
|
|
return;
|
|
}
|
|
ParseElementSurface(block);
|
|
} else {
|
|
RejectUnknown(block);
|
|
}
|
|
}
|
|
|
|
void ParseInstanceStart(const KeywordBlock& block) {
|
|
if (Parameter(block, "DEPENDENT") != nullptr ||
|
|
Parameter(block, "INDEPENDENT") != nullptr) {
|
|
InputFailure(
|
|
"unsupported-instance-mesh-semantics", block.location,
|
|
block.canonical_name, "",
|
|
"Dependent and independent instance mesh semantics are unsupported.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"NAME", "PART"})) {
|
|
return;
|
|
}
|
|
const auto* name = RequiredParameterValue(block, "NAME");
|
|
const auto* part = RequiredParameterValue(block, "PART");
|
|
if (name == nullptr || part == nullptr) {
|
|
return;
|
|
}
|
|
if (!block.data.empty()) {
|
|
InputFailure("unsupported-instance-transform",
|
|
block.data.front().location, block.canonical_name, *name,
|
|
"Instance translation or rotation data is unsupported.");
|
|
return;
|
|
}
|
|
if (ContainsName(instances_, *name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name, "Instance names are globally unique.");
|
|
return;
|
|
}
|
|
instances_.push_back({*name, *part, block.location});
|
|
in_instance_ = true;
|
|
}
|
|
|
|
void ParseInstanceBlock(const KeywordBlock& block) {
|
|
if (block.canonical_name == "END INSTANCE") {
|
|
if (!ValidateParameters(block, {}) || !RequireNoData(block)) {
|
|
return;
|
|
}
|
|
in_instance_ = false;
|
|
return;
|
|
}
|
|
if (block.canonical_name == "ASSEMBLY") {
|
|
InputFailure("unsupported-nested-assembly", block.location,
|
|
block.canonical_name, "",
|
|
"An assembly cannot be nested in an instance.");
|
|
return;
|
|
}
|
|
if (block.canonical_name == "SURFACE") {
|
|
InputFailure("unsupported-surface-scope", block.location,
|
|
block.canonical_name, instances_.back().name,
|
|
"Pressure surfaces are unsupported inside an instance.");
|
|
return;
|
|
}
|
|
InputFailure("unsupported-instance-mesh-semantics", block.location,
|
|
block.canonical_name, instances_.back().name,
|
|
"Instance-local mesh definitions are unsupported.");
|
|
}
|
|
|
|
void ParseAssemblySet(const KeywordBlock& block, bool node_set) {
|
|
const std::string_view name_parameter = node_set ? "NSET" : "ELSET";
|
|
const std::vector<std::string_view> allowed_parameters =
|
|
node_set ? std::vector<std::string_view>{"NSET", "INSTANCE", "GENERATE"}
|
|
: std::vector<std::string_view>{"ELSET", "INSTANCE",
|
|
"GENERATE", "INTERNAL"};
|
|
if (!ValidateParameters(block, allowed_parameters)) {
|
|
return;
|
|
}
|
|
const auto* name = RequiredParameterValue(block, name_parameter);
|
|
if (name == nullptr) {
|
|
return;
|
|
}
|
|
const auto* instance_parameter = Parameter(block, "INSTANCE");
|
|
if (instance_parameter == nullptr || !instance_parameter->value ||
|
|
instance_parameter->value->empty()) {
|
|
InputFailure(
|
|
node_set ? "invalid-keyword-parameter" : "implicit-instance-elset",
|
|
block.location, block.canonical_name, "INSTANCE",
|
|
"Assembly element sets require an explicit INSTANCE.");
|
|
return;
|
|
}
|
|
const std::string& instance = *instance_parameter->value;
|
|
const auto* generate_parameter = Parameter(block, "GENERATE");
|
|
if (generate_parameter != nullptr && generate_parameter->value) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, "GENERATE",
|
|
"GENERATE is a valueless flag.");
|
|
return;
|
|
}
|
|
const auto* internal_parameter = Parameter(block, "INTERNAL");
|
|
if (internal_parameter != nullptr && internal_parameter->value) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, "INTERNAL",
|
|
"INTERNAL is a valueless result-membership flag.");
|
|
return;
|
|
}
|
|
if (ContainsAssemblySetName(node_set, *name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name,
|
|
"Assembly set names are unique within their node-set or "
|
|
"element-set namespace.");
|
|
return;
|
|
}
|
|
if (!node_set && generate_parameter == nullptr) {
|
|
for (const auto& row : block.data) {
|
|
for (const auto& field : WithoutTrailingEmpty(row.fields)) {
|
|
if (field.empty()) {
|
|
continue;
|
|
}
|
|
const bool numeric =
|
|
!field.empty() &&
|
|
std::all_of(field.begin(), field.end(), [](const char value) {
|
|
return value >= '0' && value <= '9';
|
|
});
|
|
if (!numeric) {
|
|
const std::string code = AsciiCaseInsensitiveEquals(field, *name)
|
|
? "cyclic-elset"
|
|
: "nested-elset";
|
|
InputFailure(code, row.location, block.canonical_name, field,
|
|
"Assembly ELSET members must be direct labels.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
RawAssemblySet set{node_set, *name, instance, {}, block.location};
|
|
if (!ParseSetMembers(block, generate_parameter != nullptr, set.members)) {
|
|
return;
|
|
}
|
|
assembly_sets_.push_back(std::move(set));
|
|
}
|
|
|
|
void ParseElementSurface(const KeywordBlock& block) {
|
|
if (!ValidateParameters(block, {"TYPE", "NAME"}, "unsupported-parameter")) {
|
|
return;
|
|
}
|
|
const auto* type = RequiredParameterValue(block, "TYPE");
|
|
const auto* name = RequiredParameterValue(block, "NAME");
|
|
if (type == nullptr || name == nullptr) {
|
|
return;
|
|
}
|
|
if (!IsPressureNameToken(*name)) {
|
|
InputFailure("invalid-surface-name", block.location, block.canonical_name,
|
|
*name,
|
|
"Surface names must use the approved ASCII name syntax.");
|
|
return;
|
|
}
|
|
if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) {
|
|
InputFailure("unsupported-surface-type", block.location,
|
|
block.canonical_name, *type,
|
|
"Only TYPE=ELEMENT belongs to the approved surface subset.");
|
|
return;
|
|
}
|
|
if (ContainsSurfaceName(*name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name,
|
|
"Surface names are unique in the assembly namespace.");
|
|
return;
|
|
}
|
|
if (block.data.empty()) {
|
|
InputFailure("empty-surface", block.location, block.canonical_name, *name,
|
|
"An element surface requires at least one member.");
|
|
return;
|
|
}
|
|
|
|
RawElementSurface surface{
|
|
*name, block.location, element_surfaces_.size(), {}};
|
|
for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) {
|
|
const auto& row = block.data[ordinal];
|
|
if (row.fields.size() != 2U || row.fields[0].empty() ||
|
|
row.fields[1].empty()) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
*name,
|
|
"Surface rows require one member and SPOS or SNEG.");
|
|
return;
|
|
}
|
|
ShellSurfaceSide side = ShellSurfaceSide::kSpos;
|
|
if (AsciiCaseInsensitiveEquals(row.fields[1], "SPOS")) {
|
|
side = ShellSurfaceSide::kSpos;
|
|
} else if (AsciiCaseInsensitiveEquals(row.fields[1], "SNEG")) {
|
|
side = ShellSurfaceSide::kSneg;
|
|
} else {
|
|
InputFailure("unsupported-surface-side", row.location,
|
|
block.canonical_name, row.fields[1],
|
|
"Surface sides must be SPOS or SNEG.");
|
|
return;
|
|
}
|
|
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) {
|
|
if (!assembly_seen_ || model_boundary_seen_) {
|
|
InvalidKeywordLocation(block,
|
|
"Material definitions follow the assembly and "
|
|
"precede model boundaries.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"NAME"}) || !RequireNoData(block)) {
|
|
return;
|
|
}
|
|
const auto* name = RequiredParameterValue(block, "NAME");
|
|
if (name == nullptr) {
|
|
return;
|
|
}
|
|
if (ContainsName(materials_, *name)) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
*name, "Material names are globally unique.");
|
|
return;
|
|
}
|
|
materials_.push_back({*name,
|
|
0.0,
|
|
0.0,
|
|
false,
|
|
std::nullopt,
|
|
std::nullopt,
|
|
std::nullopt,
|
|
block.location,
|
|
{},
|
|
{}});
|
|
material_eligible_ = materials_.size() - 1U;
|
|
}
|
|
|
|
void ParseElastic(const KeywordBlock& block) {
|
|
if (model_element_family_ == ElementFamily::kThermalShell) {
|
|
InputFailure("unsupported-keyword", block.location, block.canonical_name,
|
|
"", "ELASTIC is outside the thermal material subset.");
|
|
return;
|
|
}
|
|
if (!material_eligible_) {
|
|
InputFailure("invalid-keyword-location", block.location,
|
|
block.canonical_name, "",
|
|
"ELASTIC must immediately follow MATERIAL.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {}) || block.data.size() != 1U ||
|
|
block.data[0].fields.size() != 2U) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
materials_[*material_eligible_].name,
|
|
"ELASTIC requires exactly one E, nu row.");
|
|
}
|
|
return;
|
|
}
|
|
RawMaterial& material = materials_[*material_eligible_];
|
|
if (material.has_elastic) {
|
|
InputFailure("duplicate-entity", block.location, block.canonical_name,
|
|
material.name, "A material accepts one ELASTIC definition.");
|
|
return;
|
|
}
|
|
if (!ParseModelDouble(block.data[0].fields[0], material.youngs_modulus,
|
|
block.data[0].location, block.canonical_name,
|
|
model_element_family_ == ElementFamily::kSolid
|
|
? "invalid-solid-material"
|
|
: model_element_family_ == ElementFamily::kShell
|
|
? "invalid-shell-material"
|
|
: "invalid-beam-property",
|
|
"Elastic material values must be finite.") ||
|
|
!ParseModelDouble(block.data[0].fields[1], material.poisson_ratio,
|
|
block.data[0].location, block.canonical_name,
|
|
model_element_family_ == ElementFamily::kSolid
|
|
? "invalid-solid-material"
|
|
: model_element_family_ == ElementFamily::kShell
|
|
? "invalid-shell-material"
|
|
: "invalid-beam-property",
|
|
"Elastic material values must be finite.")) {
|
|
return;
|
|
}
|
|
material.has_elastic = true;
|
|
material.elastic_location = block.data[0].location;
|
|
}
|
|
|
|
bool ParseDensityValue(const std::string& text, double& value,
|
|
const SourceLocation& location) {
|
|
const std::string canonical = CanonicalizeAscii(text);
|
|
if (canonical == "NAN" || canonical == "+NAN" || canonical == "-NAN" ||
|
|
canonical == "INF" || canonical == "+INF" || canonical == "-INF" ||
|
|
canonical == "INFINITY" || canonical == "+INFINITY" ||
|
|
canonical == "-INFINITY") {
|
|
return ModelFailure(
|
|
"invalid-material-density", location, "DENSITY", text,
|
|
"Density must convert to a finite value greater than zero.");
|
|
}
|
|
std::size_t cursor = 0U;
|
|
if (cursor < text.size() && (text[cursor] == '+' || text[cursor] == '-')) {
|
|
++cursor;
|
|
}
|
|
const std::size_t integer_start = cursor;
|
|
while (cursor < text.size() && text[cursor] >= '0' && text[cursor] <= '9') {
|
|
++cursor;
|
|
}
|
|
const bool has_integer_digits = cursor != integer_start;
|
|
bool has_fraction_digits = false;
|
|
if (cursor < text.size() && text[cursor] == '.') {
|
|
++cursor;
|
|
const std::size_t fraction_start = cursor;
|
|
while (cursor < text.size() && text[cursor] >= '0' &&
|
|
text[cursor] <= '9') {
|
|
++cursor;
|
|
}
|
|
has_fraction_digits = cursor != fraction_start;
|
|
}
|
|
if (!has_integer_digits && !has_fraction_digits) {
|
|
return InputFailure("invalid-density-data", location, "DENSITY", text,
|
|
"Density requires C-locale decimal syntax.");
|
|
}
|
|
if (cursor < text.size() && (text[cursor] == 'E' || text[cursor] == 'e')) {
|
|
++cursor;
|
|
if (cursor < text.size() &&
|
|
(text[cursor] == '+' || text[cursor] == '-')) {
|
|
++cursor;
|
|
}
|
|
const std::size_t exponent_start = cursor;
|
|
while (cursor < text.size() && text[cursor] >= '0' &&
|
|
text[cursor] <= '9') {
|
|
++cursor;
|
|
}
|
|
if (cursor == exponent_start) {
|
|
return InputFailure("invalid-density-data", location, "DENSITY", text,
|
|
"Density exponents require one or more digits.");
|
|
}
|
|
}
|
|
if (cursor != text.size()) {
|
|
return InputFailure("invalid-density-data", location, "DENSITY", text,
|
|
"Density requires C-locale decimal syntax.");
|
|
}
|
|
const char* begin = text.data();
|
|
if (*begin == '+') {
|
|
++begin;
|
|
}
|
|
double parsed = 0.0;
|
|
const auto conversion = std::from_chars(begin, text.data() + text.size(),
|
|
parsed, std::chars_format::general);
|
|
if (conversion.ec != std::errc{} ||
|
|
conversion.ptr != text.data() + text.size() || !std::isfinite(parsed) ||
|
|
!(parsed > 0.0)) {
|
|
return ModelFailure(
|
|
"invalid-material-density", location, "DENSITY", text,
|
|
"Density must convert to a finite value greater than zero.");
|
|
}
|
|
value = parsed;
|
|
return true;
|
|
}
|
|
|
|
void ParseDensity(const KeywordBlock& block) {
|
|
if (!material_eligible_) {
|
|
InputFailure("invalid-density-data", block.location, block.canonical_name,
|
|
"", "DENSITY must be declared inside one MATERIAL.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {}, "unsupported-density-option")) {
|
|
return;
|
|
}
|
|
RawMaterial& material = materials_[*material_eligible_];
|
|
if (material.density) {
|
|
InputFailure("duplicate-density", block.location, block.canonical_name,
|
|
material.name, "A material accepts one DENSITY definition.");
|
|
return;
|
|
}
|
|
if (block.data.size() != 1U ||
|
|
(block.data[0].fields.size() != 1U &&
|
|
(block.data[0].fields.size() != 2U ||
|
|
!block.data[0].fields[1].empty())) ||
|
|
block.data[0].fields[0].empty()) {
|
|
InputFailure("invalid-density-data", block.location, block.canonical_name,
|
|
material.name, "DENSITY requires exactly rho or rho,.");
|
|
return;
|
|
}
|
|
double rho = 0.0;
|
|
if (!ParseDensityValue(block.data[0].fields[0], rho,
|
|
block.data[0].location)) {
|
|
return;
|
|
}
|
|
material.density = rho;
|
|
material.density_location = block.data[0].location;
|
|
}
|
|
|
|
void ParseThermalProperty(const KeywordBlock& block, bool conductivity) {
|
|
if (model_element_family_ != ElementFamily::kThermalShell ||
|
|
!material_eligible_ || !ValidateParameters(block, {})) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-keyword-location", block.location,
|
|
block.canonical_name, "",
|
|
"Thermal properties belong to a DS4 material.");
|
|
}
|
|
return;
|
|
}
|
|
RawMaterial& material = materials_[*material_eligible_];
|
|
std::optional<double>& destination =
|
|
conductivity ? material.conductivity : material.specific_heat;
|
|
if (destination || block.data.size() != 1U ||
|
|
(block.data[0].fields.size() != 1U &&
|
|
(block.data[0].fields.size() != 2U ||
|
|
!block.data[0].fields[1].empty()))) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
material.name,
|
|
"Thermal property requires one constant row.");
|
|
return;
|
|
}
|
|
double value = 0.0;
|
|
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,
|
|
block.canonical_name, material.name,
|
|
"Thermal material values must be finite and positive.");
|
|
}
|
|
return;
|
|
}
|
|
destination = value;
|
|
}
|
|
|
|
void ParseInitialConditions(const KeywordBlock& block) {
|
|
if (model_element_family_ != ElementFamily::kThermalShell ||
|
|
!ValidateParameters(block, {"TYPE"}) || block.data.empty()) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "Thermal initial conditions require data rows.");
|
|
}
|
|
return;
|
|
}
|
|
const auto* type = RequiredParameterValue(block, "TYPE");
|
|
if (type == nullptr || !AsciiCaseInsensitiveEquals(*type, "TEMPERATURE")) {
|
|
if (!failure_)
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "TYPE",
|
|
"Only TYPE=TEMPERATURE is supported.");
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != 2U || row.fields[0].empty()) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"", "Initial temperatures require target, value.");
|
|
return;
|
|
}
|
|
double value = 0.0;
|
|
if (!ParseThermalDouble(row.fields[1], value, row.location,
|
|
block.canonical_name))
|
|
return;
|
|
thermal_initial_conditions_.push_back(
|
|
{row.fields[0], value, row.location});
|
|
}
|
|
}
|
|
|
|
void ParseThermalBoundary(const KeywordBlock& block,
|
|
std::vector<RawThermalBoundary>& destination) {
|
|
if (!ValidateParameters(block, {}) || block.data.empty()) {
|
|
if (!failure_)
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "Thermal BOUNDARY requires data rows.");
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if ((row.fields.size() != 3U && row.fields.size() != 4U) ||
|
|
row.fields[0].empty()) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"", "Thermal boundaries require target, DOF, DOF, value.");
|
|
return;
|
|
}
|
|
std::int64_t first = 0;
|
|
std::int64_t last = 0;
|
|
if (!ParseInteger(row.fields[1], first, row.location,
|
|
block.canonical_name, true) ||
|
|
!ParseInteger(row.fields[2], last, row.location, block.canonical_name,
|
|
true) ||
|
|
first != last || (first != 11 && first != 15)) {
|
|
InputFailure("invalid-dof", row.location, block.canonical_name,
|
|
row.fields[0], "Thermal boundaries support DOF 11 or 15.");
|
|
return;
|
|
}
|
|
double value = 0.0;
|
|
if (row.fields.size() == 4U &&
|
|
!ParseThermalDouble(row.fields[3], value, row.location,
|
|
block.canonical_name))
|
|
return;
|
|
destination.push_back(
|
|
{row.fields[0], static_cast<int>(first), value, row.location});
|
|
}
|
|
}
|
|
|
|
void ParseBoundary(
|
|
const KeywordBlock& block,
|
|
std::vector<PrescribedDisplacementDefinition>& destination) {
|
|
const bool dynamic_input = std::any_of(
|
|
input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) {
|
|
return candidate.canonical_name == "DYNAMIC";
|
|
});
|
|
if (!ValidateParameters(block, {},
|
|
dynamic_input ? "unsupported-dynamic-boundary"
|
|
: "invalid-keyword-parameter") ||
|
|
block.data.empty()) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "BOUNDARY requires one or more data rows.");
|
|
}
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != 3U && row.fields.size() != 4U) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"",
|
|
"BOUNDARY rows require target, first DOF, last DOF, and "
|
|
"optional value.");
|
|
return;
|
|
}
|
|
if (row.fields[0].empty()) {
|
|
InputFailure("unresolved-reference", row.location, block.canonical_name,
|
|
"", "A boundary target cannot be empty.");
|
|
return;
|
|
}
|
|
std::int64_t first = 0;
|
|
std::int64_t last = 0;
|
|
if (!ParseInteger(row.fields[1], first, row.location,
|
|
block.canonical_name, true) ||
|
|
!ParseInteger(row.fields[2], last, row.location, block.canonical_name,
|
|
true)) {
|
|
return;
|
|
}
|
|
if (first < 1 || first > 6 || last < first || last > 6) {
|
|
InputFailure("invalid-dof", row.location, block.canonical_name,
|
|
row.fields[0],
|
|
"Boundary DOFs must be an ordered range in 1..6.");
|
|
return;
|
|
}
|
|
double value = 0.0;
|
|
if (row.fields.size() == 4U &&
|
|
!(model_element_family_ == ElementFamily::kSolid &&
|
|
row.fields[3].empty()) &&
|
|
!ParseDouble(row.fields[3], value, row.location,
|
|
block.canonical_name)) {
|
|
return;
|
|
}
|
|
destination.push_back({row.fields[0], static_cast<int>(first),
|
|
static_cast<int>(last), value, row.location});
|
|
}
|
|
}
|
|
|
|
void ParseCload(const KeywordBlock& block, std::vector<NodalLoad>& loads) {
|
|
if (step_.dynamic && model_element_family_ != ElementFamily::kSolid &&
|
|
!block.parameters.empty()) {
|
|
InputFailure("unsupported-dynamic-load", block.location,
|
|
block.canonical_name, "",
|
|
"Dynamic CLOAD does not support keyword parameters.");
|
|
return;
|
|
}
|
|
const bool solid_dynamic =
|
|
step_.dynamic && model_element_family_ == ElementFamily::kSolid;
|
|
if (!ValidateParameters(
|
|
block, solid_dynamic ? std::vector<std::string_view>{"AMPLITUDE"}
|
|
: std::vector<std::string_view>{}) ||
|
|
block.data.empty()) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "CLOAD requires one or more rows.");
|
|
}
|
|
return;
|
|
}
|
|
std::optional<EntityIndex> amplitude_index;
|
|
std::string amplitude_name;
|
|
if (Parameter(block, "AMPLITUDE")) {
|
|
const auto* name = RequiredParameterValue(block, "AMPLITUDE");
|
|
if (!name) return;
|
|
amplitude_name = *name;
|
|
for (std::size_t index = 0U; index < definition_.amplitudes.size();
|
|
++index) {
|
|
if (AsciiCaseInsensitiveEquals(definition_.amplitudes[index].name_raw,
|
|
*name)) {
|
|
amplitude_index = static_cast<EntityIndex>(index);
|
|
break;
|
|
}
|
|
}
|
|
if (!amplitude_index) {
|
|
InputFailure("unresolved-amplitude", block.location,
|
|
block.canonical_name, *name,
|
|
"CLOAD amplitude must resolve to a model-level curve.");
|
|
return;
|
|
}
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != 3U || row.fields[0].empty()) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"", "CLOAD rows require target, DOF, magnitude.");
|
|
return;
|
|
}
|
|
std::int64_t dof = 0;
|
|
double magnitude = 0.0;
|
|
if (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name,
|
|
true) ||
|
|
!ParseDouble(row.fields[2], magnitude, row.location,
|
|
block.canonical_name)) {
|
|
return;
|
|
}
|
|
if (dof < 1 || dof > 6) {
|
|
InputFailure("invalid-dof", row.location, block.canonical_name,
|
|
row.fields[0], "CLOAD DOF must be in 1..6.");
|
|
return;
|
|
}
|
|
if ((step_.dynamic || model_element_family_ == ElementFamily::kSolid) &&
|
|
dof > 3) {
|
|
InputFailure("unsupported-dynamic-load", row.location,
|
|
block.canonical_name, row.fields[1],
|
|
"Dynamic CLOAD supports global translational DOF 1 "
|
|
"through 3 only.");
|
|
return;
|
|
}
|
|
loads.push_back({row.fields[0], static_cast<int>(dof), magnitude,
|
|
row.location, amplitude_index, amplitude_name,
|
|
block.location});
|
|
}
|
|
}
|
|
|
|
void ParseDsload(const KeywordBlock& block) {
|
|
if (!ValidateParameters(block, {}, "unsupported-dsload-parameter") ||
|
|
block.data.empty()) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "DSLOAD requires one or more rows.");
|
|
}
|
|
return;
|
|
}
|
|
for (std::size_t ordinal = 0U; ordinal < block.data.size(); ++ordinal) {
|
|
const auto& row = block.data[ordinal];
|
|
if (row.fields.size() != 3U || row.fields[0].empty() ||
|
|
row.fields[1].empty() || row.fields[2].empty()) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"", "DSLOAD rows require surface, P, magnitude.");
|
|
return;
|
|
}
|
|
if (!IsPressureNameToken(row.fields[0])) {
|
|
InputFailure("invalid-surface-name", row.location, block.canonical_name,
|
|
row.fields[0],
|
|
"DSLOAD surface names must use the approved ASCII name "
|
|
"syntax.");
|
|
return;
|
|
}
|
|
if (!AsciiCaseInsensitiveEquals(row.fields[1], "P")) {
|
|
InputFailure("unsupported-load-type", row.location,
|
|
block.canonical_name, row.fields[1],
|
|
"Only uniform surface pressure load type P is supported.");
|
|
return;
|
|
}
|
|
double magnitude = 0.0;
|
|
if (!ParseSurfacePressureDouble(row.fields[2], magnitude, row.location,
|
|
block.canonical_name)) {
|
|
return;
|
|
}
|
|
step_.surface_pressure_loads.push_back(
|
|
{row.fields[0], magnitude, row.fields[2], block.location,
|
|
row.location, ordinal, step_.next_load_ordinal++});
|
|
}
|
|
}
|
|
|
|
void ParseStepStart(const KeywordBlock& block) {
|
|
if (step_seen_) {
|
|
if (step_.frequency || step_.dynamic) {
|
|
InputFailure(
|
|
"unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "",
|
|
"The frequency subset accepts exactly one procedure step.");
|
|
} else {
|
|
const auto* nlgeom = Parameter(block, "NLGEOM");
|
|
const bool nonlinear_second_step =
|
|
nlgeom != nullptr && nlgeom->value &&
|
|
AsciiCaseInsensitiveEquals(*nlgeom->value, "YES");
|
|
if (step_.nlgeom || nonlinear_second_step) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "",
|
|
"The nonlinear static subset accepts exactly one "
|
|
"procedure step.");
|
|
} else {
|
|
InputFailure("unsupported-multiple-step", block.location,
|
|
block.canonical_name, "",
|
|
"V0 accepts exactly one analysis step.");
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (!assembly_seen_ || materials_.empty()) {
|
|
InvalidKeywordLocation(block,
|
|
"The sole step follows the complete assembly and "
|
|
"material definitions.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"NAME", "NLGEOM", "PERTURBATION", "INC"}) ||
|
|
!RequireNoData(block)) {
|
|
return;
|
|
}
|
|
StaticProcedureKind procedure_kind =
|
|
StaticProcedureKind::kLinearSmallDisplacement;
|
|
bool nlgeom = false;
|
|
const auto* nlgeom_parameter = Parameter(block, "NLGEOM");
|
|
if (nlgeom_parameter != nullptr) {
|
|
if (!nlgeom_parameter->value || nlgeom_parameter->value->empty()) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, "NLGEOM",
|
|
"NLGEOM requires YES or NO in the approved subset.");
|
|
return;
|
|
}
|
|
if (AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "YES")) {
|
|
if (model_element_family_ != ElementFamily::kShell &&
|
|
model_element_family_ != ElementFamily::kSolid) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, *nlgeom_parameter->value,
|
|
"NLGEOM=YES requires a supported Shell or Solid model.");
|
|
return;
|
|
}
|
|
procedure_kind = StaticProcedureKind::kNonlinearFullNewton;
|
|
nlgeom = true;
|
|
} else if (!AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "NO")) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, *nlgeom_parameter->value,
|
|
"NLGEOM must select YES or NO.");
|
|
return;
|
|
}
|
|
}
|
|
const auto* perturbation = Parameter(block, "PERTURBATION");
|
|
if (perturbation != nullptr && perturbation->value) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "PERTURBATION",
|
|
"PERTURBATION is accepted only as a value-less flag.");
|
|
return;
|
|
}
|
|
const auto* name = Parameter(block, "NAME");
|
|
if (name != nullptr && (!name->value || name->value->empty())) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, "NAME",
|
|
"STEP NAME must not be empty when it is present.");
|
|
return;
|
|
}
|
|
const auto* increment_limit = Parameter(block, "INC");
|
|
std::uint32_t maximum_increments = 100U;
|
|
if (increment_limit != nullptr) {
|
|
if (!increment_limit->value || increment_limit->value->empty()) {
|
|
InputFailure("invalid-dynamic-data", block.location,
|
|
block.canonical_name, "INC",
|
|
"INC requires a positive uint32 value.");
|
|
return;
|
|
}
|
|
std::int64_t parsed_limit = 0;
|
|
if (!ParseInteger(*increment_limit->value, parsed_limit, block.location,
|
|
block.canonical_name, true) ||
|
|
parsed_limit > static_cast<std::int64_t>(
|
|
std::numeric_limits<std::uint32_t>::max())) {
|
|
if (!failure_) {
|
|
InputFailure("invalid-dynamic-data", block.location,
|
|
block.canonical_name, "INC",
|
|
"INC requires a positive uint32 value.");
|
|
}
|
|
return;
|
|
}
|
|
maximum_increments = static_cast<std::uint32_t>(parsed_limit);
|
|
}
|
|
step_seen_ = true;
|
|
in_step_ = true;
|
|
step_ = {};
|
|
step_.location = block.location;
|
|
step_.name_raw = name == nullptr ? "Step-1" : *name->value;
|
|
step_.name_canonical = CanonicalizeAscii(step_.name_raw);
|
|
step_.procedure_kind = procedure_kind;
|
|
step_.nlgeom = nlgeom;
|
|
step_.nlgeom_explicit = nlgeom_parameter != nullptr;
|
|
step_.perturbation_explicit = perturbation != nullptr;
|
|
step_.maximum_increments = maximum_increments;
|
|
step_load_seen_ = false;
|
|
step_no_op_seen_ = false;
|
|
}
|
|
|
|
void ParseStepBlock(const KeywordBlock& block) {
|
|
if (model_element_family_ == ElementFamily::kSolid &&
|
|
(block.canonical_name == "STATIC" ||
|
|
block.canonical_name == "FREQUENCY" ||
|
|
block.canonical_name == "DYNAMIC") &&
|
|
(step_.has_static || step_.frequency || step_.dynamic)) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, step_.name_raw,
|
|
"Solid steps accept exactly one procedure.");
|
|
return;
|
|
}
|
|
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)) {
|
|
return;
|
|
}
|
|
if (!step_.has_static && !step_.frequency && !step_.dynamic &&
|
|
!step_.has_heat_transfer) {
|
|
InvalidKeywordLocation(
|
|
block, "The sole step requires one supported procedure keyword.");
|
|
return;
|
|
}
|
|
in_step_ = false;
|
|
return;
|
|
}
|
|
if (block.canonical_name == "STEP") {
|
|
InputFailure("unsupported-multiple-step", block.location,
|
|
block.canonical_name, "",
|
|
"A second or nested step is unsupported.");
|
|
return;
|
|
}
|
|
if (block.canonical_name == "HEAT TRANSFER") {
|
|
active_output_ = false;
|
|
if (step_.has_static || step_.frequency || step_.dynamic ||
|
|
step_.has_heat_transfer || !step_.boundaries.empty() ||
|
|
!step_.loads.empty() || step_no_op_seen_) {
|
|
InvalidKeywordLocation(block,
|
|
"HEAT TRANSFER must be the first procedure.");
|
|
return;
|
|
}
|
|
ParseHeatTransfer(block);
|
|
} else if (block.canonical_name == "STATIC") {
|
|
active_output_ = false;
|
|
if (!step_.boundaries.empty() || !step_.loads.empty() ||
|
|
!step_.surface_pressure_loads.empty() || step_no_op_seen_) {
|
|
InvalidKeywordLocation(
|
|
block, "STATIC must be the first keyword in the sole step.");
|
|
return;
|
|
}
|
|
ParseStatic(block);
|
|
} else if (block.canonical_name == "FREQUENCY") {
|
|
active_output_ = false;
|
|
if (step_.has_static || step_.frequency || !step_.boundaries.empty() ||
|
|
!step_.loads.empty() || !step_.surface_pressure_loads.empty() ||
|
|
step_no_op_seen_) {
|
|
InvalidKeywordLocation(
|
|
block, "FREQUENCY must be the first procedure in the sole step.");
|
|
return;
|
|
}
|
|
ParseFrequency(block);
|
|
} else if (block.canonical_name == "DYNAMIC") {
|
|
active_output_ = false;
|
|
if (step_.has_static || step_.frequency || step_.dynamic ||
|
|
!step_.boundaries.empty() || !step_.loads.empty() ||
|
|
!step_.surface_pressure_loads.empty() || step_no_op_seen_) {
|
|
InvalidKeywordLocation(
|
|
block, "DYNAMIC must be the first procedure in the sole step.");
|
|
return;
|
|
}
|
|
ParseDynamic(block);
|
|
} else if (block.canonical_name == "BOUNDARY") {
|
|
active_output_ = false;
|
|
if ((!step_.has_static && !step_.frequency && !step_.dynamic &&
|
|
!step_.has_heat_transfer) ||
|
|
step_load_seen_ || step_no_op_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"Step boundaries follow STATIC and precede loads "
|
|
"and no-op requests.");
|
|
return;
|
|
}
|
|
if (step_.dynamic && !block.parameters.empty()) {
|
|
InputFailure("unsupported-dynamic-boundary", block.location,
|
|
block.canonical_name, "",
|
|
"Dynamic BOUNDARY does not support keyword parameters.");
|
|
return;
|
|
}
|
|
if (step_.has_heat_transfer) {
|
|
ParseThermalBoundary(block, step_.thermal_boundaries);
|
|
} else {
|
|
ParseBoundary(block, step_.boundaries);
|
|
}
|
|
if (!failure_ && step_.frequency) {
|
|
ValidateEigenBoundaries(step_.boundaries);
|
|
}
|
|
if (!failure_ && step_.dynamic) {
|
|
ValidateDynamicBoundaries(step_.boundaries);
|
|
}
|
|
} else if (block.canonical_name == "CLOAD") {
|
|
active_output_ = false;
|
|
if ((!step_.has_static && !step_.dynamic) || step_no_op_seen_) {
|
|
InvalidKeywordLocation(
|
|
block,
|
|
"CLOAD follows STATIC and boundaries and precedes no-op requests.");
|
|
return;
|
|
}
|
|
ParseCload(block, step_.loads);
|
|
if (!failure_) {
|
|
step_.next_load_ordinal += block.data.size();
|
|
}
|
|
step_load_seen_ = !failure_;
|
|
} else if (block.canonical_name == "CFLUX") {
|
|
active_output_ = false;
|
|
if (!step_.has_heat_transfer || step_no_op_seen_) {
|
|
InvalidKeywordLocation(block,
|
|
"CFLUX follows HEAT TRANSFER and boundaries.");
|
|
return;
|
|
}
|
|
ParseCflux(block);
|
|
step_load_seen_ = !failure_;
|
|
} else if (block.canonical_name == "DSLOAD") {
|
|
active_output_ = false;
|
|
if (!step_.has_static || step_.dynamic || step_no_op_seen_) {
|
|
InvalidKeywordLocation(block,
|
|
"DSLOAD follows STATIC and boundaries and "
|
|
"precedes no-op requests.");
|
|
return;
|
|
}
|
|
ParseDsload(block);
|
|
step_load_seen_ = !failure_;
|
|
} else if (block.canonical_name == "RESTART") {
|
|
active_output_ = false;
|
|
if (!step_.has_static && !step_.frequency && !step_.dynamic &&
|
|
!step_.has_heat_transfer) {
|
|
InvalidKeywordLocation(
|
|
block, "Step no-op requests follow STATIC, boundaries, and loads.");
|
|
return;
|
|
}
|
|
step_no_op_seen_ = true;
|
|
if (step_.frequency || step_.has_heat_transfer ||
|
|
model_element_family_ == ElementFamily::kSolid) {
|
|
const auto* write = Parameter(block, "WRITE");
|
|
const auto* frequency = Parameter(block, "FREQUENCY");
|
|
if (!ValidateParameters(block, {"WRITE", "FREQUENCY"}) ||
|
|
write == nullptr || write->value || frequency == nullptr ||
|
|
!frequency->value || *frequency->value != "0") {
|
|
if (!failure_) {
|
|
InputFailure(
|
|
"unsupported-keyword", block.location, block.canonical_name, "",
|
|
"Supported RESTART no-op requires WRITE, FREQUENCY=0.");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
if (RequireNoData(block)) {
|
|
AddIgnoredWarning(block);
|
|
}
|
|
} else if (block.canonical_name == "OUTPUT") {
|
|
if (!step_.has_static && !step_.frequency && !step_.dynamic &&
|
|
!step_.has_heat_transfer) {
|
|
InvalidKeywordLocation(
|
|
block, "Step no-op requests follow STATIC, boundaries, and loads.");
|
|
return;
|
|
}
|
|
step_no_op_seen_ = true;
|
|
ParseOutputRoot(block);
|
|
} else if (block.canonical_name == "NODE OUTPUT" ||
|
|
block.canonical_name == "ELEMENT OUTPUT" ||
|
|
block.canonical_name == "CONTACT OUTPUT" ||
|
|
block.canonical_name == "ENERGY OUTPUT") {
|
|
ParseOutputChild(block);
|
|
} else {
|
|
active_output_ = false;
|
|
if (step_.procedure_kind == StaticProcedureKind::kNonlinearFullNewton &&
|
|
!step_.has_static) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "",
|
|
"The nonlinear step supports only the STATIC procedure.");
|
|
return;
|
|
}
|
|
RejectUnknown(block);
|
|
}
|
|
}
|
|
|
|
bool ParseFrequencyModeCount(const std::string& text, std::int64_t& value,
|
|
const SourceLocation& location) {
|
|
if (text.empty() ||
|
|
!std::all_of(text.begin(), text.end(), [](char character) {
|
|
return character >= '0' && character <= '9';
|
|
})) {
|
|
return InputFailure(
|
|
"invalid-frequency-data", location, "FREQUENCY", text,
|
|
"The requested mode count must be positive decimal digits.");
|
|
}
|
|
const auto parsed =
|
|
std::from_chars(text.data(), text.data() + text.size(), value);
|
|
if (parsed.ec != std::errc{} || parsed.ptr != text.data() + text.size() ||
|
|
value <= 0) {
|
|
return InputFailure(
|
|
"invalid-frequency-data", location, "FREQUENCY", text,
|
|
"The requested mode count must be a positive int64 value.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ParseFrequency(const KeywordBlock& block) {
|
|
if (step_.nlgeom || (step_.perturbation_explicit && step_.has_static) ||
|
|
!ValidateParameters(
|
|
block, {"EIGENSOLVER", "SIM", "ACOUSTIC COUPLING", "NORMALIZATION"},
|
|
"unsupported-frequency-option")) {
|
|
if (!failure_) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "",
|
|
"FREQUENCY requires the original linear configuration.");
|
|
}
|
|
return;
|
|
}
|
|
if (block.data.size() != 1U || block.data[0].fields.empty() ||
|
|
block.data[0].fields.size() > 7U) {
|
|
InputFailure("invalid-frequency-data", block.location,
|
|
block.canonical_name, "",
|
|
"FREQUENCY requires one data row with one to seven fields.");
|
|
return;
|
|
}
|
|
for (std::size_t field = 1U; field < block.data[0].fields.size(); ++field) {
|
|
if (!block.data[0].fields[field].empty()) {
|
|
InputFailure("invalid-frequency-data", block.data[0].location,
|
|
block.canonical_name, std::to_string(field + 1U),
|
|
"Only the requested mode-count field may be non-empty.");
|
|
return;
|
|
}
|
|
}
|
|
FrequencyStepDefinition frequency{step_.name_raw,
|
|
step_.name_canonical,
|
|
step_.location,
|
|
block.location,
|
|
block.data[0].location,
|
|
0,
|
|
step_.nlgeom_explicit,
|
|
step_.perturbation_explicit,
|
|
false,
|
|
false,
|
|
false,
|
|
false,
|
|
FrequencyEigensolver::kLanczos,
|
|
FrequencyNormalization::kMass,
|
|
{}};
|
|
if (!ParseFrequencyModeCount(block.data[0].fields[0],
|
|
frequency.requested_mode_count,
|
|
block.data[0].location)) {
|
|
return;
|
|
}
|
|
const auto* eigensolver = Parameter(block, "EIGENSOLVER");
|
|
if (eigensolver != nullptr) {
|
|
if (!eigensolver->value ||
|
|
!AsciiCaseInsensitiveEquals(*eigensolver->value, "LANCZOS")) {
|
|
InputFailure("unsupported-frequency-option", block.location,
|
|
block.canonical_name, "EIGENSOLVER",
|
|
"Only EIGENSOLVER=LANCZOS is accepted.");
|
|
return;
|
|
}
|
|
frequency.eigensolver_explicit = true;
|
|
}
|
|
const auto* sim = Parameter(block, "SIM");
|
|
if (sim != nullptr) {
|
|
if (sim->value && !AsciiCaseInsensitiveEquals(*sim->value, "YES")) {
|
|
InputFailure("unsupported-frequency-option", block.location,
|
|
block.canonical_name, "SIM",
|
|
"SIM accepts only a flag or YES.");
|
|
return;
|
|
}
|
|
frequency.sim_explicit = true;
|
|
}
|
|
const auto* acoustic = Parameter(block, "ACOUSTIC COUPLING");
|
|
if (acoustic != nullptr) {
|
|
if (!acoustic->value ||
|
|
!AsciiCaseInsensitiveEquals(*acoustic->value, "ON")) {
|
|
InputFailure("unsupported-frequency-option", block.location,
|
|
block.canonical_name, "ACOUSTIC COUPLING",
|
|
"Only ACOUSTIC COUPLING=ON is accepted.");
|
|
return;
|
|
}
|
|
frequency.acoustic_coupling_explicit = true;
|
|
}
|
|
const auto* normalization = Parameter(block, "NORMALIZATION");
|
|
if (normalization != nullptr) {
|
|
if (!normalization->value ||
|
|
!AsciiCaseInsensitiveEquals(*normalization->value, "MASS")) {
|
|
InputFailure("unsupported-frequency-option", block.location,
|
|
block.canonical_name, "NORMALIZATION",
|
|
"Only NORMALIZATION=MASS is accepted.");
|
|
return;
|
|
}
|
|
frequency.normalization_explicit = true;
|
|
}
|
|
step_.frequency = std::move(frequency);
|
|
definition_.warnings.push_back(
|
|
{Severity::kWarning, "source-eigensolver-mapped-to-feast",
|
|
block.location, block.canonical_name, step_.name_raw,
|
|
"The accepted source eigensolver is mapped to the FEAST backend."});
|
|
if (step_.frequency->sim_explicit) {
|
|
definition_.warnings.push_back(
|
|
{Severity::kWarning, "source-sim-option-not-applied", block.location,
|
|
block.canonical_name, step_.name_raw,
|
|
"The accepted source SIM option has no FESA runtime effect."});
|
|
}
|
|
if (step_.frequency->acoustic_coupling_explicit) {
|
|
definition_.warnings.push_back(
|
|
{Severity::kWarning, "acoustic-option-inapplicable-structural-only",
|
|
block.location, block.canonical_name, step_.name_raw,
|
|
"Acoustic coupling is inapplicable to this structural-only model."});
|
|
}
|
|
}
|
|
|
|
void ValidateEigenBoundaries(
|
|
const std::vector<PrescribedDisplacementDefinition>& boundaries) {
|
|
for (const auto& boundary : boundaries) {
|
|
if (boundary.value != 0.0) {
|
|
InputFailure("unsupported-eigen-boundary", boundary.location,
|
|
"BOUNDARY", boundary.target,
|
|
"Frequency boundaries must prescribe exact zero.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ValidateDynamicBoundaries(
|
|
const std::vector<PrescribedDisplacementDefinition>& boundaries) {
|
|
for (const auto& boundary : boundaries) {
|
|
if (boundary.value != 0.0) {
|
|
InputFailure("unsupported-dynamic-boundary", boundary.location,
|
|
"BOUNDARY", boundary.target,
|
|
"Dynamic boundaries must be homogeneous and constant.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
void ParseDynamic(const KeywordBlock& block) {
|
|
if (step_.perturbation_explicit ||
|
|
(model_element_family_ != ElementFamily::kSolid &&
|
|
(!step_.nlgeom || model_element_family_ != ElementFamily::kShell)) ||
|
|
!ValidateParameters(block, {"DIRECT"},
|
|
"unsupported-dynamic-parameter")) {
|
|
if (!failure_) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "",
|
|
"DYNAMIC requires nonlinear Shell or linear/nonlinear "
|
|
"Solid geometry.");
|
|
}
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kSolid &&
|
|
block.data.size() == 1U && block.data[0].fields.size() > 3U) {
|
|
InputFailure("invalid-dynamic-data", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"Solid DYNAMIC requires h,T with at most one blank field.");
|
|
return;
|
|
}
|
|
const auto* direct = Parameter(block, "DIRECT");
|
|
if (direct == nullptr || direct->value || block.data.size() != 1U ||
|
|
block.data[0].fields.size() < 2U ||
|
|
std::any_of(block.data[0].fields.begin() + 2,
|
|
block.data[0].fields.end(),
|
|
[](const auto& field) { return !field.empty(); })) {
|
|
InputFailure(
|
|
"invalid-dynamic-data", block.location, block.canonical_name, "",
|
|
"DYNAMIC,DIRECT requires one row with increment and time period.");
|
|
return;
|
|
}
|
|
DynamicStepDefinition dynamic{step_.name_raw,
|
|
step_.name_canonical,
|
|
step_.location,
|
|
block.location,
|
|
block.data[0].location,
|
|
0.0,
|
|
0.0,
|
|
0U,
|
|
step_.maximum_increments,
|
|
-0.05,
|
|
0.275625,
|
|
0.55,
|
|
{},
|
|
{}};
|
|
if (!ParseDynamicDouble(block.data[0].fields[0], dynamic.initial_increment,
|
|
block.data[0].location, block.canonical_name) ||
|
|
!ParseDynamicDouble(block.data[0].fields[1], dynamic.time_period,
|
|
block.data[0].location, block.canonical_name) ||
|
|
!(dynamic.initial_increment > 0.0) || !(dynamic.time_period > 0.0)) {
|
|
if (!failure_) {
|
|
InputFailure(
|
|
"invalid-dynamic-data", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"Dynamic increment and time period must be finite and positive.");
|
|
}
|
|
return;
|
|
}
|
|
const double count = dynamic.time_period / dynamic.initial_increment;
|
|
const double rounded_count = std::round(count);
|
|
if (!std::isfinite(count) || rounded_count < 1.0 ||
|
|
rounded_count >
|
|
static_cast<double>(std::numeric_limits<std::uint32_t>::max()) ||
|
|
dynamic.initial_increment * rounded_count != dynamic.time_period) {
|
|
InputFailure("invalid-dynamic-schedule", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"DYNAMIC,DIRECT requires an exact positive integer number "
|
|
"of increments.");
|
|
return;
|
|
}
|
|
dynamic.increment_count = static_cast<std::uint32_t>(rounded_count);
|
|
if (dynamic.increment_count > dynamic.maximum_increments) {
|
|
InputFailure("invalid-dynamic-schedule", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"DYNAMIC,DIRECT increment count cannot exceed STEP INC.");
|
|
return;
|
|
}
|
|
dynamic.nlgeom = step_.nlgeom;
|
|
step_.dynamic = std::move(dynamic);
|
|
}
|
|
|
|
void ParseHeatTransfer(const KeywordBlock& block) {
|
|
if (model_element_family_ != ElementFamily::kThermalShell || step_.nlgeom ||
|
|
step_.perturbation_explicit ||
|
|
!ValidateParameters(block, {"END"}, "unsupported-thermal-option") ||
|
|
block.data.size() != 1U) {
|
|
if (!failure_)
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "",
|
|
"HEAT TRANSFER requires the DS4 thermal subset.");
|
|
return;
|
|
}
|
|
const auto* end = Parameter(block, "END");
|
|
if (end != nullptr &&
|
|
(!end->value || !AsciiCaseInsensitiveEquals(*end->value, "PERIOD"))) {
|
|
InputFailure("unsupported-thermal-option", block.location,
|
|
block.canonical_name, "END",
|
|
"HEAT TRANSFER accepts only END=PERIOD.");
|
|
return;
|
|
}
|
|
if (block.data[0].fields.size() > 5U) {
|
|
InputFailure(
|
|
"invalid-thermal-time-grid", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"HEAT TRANSFER permits at most three trailing empty fields.");
|
|
return;
|
|
}
|
|
const auto fields = WithoutTrailingEmpty(block.data[0].fields);
|
|
if (fields.size() != 2U) {
|
|
InputFailure("invalid-thermal-time-grid", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"HEAT TRANSFER requires increment and time period.");
|
|
return;
|
|
}
|
|
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(
|
|
"invalid-thermal-time-grid", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"Thermal increment and period must be finite and positive.");
|
|
return;
|
|
}
|
|
const auto grid =
|
|
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),
|
|
std::move(diagnostic)};
|
|
return;
|
|
}
|
|
step_.has_heat_transfer = true;
|
|
}
|
|
|
|
void ParseCflux(const KeywordBlock& block) {
|
|
if (!ValidateParameters(block, {}) || block.data.empty()) {
|
|
if (!failure_)
|
|
InputFailure("invalid-data-arity", block.location, block.canonical_name,
|
|
"", "CFLUX requires target, DOF, power rows.");
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
if (row.fields.size() != 3U || row.fields[0].empty()) {
|
|
InputFailure("invalid-data-arity", row.location, block.canonical_name,
|
|
"", "CFLUX rows require target, DOF, power.");
|
|
return;
|
|
}
|
|
std::int64_t dof = 0;
|
|
double power = 0.0;
|
|
if (!ParseInteger(row.fields[1], dof, row.location, block.canonical_name,
|
|
true) ||
|
|
!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.");
|
|
return;
|
|
}
|
|
step_.thermal_fluxes.push_back(
|
|
{row.fields[0], static_cast<int>(dof), power, row.location});
|
|
}
|
|
}
|
|
|
|
void ParseStatic(const KeywordBlock& block) {
|
|
if (model_element_family_ == ElementFamily::kSolid &&
|
|
Parameter(block, "DIRECT")) {
|
|
if (step_.perturbation_explicit || step_.has_static || step_.frequency ||
|
|
step_.dynamic) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, step_.name_raw,
|
|
"STATIC DIRECT requires one non-perturbation procedure.");
|
|
return;
|
|
}
|
|
if (!ValidateParameters(block, {"DIRECT"})) return;
|
|
const auto* direct = Parameter(block, "DIRECT");
|
|
if (direct->value || block.data.size() != 1U ||
|
|
(block.data[0].fields.size() < 2U ||
|
|
block.data[0].fields.size() > 3U ||
|
|
std::any_of(block.data[0].fields.begin() + 2,
|
|
block.data[0].fields.end(),
|
|
[](const auto& field) { return !field.empty(); }))) {
|
|
InputFailure("invalid-static-data", block.location,
|
|
block.canonical_name, "DIRECT",
|
|
"STATIC DIRECT requires exactly h,T.");
|
|
return;
|
|
}
|
|
double h = 0.0;
|
|
double period = 0.0;
|
|
if (!ParseDouble(block.data[0].fields[0], h, block.data[0].location,
|
|
"STATIC") ||
|
|
!ParseDouble(block.data[0].fields[1], period, block.data[0].location,
|
|
"STATIC"))
|
|
return;
|
|
const double count = std::round(period / h);
|
|
if (!(h > 0.0) || !(period > 0.0) || !std::isfinite(count) ||
|
|
count < 1.0 ||
|
|
count > static_cast<double>(step_.maximum_increments) ||
|
|
h * count != period) {
|
|
InputFailure("invalid-static-schedule", block.data[0].location,
|
|
block.canonical_name, "DIRECT",
|
|
"STATIC DIRECT requires positive exact N*h=T and N<=INC.");
|
|
return;
|
|
}
|
|
step_.static_values = {h, period, h, h};
|
|
step_.static_direct = true;
|
|
step_.static_increment_count = static_cast<std::uint32_t>(count);
|
|
step_.has_static = true;
|
|
return;
|
|
}
|
|
if (step_.perturbation_explicit || step_.frequency) {
|
|
InputFailure("unsupported-analysis-procedure", block.location,
|
|
block.canonical_name, "PERTURBATION",
|
|
"PERTURBATION is supported only with FREQUENCY.");
|
|
return;
|
|
}
|
|
if (step_.has_heat_transfer || step_.has_static ||
|
|
!ValidateParameters(block, {}) || block.data.size() != 1U ||
|
|
block.data[0].fields.size() != 4U) {
|
|
if (!failure_) {
|
|
InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data"
|
|
: "invalid-static-data",
|
|
block.location, block.canonical_name, "",
|
|
"STATIC requires exactly one row of four values.");
|
|
}
|
|
return;
|
|
}
|
|
for (std::size_t field = 0U; field < 4U; ++field) {
|
|
const std::string& text = block.data[0].fields[field];
|
|
const bool parsed =
|
|
step_.nlgeom
|
|
? ParseNonlinearStaticDouble(text, step_.static_values[field],
|
|
block.data[0].location,
|
|
block.canonical_name)
|
|
: ParseDouble(text, step_.static_values[field],
|
|
block.data[0].location, block.canonical_name);
|
|
if (!parsed) {
|
|
return;
|
|
}
|
|
if (step_.static_values[field] <= 0.0) {
|
|
InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data"
|
|
: "invalid-static-data",
|
|
block.data[0].location, block.canonical_name, "",
|
|
"All four STATIC fields must be positive.");
|
|
return;
|
|
}
|
|
}
|
|
if (step_.static_values[2] > step_.static_values[3]) {
|
|
InputFailure(step_.nlgeom ? "invalid-nonlinear-static-data"
|
|
: "invalid-static-data",
|
|
block.data[0].location, block.canonical_name, "",
|
|
"STATIC minimum increment cannot exceed maximum increment.");
|
|
return;
|
|
}
|
|
if (step_.nlgeom && step_.static_values[1] != 1.0) {
|
|
InputFailure("invalid-nonlinear-static-data", block.data[0].location,
|
|
block.canonical_name, "",
|
|
"The nonlinear STATIC time period must be exactly 1.0.");
|
|
return;
|
|
}
|
|
step_.has_static = true;
|
|
}
|
|
|
|
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",
|
|
"VARIABLE", "FREQUENCY"}
|
|
: std::vector<std::string_view>{"FIELD", "HISTORY",
|
|
"VARIABLE"})) {
|
|
return;
|
|
}
|
|
const auto* field = Parameter(block, "FIELD");
|
|
const auto* history = Parameter(block, "HISTORY");
|
|
const auto* variable = Parameter(block, "VARIABLE");
|
|
if ((field == nullptr) == (history == nullptr) ||
|
|
(field != nullptr && field->value) ||
|
|
(history != nullptr && history->value)) {
|
|
InputFailure(
|
|
step_.nlgeom ? "invalid-keyword-parameter" : "unsupported-keyword",
|
|
block.location, block.canonical_name, "",
|
|
"OUTPUT must select exactly FIELD or HISTORY.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kSolid &&
|
|
((variable &&
|
|
(!variable->value ||
|
|
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) ||
|
|
!RequireNoData(block))) {
|
|
if (!failure_)
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "VARIABLE",
|
|
"Solid OUTPUT accepts only PRESELECT requests.");
|
|
return;
|
|
}
|
|
if (step_.nlgeom &&
|
|
(variable == nullptr || !variable->value ||
|
|
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) {
|
|
InputFailure("invalid-keyword-parameter", block.location,
|
|
block.canonical_name, "VARIABLE",
|
|
"Nonlinear OUTPUT requires VARIABLE=PRESELECT.");
|
|
return;
|
|
}
|
|
if (step_.dynamic) {
|
|
const auto* frequency = Parameter(block, "FREQUENCY");
|
|
if (!ValidateParameters(block,
|
|
{"FIELD", "HISTORY", "VARIABLE", "FREQUENCY"}) ||
|
|
variable == nullptr || !variable->value ||
|
|
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT") ||
|
|
(frequency != nullptr &&
|
|
(!frequency->value || *frequency->value != "1")) ||
|
|
!RequireNoData(block)) {
|
|
if (!failure_) {
|
|
InputFailure(
|
|
"unsupported-keyword", block.location, block.canonical_name, "",
|
|
"Dynamic OUTPUT accepts only VARIABLE=PRESELECT, FREQUENCY=1.");
|
|
}
|
|
return;
|
|
}
|
|
} else if (step_.frequency &&
|
|
((variable != nullptr &&
|
|
(!variable->value || !AsciiCaseInsensitiveEquals(
|
|
*variable->value, "PRESELECT"))) ||
|
|
!RequireNoData(block))) {
|
|
if (!failure_) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "",
|
|
"Frequency OUTPUT accepts only VARIABLE=PRESELECT.");
|
|
}
|
|
return;
|
|
}
|
|
active_output_ = true;
|
|
active_output_is_field_ = field != nullptr;
|
|
AddIgnoredWarning(block);
|
|
}
|
|
|
|
void ParseOutputChild(const KeywordBlock& block) {
|
|
if (step_.has_heat_transfer) {
|
|
InputFailure("unsupported-keyword", block.location, block.canonical_name,
|
|
"",
|
|
"Thermal output children are outside the approved subset.");
|
|
return;
|
|
}
|
|
if (!active_output_) {
|
|
InputFailure(
|
|
"invalid-keyword-location", block.location, block.canonical_name, "",
|
|
"Output variable keywords require an active OUTPUT request.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kSolid && step_.dynamic) {
|
|
InputFailure(
|
|
"unsupported-keyword", block.location, block.canonical_name, "",
|
|
"Solid dynamic output children are outside the approved subset.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kSolid && step_.has_static) {
|
|
const auto* variable = Parameter(block, "VARIABLE");
|
|
const auto* directions = Parameter(block, "DIRECTIONS");
|
|
const bool node = block.canonical_name == "NODE OUTPUT";
|
|
const bool element = block.canonical_name == "ELEMENT OUTPUT";
|
|
const bool contact = block.canonical_name == "CONTACT OUTPUT";
|
|
if (!active_output_is_field_ || (!node && !element && !contact) ||
|
|
!ValidateParameters(
|
|
block,
|
|
element ? std::vector<std::string_view>{"VARIABLE", "DIRECTIONS"}
|
|
: std::vector<std::string_view>{"VARIABLE"}) ||
|
|
(directions &&
|
|
(!directions->value ||
|
|
!AsciiCaseInsensitiveEquals(*directions->value, "YES"))) ||
|
|
(variable &&
|
|
(!variable->value ||
|
|
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) ||
|
|
(contact && (!variable || !block.data.empty()))) {
|
|
if (!failure_)
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "",
|
|
"Solid static output child is unsupported.");
|
|
return;
|
|
}
|
|
for (const auto& row : block.data) {
|
|
for (const auto& field : row.fields) {
|
|
const std::string token = CanonicalizeAscii(field);
|
|
if (!(node ? (token == "U" || token == "RF")
|
|
: (token == "S" || token == "E"))) {
|
|
InputFailure("unsupported-keyword", row.location,
|
|
block.canonical_name, field,
|
|
"Solid output quantities must be physical supported "
|
|
"components.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
if (step_.frequency) {
|
|
const std::string_view required_variable =
|
|
block.canonical_name == "NODE OUTPUT" ? "PRESELECT" : "ALL";
|
|
const auto* variable = Parameter(block, "VARIABLE");
|
|
if ((block.canonical_name != "NODE OUTPUT" &&
|
|
block.canonical_name != "ENERGY OUTPUT") ||
|
|
(block.canonical_name == "NODE OUTPUT" && !active_output_is_field_) ||
|
|
(block.canonical_name == "ENERGY OUTPUT" &&
|
|
active_output_is_field_) ||
|
|
!ValidateParameters(block, {"VARIABLE"}) || variable == nullptr ||
|
|
!variable->value ||
|
|
!AsciiCaseInsensitiveEquals(*variable->value, required_variable) ||
|
|
!RequireNoData(block)) {
|
|
if (!failure_) {
|
|
InputFailure(
|
|
"unsupported-keyword", block.location, block.canonical_name, "",
|
|
"Frequency output child is outside the approved subset.");
|
|
}
|
|
return;
|
|
}
|
|
}
|
|
AddIgnoredWarning(block);
|
|
}
|
|
|
|
void AddIgnoredWarning(const KeywordBlock& block) {
|
|
// One warning per allowlisted keyword keeps no-op provenance stable;
|
|
// subordinate variable rows remain attached to that keyword record.
|
|
definition_.warnings.push_back(
|
|
{Severity::kWarning,
|
|
step_.dynamic ? "ignored-output-request" : "ignored-input-keyword",
|
|
block.location, block.canonical_name, "",
|
|
"The allowlisted Abaqus keyword is "
|
|
"ignored without semantic effect."});
|
|
}
|
|
|
|
void RejectUnknown(const KeywordBlock& block) {
|
|
if (block.canonical_name == "DLOAD" &&
|
|
model_element_family_ == ElementFamily::kShell) {
|
|
InputFailure("unsupported-distributed-load", block.location,
|
|
block.canonical_name, "",
|
|
"Distributed, pressure, gravity, body, edge, and follower "
|
|
"loads are unsupported.");
|
|
return;
|
|
}
|
|
InputFailure("unsupported-keyword", block.location, block.canonical_name,
|
|
"", "The keyword is outside the approved Abaqus subset.");
|
|
}
|
|
|
|
const RawPart* FindPart(const std::string& name) const {
|
|
const auto found = std::find_if(
|
|
parts_.begin(), parts_.end(), [&name](const RawPart& part) {
|
|
return AsciiCaseInsensitiveEquals(part.name, name);
|
|
});
|
|
return found == parts_.end() ? nullptr : &*found;
|
|
}
|
|
|
|
const RawInstance* FindInstance(const std::string& name) const {
|
|
const auto found =
|
|
std::find_if(instances_.begin(), instances_.end(),
|
|
[&name](const RawInstance& instance) {
|
|
return AsciiCaseInsensitiveEquals(instance.name, name);
|
|
});
|
|
return found == instances_.end() ? nullptr : &*found;
|
|
}
|
|
|
|
std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const {
|
|
for (std::size_t index = 0U; index < materials_.size(); ++index) {
|
|
if (AsciiCaseInsensitiveEquals(materials_[index].name, name)) {
|
|
return static_cast<EntityIndex>(index);
|
|
}
|
|
}
|
|
return std::nullopt;
|
|
}
|
|
|
|
const RawSet* FindSet(const std::vector<RawSet>& sets,
|
|
const std::string& name) const {
|
|
const auto found =
|
|
std::find_if(sets.begin(), sets.end(), [&name](const RawSet& set) {
|
|
return AsciiCaseInsensitiveEquals(set.name, name);
|
|
});
|
|
return found == sets.end() ? nullptr : &*found;
|
|
}
|
|
|
|
const RawNode* FindNode(const RawPart& part, std::int64_t label) const {
|
|
const auto found = std::find_if(
|
|
part.nodes.begin(), part.nodes.end(),
|
|
[label](const RawNode& node) { return node.label == label; });
|
|
return found == part.nodes.end() ? nullptr : &*found;
|
|
}
|
|
|
|
const RawElement* FindElement(const RawPart& part, std::int64_t label) const {
|
|
const auto found = std::find_if(
|
|
part.elements.begin(), part.elements.end(),
|
|
[label](const RawElement& element) { return element.label == label; });
|
|
return found == part.elements.end() ? nullptr : &*found;
|
|
}
|
|
|
|
void ValidateCardinality() {
|
|
if (parts_.empty() || !assembly_seen_ || instances_.empty() ||
|
|
materials_.empty() || !step_seen_ ||
|
|
(!step_.has_static && !step_.frequency && !step_.dynamic &&
|
|
!step_.has_heat_transfer)) {
|
|
const auto location = input_.blocks.empty()
|
|
? SourceLocation{input_.source_path, 0U}
|
|
: input_.blocks.back().location;
|
|
InputFailure("invalid-model-cardinality", location, "", "",
|
|
"The model requires part, assembly, identity instance, "
|
|
"material, and one supported procedure step.");
|
|
}
|
|
}
|
|
|
|
void FinalizeMaterials() {
|
|
for (const auto& material : materials_) {
|
|
if (model_element_family_ == ElementFamily::kThermalShell) {
|
|
if (!material.density || !material.conductivity ||
|
|
!material.specific_heat) {
|
|
ModelFailure("missing-thermal-material-property", material.location,
|
|
"MATERIAL", material.name,
|
|
"DS4 materials require DENSITY, CONDUCTIVITY, and "
|
|
"SPECIFIC HEAT.");
|
|
return;
|
|
}
|
|
definition_.thermal_materials.push_back({{"", 0, material.name},
|
|
material.name,
|
|
*material.conductivity,
|
|
*material.density,
|
|
*material.specific_heat,
|
|
material.location});
|
|
continue;
|
|
}
|
|
if (!material.has_elastic) {
|
|
InputFailure("unresolved-reference", material.location, "MATERIAL",
|
|
material.name,
|
|
"A material must own exactly one ELASTIC row.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kShell ||
|
|
model_element_family_ == ElementFamily::kSolid) {
|
|
if (!(material.youngs_modulus > 0.0) ||
|
|
!(material.poisson_ratio > -1.0) ||
|
|
!(material.poisson_ratio < 0.5)) {
|
|
ModelFailure(model_element_family_ == ElementFamily::kSolid
|
|
? "invalid-solid-material"
|
|
: "invalid-shell-material",
|
|
material.elastic_location, "ELASTIC", material.name,
|
|
"Isotropic elasticity requires E>0 and -1<nu<0.5.");
|
|
return;
|
|
}
|
|
if (model_element_family_ == ElementFamily::kSolid) {
|
|
const double mu =
|
|
material.youngs_modulus / (2.0 * (1.0 + material.poisson_ratio));
|
|
const double lambda = material.youngs_modulus *
|
|
material.poisson_ratio /
|
|
((1.0 + material.poisson_ratio) *
|
|
(1.0 - 2.0 * material.poisson_ratio));
|
|
if (!std::isfinite(mu) || !std::isfinite(lambda)) {
|
|
ModelFailure("invalid-solid-material", material.elastic_location,
|
|
"ELASTIC", material.name,
|
|
"Solid Lame constants must be finite.");
|
|
return;
|
|
}
|
|
}
|
|
definition_.materials.push_back({material.name, material.youngs_modulus,
|
|
material.poisson_ratio,
|
|
material.location});
|
|
continue;
|
|
}
|
|
const double denominator = 2.0 * (1.0 + material.poisson_ratio);
|
|
const double shear_modulus = material.youngs_modulus / denominator;
|
|
// The approved ledger deliberately validates derived G, not an
|
|
// independent upper bound on Poisson's ratio.
|
|
if (!(material.youngs_modulus > 0.0) || !std::isfinite(shear_modulus) ||
|
|
!(shear_modulus > 0.0)) {
|
|
ModelFailure("invalid-beam-property", material.elastic_location,
|
|
"ELASTIC", material.name,
|
|
"E and the derived G=E/(2*(1+nu)) must be positive.");
|
|
return;
|
|
}
|
|
definition_.materials.push_back({material.name, material.youngs_modulus,
|
|
material.poisson_ratio,
|
|
material.location});
|
|
}
|
|
}
|
|
|
|
void FinalizeDensities() {
|
|
if (model_element_family_ == ElementFamily::kThermalShell) {
|
|
for (const auto& block : input_.blocks) {
|
|
if (block.canonical_name != "PREPRINT") continue;
|
|
if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}))
|
|
return;
|
|
if (block.parameters.size() != 4U) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "",
|
|
"Thermal PREPRINT requires four NO parameters.");
|
|
return;
|
|
}
|
|
for (const auto& parameter : block.parameters) {
|
|
if (!parameter.value ||
|
|
!AsciiCaseInsensitiveEquals(*parameter.value, "NO")) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, parameter.name,
|
|
"Thermal PREPRINT parameters must equal NO.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
return;
|
|
}
|
|
if (physical_constants_location_) {
|
|
InputFailure("unsupported-keyword", *physical_constants_location_,
|
|
"PHYSICAL CONSTANTS", "",
|
|
"PHYSICAL CONSTANTS is supported only by thermal input.");
|
|
return;
|
|
}
|
|
if (!step_.frequency && !step_.dynamic &&
|
|
model_element_family_ == ElementFamily::kSolid) {
|
|
for (std::size_t index = 0U; index < materials_.size(); ++index) {
|
|
if (materials_[index].density)
|
|
definition_.densities.push_back({static_cast<EntityIndex>(index),
|
|
*materials_[index].density,
|
|
materials_[index].density_location});
|
|
}
|
|
return;
|
|
}
|
|
if (!step_.frequency && !step_.dynamic) {
|
|
if (std::any_of(materials_.begin(), materials_.end(),
|
|
[](const RawMaterial& material) {
|
|
return material.density.has_value();
|
|
})) {
|
|
InputFailure("invalid-density-data", step_.location, "DENSITY", "",
|
|
"DENSITY is supported only by the FREQUENCY procedure.");
|
|
}
|
|
return;
|
|
}
|
|
if (step_.frequency) {
|
|
ValidateFrequencyPreprint();
|
|
if (failure_) {
|
|
return;
|
|
}
|
|
}
|
|
if (step_.dynamic) {
|
|
ValidateDynamicPreprint();
|
|
if (failure_) {
|
|
return;
|
|
}
|
|
for (auto& warning : definition_.warnings) {
|
|
if (warning.keyword == "PREPRINT") {
|
|
warning.code = "ignored-output-request";
|
|
}
|
|
}
|
|
}
|
|
if (model_element_family_ != ElementFamily::kShell &&
|
|
model_element_family_ != ElementFamily::kSolid) {
|
|
InputFailure("unsupported-analysis-procedure", step_.location,
|
|
step_.frequency ? "FREQUENCY" : "DYNAMIC", step_.name_raw,
|
|
"The procedure requires an approved Shell or Solid model.");
|
|
return;
|
|
}
|
|
if (!element_surfaces_.empty()) {
|
|
InputFailure("unsupported-keyword", element_surfaces_.front().location,
|
|
"SURFACE", element_surfaces_.front().name,
|
|
"The selected procedure does not support element surfaces.");
|
|
return;
|
|
}
|
|
for (std::size_t index = 0U; index < materials_.size(); ++index) {
|
|
const RawMaterial& material = materials_[index];
|
|
if (!material.density) {
|
|
bool active = false;
|
|
for (const auto& section : definition_.shell_sections) {
|
|
if (section.material_index == static_cast<EntityIndex>(index)) {
|
|
active = true;
|
|
break;
|
|
}
|
|
}
|
|
for (const auto& section : definition_.solid_sections) {
|
|
if (section.MaterialIndex() == static_cast<EntityIndex>(index))
|
|
active = true;
|
|
}
|
|
if (active) {
|
|
ModelFailure(
|
|
"missing-material-density", material.location, "MATERIAL",
|
|
material.name,
|
|
"Each active dynamic or frequency material requires DENSITY.");
|
|
return;
|
|
}
|
|
continue;
|
|
}
|
|
definition_.densities.push_back({static_cast<EntityIndex>(index),
|
|
*material.density,
|
|
material.density_location});
|
|
}
|
|
}
|
|
|
|
void ValidateFrequencyPreprint() {
|
|
for (const auto& block : input_.blocks) {
|
|
if (block.canonical_name != "PREPRINT") {
|
|
continue;
|
|
}
|
|
if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) ||
|
|
block.parameters.size() != 4U) {
|
|
if (!failure_) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "",
|
|
"FREQUENCY requires PREPRINT with ECHO, MODEL, HISTORY, "
|
|
"and CONTACT parameters.");
|
|
}
|
|
return;
|
|
}
|
|
for (const auto& parameter : block.parameters) {
|
|
if (!parameter.value ||
|
|
!AsciiCaseInsensitiveEquals(*parameter.value, "NO")) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, parameter.name,
|
|
"FREQUENCY requires PREPRINT parameters to equal NO.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for (auto& warning : definition_.warnings) {
|
|
if (warning.keyword == "PREPRINT") {
|
|
warning.message = "mandatory-eigen-output-unchanged";
|
|
}
|
|
}
|
|
}
|
|
|
|
void ValidateDynamicPreprint() {
|
|
for (const auto& block : input_.blocks) {
|
|
if (block.canonical_name != "PREPRINT") {
|
|
continue;
|
|
}
|
|
if (!ValidateParameters(block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) ||
|
|
block.parameters.size() != 4U) {
|
|
if (!failure_) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, "",
|
|
"DYNAMIC requires PREPRINT with ECHO, MODEL, HISTORY, "
|
|
"and CONTACT parameters.");
|
|
}
|
|
return;
|
|
}
|
|
for (const auto& parameter : block.parameters) {
|
|
if (!parameter.value ||
|
|
!AsciiCaseInsensitiveEquals(*parameter.value, "NO")) {
|
|
InputFailure("unsupported-keyword", block.location,
|
|
block.canonical_name, parameter.name,
|
|
"DYNAMIC requires PREPRINT parameters to equal NO.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
void FinalizePartsAndSections() {
|
|
part_section_assignments_.resize(parts_.size());
|
|
part_shell_section_assignments_.resize(parts_.size());
|
|
const bool shell_model =
|
|
model_element_family_ == ElementFamily::kShell ||
|
|
model_element_family_ == ElementFamily::kThermalShell;
|
|
for (std::size_t part_index = 0U; part_index < parts_.size();
|
|
++part_index) {
|
|
const RawPart& part = parts_[part_index];
|
|
if (part.nodes.empty() || part.elements.empty() ||
|
|
(!shell_model && model_element_family_ != ElementFamily::kSolid &&
|
|
part.sections.empty())) {
|
|
InputFailure("invalid-model-cardinality", part.location, "PART",
|
|
part.name,
|
|
"A part requires nodes, elements, and its approved "
|
|
"section form.");
|
|
return;
|
|
}
|
|
|
|
PartDefinition part_definition{};
|
|
part_definition.name = part.name;
|
|
part_definition.location = part.location;
|
|
for (const auto& node : part.nodes) {
|
|
part_definition.node_source_labels.push_back(node.label);
|
|
}
|
|
for (const auto& element : part.elements) {
|
|
part_definition.element_source_labels.push_back(element.label);
|
|
for (const auto node_label : element.node_labels) {
|
|
if (FindNode(part, node_label) == nullptr) {
|
|
InputFailure(shell_model ? "invalid-shell-connectivity"
|
|
: "unresolved-reference",
|
|
element.location, "ELEMENT", element.label_text,
|
|
"Element connectivity must resolve within its part.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& set : part.node_sets) {
|
|
part_definition.node_set_names.push_back(set.name);
|
|
for (const auto label : set.members) {
|
|
if (FindNode(part, label) == nullptr) {
|
|
InputFailure("unresolved-reference", set.location, "NSET", set.name,
|
|
"Every node-set member must resolve within its part.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& set : part.element_sets) {
|
|
part_definition.element_set_names.push_back(set.name);
|
|
for (const auto label : set.members) {
|
|
if (FindElement(part, label) == nullptr) {
|
|
InputFailure(
|
|
"unresolved-reference", set.location, "ELSET", set.name,
|
|
"Every element-set member must resolve within its part.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
definition_.parts.push_back(std::move(part_definition));
|
|
|
|
if (model_element_family_ == ElementFamily::kSolid) {
|
|
auto& assignments = part_shell_section_assignments_[part_index];
|
|
for (const auto& section : part.solid_sections) {
|
|
const auto* element_set =
|
|
FindSet(part.element_sets, section.element_set_name);
|
|
const auto material = FindMaterialIndex(section.material_name);
|
|
if (!element_set || !material) {
|
|
InputFailure("unresolved-solid-section", section.location,
|
|
"SOLID SECTION", section.element_set_name,
|
|
"Solid ELSET and MATERIAL must resolve.");
|
|
return;
|
|
}
|
|
const auto index =
|
|
static_cast<EntityIndex>(definition_.solid_sections.size());
|
|
definition_.solid_sections.emplace_back(
|
|
SourceEntityId{part.name, static_cast<std::int64_t>(index),
|
|
section.element_set_name},
|
|
section.element_set_name, *material, section.location);
|
|
for (const auto label : element_set->members) {
|
|
if (!assignments.emplace(label, std::make_pair(index, *material))
|
|
.second) {
|
|
InputFailure(
|
|
"invalid-solid-section-assignment", section.location,
|
|
"SOLID SECTION", std::to_string(label),
|
|
"Solid elements require exactly one section assignment.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& element : part.elements) {
|
|
if (assignments.find(element.label) == assignments.end()) {
|
|
InputFailure("invalid-solid-section-assignment", element.location,
|
|
"ELEMENT", element.label_text,
|
|
"A Solid section assignment is missing.");
|
|
return;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
if (shell_model) {
|
|
auto& assignments = part_shell_section_assignments_[part_index];
|
|
for (const auto& section : part.shell_sections) {
|
|
const auto* element_set =
|
|
FindSet(part.element_sets, section.element_set_name);
|
|
const auto material_index = FindMaterialIndex(section.material_name);
|
|
if (element_set == nullptr || !material_index) {
|
|
InputFailure("unresolved-shell-section", section.location,
|
|
"SHELL SECTION", section.element_set_name,
|
|
"Shell section ELSET and MATERIAL references "
|
|
"must resolve.");
|
|
return;
|
|
}
|
|
const EntityIndex section_index =
|
|
model_element_family_ == ElementFamily::kThermalShell
|
|
? static_cast<EntityIndex>(
|
|
definition_.thermal_sections.size())
|
|
: static_cast<EntityIndex>(definition_.shell_sections.size());
|
|
if (model_element_family_ == ElementFamily::kThermalShell) {
|
|
definition_.thermal_sections.push_back(
|
|
{{part.name, static_cast<std::int64_t>(section_index),
|
|
section.element_set_name},
|
|
section.element_set_name,
|
|
section.thickness,
|
|
*material_index,
|
|
section.location});
|
|
} else {
|
|
definition_.shell_sections.push_back(
|
|
{section.element_set_name, section.thickness, *material_index,
|
|
section.location});
|
|
}
|
|
for (const auto element_label : element_set->members) {
|
|
if (!assignments
|
|
.emplace(element_label,
|
|
std::make_pair(section_index, *material_index))
|
|
.second) {
|
|
InputFailure("invalid-shell-section-assignment", section.location,
|
|
"SHELL SECTION", std::to_string(element_label),
|
|
"A shell element cannot receive multiple "
|
|
"section assignments.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& element : part.elements) {
|
|
if (assignments.find(element.label) == assignments.end()) {
|
|
InputFailure("invalid-shell-section-assignment", element.location,
|
|
"ELEMENT", element.label_text,
|
|
"Every shell element requires exactly one "
|
|
"resolved section assignment.");
|
|
return;
|
|
}
|
|
}
|
|
continue;
|
|
}
|
|
|
|
auto& assignments = part_section_assignments_[part_index];
|
|
for (const auto& section : part.sections) {
|
|
const auto* element_set =
|
|
FindSet(part.element_sets, section.element_set_name);
|
|
const auto material_index = FindMaterialIndex(section.material_name);
|
|
if (element_set == nullptr || !material_index) {
|
|
InputFailure("unresolved-reference", section.location,
|
|
"BEAM GENERAL SECTION", section.element_set_name,
|
|
"Section ELSET and MATERIAL references must resolve.");
|
|
return;
|
|
}
|
|
if (section.properties[2] != 0.0) {
|
|
ModelFailure("unsupported-coupled-section", section.location,
|
|
"BEAM GENERAL SECTION", section.element_set_name,
|
|
"V0 requires exact I12=0.");
|
|
return;
|
|
}
|
|
if (!(section.properties[0] > 0.0) || !(section.properties[1] > 0.0) ||
|
|
!(section.properties[3] > 0.0) || !(section.properties[4] > 0.0)) {
|
|
ModelFailure("invalid-beam-property", section.location,
|
|
"BEAM GENERAL SECTION", section.element_set_name,
|
|
"A, I11, I22, and J must be positive.");
|
|
return;
|
|
}
|
|
const EntityIndex section_index =
|
|
static_cast<EntityIndex>(definition_.sections.size());
|
|
definition_.sections.push_back(
|
|
{section.element_set_name, section.properties[0],
|
|
section.properties[1], section.properties[2],
|
|
section.properties[3], section.properties[4], section.first_axis,
|
|
section.section_points, section.location});
|
|
for (const auto element_label : element_set->members) {
|
|
if (!assignments
|
|
.emplace(element_label,
|
|
std::make_pair(section_index, *material_index))
|
|
.second) {
|
|
InputFailure("duplicate-entity", section.location,
|
|
"BEAM GENERAL SECTION", std::to_string(element_label),
|
|
"An element cannot receive multiple section "
|
|
"assignments.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& element : part.elements) {
|
|
if (assignments.find(element.label) == assignments.end()) {
|
|
InputFailure(
|
|
"unresolved-reference", element.location, "ELEMENT",
|
|
element.label_text,
|
|
"Every B33 element requires a resolved section assignment.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
}
|
|
|
|
std::size_t PartIndex(const RawPart& part) const {
|
|
return static_cast<std::size_t>(&part - parts_.data());
|
|
}
|
|
|
|
bool ValidateGeometry(const RawElement& raw, const Node& first,
|
|
const Node& second, const GeneralBeamSection& section) {
|
|
const auto maximum_absolute = [](const Vector3& vector) {
|
|
return std::max(
|
|
{std::abs(vector[0]), std::abs(vector[1]), std::abs(vector[2])});
|
|
};
|
|
const Vector3 first_position{first.coordinates};
|
|
const Vector3 second_position{second.coordinates};
|
|
|
|
// Compare both approved inequalities after a common scaling. This
|
|
// preserves the exact ratios while avoiding overflow in x*x and in
|
|
// subtraction between large finite coordinates.
|
|
const double global_coordinate_scale =
|
|
std::max({1.0, maximum_absolute(first_position),
|
|
maximum_absolute(second_position)});
|
|
const Vector3 first_scaled = first_position / global_coordinate_scale;
|
|
const Vector3 second_scaled = second_position / global_coordinate_scale;
|
|
const Vector3 delta_scaled = second_scaled - first_scaled;
|
|
const double length_ratio = delta_scaled.Norm();
|
|
const double coordinate_norm_ratio =
|
|
std::max({1.0 / global_coordinate_scale, first_scaled.Norm(),
|
|
second_scaled.Norm()});
|
|
if (!(length_ratio > 1.0e-12 * coordinate_norm_ratio)) {
|
|
return ModelFailure(
|
|
"invalid-beam-length", raw.location, "ELEMENT", raw.label_text,
|
|
"Beam length fails the approved scale-aware threshold.");
|
|
}
|
|
const Vector3 tangent = delta_scaled / length_ratio;
|
|
|
|
const Vector3 guide{section.first_axis};
|
|
const double global_guide_scale = std::max(1.0, maximum_absolute(guide));
|
|
const Vector3 guide_scaled = guide / global_guide_scale;
|
|
const double projection = guide_scaled.Dot(tangent);
|
|
const Vector3 perpendicular = guide_scaled - projection * tangent;
|
|
const double guide_norm_ratio =
|
|
std::max(1.0 / global_guide_scale, guide_scaled.Norm());
|
|
if (!(perpendicular.Norm() > 1.0e-12 * guide_norm_ratio)) {
|
|
return ModelFailure(
|
|
"invalid-beam-guide-vector", section.location, "BEAM GENERAL SECTION",
|
|
raw.label_text,
|
|
"The first section axis cannot be zero or tangent-parallel.");
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ExpandInstances() {
|
|
for (const auto& raw_instance : instances_) {
|
|
const RawPart* part = FindPart(raw_instance.part_name);
|
|
if (part == nullptr) {
|
|
InputFailure("unresolved-reference", raw_instance.location, "INSTANCE",
|
|
raw_instance.name,
|
|
"INSTANCE PART must resolve case-insensitively.");
|
|
return;
|
|
}
|
|
InstanceDefinition instance{
|
|
raw_instance.name, part->name, {}, {}, raw_instance.location};
|
|
std::map<std::int64_t, EntityIndex> node_indices;
|
|
std::map<std::int64_t, EntityIndex> element_indices;
|
|
|
|
// Instance order, followed by part-local declaration order, is the
|
|
// sole source of stable expanded internal IDs.
|
|
for (const auto& raw_node : part->nodes) {
|
|
if (definition_.nodes.size() >
|
|
std::numeric_limits<EntityIndex>::max()) {
|
|
InputFailure("entity-index-overflow", raw_node.location, "NODE",
|
|
raw_node.label_text,
|
|
"Expanded node count exceeds EntityIndex capacity.");
|
|
return;
|
|
}
|
|
const EntityIndex index =
|
|
static_cast<EntityIndex>(definition_.nodes.size());
|
|
definition_.nodes.push_back(
|
|
{{raw_instance.name, raw_node.label, raw_node.label_text},
|
|
raw_node.coordinates,
|
|
raw_node.location});
|
|
node_indices.emplace(raw_node.label, index);
|
|
instance.node_mappings.push_back({raw_node.label, index});
|
|
}
|
|
|
|
for (const auto& raw_element : part->elements) {
|
|
EntityIndex index = 0U;
|
|
if (raw_element.type == RawElement::Type::kB33) {
|
|
const auto first = node_indices.find(raw_element.node_labels[0]);
|
|
const auto second = node_indices.find(raw_element.node_labels[1]);
|
|
const auto& assignments = part_section_assignments_[PartIndex(*part)];
|
|
const auto assignment = assignments.find(raw_element.label);
|
|
if (first == node_indices.end() || second == node_indices.end() ||
|
|
assignment == assignments.end()) {
|
|
InputFailure(
|
|
"unresolved-reference", raw_element.location, "ELEMENT",
|
|
raw_element.label_text,
|
|
"Expanded connectivity and section assignment must resolve.");
|
|
return;
|
|
}
|
|
if (definition_.elements.size() >
|
|
std::numeric_limits<EntityIndex>::max()) {
|
|
InputFailure(
|
|
"entity-index-overflow", raw_element.location, "ELEMENT",
|
|
raw_element.label_text,
|
|
"Expanded element count exceeds EntityIndex capacity.");
|
|
return;
|
|
}
|
|
index = static_cast<EntityIndex>(definition_.elements.size());
|
|
const auto& section = definition_.sections[assignment->second.first];
|
|
if (!ValidateGeometry(raw_element, definition_.nodes[first->second],
|
|
definition_.nodes[second->second], section)) {
|
|
return;
|
|
}
|
|
definition_.elements.push_back(
|
|
{{raw_instance.name, raw_element.label, raw_element.label_text},
|
|
{first->second, second->second},
|
|
assignment->second.second,
|
|
assignment->second.first,
|
|
raw_element.location});
|
|
} else if (raw_element.type == RawElement::Type::kC3d8) {
|
|
std::array<EntityIndex, 8> connected_nodes{};
|
|
for (std::size_t node = 0U; node < connected_nodes.size(); ++node) {
|
|
const auto found = node_indices.find(raw_element.node_labels[node]);
|
|
if (found == node_indices.end()) {
|
|
InputFailure("invalid-solid-connectivity", raw_element.location,
|
|
"ELEMENT", raw_element.label_text,
|
|
"Solid nodes must resolve.");
|
|
return;
|
|
}
|
|
connected_nodes[node] = found->second;
|
|
}
|
|
const auto& assignments =
|
|
part_shell_section_assignments_[PartIndex(*part)];
|
|
const auto assignment = assignments.find(raw_element.label);
|
|
if (assignment == assignments.end() ||
|
|
definition_.solid_elements.size() >
|
|
std::numeric_limits<EntityIndex>::max()) {
|
|
InputFailure("invalid-solid-section-assignment",
|
|
raw_element.location, "ELEMENT",
|
|
raw_element.label_text,
|
|
"Solid assignment must resolve.");
|
|
return;
|
|
}
|
|
index = static_cast<EntityIndex>(definition_.solid_elements.size());
|
|
definition_.solid_elements.emplace_back(
|
|
SourceEntityId{raw_instance.name, raw_element.label,
|
|
raw_element.label_text},
|
|
connected_nodes, assignment->second.second,
|
|
assignment->second.first, raw_element.location);
|
|
} else {
|
|
std::array<EntityIndex, 4> connected_nodes{};
|
|
for (std::size_t node = 0U; node < connected_nodes.size(); ++node) {
|
|
const auto found = node_indices.find(raw_element.node_labels[node]);
|
|
if (found == node_indices.end()) {
|
|
InputFailure("invalid-shell-connectivity", raw_element.location,
|
|
"ELEMENT", raw_element.label_text,
|
|
"Expanded shell connectivity must resolve.");
|
|
return;
|
|
}
|
|
connected_nodes[node] = found->second;
|
|
}
|
|
const auto& assignments =
|
|
part_shell_section_assignments_[PartIndex(*part)];
|
|
const auto assignment = assignments.find(raw_element.label);
|
|
if (assignment == assignments.end()) {
|
|
InputFailure(
|
|
"invalid-shell-section-assignment", raw_element.location,
|
|
"ELEMENT", raw_element.label_text,
|
|
"Expanded shell section assignment must resolve exactly once.");
|
|
return;
|
|
}
|
|
if (raw_element.type == RawElement::Type::kDs4) {
|
|
if (definition_.thermal_elements.size() >
|
|
std::numeric_limits<EntityIndex>::max()) {
|
|
InputFailure("entity-index-overflow", raw_element.location,
|
|
"ELEMENT", raw_element.label_text,
|
|
"Expanded thermal element count exceeds EntityIndex "
|
|
"capacity.");
|
|
return;
|
|
}
|
|
index =
|
|
static_cast<EntityIndex>(definition_.thermal_elements.size());
|
|
definition_.thermal_elements.push_back(
|
|
{{raw_instance.name, raw_element.label, raw_element.label_text},
|
|
connected_nodes,
|
|
assignment->second.first,
|
|
raw_element.location});
|
|
element_indices.emplace(raw_element.label, index);
|
|
instance.element_mappings.push_back({raw_element.label, index});
|
|
continue;
|
|
}
|
|
if (definition_.shell_elements.size() >
|
|
std::numeric_limits<EntityIndex>::max()) {
|
|
InputFailure(
|
|
"entity-index-overflow", raw_element.location, "ELEMENT",
|
|
raw_element.label_text,
|
|
"Expanded shell element count exceeds EntityIndex capacity.");
|
|
return;
|
|
}
|
|
index = static_cast<EntityIndex>(definition_.shell_elements.size());
|
|
definition_.shell_elements.push_back(
|
|
{{raw_instance.name, raw_element.label, raw_element.label_text},
|
|
raw_element.type == RawElement::Type::kS4
|
|
? ShellSourceElementType::kS4
|
|
: ShellSourceElementType::kS4r,
|
|
connected_nodes,
|
|
assignment->second.second,
|
|
assignment->second.first,
|
|
raw_element.location});
|
|
}
|
|
element_indices.emplace(raw_element.label, index);
|
|
instance.element_mappings.push_back({raw_element.label, index});
|
|
}
|
|
|
|
for (const auto& raw_set : part->node_sets) {
|
|
NodeSet set{raw_set.name, raw_instance.name, {}, raw_set.location};
|
|
for (const auto member : raw_set.members) {
|
|
const auto found = node_indices.find(member);
|
|
if (found == node_indices.end()) {
|
|
InputFailure("unresolved-reference", raw_set.location, "NSET",
|
|
raw_set.name, "Part node-set expansion failed.");
|
|
return;
|
|
}
|
|
set.node_indices.push_back(found->second);
|
|
}
|
|
definition_.node_sets.push_back(std::move(set));
|
|
}
|
|
for (const auto& raw_set : part->element_sets) {
|
|
ElementSet set{raw_set.name, raw_instance.name, {}, raw_set.location};
|
|
for (const auto member : raw_set.members) {
|
|
const auto found = element_indices.find(member);
|
|
if (found == element_indices.end()) {
|
|
InputFailure("unresolved-reference", raw_set.location, "ELSET",
|
|
raw_set.name, "Part element-set expansion failed.");
|
|
return;
|
|
}
|
|
set.element_indices.push_back(found->second);
|
|
}
|
|
definition_.element_sets.push_back(std::move(set));
|
|
}
|
|
definition_.instances.push_back(std::move(instance));
|
|
}
|
|
}
|
|
|
|
void ExpandAssemblySets() {
|
|
for (const auto& raw_set : assembly_sets_) {
|
|
const RawInstance* raw_instance = FindInstance(raw_set.instance_name);
|
|
if (raw_instance == nullptr) {
|
|
InputFailure("unresolved-reference", raw_set.location,
|
|
raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name,
|
|
"Assembly set INSTANCE must resolve.");
|
|
return;
|
|
}
|
|
const auto definition_instance = std::find_if(
|
|
definition_.instances.begin(), definition_.instances.end(),
|
|
[&raw_instance](const InstanceDefinition& instance) {
|
|
return AsciiCaseInsensitiveEquals(instance.name,
|
|
raw_instance->name);
|
|
});
|
|
if (definition_instance == definition_.instances.end()) {
|
|
InputFailure("unresolved-reference", raw_set.location,
|
|
raw_set.is_node_set ? "NSET" : "ELSET", raw_set.name,
|
|
"Assembly set instance expansion is unavailable.");
|
|
return;
|
|
}
|
|
if (raw_set.is_node_set) {
|
|
definition_.node_sets.erase(
|
|
std::remove_if(
|
|
definition_.node_sets.begin(), definition_.node_sets.end(),
|
|
[&raw_set](const NodeSet& set) {
|
|
return AsciiCaseInsensitiveEquals(set.name, raw_set.name);
|
|
}),
|
|
definition_.node_sets.end());
|
|
NodeSet set{
|
|
raw_set.name, definition_instance->name, {}, raw_set.location};
|
|
for (const auto member : raw_set.members) {
|
|
const auto mapping =
|
|
std::find_if(definition_instance->node_mappings.begin(),
|
|
definition_instance->node_mappings.end(),
|
|
[member](const SourceIndexMapping& value) {
|
|
return value.source_label == member;
|
|
});
|
|
if (mapping == definition_instance->node_mappings.end()) {
|
|
InputFailure("unresolved-reference", raw_set.location, "NSET",
|
|
raw_set.name,
|
|
"Assembly node-set member must resolve in its "
|
|
"instance.");
|
|
return;
|
|
}
|
|
set.node_indices.push_back(mapping->internal_index);
|
|
}
|
|
definition_.node_sets.push_back(std::move(set));
|
|
} else {
|
|
definition_.element_sets.erase(
|
|
std::remove_if(definition_.element_sets.begin(),
|
|
definition_.element_sets.end(),
|
|
[&raw_set](const ElementSet& set) {
|
|
return AsciiCaseInsensitiveEquals(set.name,
|
|
raw_set.name);
|
|
}),
|
|
definition_.element_sets.end());
|
|
ElementSet set{
|
|
raw_set.name, definition_instance->name, {}, raw_set.location};
|
|
for (const auto member : raw_set.members) {
|
|
const auto mapping =
|
|
std::find_if(definition_instance->element_mappings.begin(),
|
|
definition_instance->element_mappings.end(),
|
|
[member](const SourceIndexMapping& value) {
|
|
return value.source_label == member;
|
|
});
|
|
if (mapping == definition_instance->element_mappings.end()) {
|
|
InputFailure("unresolved-reference", raw_set.location, "ELSET",
|
|
raw_set.name,
|
|
"Assembly element-set member must resolve in "
|
|
"its instance.");
|
|
return;
|
|
}
|
|
set.element_indices.push_back(mapping->internal_index);
|
|
}
|
|
definition_.element_sets.push_back(std::move(set));
|
|
}
|
|
}
|
|
}
|
|
|
|
bool AppendSurfaceFacet(
|
|
const RawSurfaceMember& member, const RawInstance& raw_instance,
|
|
std::size_t instance_ordinal, EntityIndex element_index,
|
|
SurfaceMemberKind member_kind, const std::string& elset_name_raw,
|
|
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
|
candidates) {
|
|
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,
|
|
"Element surfaces may resolve only to S4 or S4R shell elements.");
|
|
}
|
|
const RawPart* part = FindPart(raw_instance.part_name);
|
|
if (part == nullptr) {
|
|
return InputFailure(
|
|
"unresolved-reference", member.location, "SURFACE", member.target,
|
|
"The surface instance must resolve to its source part.");
|
|
}
|
|
const auto& source_id =
|
|
thermal ? definition_.thermal_elements[element_index].source_id
|
|
: definition_.shell_elements[element_index].source_id;
|
|
const std::string source_type =
|
|
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),
|
|
source_id.source_label,
|
|
source_id.source_label_text,
|
|
source_type,
|
|
member.side,
|
|
member.location,
|
|
member.ordinal,
|
|
member_kind,
|
|
elset_name_raw,
|
|
member_kind == SurfaceMemberKind::kElset
|
|
? CanonicalizeAscii(elset_name_raw)
|
|
: ""};
|
|
candidates->push_back({std::move(facet), instance_ordinal});
|
|
return true;
|
|
}
|
|
|
|
bool ResolveDirectSurfaceMember(
|
|
const RawSurfaceMember& member,
|
|
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
|
candidates) {
|
|
const std::size_t dot = member.target.find('.');
|
|
if (dot == std::string::npos || dot == 0U ||
|
|
dot + 1U >= member.target.size() ||
|
|
member.target.find('.', dot + 1U) != std::string::npos) {
|
|
return InputFailure(
|
|
"unresolved-reference", member.location, "SURFACE", member.target,
|
|
"A direct surface member must be instance.positive-element-label.");
|
|
}
|
|
const std::string instance_name = member.target.substr(0U, dot);
|
|
if (!IsPressureNameToken(instance_name)) {
|
|
return InputFailure(
|
|
"unsupported-surface-target", member.location, "SURFACE",
|
|
member.target,
|
|
"A direct surface member must use an identity instance name.");
|
|
}
|
|
std::int64_t source_label = 0;
|
|
if (!ParseInteger(member.target.substr(dot + 1U), source_label,
|
|
member.location, "SURFACE", true)) {
|
|
return false;
|
|
}
|
|
const auto raw_instance = std::find_if(
|
|
instances_.begin(), instances_.end(),
|
|
[&instance_name](const RawInstance& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, instance_name);
|
|
});
|
|
if (raw_instance == instances_.end()) {
|
|
if (FindPart(instance_name) != nullptr) {
|
|
return InputFailure(
|
|
"unsupported-surface-target", member.location, "SURFACE",
|
|
member.target,
|
|
"A direct surface member cannot use a part name as an instance.");
|
|
}
|
|
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
|
member.target,
|
|
"A direct surface member instance must resolve.");
|
|
}
|
|
const auto instance = std::find_if(
|
|
definition_.instances.begin(), definition_.instances.end(),
|
|
[&raw_instance](const InstanceDefinition& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, raw_instance->name);
|
|
});
|
|
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 a supported shell element.");
|
|
}
|
|
return AppendSurfaceFacet(member, *raw_instance,
|
|
static_cast<std::size_t>(std::distance(
|
|
instances_.begin(), raw_instance)),
|
|
element->internal_index,
|
|
SurfaceMemberKind::kDirect, "", candidates);
|
|
}
|
|
|
|
bool ResolveElsetSurfaceMember(
|
|
const RawSurfaceMember& member,
|
|
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
|
candidates) {
|
|
if (!IsPressureNameToken(member.target)) {
|
|
return InputFailure("unsupported-surface-target", member.location,
|
|
"SURFACE", member.target,
|
|
"A surface ELSET must use the approved name syntax.");
|
|
}
|
|
const auto raw_set = std::find_if(
|
|
assembly_sets_.begin(), assembly_sets_.end(),
|
|
[&member](const RawAssemblySet& value) {
|
|
return !value.is_node_set &&
|
|
AsciiCaseInsensitiveEquals(value.name, member.target);
|
|
});
|
|
if (raw_set == assembly_sets_.end()) {
|
|
const bool part_scoped = std::any_of(
|
|
parts_.begin(), parts_.end(), [this, &member](const RawPart& part) {
|
|
return FindSet(part.element_sets, member.target) != nullptr;
|
|
});
|
|
if (part_scoped) {
|
|
return InputFailure(
|
|
"cross-scope-target", member.location, "SURFACE", member.target,
|
|
"A surface ELSET must be declared at assembly scope.");
|
|
}
|
|
return InputFailure(
|
|
"missing-reference", member.location, "SURFACE", member.target,
|
|
"A surface ELSET must be assembly-scoped and explicit-instance.");
|
|
}
|
|
const auto raw_instance = std::find_if(
|
|
instances_.begin(), instances_.end(),
|
|
[&raw_set](const RawInstance& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, raw_set->instance_name);
|
|
});
|
|
if (raw_instance == instances_.end()) {
|
|
return InputFailure("unresolved-reference", member.location, "SURFACE",
|
|
member.target,
|
|
"The surface ELSET instance must resolve.");
|
|
}
|
|
const auto element_set = std::find_if(
|
|
definition_.element_sets.begin(), definition_.element_sets.end(),
|
|
[&raw_set](const ElementSet& value) {
|
|
return AsciiCaseInsensitiveEquals(value.name, raw_set->name) &&
|
|
value.instance_name &&
|
|
AsciiCaseInsensitiveEquals(*value.instance_name,
|
|
raw_set->instance_name);
|
|
});
|
|
if (element_set == definition_.element_sets.end()) {
|
|
return InputFailure(
|
|
"unresolved-reference", member.location, "SURFACE", member.target,
|
|
"The surface ELSET must expand in its explicit instance.");
|
|
}
|
|
for (const EntityIndex element_index : element_set->element_indices) {
|
|
if (!AppendSurfaceFacet(member, *raw_instance,
|
|
static_cast<std::size_t>(std::distance(
|
|
instances_.begin(), raw_instance)),
|
|
element_index, SurfaceMemberKind::kElset,
|
|
member.target, candidates)) {
|
|
return false;
|
|
}
|
|
}
|
|
return true;
|
|
}
|
|
|
|
void ResolveElementSurfaces() {
|
|
std::vector<ElementSurfaceDefinition> resolved_surfaces;
|
|
resolved_surfaces.reserve(element_surfaces_.size());
|
|
for (const auto& surface : element_surfaces_) {
|
|
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>
|
|
candidates;
|
|
for (const auto& member : surface.members) {
|
|
const bool is_direct = member.target.find('.') != std::string::npos;
|
|
if (!(is_direct ? ResolveDirectSurfaceMember(member, &candidates)
|
|
: ResolveElsetSurfaceMember(member, &candidates))) {
|
|
return;
|
|
}
|
|
}
|
|
if (candidates.empty()) {
|
|
InputFailure("empty-surface", surface.location, "SURFACE", surface.name,
|
|
"An element surface must resolve at least one facet.");
|
|
return;
|
|
}
|
|
|
|
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>
|
|
unique_facets;
|
|
for (auto& candidate : candidates) {
|
|
const auto same_element =
|
|
std::find_if(unique_facets.begin(), unique_facets.end(),
|
|
[&candidate](const auto& existing) {
|
|
return existing.first.instance_name_canonical ==
|
|
candidate.first.instance_name_canonical &&
|
|
existing.first.source_element_label ==
|
|
candidate.first.source_element_label;
|
|
});
|
|
if (same_element != unique_facets.end()) {
|
|
if (same_element->first.side != candidate.first.side) {
|
|
InputFailure(
|
|
"conflicting-surface-membership",
|
|
candidate.first.first_member_location, "SURFACE", surface.name,
|
|
"One shell element cannot appear on both surface sides.");
|
|
return;
|
|
}
|
|
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));
|
|
}
|
|
std::sort(unique_facets.begin(), unique_facets.end(),
|
|
[](const auto& lhs, const auto& rhs) {
|
|
const int lhs_side =
|
|
lhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1;
|
|
const int rhs_side =
|
|
rhs.first.side == ShellSurfaceSide::kSpos ? 0 : 1;
|
|
return std::tie(lhs.second, lhs.first.source_element_label,
|
|
lhs_side) <
|
|
std::tie(rhs.second, rhs.first.source_element_label,
|
|
rhs_side);
|
|
});
|
|
|
|
ElementSurfaceDefinition resolved{surface.name,
|
|
CanonicalizeAscii(surface.name),
|
|
{},
|
|
surface.location,
|
|
surface.declaration_ordinal};
|
|
resolved.facets.reserve(unique_facets.size());
|
|
for (std::size_t ordinal = 0U; ordinal < unique_facets.size();
|
|
++ordinal) {
|
|
unique_facets[ordinal].first.facet_ordinal = ordinal;
|
|
resolved.facets.push_back(std::move(unique_facets[ordinal].first));
|
|
}
|
|
resolved_surfaces.push_back(std::move(resolved));
|
|
}
|
|
definition_.element_surfaces = std::move(resolved_surfaces);
|
|
}
|
|
|
|
/// @brief Builds one stable node-target index from the completed candidate.
|
|
SourceTargetIndex BuildSourceTargetIndex() const {
|
|
std::vector<SourceTargetIndexEntry> entries;
|
|
std::size_t declaration_order = 0U;
|
|
for (std::size_t node = 0U; node < definition_.nodes.size(); ++node) {
|
|
const auto& source_id = definition_.nodes[node].source_id;
|
|
entries.push_back({SourceEntityKind::kNode, source_id.instance_name, "",
|
|
source_id, static_cast<EntityIndex>(node),
|
|
declaration_order++});
|
|
}
|
|
for (const auto& set : definition_.node_sets) {
|
|
for (const EntityIndex node : set.node_indices) {
|
|
const auto& source_id = definition_.nodes[node].source_id;
|
|
entries.push_back({SourceEntityKind::kNode,
|
|
set.instance_name.value_or(source_id.instance_name),
|
|
set.name, source_id, node, declaration_order++});
|
|
}
|
|
}
|
|
return SourceTargetIndex{std::move(entries)};
|
|
}
|
|
|
|
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
|
|
const SourceTargetResolver& resolver, const std::string& target,
|
|
const SourceLocation& location, const std::string& keyword) {
|
|
auto resolved = resolver.Resolve({SourceEntityKind::kNode, "", target});
|
|
if (!resolved.HasValue()) {
|
|
InputFailure(
|
|
"unresolved-reference", location, keyword, target,
|
|
"The boundary or load target must resolve unambiguously to one "
|
|
"node or one node set.");
|
|
return std::nullopt;
|
|
}
|
|
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;
|
|
}
|
|
|
|
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 =
|
|
[this, &resolver](
|
|
const std::vector<RawThermalBoundary>& rows,
|
|
std::map<std::pair<EntityIndex, int>, double>* values,
|
|
std::vector<ThermalBoundaryConditionDefinition>* out) {
|
|
for (const auto& row : rows) {
|
|
const auto nodes = ResolveNodeTarget(resolver, row.target,
|
|
row.location, "BOUNDARY");
|
|
if (!nodes) return false;
|
|
for (const EntityIndex node : *nodes) {
|
|
const auto key = std::make_pair(node, row.dof);
|
|
const auto existing = values->find(key);
|
|
if (existing != values->end() &&
|
|
existing->second != row.value) {
|
|
return InputFailure("conflicting-boundary-condition",
|
|
row.location, "BOUNDARY", row.target,
|
|
"Expanded thermal boundaries prescribe "
|
|
"different values.");
|
|
}
|
|
(*values)[key] = row.value;
|
|
out->push_back({node, row.dof, row.value, row.location});
|
|
}
|
|
}
|
|
return true;
|
|
};
|
|
std::map<std::pair<EntityIndex, int>, double> model_values;
|
|
if (!append_boundaries(thermal_model_boundaries_, &model_values,
|
|
&definition_.thermal_model_boundaries))
|
|
return;
|
|
std::vector<ThermalBoundaryConditionDefinition> step_boundaries;
|
|
std::map<std::pair<EntityIndex, int>, double> step_values;
|
|
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});
|
|
}
|
|
}
|
|
std::vector<ThermalFluxLoadDefinition> loads;
|
|
for (const auto& row : step_.thermal_fluxes) {
|
|
const auto nodes =
|
|
ResolveNodeTarget(resolver, row.target, row.location, "CFLUX");
|
|
if (!nodes) return;
|
|
for (const EntityIndex node : *nodes) {
|
|
loads.push_back(
|
|
{node, row.dof, row.power, row.location, loads.size()});
|
|
}
|
|
}
|
|
definition_.thermal_step = {step_.name_raw,
|
|
step_.thermal_increment,
|
|
step_.thermal_period,
|
|
step_.maximum_increments,
|
|
false,
|
|
std::move(step_boundaries),
|
|
std::move(loads),
|
|
step_.location};
|
|
return;
|
|
}
|
|
std::vector<PrescribedDisplacementDefinition> boundaries =
|
|
model_boundaries_;
|
|
boundaries.insert(boundaries.end(), step_.boundaries.begin(),
|
|
step_.boundaries.end());
|
|
if (step_.frequency) {
|
|
ValidateEigenBoundaries(boundaries);
|
|
if (failure_) {
|
|
return;
|
|
}
|
|
}
|
|
if (step_.dynamic) {
|
|
ValidateDynamicBoundaries(boundaries);
|
|
if (failure_) {
|
|
return;
|
|
}
|
|
}
|
|
const SourceTargetIndex target_index = BuildSourceTargetIndex();
|
|
const SourceTargetResolver target_resolver{target_index};
|
|
|
|
if (model_element_family_ == ElementFamily::kSolid) {
|
|
std::vector<PrescribedDisplacementDefinition> physical;
|
|
for (auto boundary : boundaries) {
|
|
if (!ResolveNodeTarget(target_resolver, boundary.target,
|
|
boundary.location, "BOUNDARY"))
|
|
return;
|
|
if (boundary.last_dof > 3) {
|
|
if (boundary.value != 0.0) {
|
|
InputFailure("unsupported-solid-rotation-boundary",
|
|
boundary.location, "BOUNDARY", boundary.target,
|
|
"Solid rotational boundaries must be exact zero to be "
|
|
"inactive.");
|
|
return;
|
|
}
|
|
definition_.warnings.push_back(
|
|
{Severity::kWarning, "inactive-solid-rotation-boundary",
|
|
boundary.location, "BOUNDARY", boundary.target,
|
|
"Exact-zero rotational boundary DOFs " +
|
|
std::to_string(std::max(4, boundary.first_dof)) + ".." +
|
|
std::to_string(boundary.last_dof) +
|
|
" are inactive; Solid equations contain translations "
|
|
"only."});
|
|
boundary.last_dof = 3;
|
|
}
|
|
if (boundary.first_dof <= boundary.last_dof)
|
|
physical.push_back(std::move(boundary));
|
|
}
|
|
boundaries = std::move(physical);
|
|
std::vector<bool> active(definition_.nodes.size(), false);
|
|
for (const auto& element : definition_.solid_elements)
|
|
for (const auto node : element.node_indices) active[node] = true;
|
|
for (std::size_t node = 0U; node < active.size(); ++node) {
|
|
if (!active[node]) {
|
|
ModelFailure("unused-solid-node", definition_.nodes[node].location,
|
|
"NODE",
|
|
definition_.nodes[node].source_id.source_label_text,
|
|
"Every Solid node must connect to a C3D8 element.");
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
|
|
for (const auto& boundary : boundaries) {
|
|
auto target = ResolveNodeTarget(target_resolver, boundary.target,
|
|
boundary.location, "BOUNDARY");
|
|
if (!target) {
|
|
return;
|
|
}
|
|
for (const auto node : *target) {
|
|
for (int dof = boundary.first_dof; dof <= boundary.last_dof; ++dof) {
|
|
const auto key = std::make_pair(node, dof);
|
|
const auto existing = prescribed_values.find(key);
|
|
if (existing != prescribed_values.end() &&
|
|
existing->second != boundary.value) {
|
|
InputFailure("conflicting-boundary-condition", boundary.location,
|
|
"BOUNDARY", boundary.target,
|
|
"Expanded boundary rows prescribe different values to "
|
|
"one node/DOF.");
|
|
return;
|
|
}
|
|
prescribed_values[key] = boundary.value;
|
|
}
|
|
}
|
|
}
|
|
for (const auto& load : step_.loads) {
|
|
if (!ResolveNodeTarget(target_resolver, load.target, load.location,
|
|
"CLOAD")) {
|
|
return;
|
|
}
|
|
}
|
|
if (step_.frequency) {
|
|
step_.frequency->boundaries = std::move(boundaries);
|
|
definition_.frequency_step = std::move(step_.frequency);
|
|
return;
|
|
}
|
|
if (step_.dynamic) {
|
|
step_.dynamic->boundaries = std::move(boundaries);
|
|
step_.dynamic->loads = step_.loads;
|
|
definition_.dynamic_step = std::move(step_.dynamic);
|
|
return;
|
|
}
|
|
std::vector<SurfacePressureLoadDefinition> surface_pressure_loads;
|
|
surface_pressure_loads.reserve(step_.surface_pressure_loads.size());
|
|
for (const auto& load : step_.surface_pressure_loads) {
|
|
const auto surface = std::find_if(
|
|
definition_.element_surfaces.begin(),
|
|
definition_.element_surfaces.end(), [&load](const auto& candidate) {
|
|
return AsciiCaseInsensitiveEquals(candidate.surface_name_raw,
|
|
load.surface_name);
|
|
});
|
|
if (surface == definition_.element_surfaces.end()) {
|
|
InputFailure("missing-reference", load.data_location, "DSLOAD",
|
|
load.surface_name,
|
|
"A DSLOAD surface must resolve after surface validation.");
|
|
return;
|
|
}
|
|
surface_pressure_loads.push_back(
|
|
{step_.name_raw, step_.name_canonical,
|
|
static_cast<EntityIndex>(
|
|
std::distance(definition_.element_surfaces.begin(), surface)),
|
|
load.surface_name, CanonicalizeAscii(load.surface_name), "P",
|
|
load.magnitude, load.magnitude_raw, load.keyword_location,
|
|
load.data_location, load.data_ordinal, load.load_ordinal});
|
|
}
|
|
|
|
// Static time-control values are provenance only: V0 creates exactly
|
|
// the canonical final frame 0 and performs no increment loop here.
|
|
definition_.steps.push_back(
|
|
{model_element_family_ == ElementFamily::kSolid ? step_.name_raw
|
|
: "Step-1",
|
|
std::move(boundaries), step_.loads, std::move(surface_pressure_loads),
|
|
step_.procedure_kind, step_.nlgeom, step_.static_values[0],
|
|
step_.static_values[1], step_.static_values[2], step_.static_values[3],
|
|
step_.location});
|
|
definition_.steps.back().direct = step_.static_direct;
|
|
definition_.steps.back().increment_count = step_.static_increment_count;
|
|
definition_.steps.back().maximum_increments = step_.maximum_increments;
|
|
}
|
|
|
|
const ParsedInput& input_;
|
|
ModelDefinition definition_{};
|
|
std::optional<MappingFailure> failure_;
|
|
|
|
std::vector<RawPart> parts_;
|
|
std::vector<RawInstance> instances_;
|
|
std::vector<RawAssemblySet> assembly_sets_;
|
|
std::vector<RawElementSurface> element_surfaces_;
|
|
std::vector<RawMaterial> materials_;
|
|
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_;
|
|
std::vector<std::map<std::int64_t, std::pair<EntityIndex, EntityIndex>>>
|
|
part_shell_section_assignments_;
|
|
|
|
std::optional<std::size_t> current_part_;
|
|
std::optional<std::size_t> pending_section_;
|
|
std::optional<std::size_t> material_eligible_;
|
|
std::optional<ElementFamily> model_element_family_;
|
|
bool heading_seen_{false};
|
|
bool part_elements_seen_{false};
|
|
bool part_sets_seen_{false};
|
|
bool part_sections_seen_{false};
|
|
bool beam_section_context_active_{false};
|
|
bool assembly_seen_{false};
|
|
bool assembly_set_seen_{false};
|
|
bool model_boundary_seen_{false};
|
|
bool in_assembly_{false};
|
|
bool in_instance_{false};
|
|
bool step_seen_{false};
|
|
bool in_step_{false};
|
|
bool step_load_seen_{false};
|
|
bool step_no_op_seen_{false};
|
|
bool active_output_{false};
|
|
bool active_output_is_field_{false};
|
|
std::optional<SourceLocation> physical_constants_location_;
|
|
};
|
|
|
|
DomainMappingContext::DomainMappingContext(const ParsedInput& input)
|
|
: impl_{std::make_unique<Impl>(input)} {}
|
|
|
|
DomainMappingContext::~DomainMappingContext() = default;
|
|
|
|
DomainMappingCandidateSummary DomainMappingContext::CandidateSummary()
|
|
const noexcept {
|
|
return impl_->CandidateSummary();
|
|
}
|
|
|
|
Status DomainMappingContext::ParseSyntax() { return impl_->ParseSyntax(); }
|
|
|
|
Status DomainMappingContext::FinalizeMaterialProperties() {
|
|
return impl_->FinalizeMaterialProperties();
|
|
}
|
|
|
|
Status DomainMappingContext::FinalizeTopology() {
|
|
return impl_->FinalizeTopology();
|
|
}
|
|
|
|
Status DomainMappingContext::FinalizeElementSurfaces() {
|
|
return impl_->FinalizeElementSurfaces();
|
|
}
|
|
|
|
Status DomainMappingContext::FinalizeStep() {
|
|
return impl_->FinalizeStepCandidate();
|
|
}
|
|
|
|
Result<Domain> DomainMappingContext::Commit() { return impl_->Commit(); }
|
|
|
|
} // namespace fesa::abaqus_internal
|