Integrate Refactor solver sources and dual MSVC CMake builds
This commit is contained in:
File diff suppressed because it is too large
Load Diff
@@ -12,6 +12,8 @@
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/ascii.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
@@ -40,16 +42,6 @@ std::string Trim(std::string_view text) {
|
||||
return std::string{text};
|
||||
}
|
||||
|
||||
std::string UppercaseAscii(std::string value) {
|
||||
std::transform(value.begin(), value.end(), value.begin(), [](char character) {
|
||||
if (character >= 'a' && character <= 'z') {
|
||||
return static_cast<char>(character - 'a' + 'A');
|
||||
}
|
||||
return character;
|
||||
});
|
||||
return value;
|
||||
}
|
||||
|
||||
std::vector<std::string> SplitFields(std::string_view line) {
|
||||
std::vector<std::string> fields;
|
||||
std::size_t field_start = 0U;
|
||||
@@ -88,13 +80,14 @@ bool AppendKeywordParameters(KeywordBlock& block,
|
||||
}
|
||||
|
||||
const std::size_t equals = field.find('=');
|
||||
const std::string parameter_name =
|
||||
std::string parameter_name =
|
||||
Trim(std::string_view{field}.substr(0U, equals));
|
||||
if (parameter_name.empty()) {
|
||||
return false;
|
||||
}
|
||||
|
||||
KeywordParameter parameter{UppercaseAscii(parameter_name), std::nullopt};
|
||||
KeywordParameter parameter{AsciiUppercase(std::move(parameter_name)),
|
||||
std::nullopt};
|
||||
if (equals != std::string::npos) {
|
||||
parameter.value = Trim(std::string_view{field}.substr(equals + 1U));
|
||||
}
|
||||
@@ -132,8 +125,10 @@ Result<ParsedInput> Failure(const std::filesystem::path& source_path,
|
||||
std::move(keyword),
|
||||
"",
|
||||
std::move(message)};
|
||||
std::vector<Diagnostic> diagnostics;
|
||||
diagnostics.push_back(std::move(diagnostic));
|
||||
return Result<ParsedInput>::Failure(
|
||||
Status::Failure(FailureCategory::kInput, {std::move(diagnostic)}));
|
||||
Status::Failure(FailureCategory::kInput, std::move(diagnostics)));
|
||||
}
|
||||
|
||||
} // namespace
|
||||
@@ -182,8 +177,9 @@ Result<ParsedInput> AbaqusInputReader::Read(
|
||||
}
|
||||
}
|
||||
|
||||
std::string trimmed_line = Trim(original_line);
|
||||
physical_lines.push_back(
|
||||
{original_line, Trim(original_line), line_number});
|
||||
{std::move(original_line), std::move(trimmed_line), line_number});
|
||||
|
||||
if (newline == std::string::npos) {
|
||||
break;
|
||||
@@ -209,7 +205,7 @@ Result<ParsedInput> AbaqusInputReader::Read(
|
||||
"A keyword line requires a non-empty keyword name.");
|
||||
}
|
||||
|
||||
KeywordBlock block{UppercaseAscii(keyword_text),
|
||||
KeywordBlock block{AsciiUppercase(keyword_text),
|
||||
line.original_line,
|
||||
{line.original_line},
|
||||
{},
|
||||
|
||||
@@ -0,0 +1,105 @@
|
||||
#ifndef FESA_IO_ABAQUS_MAPPING_DIAGNOSTICS_H_
|
||||
#define FESA_IO_ABAQUS_MAPPING_DIAGNOSTICS_H_
|
||||
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/status.h"
|
||||
#include "fesa/io/abaqus/input_syntax.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
struct MappingFailure {
|
||||
FailureCategory category;
|
||||
Diagnostic diagnostic;
|
||||
};
|
||||
|
||||
/// @brief Drops only trailing empty syntax fields without changing interior fields.
|
||||
std::vector<std::string> WithoutTrailingEmpty(std::vector<std::string> fields);
|
||||
|
||||
/// @brief Retains the first mapping failure and shared lexical validation.
|
||||
/// @note Mapping stage candidates borrow this context-owned service.
|
||||
class MappingDiagnostics {
|
||||
public:
|
||||
/// @brief Reports whether a previous mapping stage retained a failure.
|
||||
bool HasFailure() const noexcept { return failure_.has_value(); }
|
||||
|
||||
/// @brief Reports the existing unsupported-keyword policy for this model
|
||||
/// family.
|
||||
void RejectUnknown(const KeywordBlock& block, bool shell_model);
|
||||
|
||||
/// @brief Copies the retained first failure or returns successful mapping
|
||||
/// status.
|
||||
Status CurrentStatus() const;
|
||||
|
||||
/// @brief Retains the first diagnostic of a failed nonempty stage status.
|
||||
void RecordFailure(const Status& status);
|
||||
|
||||
/// @brief Borrows one canonical parameter from its source keyword block.
|
||||
const KeywordParameter* Parameter(const KeywordBlock& block,
|
||||
std::string_view name) const;
|
||||
|
||||
/// @brief Retains one diagnostic only if no earlier mapping failure exists.
|
||||
bool Fail(FailureCategory category, std::string code,
|
||||
const SourceLocation& location, std::string keyword,
|
||||
std::string entity_identity, std::string message);
|
||||
|
||||
/// @brief Retains the first input-category diagnostic.
|
||||
bool InputFailure(std::string code, const SourceLocation& location,
|
||||
std::string keyword, std::string entity_identity,
|
||||
std::string message);
|
||||
|
||||
/// @brief Retains the first model-category diagnostic.
|
||||
bool ModelFailure(std::string code, const SourceLocation& location,
|
||||
std::string keyword, std::string entity_identity,
|
||||
std::string message);
|
||||
|
||||
/// @brief Reports a keyword outside its approved source scope.
|
||||
bool InvalidKeywordLocation(const KeywordBlock& block, std::string message);
|
||||
|
||||
/// @brief Rejects duplicate or unsupported parameters in declaration order.
|
||||
bool ValidateParameters(
|
||||
const KeywordBlock& block, const std::vector<std::string_view>& allowed,
|
||||
std::string_view unsupported_code = "invalid-keyword-parameter");
|
||||
|
||||
/// @brief Borrows a required nonempty value from its source keyword block.
|
||||
const std::string* RequiredParameterValue(const KeywordBlock& block,
|
||||
std::string_view name);
|
||||
|
||||
/// @brief Rejects the first source row of a data-free keyword.
|
||||
bool RequireNoData(const KeywordBlock& block);
|
||||
|
||||
/// @brief Parses the existing base-10 scalar policy with contextual
|
||||
/// diagnostics.
|
||||
bool ParseInteger(const std::string& text, std::int64_t& value,
|
||||
const SourceLocation& location, const std::string& keyword,
|
||||
bool positive);
|
||||
|
||||
/// @brief Parses finite structural scalars with the existing input failure
|
||||
/// policy.
|
||||
bool ParseDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location, const std::string& keyword);
|
||||
|
||||
/// @brief Parses thermal decimal scalars while preserving representable
|
||||
/// subnormals.
|
||||
bool ParseThermalDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword);
|
||||
|
||||
/// @brief Distinguishes malformed syntax from nonfinite model property
|
||||
/// values.
|
||||
bool ParseModelDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword,
|
||||
const std::string& diagnostic_code,
|
||||
const std::string& message);
|
||||
|
||||
protected:
|
||||
std::optional<MappingFailure> failure_;
|
||||
};
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
|
||||
#endif // FESA_IO_ABAQUS_MAPPING_DIAGNOSTICS_H_
|
||||
@@ -0,0 +1,185 @@
|
||||
#ifndef FESA_IO_ABAQUS_MATERIAL_PROPERTY_CANDIDATES_H_
|
||||
#define FESA_IO_ABAQUS_MATERIAL_PROPERTY_CANDIDATES_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <map>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/model/model_types.h"
|
||||
#include "io/abaqus/mapping_diagnostics.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
enum class ElementFamily;
|
||||
struct RawPart;
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/// @brief Owns source section candidates for one part before resolution.
|
||||
struct PartPropertyCandidates {
|
||||
std::vector<RawSection> sections;
|
||||
std::vector<RawShellSection> shell_sections;
|
||||
std::vector<RawSolidSection> solid_sections;
|
||||
};
|
||||
|
||||
/// @brief Supplies procedure policy without giving material mapping step state.
|
||||
struct MaterialProcedureView {
|
||||
struct Surface {
|
||||
SourceLocation location;
|
||||
std::string name;
|
||||
};
|
||||
bool frequency;
|
||||
bool dynamic;
|
||||
const SourceLocation& location;
|
||||
const std::string& name_raw;
|
||||
std::optional<Surface> first_surface;
|
||||
const std::optional<SourceLocation>& physical_constants_location;
|
||||
};
|
||||
|
||||
/// @brief Owns material candidates, their validation, and section assignments.
|
||||
/// @note The context-owned diagnostics, input, and family outlive this object.
|
||||
class MaterialPropertyCandidates {
|
||||
public:
|
||||
using Assignments =
|
||||
std::map<std::int64_t, std::pair<EntityIndex, EntityIndex>>;
|
||||
|
||||
/// @brief Borrows context services while owning this uncommitted mapping
|
||||
/// stage.
|
||||
MaterialPropertyCandidates(MappingDiagnostics& diagnostics,
|
||||
const ParsedInput& input,
|
||||
const std::optional<ElementFamily>& family)
|
||||
: diagnostics_{diagnostics},
|
||||
input_{input},
|
||||
model_element_family_{family} {}
|
||||
|
||||
/// @brief Returns the source material declaration count.
|
||||
std::size_t Count() const noexcept { return materials_.size(); }
|
||||
/// @brief Reports whether no source material has been declared.
|
||||
bool Empty() const noexcept { return materials_.empty(); }
|
||||
/// @brief Reports whether a material child may follow the current
|
||||
/// declaration.
|
||||
bool HasActiveMaterial() const noexcept {
|
||||
return material_eligible_.has_value();
|
||||
}
|
||||
/// @brief Ends the optional section-point continuation scope.
|
||||
void EndSection() noexcept { pending_section_.reset(); }
|
||||
/// @brief Ends the material-child continuation scope.
|
||||
void EndMaterial() noexcept { material_eligible_.reset(); }
|
||||
/// @brief Borrows resolved beam section and material indices for one part.
|
||||
const Assignments& BeamAssignments(std::size_t part) const {
|
||||
return part_section_assignments_[part];
|
||||
}
|
||||
/// @brief Borrows resolved shell or solid property and material indices for
|
||||
/// one part.
|
||||
const Assignments& ShellAssignments(std::size_t part) const {
|
||||
return part_shell_section_assignments_[part];
|
||||
}
|
||||
|
||||
/// @brief Parses material-only solid assignments without introducing
|
||||
/// thickness.
|
||||
void ParseSolidSection(const KeywordBlock& block,
|
||||
PartPropertyCandidates& properties);
|
||||
|
||||
/// @brief Parses centered shell thickness and source integration-point
|
||||
/// metadata.
|
||||
void ParseShellSection(const KeywordBlock& block,
|
||||
PartPropertyCandidates& properties);
|
||||
|
||||
/// @brief Parses general beam properties and their global guide vector.
|
||||
void ParseBeamSection(const KeywordBlock& block,
|
||||
PartPropertyCandidates& properties);
|
||||
|
||||
/// @brief Parses optional beam stress points in the active section scope.
|
||||
void ParseSectionPoints(const KeywordBlock& block,
|
||||
PartPropertyCandidates& properties);
|
||||
|
||||
/// @brief Starts one source material after syntax ordering has been checked.
|
||||
void ParseMaterial(const KeywordBlock& block);
|
||||
|
||||
/// @brief Parses an elastic row without conflating beam and shell material
|
||||
/// ranges.
|
||||
void ParseElastic(const KeywordBlock& block);
|
||||
|
||||
/// @brief Parses positive finite density with its source-specific failure
|
||||
/// policy.
|
||||
void ParseDensity(const KeywordBlock& block);
|
||||
|
||||
/// @brief Parses constant conductivity or specific heat in the thermal
|
||||
/// subset.
|
||||
void ParseThermalProperty(const KeywordBlock& block, bool conductivity);
|
||||
|
||||
/// @brief Appends validated material definitions in source order.
|
||||
void FinalizeMaterials(ModelDefinition& definition);
|
||||
|
||||
/// @brief Validates part references and unique section/material assignments.
|
||||
void FinalizePartsAndSections(const std::vector<RawPart>& parts,
|
||||
ModelDefinition& definition);
|
||||
|
||||
/// @brief Checks procedure policy before appending active material densities.
|
||||
void FinalizeDensities(const MaterialProcedureView& procedure,
|
||||
ModelDefinition& definition);
|
||||
|
||||
private:
|
||||
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;
|
||||
};
|
||||
|
||||
bool ParseDensityValue(const std::string& text, double& value,
|
||||
const SourceLocation& location);
|
||||
|
||||
std::optional<EntityIndex> FindMaterialIndex(const std::string& name) const;
|
||||
|
||||
void ValidateFrequencyPreprint(ModelDefinition& definition);
|
||||
|
||||
void ValidateDynamicPreprint();
|
||||
|
||||
bool ContainsName(const std::vector<RawMaterial>& values,
|
||||
const std::string& name) const;
|
||||
|
||||
MappingDiagnostics& diagnostics_;
|
||||
const ParsedInput& input_;
|
||||
const std::optional<ElementFamily>& model_element_family_;
|
||||
std::vector<RawMaterial> materials_;
|
||||
std::optional<std::size_t> material_eligible_;
|
||||
std::optional<std::size_t> pending_section_;
|
||||
std::vector<Assignments> part_section_assignments_;
|
||||
std::vector<Assignments> part_shell_section_assignments_;
|
||||
};
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
|
||||
#endif // FESA_IO_ABAQUS_MATERIAL_PROPERTY_CANDIDATES_H_
|
||||
@@ -1,9 +1,927 @@
|
||||
#include "io/abaqus/material_property_mapper.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <charconv>
|
||||
#include <cmath>
|
||||
#include <system_error>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/core/ascii.h"
|
||||
#include "io/abaqus/domain_mapping_context.h"
|
||||
#include "io/abaqus/material_property_candidates.h"
|
||||
#include "io/abaqus/topology_candidates.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
void MaterialPropertyCandidates::ParseMaterial(const KeywordBlock& block) {
|
||||
if (!diagnostics_.ValidateParameters(block, {"NAME"}) ||
|
||||
!diagnostics_.RequireNoData(block)) {
|
||||
return;
|
||||
}
|
||||
const auto* name = diagnostics_.RequiredParameterValue(block, "NAME");
|
||||
if (name == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (ContainsName(materials_, *name)) {
|
||||
diagnostics_.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 MaterialPropertyCandidates::ParseElastic(const KeywordBlock& block) {
|
||||
if (model_element_family_ == ElementFamily::kThermalShell) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-keyword", block.location, block.canonical_name, "",
|
||||
"ELASTIC is outside the thermal material subset.");
|
||||
return;
|
||||
}
|
||||
if (!material_eligible_) {
|
||||
diagnostics_.InputFailure("invalid-keyword-location", block.location,
|
||||
block.canonical_name, "",
|
||||
"ELASTIC must immediately follow MATERIAL.");
|
||||
return;
|
||||
}
|
||||
if (!diagnostics_.ValidateParameters(block, {}) || block.data.size() != 1U ||
|
||||
block.data[0].fields.size() != 2U) {
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.InputFailure("duplicate-entity", block.location,
|
||||
block.canonical_name, material.name,
|
||||
"A material accepts one ELASTIC definition.");
|
||||
return;
|
||||
}
|
||||
if (!diagnostics_.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.") ||
|
||||
!diagnostics_.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 MaterialPropertyCandidates::ParseDensityValue(
|
||||
const std::string& text, double& value, const SourceLocation& location) {
|
||||
const std::string canonical = AsciiUppercase(text);
|
||||
if (canonical == "NAN" || canonical == "+NAN" || canonical == "-NAN" ||
|
||||
canonical == "INF" || canonical == "+INF" || canonical == "-INF" ||
|
||||
canonical == "INFINITY" || canonical == "+INFINITY" ||
|
||||
canonical == "-INFINITY") {
|
||||
return diagnostics_.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 diagnostics_.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 diagnostics_.InputFailure(
|
||||
"invalid-density-data", location, "DENSITY", text,
|
||||
"Density exponents require one or more digits.");
|
||||
}
|
||||
}
|
||||
if (cursor != text.size()) {
|
||||
return diagnostics_.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 diagnostics_.ModelFailure(
|
||||
"invalid-material-density", location, "DENSITY", text,
|
||||
"Density must convert to a finite value greater than zero.");
|
||||
}
|
||||
value = parsed;
|
||||
return true;
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::ParseDensity(const KeywordBlock& block) {
|
||||
if (!material_eligible_) {
|
||||
diagnostics_.InputFailure("invalid-density-data", block.location,
|
||||
block.canonical_name, "",
|
||||
"DENSITY must be declared inside one MATERIAL.");
|
||||
return;
|
||||
}
|
||||
if (!diagnostics_.ValidateParameters(block, {},
|
||||
"unsupported-density-option")) {
|
||||
return;
|
||||
}
|
||||
RawMaterial& material = materials_[*material_eligible_];
|
||||
if (material.density) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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 MaterialPropertyCandidates::ParseThermalProperty(const KeywordBlock& block,
|
||||
bool conductivity) {
|
||||
if (model_element_family_ != ElementFamily::kThermalShell ||
|
||||
!material_eligible_ || !diagnostics_.ValidateParameters(block, {})) {
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
diagnostics_.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()))) {
|
||||
diagnostics_.InputFailure("invalid-data-arity", block.location,
|
||||
block.canonical_name, material.name,
|
||||
"Thermal property requires one constant row.");
|
||||
return;
|
||||
}
|
||||
double value = 0.0;
|
||||
if (!diagnostics_.ParseThermalDouble(block.data[0].fields[0], value,
|
||||
block.data[0].location,
|
||||
block.canonical_name) ||
|
||||
!(value > 0.0)) {
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
diagnostics_.ModelFailure(
|
||||
"invalid-thermal-material", block.data[0].location,
|
||||
block.canonical_name, material.name,
|
||||
"Thermal material values must be finite and positive.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
destination = value;
|
||||
}
|
||||
|
||||
std::optional<EntityIndex> MaterialPropertyCandidates::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;
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::FinalizeMaterials(
|
||||
ModelDefinition& definition) {
|
||||
for (const auto& material : materials_) {
|
||||
if (model_element_family_ == ElementFamily::kThermalShell) {
|
||||
if (!material.density || !material.conductivity ||
|
||||
!material.specific_heat) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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)) {
|
||||
diagnostics_.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)) {
|
||||
diagnostics_.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)) {
|
||||
diagnostics_.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 MaterialPropertyCandidates::FinalizeDensities(
|
||||
const MaterialProcedureView& procedure, ModelDefinition& definition) {
|
||||
if (model_element_family_ == ElementFamily::kThermalShell) {
|
||||
for (const auto& block : input_.blocks) {
|
||||
if (block.canonical_name != "PREPRINT") continue;
|
||||
if (!diagnostics_.ValidateParameters(
|
||||
block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}))
|
||||
return;
|
||||
if (block.parameters.size() != 4U) {
|
||||
diagnostics_.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")) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-keyword", block.location, block.canonical_name,
|
||||
parameter.name, "Thermal PREPRINT parameters must equal NO.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (procedure.physical_constants_location) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-keyword", *procedure.physical_constants_location,
|
||||
"PHYSICAL CONSTANTS", "",
|
||||
"PHYSICAL CONSTANTS is supported only by thermal input.");
|
||||
return;
|
||||
}
|
||||
if (!procedure.frequency && !procedure.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 (!procedure.frequency && !procedure.dynamic) {
|
||||
if (std::any_of(materials_.begin(), materials_.end(),
|
||||
[](const RawMaterial& material) {
|
||||
return material.density.has_value();
|
||||
})) {
|
||||
diagnostics_.InputFailure(
|
||||
"invalid-density-data", procedure.location, "DENSITY", "",
|
||||
"DENSITY is supported only by the FREQUENCY procedure.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
if (procedure.frequency) {
|
||||
ValidateFrequencyPreprint(definition);
|
||||
if (diagnostics_.HasFailure()) {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (procedure.dynamic) {
|
||||
ValidateDynamicPreprint();
|
||||
if (diagnostics_.HasFailure()) {
|
||||
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) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-analysis-procedure", procedure.location,
|
||||
procedure.frequency ? "FREQUENCY" : "DYNAMIC", procedure.name_raw,
|
||||
"The procedure requires an approved Shell or Solid model.");
|
||||
return;
|
||||
}
|
||||
if (procedure.first_surface.has_value()) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-keyword", procedure.first_surface->location, "SURFACE",
|
||||
procedure.first_surface->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) {
|
||||
diagnostics_.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 MaterialPropertyCandidates::ValidateFrequencyPreprint(
|
||||
ModelDefinition& definition) {
|
||||
for (const auto& block : input_.blocks) {
|
||||
if (block.canonical_name != "PREPRINT") {
|
||||
continue;
|
||||
}
|
||||
if (!diagnostics_.ValidateParameters(
|
||||
block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) ||
|
||||
block.parameters.size() != 4U) {
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
diagnostics_.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")) {
|
||||
diagnostics_.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 MaterialPropertyCandidates::ValidateDynamicPreprint() {
|
||||
for (const auto& block : input_.blocks) {
|
||||
if (block.canonical_name != "PREPRINT") {
|
||||
continue;
|
||||
}
|
||||
if (!diagnostics_.ValidateParameters(
|
||||
block, {"ECHO", "MODEL", "HISTORY", "CONTACT"}) ||
|
||||
block.parameters.size() != 4U) {
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
diagnostics_.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")) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-keyword", block.location, block.canonical_name,
|
||||
parameter.name,
|
||||
"DYNAMIC requires PREPRINT parameters to equal NO.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::FinalizePartsAndSections(
|
||||
const std::vector<RawPart>& parts, ModelDefinition& definition) {
|
||||
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.properties.sections.empty())) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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.properties.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) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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.properties.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) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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.properties.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) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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)) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.InputFailure(
|
||||
"unresolved-reference", element.location, "ELEMENT",
|
||||
element.label_text,
|
||||
"Every B33 element requires a resolved section assignment.");
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
bool MaterialPropertyCandidates::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);
|
||||
});
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::ParseSolidSection(
|
||||
const KeywordBlock& block, PartPropertyCandidates& properties) {
|
||||
if (!diagnostics_.ValidateParameters(block, {"ELSET", "MATERIAL"})) return;
|
||||
const auto* elset = diagnostics_.RequiredParameterValue(block, "ELSET");
|
||||
const auto* material = diagnostics_.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(); }))) {
|
||||
diagnostics_.InputFailure(
|
||||
"invalid-solid-section", block.location, block.canonical_name, *elset,
|
||||
"SOLID SECTION permits omitted or blank data only.");
|
||||
return;
|
||||
}
|
||||
properties.solid_sections.push_back({*elset, *material, block.location});
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::ParseShellSection(
|
||||
const KeywordBlock& block, PartPropertyCandidates& properties) {
|
||||
for (const auto& candidate : block.parameters) {
|
||||
if (candidate.name != "ELSET" && candidate.name != "MATERIAL") {
|
||||
diagnostics_.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 (!diagnostics_.ValidateParameters(block, {"ELSET", "MATERIAL"})) {
|
||||
return;
|
||||
}
|
||||
const auto* element_set = diagnostics_.RequiredParameterValue(block, "ELSET");
|
||||
const auto* material = diagnostics_.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) {
|
||||
diagnostics_.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
|
||||
? diagnostics_.ParseThermalDouble(block.data[0].fields[0], thickness,
|
||||
block.data[0].location,
|
||||
block.canonical_name)
|
||||
: diagnostics_.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)) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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;
|
||||
}
|
||||
}
|
||||
properties.shell_sections.push_back(
|
||||
{*element_set, *material, thickness, block.location});
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::ParseBeamSection(
|
||||
const KeywordBlock& block, PartPropertyCandidates& properties) {
|
||||
pending_section_.reset();
|
||||
if (!diagnostics_.ValidateParameters(block,
|
||||
{"ELSET", "MATERIAL", "SECTION"})) {
|
||||
return;
|
||||
}
|
||||
const auto* element_set = diagnostics_.RequiredParameterValue(block, "ELSET");
|
||||
const auto* material = diagnostics_.RequiredParameterValue(block, "MATERIAL");
|
||||
const auto* section = diagnostics_.RequiredParameterValue(block, "SECTION");
|
||||
if (element_set == nullptr || material == nullptr || section == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!AsciiCaseInsensitiveEquals(*section, "GENERAL")) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-section-formulation", block.location, block.canonical_name,
|
||||
*section, "Only SECTION=GENERAL belongs to the approved subset.");
|
||||
return;
|
||||
}
|
||||
if (std::any_of(properties.sections.begin(), properties.sections.end(),
|
||||
[element_set](const RawSection& existing) {
|
||||
return AsciiCaseInsensitiveEquals(existing.element_set_name,
|
||||
*element_set);
|
||||
})) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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 (!diagnostics_.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 (!diagnostics_.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;
|
||||
}
|
||||
}
|
||||
properties.sections.push_back(std::move(raw));
|
||||
pending_section_ = properties.sections.size() - 1U;
|
||||
}
|
||||
|
||||
void MaterialPropertyCandidates::ParseSectionPoints(
|
||||
const KeywordBlock& block, PartPropertyCandidates& properties) {
|
||||
if (!pending_section_ || !diagnostics_.ValidateParameters(block, {}) ||
|
||||
block.data.empty()) {
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
diagnostics_.InputFailure(
|
||||
"invalid-keyword-location", block.location, block.canonical_name, "",
|
||||
"SECTION POINTS must immediately follow a general section.");
|
||||
}
|
||||
return;
|
||||
}
|
||||
RawSection& section = properties.sections[*pending_section_];
|
||||
for (const auto& row : block.data) {
|
||||
if (row.fields.size() != 2U) {
|
||||
diagnostics_.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 (!diagnostics_.ParseDouble(row.fields[0], point[0], row.location,
|
||||
block.canonical_name) ||
|
||||
!diagnostics_.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()) {
|
||||
diagnostics_.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();
|
||||
}
|
||||
|
||||
Status MaterialPropertyMapper::Map(DomainMappingContext& context) const {
|
||||
return context.FinalizeMaterialProperties();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,196 @@
|
||||
#ifndef FESA_IO_ABAQUS_STEP_DEFINITION_CANDIDATES_H_
|
||||
#define FESA_IO_ABAQUS_STEP_DEFINITION_CANDIDATES_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstddef>
|
||||
#include <cstdint>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/model/source_target_resolver.h"
|
||||
#include "io/abaqus/topology_candidates.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
struct DomainMappingCandidateSummary;
|
||||
|
||||
/// @brief Owns uncommitted step, boundary, load, and initial-condition syntax.
|
||||
/// @note The context-owned input, family, diagnostics, and candidate outlive
|
||||
/// it.
|
||||
class StepDefinitionCandidates {
|
||||
public:
|
||||
/// @brief Borrows context services while owning this uncommitted mapping
|
||||
/// stage.
|
||||
StepDefinitionCandidates(MappingDiagnostics& diagnostics,
|
||||
const ParsedInput& input,
|
||||
const std::optional<ElementFamily>& family,
|
||||
ModelDefinition& definition)
|
||||
: diagnostics_{diagnostics},
|
||||
input_{input},
|
||||
model_element_family_{family},
|
||||
definition_{definition} {}
|
||||
|
||||
/// @brief Reports whether the sole source step has been declared.
|
||||
bool HasStep() const noexcept { return step_seen_; }
|
||||
/// @brief Reports whether source step syntax remains open.
|
||||
bool InStep() const noexcept { return in_step_; }
|
||||
/// @brief Reports whether the step selected an existing supported procedure.
|
||||
bool HasSupportedProcedure() const noexcept;
|
||||
/// @brief Ends output-child scope when source syntax routing changes.
|
||||
void EndOutput() noexcept { active_output_ = false; }
|
||||
/// @brief Copies stable candidate counts without committing a Domain.
|
||||
void FillSummary(DomainMappingCandidateSummary& summary) const noexcept;
|
||||
/// @brief Borrows procedure provenance for immediate material validation.
|
||||
MaterialProcedureView MaterialProcedure(
|
||||
std::optional<MaterialProcedureView::Surface> first_surface,
|
||||
const std::optional<SourceLocation>& physical_constants) const;
|
||||
/// @brief Owns model-level structural or thermal boundary rows.
|
||||
void ParseModelBoundary(const KeywordBlock& block);
|
||||
|
||||
/// @brief Parses step controls while preserving repeated-step first failure.
|
||||
void ParseStepStart(const KeywordBlock& block, bool model_ready);
|
||||
|
||||
/// @brief Parses children with the existing procedure allowlist and ordering.
|
||||
void ParseStepBlock(const KeywordBlock& block);
|
||||
|
||||
/// @brief Owns supported model-level load amplitude source records.
|
||||
void ParseAmplitude(const KeywordBlock& block);
|
||||
|
||||
/// @brief Owns supported thermal initial-temperature source rows.
|
||||
void ParseInitialConditions(const KeywordBlock& block);
|
||||
|
||||
/// @brief Appends source no-op provenance with its procedure warning code.
|
||||
void AddIgnoredWarning(const KeywordBlock& block);
|
||||
|
||||
/// @brief Resolves source targets and appends complete step definitions.
|
||||
void FinalizeStep();
|
||||
|
||||
private:
|
||||
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 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};
|
||||
};
|
||||
|
||||
bool ParseSurfacePressureDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword);
|
||||
|
||||
bool ParseNonlinearStaticDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword);
|
||||
|
||||
bool ParseDynamicDouble(const std::string& text, double& value,
|
||||
const SourceLocation& location,
|
||||
const std::string& keyword);
|
||||
|
||||
void ParseThermalBoundary(const KeywordBlock& block,
|
||||
std::vector<RawThermalBoundary>& destination);
|
||||
|
||||
void ParseBoundary(
|
||||
const KeywordBlock& block,
|
||||
std::vector<PrescribedDisplacementDefinition>& destination);
|
||||
|
||||
void ParseCload(const KeywordBlock& block, std::vector<NodalLoad>& loads);
|
||||
|
||||
void ParseDsload(const KeywordBlock& block);
|
||||
|
||||
bool ParseFrequencyModeCount(const std::string& text, std::int64_t& value,
|
||||
const SourceLocation& location);
|
||||
|
||||
void ParseFrequency(const KeywordBlock& block);
|
||||
|
||||
void ValidateEigenBoundaries(
|
||||
const std::vector<PrescribedDisplacementDefinition>& boundaries);
|
||||
|
||||
void ValidateDynamicBoundaries(
|
||||
const std::vector<PrescribedDisplacementDefinition>& boundaries);
|
||||
|
||||
void ParseDynamic(const KeywordBlock& block);
|
||||
|
||||
void ParseHeatTransfer(const KeywordBlock& block);
|
||||
|
||||
void ParseCflux(const KeywordBlock& block);
|
||||
|
||||
void ParseStatic(const KeywordBlock& block);
|
||||
|
||||
void ParseOutputRoot(const KeywordBlock& block);
|
||||
|
||||
void ParseOutputChild(const KeywordBlock& block);
|
||||
|
||||
std::optional<std::vector<EntityIndex>> ResolveNodeTarget(
|
||||
const SourceTargetResolver& resolver, const std::string& target,
|
||||
const SourceLocation& location, const std::string& keyword);
|
||||
|
||||
MappingDiagnostics& diagnostics_;
|
||||
const ParsedInput& input_;
|
||||
const std::optional<ElementFamily>& model_element_family_;
|
||||
ModelDefinition& definition_;
|
||||
RawStep step_{};
|
||||
std::vector<PrescribedDisplacementDefinition> model_boundaries_;
|
||||
std::vector<RawThermalBoundary> thermal_model_boundaries_;
|
||||
std::vector<RawThermalInitial> thermal_initial_conditions_;
|
||||
std::vector<bool> thermal_active_nodes_;
|
||||
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};
|
||||
};
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
|
||||
#endif // FESA_IO_ABAQUS_STEP_DEFINITION_CANDIDATES_H_
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,87 @@
|
||||
#ifndef FESA_IO_ABAQUS_SURFACE_DEFINITION_CANDIDATES_H_
|
||||
#define FESA_IO_ABAQUS_SURFACE_DEFINITION_CANDIDATES_H_
|
||||
|
||||
#include <cstddef>
|
||||
#include <optional>
|
||||
#include <string>
|
||||
#include <string_view>
|
||||
#include <utility>
|
||||
#include <vector>
|
||||
|
||||
#include "io/abaqus/topology_candidates.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
/// @brief Checks the source name syntax shared by surface and pressure records.
|
||||
bool IsPressureNameToken(std::string_view text) noexcept;
|
||||
|
||||
/// @brief Owns assembly surface candidates and ordered facet resolution.
|
||||
/// @note Context diagnostics, topology, family, and candidate outlive this
|
||||
/// stage.
|
||||
class SurfaceDefinitionCandidates {
|
||||
public:
|
||||
/// @brief Borrows context services while owning this uncommitted mapping
|
||||
/// stage.
|
||||
SurfaceDefinitionCandidates(MappingDiagnostics& diagnostics,
|
||||
const TopologyCandidates& topology,
|
||||
const std::optional<ElementFamily>& family,
|
||||
ModelDefinition& definition)
|
||||
: diagnostics_{diagnostics},
|
||||
topology_{topology},
|
||||
model_element_family_{family},
|
||||
definition_{definition} {}
|
||||
|
||||
/// @brief Copies first-surface provenance for material procedure validation.
|
||||
std::optional<MaterialProcedureView::Surface> FirstSurface() const;
|
||||
|
||||
/// @brief Owns one source surface after assembly-scope validation.
|
||||
void ParseElementSurface(const KeywordBlock& block);
|
||||
|
||||
/// @brief Resolves ordered facets while retaining first failure and warning
|
||||
/// order.
|
||||
void ResolveElementSurfaces();
|
||||
|
||||
private:
|
||||
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;
|
||||
};
|
||||
|
||||
bool ContainsSurfaceName(const std::string& name) const;
|
||||
|
||||
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);
|
||||
|
||||
bool ResolveDirectSurfaceMember(
|
||||
const RawSurfaceMember& member,
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
||||
candidates);
|
||||
|
||||
bool ResolveElsetSurfaceMember(
|
||||
const RawSurfaceMember& member,
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
||||
candidates);
|
||||
|
||||
MappingDiagnostics& diagnostics_;
|
||||
const TopologyCandidates& topology_;
|
||||
const std::optional<ElementFamily>& model_element_family_;
|
||||
ModelDefinition& definition_;
|
||||
std::vector<RawElementSurface> element_surfaces_;
|
||||
};
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
|
||||
#endif // FESA_IO_ABAQUS_SURFACE_DEFINITION_CANDIDATES_H_
|
||||
@@ -1,9 +1,363 @@
|
||||
#include "io/abaqus/surface_definition_mapper.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cstdint>
|
||||
#include <tuple>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/core/ascii.h"
|
||||
#include "io/abaqus/domain_mapping_context.h"
|
||||
#include "io/abaqus/surface_definition_candidates.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
bool IsPressureNameToken(const std::string_view text) 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);
|
||||
}
|
||||
|
||||
std::optional<MaterialProcedureView::Surface>
|
||||
SurfaceDefinitionCandidates::FirstSurface() const {
|
||||
if (element_surfaces_.empty()) return std::nullopt;
|
||||
return MaterialProcedureView::Surface{element_surfaces_.front().location,
|
||||
element_surfaces_.front().name};
|
||||
}
|
||||
|
||||
bool SurfaceDefinitionCandidates::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);
|
||||
});
|
||||
}
|
||||
|
||||
void SurfaceDefinitionCandidates::ParseElementSurface(
|
||||
const KeywordBlock& block) {
|
||||
if (!diagnostics_.ValidateParameters(block, {"TYPE", "NAME"},
|
||||
"unsupported-parameter")) {
|
||||
return;
|
||||
}
|
||||
const auto* type = diagnostics_.RequiredParameterValue(block, "TYPE");
|
||||
const auto* name = diagnostics_.RequiredParameterValue(block, "NAME");
|
||||
if (type == nullptr || name == nullptr) {
|
||||
return;
|
||||
}
|
||||
if (!IsPressureNameToken(*name)) {
|
||||
diagnostics_.InputFailure(
|
||||
"invalid-surface-name", block.location, block.canonical_name, *name,
|
||||
"Surface names must use the approved ASCII name syntax.");
|
||||
return;
|
||||
}
|
||||
if (!AsciiCaseInsensitiveEquals(*type, "ELEMENT")) {
|
||||
diagnostics_.InputFailure(
|
||||
"unsupported-surface-type", block.location, block.canonical_name, *type,
|
||||
"Only TYPE=ELEMENT belongs to the approved surface subset.");
|
||||
return;
|
||||
}
|
||||
if (ContainsSurfaceName(*name)) {
|
||||
diagnostics_.InputFailure(
|
||||
"duplicate-entity", block.location, block.canonical_name, *name,
|
||||
"Surface names are unique in the assembly namespace.");
|
||||
return;
|
||||
}
|
||||
if (block.data.empty()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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 {
|
||||
diagnostics_.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));
|
||||
}
|
||||
|
||||
bool SurfaceDefinitionCandidates::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 diagnostics_.InputFailure(
|
||||
"unsupported-element-formulation", member.location, "SURFACE",
|
||||
member.target,
|
||||
"Element surfaces may resolve only to S4 or S4R shell elements.");
|
||||
}
|
||||
const RawPart* part = topology_.FindPart(raw_instance.part_name);
|
||||
if (part == nullptr) {
|
||||
return diagnostics_.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,
|
||||
AsciiUppercase(part->name),
|
||||
raw_instance.name,
|
||||
AsciiUppercase(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
|
||||
? AsciiUppercase(elset_name_raw)
|
||||
: ""};
|
||||
candidates->push_back({std::move(facet), instance_ordinal});
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SurfaceDefinitionCandidates::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 diagnostics_.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 diagnostics_.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 (!diagnostics_.ParseInteger(member.target.substr(dot + 1U), source_label,
|
||||
member.location, "SURFACE", true)) {
|
||||
return false;
|
||||
}
|
||||
const auto raw_instance = std::find_if(
|
||||
topology_.Instances().begin(), topology_.Instances().end(),
|
||||
[&instance_name](const RawInstance& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.name, instance_name);
|
||||
});
|
||||
if (raw_instance == topology_.Instances().end()) {
|
||||
if (topology_.FindPart(instance_name) != nullptr) {
|
||||
return diagnostics_.InputFailure(
|
||||
"unsupported-surface-target", member.location, "SURFACE",
|
||||
member.target,
|
||||
"A direct surface member cannot use a part name as an instance.");
|
||||
}
|
||||
return diagnostics_.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 diagnostics_.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(
|
||||
topology_.Instances().begin(), raw_instance)),
|
||||
element->internal_index, SurfaceMemberKind::kDirect,
|
||||
"", candidates);
|
||||
}
|
||||
|
||||
bool SurfaceDefinitionCandidates::ResolveElsetSurfaceMember(
|
||||
const RawSurfaceMember& member,
|
||||
std::vector<std::pair<ElementSurfaceFacetDefinition, std::size_t>>*
|
||||
candidates) {
|
||||
if (!IsPressureNameToken(member.target)) {
|
||||
return diagnostics_.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(
|
||||
topology_.AssemblySets().begin(), topology_.AssemblySets().end(),
|
||||
[&member](const RawAssemblySet& value) {
|
||||
return !value.is_node_set &&
|
||||
AsciiCaseInsensitiveEquals(value.name, member.target);
|
||||
});
|
||||
if (raw_set == topology_.AssemblySets().end()) {
|
||||
const bool part_scoped = std::any_of(
|
||||
topology_.Parts().begin(), topology_.Parts().end(),
|
||||
[this, &member](const RawPart& part) {
|
||||
return FindSet(part.element_sets, member.target) != nullptr;
|
||||
});
|
||||
if (part_scoped) {
|
||||
return diagnostics_.InputFailure(
|
||||
"cross-scope-target", member.location, "SURFACE", member.target,
|
||||
"A surface ELSET must be declared at assembly scope.");
|
||||
}
|
||||
return diagnostics_.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(
|
||||
topology_.Instances().begin(), topology_.Instances().end(),
|
||||
[&raw_set](const RawInstance& value) {
|
||||
return AsciiCaseInsensitiveEquals(value.name, raw_set->instance_name);
|
||||
});
|
||||
if (raw_instance == topology_.Instances().end()) {
|
||||
return diagnostics_.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 diagnostics_.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(
|
||||
topology_.Instances().begin(), raw_instance)),
|
||||
element_index, SurfaceMemberKind::kElset,
|
||||
member.target, candidates)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
void SurfaceDefinitionCandidates::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()) {
|
||||
diagnostics_.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) {
|
||||
diagnostics_.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,
|
||||
AsciiUppercase(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);
|
||||
}
|
||||
|
||||
Status SurfaceDefinitionMapper::Map(DomainMappingContext& context) const {
|
||||
return context.FinalizeElementSurfaces();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,133 @@
|
||||
#ifndef FESA_IO_ABAQUS_TOPOLOGY_CANDIDATES_H_
|
||||
#define FESA_IO_ABAQUS_TOPOLOGY_CANDIDATES_H_
|
||||
|
||||
#include <array>
|
||||
#include <cstdint>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
#include "io/abaqus/material_property_candidates.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
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 RawPart {
|
||||
std::string name;
|
||||
SourceLocation location;
|
||||
std::vector<RawNode> nodes;
|
||||
std::vector<RawElement> elements;
|
||||
std::vector<RawSet> node_sets;
|
||||
std::vector<RawSet> element_sets;
|
||||
PartPropertyCandidates properties;
|
||||
};
|
||||
|
||||
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;
|
||||
};
|
||||
|
||||
/// @brief Borrows a part-local set by ASCII-insensitive name or returns null.
|
||||
const RawSet* FindSet(const std::vector<RawSet>& sets, const std::string& name);
|
||||
|
||||
/// @brief Borrows a part-local source node by label or returns null.
|
||||
const RawNode* FindNode(const RawPart& part, std::int64_t label);
|
||||
|
||||
/// @brief Borrows a part-local source element by label or returns null.
|
||||
const RawElement* FindElement(const RawPart& part, std::int64_t label);
|
||||
|
||||
/// @brief Owns raw topology and its deterministic expansion into a candidate.
|
||||
/// @note The syntax coordinator may mutate raw records until finalization.
|
||||
/// Context-owned diagnostics outlive this stage and its borrowed consumers.
|
||||
class TopologyCandidates {
|
||||
public:
|
||||
/// @brief Borrows context services while owning this uncommitted mapping
|
||||
/// stage.
|
||||
explicit TopologyCandidates(MappingDiagnostics& diagnostics)
|
||||
: diagnostics_{diagnostics} {}
|
||||
|
||||
/// @brief Provides raw part records to mapping stages and the syntax
|
||||
/// coordinator.
|
||||
std::vector<RawPart>& Parts() noexcept { return parts_; }
|
||||
/// @brief Provides raw part records to mapping stages and the syntax
|
||||
/// coordinator.
|
||||
const std::vector<RawPart>& Parts() const noexcept { return parts_; }
|
||||
/// @brief Provides raw identity instances in source declaration order.
|
||||
std::vector<RawInstance>& Instances() noexcept { return instances_; }
|
||||
/// @brief Provides raw identity instances in source declaration order.
|
||||
const std::vector<RawInstance>& Instances() const noexcept {
|
||||
return instances_;
|
||||
}
|
||||
/// @brief Provides raw assembly sets in source declaration order.
|
||||
std::vector<RawAssemblySet>& AssemblySets() noexcept {
|
||||
return assembly_sets_;
|
||||
}
|
||||
/// @brief Provides raw assembly sets in source declaration order.
|
||||
const std::vector<RawAssemblySet>& AssemblySets() const noexcept {
|
||||
return assembly_sets_;
|
||||
}
|
||||
|
||||
/// @brief Borrows the matching source part or returns null.
|
||||
const RawPart* FindPart(const std::string& name) const;
|
||||
|
||||
/// @brief Borrows the matching identity instance or returns null.
|
||||
const RawInstance* FindInstance(const std::string& name) const;
|
||||
|
||||
/// @brief Expands instances, preprocesses shell geometry, then expands
|
||||
/// assembly sets.
|
||||
void Finalize(const MaterialPropertyCandidates& material_properties,
|
||||
ModelDefinition& definition);
|
||||
|
||||
private:
|
||||
std::size_t PartIndex(const RawPart& part) const;
|
||||
|
||||
bool ValidateGeometry(const RawElement& raw, const Node& first,
|
||||
const Node& second, const GeneralBeamSection& section);
|
||||
|
||||
void ExpandInstances(const MaterialPropertyCandidates& material_properties,
|
||||
ModelDefinition& definition);
|
||||
|
||||
void ExpandAssemblySets(ModelDefinition& definition);
|
||||
|
||||
MappingDiagnostics& diagnostics_;
|
||||
std::vector<RawPart> parts_;
|
||||
std::vector<RawInstance> instances_;
|
||||
std::vector<RawAssemblySet> assembly_sets_;
|
||||
};
|
||||
|
||||
} // namespace fesa::abaqus_internal
|
||||
|
||||
#endif // FESA_IO_ABAQUS_TOPOLOGY_CANDIDATES_H_
|
||||
@@ -1,9 +1,414 @@
|
||||
#include "io/abaqus/topology_mapper.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <cmath>
|
||||
#include <limits>
|
||||
#include <map>
|
||||
#include <utility>
|
||||
|
||||
#include "fesa/core/ascii.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "fesa/model/shell_geometry.h"
|
||||
#include "io/abaqus/domain_mapping_context.h"
|
||||
#include "io/abaqus/topology_candidates.h"
|
||||
|
||||
namespace fesa::abaqus_internal {
|
||||
|
||||
const RawSet* FindSet(const std::vector<RawSet>& sets,
|
||||
const std::string& name) {
|
||||
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 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 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;
|
||||
}
|
||||
|
||||
const RawPart* TopologyCandidates::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* TopologyCandidates::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::size_t TopologyCandidates::PartIndex(const RawPart& part) const {
|
||||
return static_cast<std::size_t>(&part - parts_.data());
|
||||
}
|
||||
|
||||
bool TopologyCandidates::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 diagnostics_.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 diagnostics_.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 TopologyCandidates::ExpandInstances(
|
||||
const MaterialPropertyCandidates& material_properties,
|
||||
ModelDefinition& definition) {
|
||||
for (const auto& raw_instance : instances_) {
|
||||
const RawPart* part = FindPart(raw_instance.part_name);
|
||||
if (part == nullptr) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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 =
|
||||
material_properties.BeamAssignments(PartIndex(*part));
|
||||
const auto assignment = assignments.find(raw_element.label);
|
||||
if (first == node_indices.end() || second == node_indices.end() ||
|
||||
assignment == assignments.end()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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 =
|
||||
material_properties.ShellAssignments(PartIndex(*part));
|
||||
const auto assignment = assignments.find(raw_element.label);
|
||||
if (assignment == assignments.end() ||
|
||||
definition.solid_elements.size() >
|
||||
std::numeric_limits<EntityIndex>::max()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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 =
|
||||
material_properties.ShellAssignments(PartIndex(*part));
|
||||
const auto assignment = assignments.find(raw_element.label);
|
||||
if (assignment == assignments.end()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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 TopologyCandidates::ExpandAssemblySets(ModelDefinition& definition) {
|
||||
for (const auto& raw_set : assembly_sets_) {
|
||||
const RawInstance* raw_instance = FindInstance(raw_set.instance_name);
|
||||
if (raw_instance == nullptr) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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()) {
|
||||
diagnostics_.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));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
void TopologyCandidates::Finalize(
|
||||
const MaterialPropertyCandidates& material_properties,
|
||||
ModelDefinition& definition) {
|
||||
if (diagnostics_.HasFailure()) {
|
||||
return;
|
||||
}
|
||||
ExpandInstances(material_properties, definition);
|
||||
if (!diagnostics_.HasFailure() && !definition.shell_elements.empty()) {
|
||||
auto geometry = PreprocessShellGeometry(
|
||||
definition.nodes, definition.shell_elements, definition.shell_sections);
|
||||
if (!geometry.HasValue()) {
|
||||
const auto& status = geometry.GetStatus();
|
||||
diagnostics_.RecordFailure(status);
|
||||
} else {
|
||||
definition.shell_node_initial_frames =
|
||||
std::move(geometry.Value().nodal_frames);
|
||||
}
|
||||
}
|
||||
if (!diagnostics_.HasFailure()) {
|
||||
ExpandAssemblySets(definition);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
Status TopologyMapper::Map(DomainMappingContext& context) const {
|
||||
return context.ParseSyntax();
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/build_info.h"
|
||||
#include "fesa/core/diagnostic.h"
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "io/hdf5/hdf5_atomic_file.h"
|
||||
#include "io/hdf5/hdf5_model_writer.h"
|
||||
@@ -24,6 +23,10 @@ namespace {
|
||||
|
||||
using hdf5_internal::Hdf5Failure;
|
||||
using hdf5_internal::Hdf5Handle;
|
||||
using hdf5_internal::CopyVectorValues;
|
||||
using hdf5_internal::FlattenNodalTriads;
|
||||
using hdf5_internal::FlattenNodalDirectors;
|
||||
using hdf5_internal::SpatialReactionValues;
|
||||
|
||||
struct AcceptedFrameRow {
|
||||
std::uint64_t frame_index;
|
||||
@@ -228,11 +231,6 @@ Status Failure(const char* code, const std::string& message) {
|
||||
return hdf5_internal::OutputFailure(code, message);
|
||||
}
|
||||
|
||||
/// @brief Copies one nonempty full-space vector in node-major order.
|
||||
std::vector<double> Values(const Vector& values) {
|
||||
return std::vector<double>{values.Data(), values.Data() + values.Size()};
|
||||
}
|
||||
|
||||
/// @brief Removes inherited metadata before applying the dynamic schema.
|
||||
void DeleteAttribute(const hid_t object, const char* name) {
|
||||
if (H5Aexists(object, name) > 0) {
|
||||
@@ -401,26 +399,6 @@ void ProcessDataset(const hid_t file, const std::string& path,
|
||||
}
|
||||
}
|
||||
|
||||
/// @brief Flattens nodal triad axes in A,B,D row order.
|
||||
std::vector<double> FlattenTriad(const DynamicAcceptedFrameState& frame) {
|
||||
std::vector<double> values;
|
||||
values.reserve(frame.nodal_triad.size() * 9U);
|
||||
for (const auto& triad : frame.nodal_triad) {
|
||||
for (const auto& axis : triad)
|
||||
values.insert(values.end(), axis.begin(), axis.end());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/// @brief Flattens nodal directors in stable node order.
|
||||
std::vector<double> FlattenDirector(const DynamicAcceptedFrameState& frame) {
|
||||
std::vector<double> values;
|
||||
values.reserve(frame.nodal_director.size() * 3U);
|
||||
for (const auto& director : frame.nodal_director)
|
||||
values.insert(values.end(), director.begin(), director.end());
|
||||
return values;
|
||||
}
|
||||
|
||||
/// @brief Flattens physical recovery in element and Gauss order.
|
||||
std::vector<double> FlattenShell(const DynamicAcceptedFrameState& frame,
|
||||
const int quantity) {
|
||||
@@ -442,27 +420,6 @@ std::vector<double> FlattenShell(const DynamicAcceptedFrameState& frame,
|
||||
return values;
|
||||
}
|
||||
|
||||
/// @brief Maps generalized support moments to work-conjugate spatial moments.
|
||||
std::vector<double> SpatialReaction(const DynamicAcceptedFrameState& frame) {
|
||||
std::vector<double> values = Values(frame.reaction);
|
||||
for (std::size_t node = 0U; node < frame.nodal_internal_order.size();
|
||||
++node) {
|
||||
const std::size_t offset = node * 6U;
|
||||
const Vector3 phi{frame.generalized_coordinates[offset + 3U],
|
||||
frame.generalized_coordinates[offset + 4U],
|
||||
frame.generalized_coordinates[offset + 5U]};
|
||||
const Matrix transform = InverseTransposeLeftJacobianSO3(phi);
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
values[offset + 3U + row] = 0.0;
|
||||
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||
values[offset + 3U + row] +=
|
||||
transform(row, column) * frame.reaction[offset + 3U + column];
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
/// @brief Writes or reads back all mandatory accepted-endpoint quantities.
|
||||
void WriteFrame(const hid_t file, const Domain& domain,
|
||||
const DynamicAcceptedFrameState& frame,
|
||||
@@ -491,52 +448,55 @@ void WriteFrame(const hid_t file, const Domain& domain,
|
||||
"force,force,force,force*length,force*length,force*length";
|
||||
const std::vector<hsize_t> nodal{nodes, 6U};
|
||||
WriteDataset(file, root + "/nodal/displacement", nodal,
|
||||
Values(frame.generalized_coordinates), "UX,UY,UZ,URX,URY,URZ",
|
||||
CopyVectorValues(frame.generalized_coordinates), "UX,UY,UZ,URX,URY,URZ",
|
||||
"length,length,length,1,1,1", "global-cartesian", "nodal",
|
||||
frame);
|
||||
WriteDataset(file, root + "/nodal/velocity", nodal, Values(frame.velocity),
|
||||
WriteDataset(file, root + "/nodal/velocity", nodal, CopyVectorValues(frame.velocity),
|
||||
"V1,V2,V3,PHIDOT1,PHIDOT2,PHIDOT3",
|
||||
"length/time,length/time,length/time,1/time,1/time,1/time",
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(
|
||||
file, root + "/nodal/acceleration", nodal, Values(frame.acceleration),
|
||||
file, root + "/nodal/acceleration", nodal, CopyVectorValues(frame.acceleration),
|
||||
"A1,A2,A3,PHIDDOT1,PHIDDOT2,PHIDDOT3",
|
||||
"length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2",
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(
|
||||
file, root + "/nodal/algorithmic_acceleration", nodal,
|
||||
Values(frame.algorithmic_acceleration), "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6",
|
||||
CopyVectorValues(frame.algorithmic_acceleration), "AUX1,AUX2,AUX3,AUX4,AUX5,AUX6",
|
||||
"length/time^2,length/time^2,length/time^2,1/time^2,1/time^2,1/time^2",
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/angular_velocity", {nodes, 3U},
|
||||
Values(frame.angular_velocity), "OMEGA1,OMEGA2,OMEGA3",
|
||||
CopyVectorValues(frame.angular_velocity), "OMEGA1,OMEGA2,OMEGA3",
|
||||
"1/time,1/time,1/time", "global-cartesian", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/angular_acceleration", {nodes, 3U},
|
||||
Values(frame.angular_acceleration),
|
||||
CopyVectorValues(frame.angular_acceleration),
|
||||
"OMEGADOT1,OMEGADOT2,OMEGADOT3", "1/time^2,1/time^2,1/time^2",
|
||||
"global-cartesian", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/triad", {nodes, 3U, 3U},
|
||||
FlattenTriad(frame), "X,Y,Z", "1,1,1", "global-cartesian",
|
||||
FlattenNodalTriads(frame.nodal_triad), "X,Y,Z", "1,1,1", "global-cartesian",
|
||||
"nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/director", {nodes, 3U},
|
||||
FlattenDirector(frame), "D1,D2,D3", "1,1,1", "global-cartesian",
|
||||
FlattenNodalDirectors(frame.nodal_director), "D1,D2,D3", "1,1,1", "global-cartesian",
|
||||
"nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/generalized_inertia_force", nodal,
|
||||
Values(frame.inertia_force), kGeneralized, kGeneralizedUnits,
|
||||
CopyVectorValues(frame.inertia_force), kGeneralized, kGeneralizedUnits,
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/generalized_internal_force", nodal,
|
||||
Values(frame.internal_force), kGeneralized, kGeneralizedUnits,
|
||||
CopyVectorValues(frame.internal_force), kGeneralized, kGeneralizedUnits,
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/generalized_external_force", nodal,
|
||||
Values(frame.external_force), kGeneralized, kGeneralizedUnits,
|
||||
CopyVectorValues(frame.external_force), kGeneralized, kGeneralizedUnits,
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/generalized_residual", nodal,
|
||||
Values(frame.residual), kGeneralized, kGeneralizedUnits,
|
||||
CopyVectorValues(frame.residual), kGeneralized, kGeneralizedUnits,
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/generalized_reaction", nodal,
|
||||
Values(frame.reaction), kGeneralized, kGeneralizedUnits,
|
||||
CopyVectorValues(frame.reaction), kGeneralized, kGeneralizedUnits,
|
||||
"fixed-initial-global-chart", "nodal", frame);
|
||||
WriteDataset(file, root + "/nodal/reaction", nodal, SpatialReaction(frame),
|
||||
WriteDataset(file, root + "/nodal/reaction", nodal,
|
||||
SpatialReactionValues(frame.generalized_coordinates,
|
||||
frame.reaction,
|
||||
frame.nodal_internal_order.size()),
|
||||
"RF1,RF2,RF3,RM1,RM2,RM3", kGeneralizedUnits, "global-cartesian",
|
||||
"nodal", frame);
|
||||
const std::string shell = root + "/element/shell/";
|
||||
@@ -810,13 +770,7 @@ void CheckModel(const hid_t file, const Domain& domain,
|
||||
model.prescribed_displacement);
|
||||
std::vector<double> directors;
|
||||
std::vector<double> frames;
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
directors.insert(directors.end(), frame.director.begin(),
|
||||
frame.director.end());
|
||||
frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end());
|
||||
frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end());
|
||||
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||
}
|
||||
hdf5_internal::AppendShellReferenceFrames(domain, directors, frames);
|
||||
(void)CheckDoubles(file, "/model/shell/nodal_director", {count, 3U},
|
||||
directors);
|
||||
(void)CheckDoubles(file, "/model/shell/nodal_frame", {count, 3U, 3U}, frames);
|
||||
|
||||
@@ -20,23 +20,16 @@
|
||||
#include "io/hdf5/hdf5_model_writer.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "math/finite_values.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
constexpr const char* kEigenRoot = "/steps/Step-1/eigen";
|
||||
using math_internal::IsFinite;
|
||||
|
||||
void WriteQuantityAttributes(hid_t dataset, const std::string& components,
|
||||
const std::string& units,
|
||||
const std::string& coordinates,
|
||||
const std::string& location) {
|
||||
hdf5_internal::WriteStringAttribute(dataset, "component_names", components);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "component_unit_dimensions",
|
||||
units);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "coordinate_system",
|
||||
coordinates);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "location", location);
|
||||
}
|
||||
using hdf5_internal::WriteQuantityAttributes;
|
||||
|
||||
constexpr const char* kEigenRoot = "/steps/Step-1/eigen";
|
||||
|
||||
void WriteModalAttributes(hid_t dataset, const std::string& source_step) {
|
||||
hdf5_internal::WriteUint64Attribute(dataset, "eigen_schema_version", 1U);
|
||||
@@ -44,17 +37,6 @@ void WriteModalAttributes(hid_t dataset, const std::string& source_step) {
|
||||
hdf5_internal::WriteStringAttribute(dataset, "domain", "mode");
|
||||
}
|
||||
|
||||
bool IsFinite(const Matrix& matrix) {
|
||||
for (std::size_t row = 0U; row < matrix.Rows(); ++row) {
|
||||
for (std::size_t column = 0U; column < matrix.Columns(); ++column) {
|
||||
if (!std::isfinite(matrix(row, column))) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
Status Validate(const Domain& domain, const EigenAnalysisState& state,
|
||||
const std::vector<Diagnostic>& diagnostics) {
|
||||
const FrequencyStepDefinition* frequency = domain.Frequency();
|
||||
|
||||
@@ -489,13 +489,7 @@ bool CheckModelRows(hid_t file, const Domain& domain) {
|
||||
[&](auto i) { return domain.ShellSections()[i].thickness; }))
|
||||
return false;
|
||||
std::vector<double> directors, frames;
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
directors.insert(directors.end(), frame.director.begin(),
|
||||
frame.director.end());
|
||||
frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end());
|
||||
frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end());
|
||||
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||
}
|
||||
hdf5_internal::AppendShellReferenceFrames(domain, directors, frames);
|
||||
return CheckArray<double>(file, "/model/shell/nodal_director", H5T_IEEE_F64LE,
|
||||
H5T_NATIVE_DOUBLE,
|
||||
{static_cast<hsize_t>(domain.Nodes().size()), 3U},
|
||||
|
||||
@@ -419,13 +419,7 @@ void WriteShellModelData(const hid_t file, const Domain& domain,
|
||||
directors.reserve(domain.Nodes().size() * 3U);
|
||||
std::vector<double> frames;
|
||||
frames.reserve(domain.Nodes().size() * 9U);
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
directors.insert(directors.end(), frame.director.begin(),
|
||||
frame.director.end());
|
||||
frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end());
|
||||
frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end());
|
||||
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||
}
|
||||
hdf5_internal::AppendShellReferenceFrames(domain, directors, frames);
|
||||
WriteModelDoubleDataset(file, "/model/shell/nodal_director",
|
||||
{static_cast<hsize_t>(domain.Nodes().size()), 3U},
|
||||
directors.data(), directors.size(), "D1,D2,D3",
|
||||
|
||||
@@ -5,9 +5,13 @@
|
||||
#include <cstdint>
|
||||
#include <filesystem>
|
||||
#include <limits>
|
||||
#include <stdexcept>
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
|
||||
Status OutputFailure(const std::string& code, const std::string& message) {
|
||||
@@ -60,10 +64,20 @@ bool IsValidUtf8(const std::string& value) {
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SameIdentity(const SourceEntityId& left, const SourceEntityId& right) {
|
||||
return left.instance_name == right.instance_name &&
|
||||
left.source_label == right.source_label &&
|
||||
left.source_label_text == right.source_label_text;
|
||||
bool SameIdentity(const SourceEntityId& left,
|
||||
const SourceEntityId& right) {
|
||||
return fesa::SourceIdentityEquals(left, right);
|
||||
}
|
||||
|
||||
void AppendShellReferenceFrames(const Domain& domain,
|
||||
std::vector<double>& directors,
|
||||
std::vector<double>& frames) {
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
directors.insert(directors.end(), frame.director.begin(), frame.director.end());
|
||||
frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end());
|
||||
frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end());
|
||||
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||
}
|
||||
}
|
||||
|
||||
bool IsShellDomain(const Domain& domain) noexcept {
|
||||
@@ -95,6 +109,55 @@ std::string SourceInputIdentity(const Domain& domain) {
|
||||
";content_identity=" + domain.SourceContentIdentity();
|
||||
}
|
||||
|
||||
std::vector<double> CopyVectorValues(const Vector& source) {
|
||||
std::vector<double> values(source.Size());
|
||||
for (std::size_t index = 0U; index < source.Size(); ++index) {
|
||||
values[index] = source[index];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> FlattenNodalTriads(
|
||||
const std::vector<std::array<std::array<double, 3>, 3>>& triads) {
|
||||
std::vector<double> values;
|
||||
values.reserve(triads.size() * 9U);
|
||||
for (const auto& triad : triads) {
|
||||
for (const auto& axis : triad) {
|
||||
values.insert(values.end(), axis.begin(), axis.end());
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> FlattenNodalDirectors(
|
||||
const std::vector<std::array<double, 3>>& directors) {
|
||||
std::vector<double> values;
|
||||
values.reserve(directors.size() * 3U);
|
||||
for (const auto& director : directors) {
|
||||
values.insert(values.end(), director.begin(), director.end());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> SpatialReactionValues(const Vector& rotation,
|
||||
const Vector& reaction,
|
||||
const std::size_t node_count) {
|
||||
std::vector<double> values = CopyVectorValues(reaction);
|
||||
for (std::size_t node = 0U; node < node_count; ++node) {
|
||||
const std::size_t offset = kDofsPerNode * node;
|
||||
const Vector3 phi{rotation[offset + 3U], rotation[offset + 4U],
|
||||
rotation[offset + 5U]};
|
||||
const Vector3 generalized{reaction[offset + 3U], reaction[offset + 4U],
|
||||
reaction[offset + 5U]};
|
||||
const auto moment = SpatialMomentSO3(phi, generalized);
|
||||
if (!moment.HasValue())
|
||||
throw std::runtime_error{"Spatial reaction moment is nonfinite."};
|
||||
for (std::size_t row = 0U; row < 3U; ++row)
|
||||
values[offset + 3U + row] = moment.Value()[row];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
Hdf5Handle MakeUtf8StringType() {
|
||||
Hdf5Handle type{
|
||||
RequireHdf5Id(H5Tcopy(H5T_C_S1), "Unable to copy an HDF5 string type."),
|
||||
@@ -186,7 +249,7 @@ Hdf5Handle CreateDatasetSpace(const std::vector<hsize_t>& dimensions) {
|
||||
H5Sclose};
|
||||
}
|
||||
|
||||
void WriteResultAttributes(const hid_t dataset,
|
||||
void WriteQuantityAttributes(const hid_t dataset,
|
||||
const std::string& component_names,
|
||||
const std::string& component_units,
|
||||
const std::string& coordinate_system,
|
||||
@@ -195,6 +258,15 @@ void WriteResultAttributes(const hid_t dataset,
|
||||
WriteStringAttribute(dataset, "component_unit_dimensions", component_units);
|
||||
WriteStringAttribute(dataset, "coordinate_system", coordinate_system);
|
||||
WriteStringAttribute(dataset, "location", location);
|
||||
}
|
||||
|
||||
void WriteResultAttributes(const hid_t dataset,
|
||||
const std::string& component_names,
|
||||
const std::string& component_units,
|
||||
const std::string& coordinate_system,
|
||||
const std::string& location) {
|
||||
WriteQuantityAttributes(dataset, component_names, component_units,
|
||||
coordinate_system, location);
|
||||
WriteStringAttribute(dataset, "step_name", kStepName);
|
||||
WriteUint64Attribute(dataset, "frame_index", 0U);
|
||||
}
|
||||
@@ -238,11 +310,8 @@ void WriteModelDoubleDataset(const hid_t file, const std::string& path,
|
||||
const std::string& coordinate_system,
|
||||
const std::string& location) {
|
||||
auto dataset = WriteDoubleValues(file, path, dimensions, values, value_count);
|
||||
WriteStringAttribute(dataset.Get(), "component_names", component_names);
|
||||
WriteStringAttribute(dataset.Get(), "component_unit_dimensions",
|
||||
component_units);
|
||||
WriteStringAttribute(dataset.Get(), "coordinate_system", coordinate_system);
|
||||
WriteStringAttribute(dataset.Get(), "location", location);
|
||||
WriteQuantityAttributes(dataset.Get(), component_names, component_units,
|
||||
coordinate_system, location);
|
||||
}
|
||||
|
||||
void WriteUint8Dataset(const hid_t file, const std::string& path,
|
||||
|
||||
@@ -13,11 +13,15 @@
|
||||
|
||||
#include "fesa/core/source_identity.h"
|
||||
#include "fesa/core/status.h"
|
||||
#include "fesa/math/vector.h"
|
||||
#include "fesa/model/domain.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "math/finite_values.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
|
||||
using math_internal::IsFinite;
|
||||
|
||||
inline constexpr std::size_t kDofsPerNode = 6U;
|
||||
inline constexpr std::size_t kEndpointCount = 2U;
|
||||
inline constexpr std::size_t kGaussPointCount = 2U;
|
||||
@@ -64,16 +68,11 @@ Status OutputFailure(const std::string& code, const std::string& message);
|
||||
/// @brief Tests exact preserved source identity.
|
||||
bool SameIdentity(const SourceEntityId& left, const SourceEntityId& right);
|
||||
|
||||
/// @brief Tests a fixed component record for finite values.
|
||||
template <std::size_t Size>
|
||||
bool IsFinite(const std::array<double, Size>& values) {
|
||||
for (const double value : values) {
|
||||
if (!std::isfinite(value)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
/// @brief Appends source-ordered reference directors and a,b,d frame columns.
|
||||
/// @note Callers retain allocation, schema, write/readback and failure ownership.
|
||||
void AppendShellReferenceFrames(const Domain& domain,
|
||||
std::vector<double>& directors,
|
||||
std::vector<double>& frames);
|
||||
|
||||
/// @brief Validates UTF-8 without accepting overlong or surrogate encodings.
|
||||
bool IsValidUtf8(const std::string& value);
|
||||
@@ -93,6 +92,25 @@ std::string NormalizedPathString(const std::filesystem::path& path);
|
||||
/// @brief Returns schema-v0 source path and content identity metadata.
|
||||
std::string SourceInputIdentity(const Domain& domain);
|
||||
|
||||
/// @brief Copies validated vector values in their existing component order.
|
||||
std::vector<double> CopyVectorValues(const Vector& source);
|
||||
|
||||
/// @brief Flattens global nodal A,B,D axes in stable node/axis/component order.
|
||||
std::vector<double> FlattenNodalTriads(
|
||||
const std::vector<std::array<std::array<double, 3>, 3>>& triads);
|
||||
|
||||
/// @brief Flattens global nodal directors in stable node/component order.
|
||||
std::vector<double> FlattenNodalDirectors(
|
||||
const std::vector<std::array<double, 3>>& directors);
|
||||
|
||||
/// @brief Preserves force rows and maps generalized moments to spatial moments.
|
||||
/// @pre Both six-DOF vectors and node_count were validated by the typed writer.
|
||||
/// @note Rotation entries are at offsets 3..5; conversion uses J^T m = q.
|
||||
/// @throws std::runtime_error when the existing spatial-moment solve fails.
|
||||
std::vector<double> SpatialReactionValues(const Vector& rotation,
|
||||
const Vector& reaction,
|
||||
std::size_t node_count);
|
||||
|
||||
Hdf5Handle MakeUtf8StringType();
|
||||
void WriteStringAttribute(hid_t object, const char* name,
|
||||
const std::string& value);
|
||||
@@ -101,6 +119,11 @@ void WriteUint8Attribute(hid_t object, const char* name, std::uint8_t value);
|
||||
void WriteDoubleAttribute(hid_t object, const char* name, double value);
|
||||
Hdf5Handle CreateGroup(hid_t file, const char* path);
|
||||
Hdf5Handle CreateDatasetSpace(const std::vector<hsize_t>& dimensions);
|
||||
/// @brief Writes quantity identity attributes, without family/frame metadata.
|
||||
void WriteQuantityAttributes(hid_t dataset, const std::string& component_names,
|
||||
const std::string& component_units,
|
||||
const std::string& coordinate_system,
|
||||
const std::string& location);
|
||||
void WriteResultAttributes(hid_t dataset, const std::string& component_names,
|
||||
const std::string& component_units,
|
||||
const std::string& coordinate_system,
|
||||
|
||||
@@ -756,27 +756,19 @@ void WriteResultDatasets(const hid_t file, const Domain& domain,
|
||||
WriteStress(file, state);
|
||||
}
|
||||
|
||||
std::vector<double> CopyVector(const Vector& source) {
|
||||
std::vector<double> values(source.Size());
|
||||
for (std::size_t index = 0U; index < source.Size(); ++index) {
|
||||
values[index] = source[index];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<DoubleDatasetPlan> BuildResultDatasetPlans(
|
||||
const Domain& domain, const AnalysisState& state) {
|
||||
std::vector<DoubleDatasetPlan> datasets;
|
||||
const std::vector<std::size_t> nodal_dimensions{domain.Nodes().size(),
|
||||
kDofsPerNode};
|
||||
datasets.push_back({std::string{kStepRoot} + "/nodal/displacement",
|
||||
nodal_dimensions, CopyVector(state.Displacement()),
|
||||
nodal_dimensions, CopyVectorValues(state.Displacement()),
|
||||
"UX,UY,UZ,URX,URY,URZ",
|
||||
"length,length,length,radian,radian,radian",
|
||||
"global-cartesian", "nodal"});
|
||||
datasets.push_back(
|
||||
{std::string{kStepRoot} + "/nodal/reaction", nodal_dimensions,
|
||||
CopyVector(state.Reaction()), "RF1,RF2,RF3,RM1,RM2,RM3",
|
||||
CopyVectorValues(state.Reaction()), "RF1,RF2,RF3,RM1,RM2,RM3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian", "nodal"});
|
||||
if (IsShellDomain(domain)) {
|
||||
|
||||
@@ -11,6 +11,7 @@
|
||||
|
||||
#include "fesa/build_info.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
|
||||
namespace fesa::hdf5_internal {
|
||||
|
||||
@@ -596,15 +597,7 @@ void SelfCheckFile(const std::filesystem::path& path, const Domain& domain,
|
||||
std::vector<double> frames;
|
||||
directors.reserve(domain.Nodes().size() * 3U);
|
||||
frames.reserve(domain.Nodes().size() * 9U);
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
directors.insert(directors.end(), frame.director.begin(),
|
||||
frame.director.end());
|
||||
frames.insert(frames.end(), frame.tangent_a.begin(),
|
||||
frame.tangent_a.end());
|
||||
frames.insert(frames.end(), frame.tangent_b.begin(),
|
||||
frame.tangent_b.end());
|
||||
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||
}
|
||||
hdf5_internal::AppendShellReferenceFrames(domain, directors, frames);
|
||||
RequireModelDoubleDataset(file.Get(), "/model/shell/nodal_director",
|
||||
{static_cast<hsize_t>(domain.Nodes().size()), 3U},
|
||||
"D1,D2,D3", "1,1,1", "global-cartesian", "nodal",
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
#include "fesa/io/hdf5/nonlinear_hdf5_results_writer.h"
|
||||
|
||||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <cmath>
|
||||
#include <cstddef>
|
||||
@@ -14,17 +13,20 @@
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/build_info.h"
|
||||
#include "fesa/elements/finite_rotation.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "fesa/math/vector3.h"
|
||||
#include "io/hdf5/hdf5_atomic_file.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
#include "io/hdf5/hdf5_self_check.h"
|
||||
#include "math/finite_values.h"
|
||||
#include "results/shell_result_recovery.h"
|
||||
|
||||
namespace fesa {
|
||||
namespace {
|
||||
|
||||
using math_internal::IsFinite;
|
||||
|
||||
using hdf5_internal::Hdf5Failure;
|
||||
using hdf5_internal::Hdf5Handle;
|
||||
|
||||
@@ -90,43 +92,9 @@ struct DiagnosticWriteRow {
|
||||
const char* message;
|
||||
};
|
||||
|
||||
template <std::size_t Size>
|
||||
bool IsFinite(const std::array<double, Size>& values) {
|
||||
return std::all_of(values.begin(), values.end(),
|
||||
[](const double value) { return std::isfinite(value); });
|
||||
}
|
||||
|
||||
bool IsFinite(const Vector& values) {
|
||||
for (std::size_t index = 0U; index < values.Size(); ++index) {
|
||||
if (!std::isfinite(values[index])) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
|
||||
bool SameIdentity(const SourceEntityId& left, const SourceEntityId& right) {
|
||||
return left.instance_name == right.instance_name &&
|
||||
left.source_label == right.source_label &&
|
||||
left.source_label_text == right.source_label_text;
|
||||
}
|
||||
|
||||
bool IsFinite(const ShellResultRow& row) {
|
||||
if (!IsFinite(row.natural_coordinates) || !IsFinite(row.generalized_strain) ||
|
||||
!IsFinite(row.section_resultant)) {
|
||||
return false;
|
||||
}
|
||||
for (const auto& axis : row.local_frame) {
|
||||
if (!IsFinite(axis)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const auto& stress : row.stress) {
|
||||
if (!std::isfinite(stress.zeta) || !IsFinite(stress.components)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
bool SameIdentity(const SourceEntityId& left,
|
||||
const SourceEntityId& right) {
|
||||
return fesa::SourceIdentityEquals(left, right);
|
||||
}
|
||||
|
||||
Status Failure(const char* code, const std::string& message) {
|
||||
@@ -300,7 +268,8 @@ Status ValidateState(const Domain& domain, const NonlinearAnalysisState& state)
|
||||
const std::size_t element = row_index / kShellLocationsPerElement;
|
||||
const std::size_t location = row_index % kShellLocationsPerElement;
|
||||
if (row.element != element || row.location != locations[location] ||
|
||||
row.natural_coordinates != coordinates[location] || !IsFinite(row)) {
|
||||
row.natural_coordinates != coordinates[location] ||
|
||||
!results_internal::IsFiniteShellRow(row)) {
|
||||
return Failure("invalid-result-state",
|
||||
"Accepted shell rows must preserve element and GP order.");
|
||||
}
|
||||
@@ -315,13 +284,8 @@ void WriteCommonNumericAttributes(const hid_t dataset,
|
||||
const std::string& coordinate_system,
|
||||
const std::string& location) {
|
||||
hdf5_internal::WriteUint64Attribute(dataset, "schema_version", 1U);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "component_names",
|
||||
component_names);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "component_unit_dimensions",
|
||||
component_units);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "coordinate_system",
|
||||
coordinate_system);
|
||||
hdf5_internal::WriteStringAttribute(dataset, "location", location);
|
||||
hdf5_internal::WriteQuantityAttributes(
|
||||
dataset, component_names, component_units, coordinate_system, location);
|
||||
}
|
||||
|
||||
void WriteFrameAttributes(const hid_t dataset, const AcceptedFrameState& frame) {
|
||||
@@ -660,12 +624,7 @@ void WriteModelData(const hid_t file, const Domain& domain,
|
||||
std::vector<double> frames;
|
||||
directors.reserve(domain.Nodes().size() * 3U);
|
||||
frames.reserve(domain.Nodes().size() * 9U);
|
||||
for (const auto& frame : domain.ShellNodeInitialFrames()) {
|
||||
directors.insert(directors.end(), frame.director.begin(), frame.director.end());
|
||||
frames.insert(frames.end(), frame.tangent_a.begin(), frame.tangent_a.end());
|
||||
frames.insert(frames.end(), frame.tangent_b.begin(), frame.tangent_b.end());
|
||||
frames.insert(frames.end(), frame.director.begin(), frame.director.end());
|
||||
}
|
||||
hdf5_internal::AppendShellReferenceFrames(domain, directors, frames);
|
||||
const hsize_t node_count = static_cast<hsize_t>(domain.Nodes().size());
|
||||
(void)WriteModelDoubleDataset(file, "/model/shell/nodal_director",
|
||||
{node_count, 3U}, directors.data(),
|
||||
@@ -772,14 +731,6 @@ void WriteAcceptedFrameTable(const hid_t file,
|
||||
hdf5_internal::WriteUint64Attribute(dataset.Get(), "schema_version", 1U);
|
||||
}
|
||||
|
||||
std::vector<double> CopyVector(const Vector& source) {
|
||||
std::vector<double> values(source.Size());
|
||||
for (std::size_t index = 0U; index < source.Size(); ++index) {
|
||||
values[index] = source[index];
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> NodalDisplacement(const AcceptedFrameState& frame) {
|
||||
std::vector<double> values(frame.displacement.Size());
|
||||
for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); ++node) {
|
||||
@@ -793,47 +744,6 @@ std::vector<double> NodalDisplacement(const AcceptedFrameState& frame) {
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> FlattenTriads(const AcceptedFrameState& frame) {
|
||||
std::vector<double> values;
|
||||
values.reserve(frame.nodal_triad.size() * 9U);
|
||||
for (const auto& triad : frame.nodal_triad) {
|
||||
for (const auto& axis : triad) {
|
||||
values.insert(values.end(), axis.begin(), axis.end());
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> FlattenDirectors(const AcceptedFrameState& frame) {
|
||||
std::vector<double> values;
|
||||
values.reserve(frame.nodal_director.size() * 3U);
|
||||
for (const auto& director : frame.nodal_director) {
|
||||
values.insert(values.end(), director.begin(), director.end());
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
std::vector<double> SpatialReaction(const AcceptedFrameState& frame) {
|
||||
std::vector<double> values = CopyVector(frame.reaction);
|
||||
for (std::size_t node = 0U; node < frame.nodal_internal_order.size(); ++node) {
|
||||
const std::size_t offset = kDofsPerNode * node;
|
||||
const Vector3 phi{frame.rotation_vector[offset + 3U],
|
||||
frame.rotation_vector[offset + 4U],
|
||||
frame.rotation_vector[offset + 5U]};
|
||||
const Matrix transform = InverseTransposeLeftJacobianSO3(phi);
|
||||
const std::array<double, 3> generalized{
|
||||
frame.reaction[offset + 3U], frame.reaction[offset + 4U],
|
||||
frame.reaction[offset + 5U]};
|
||||
for (std::size_t row = 0U; row < 3U; ++row) {
|
||||
values[offset + 3U + row] = 0.0;
|
||||
for (std::size_t column = 0U; column < 3U; ++column) {
|
||||
values[offset + 3U + row] += transform(row, column) * generalized[column];
|
||||
}
|
||||
}
|
||||
}
|
||||
return values;
|
||||
}
|
||||
|
||||
void WriteShellIdentity(const hid_t dataset, const bool has_section_positions,
|
||||
const std::string& local_frame_path) {
|
||||
hdf5_internal::WriteStringAttribute(dataset, "source_element_type_dataset",
|
||||
@@ -864,35 +774,36 @@ void WriteFrame(const hid_t file, const Domain& domain,
|
||||
file, root + "/nodal/displacement", nodal_dimensions, displacement.data(),
|
||||
displacement.size(), "UX,UY,UZ,URX,URY,URZ", "length,length,length,1,1,1",
|
||||
"global-cartesian", "nodal", frame);
|
||||
const auto triads = FlattenTriads(frame);
|
||||
const auto triads = hdf5_internal::FlattenNodalTriads(frame.nodal_triad);
|
||||
(void)WriteFrameDoubleDataset(file, root + "/nodal/triad",
|
||||
{node_count, 3U, 3U}, triads.data(), triads.size(),
|
||||
"X,Y,Z", "1,1,1", "global-cartesian",
|
||||
"nodal-triad", frame);
|
||||
const auto directors = FlattenDirectors(frame);
|
||||
const auto directors = hdf5_internal::FlattenNodalDirectors(frame.nodal_director);
|
||||
(void)WriteFrameDoubleDataset(file, root + "/nodal/director", {node_count, 3U},
|
||||
directors.data(), directors.size(), "D1,D2,D3",
|
||||
"1,1,1", "global-cartesian", "nodal-director",
|
||||
frame);
|
||||
const auto internal = CopyVector(frame.internal_force);
|
||||
const auto internal = hdf5_internal::CopyVectorValues(frame.internal_force);
|
||||
(void)WriteFrameDoubleDataset(
|
||||
file, root + "/nodal/generalized_internal_force", nodal_dimensions,
|
||||
internal.data(), internal.size(), "FI1,FI2,FI3,MI_PHI1,MI_PHI2,MI_PHI3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian", "nodal", frame);
|
||||
const auto external = CopyVector(frame.external_force);
|
||||
const auto external = hdf5_internal::CopyVectorValues(frame.external_force);
|
||||
(void)WriteFrameDoubleDataset(
|
||||
file, root + "/nodal/generalized_external_force", nodal_dimensions,
|
||||
external.data(), external.size(), "FE1,FE2,FE3,ME_PHI1,ME_PHI2,ME_PHI3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian", "nodal", frame);
|
||||
const auto residual = CopyVector(frame.residual);
|
||||
const auto residual = hdf5_internal::CopyVectorValues(frame.residual);
|
||||
(void)WriteFrameDoubleDataset(
|
||||
file, root + "/nodal/generalized_residual", nodal_dimensions,
|
||||
residual.data(), residual.size(), "RU1,RU2,RU3,RPHI1,RPHI2,RPHI3",
|
||||
"force,force,force,force*length,force*length,force*length",
|
||||
"global-cartesian", "nodal", frame);
|
||||
const auto reaction = SpatialReaction(frame);
|
||||
const auto reaction = hdf5_internal::SpatialReactionValues(
|
||||
frame.rotation_vector, frame.reaction, frame.nodal_internal_order.size());
|
||||
(void)WriteFrameDoubleDataset(
|
||||
file, root + "/nodal/reaction", nodal_dimensions, reaction.data(),
|
||||
reaction.size(), "RF1,RF2,RF3,RM1,RM2,RM3",
|
||||
|
||||
@@ -27,11 +27,6 @@ using U = std::vector<std::uint64_t>;
|
||||
using B = std::vector<std::uint8_t>;
|
||||
using D = std::vector<double>;
|
||||
using S = std::vector<std::string>;
|
||||
std::vector<double> Values(const Vector& vector) {
|
||||
D result;
|
||||
for (std::size_t i = 0; i < vector.Size(); ++i) result.push_back(vector[i]);
|
||||
return result;
|
||||
}
|
||||
void Parent(hid_t file, const std::string& path) {
|
||||
const auto parent = path.substr(0, path.find_last_of('/'));
|
||||
if (!parent.empty() && H5Lexists(file, parent.c_str(), H5P_DEFAULT) <= 0)
|
||||
@@ -110,11 +105,11 @@ void FrameData(SolidPlan& plan, const Domain& domain, std::size_t frame,
|
||||
value.doubles = {{dynamic ? "time" : "load_factor", coordinate}};
|
||||
plan.datasets.push_back(std::move(value));
|
||||
};
|
||||
add(Data(root + "/nodal/displacement", {n, 3}, Values(u), "U1,U2,U3",
|
||||
add(Data(root + "/nodal/displacement", {n, 3}, CopyVectorValues(u), "U1,U2,U3",
|
||||
"length", "nodal"));
|
||||
add(Data(root + "/nodal/reaction", {n, 3}, Values(rf), "RF1,RF2,RF3", "force",
|
||||
add(Data(root + "/nodal/reaction", {n, 3}, CopyVectorValues(rf), "RF1,RF2,RF3", "force",
|
||||
"nodal"));
|
||||
add(Data(root + "/nodal/residual", {n, 3}, Values(r), "R1,R2,R3", "force",
|
||||
add(Data(root + "/nodal/residual", {n, 3}, CopyVectorValues(r), "R1,R2,R3", "force",
|
||||
"nodal"));
|
||||
D stress, strain;
|
||||
Check(points.size() == 8 * e, "Solid point count is invalid.");
|
||||
@@ -249,7 +244,7 @@ void Model(SolidPlan& plan, const Domain& domain, const DofManager& dofs) {
|
||||
for (std::size_t i = 0; i < domain.Elements().Size(); ++i) {
|
||||
auto element = factory.Create(domain.Elements()[i], domain);
|
||||
Check(element.HasValue(), "Solid factory reference failed.");
|
||||
const auto* hex = dynamic_cast<const Hex8Element*>(element.Value().get());
|
||||
auto hex = factory.BindSolidTrial(*element.Value());
|
||||
Check(hex != nullptr, "Solid kernel identity failed.");
|
||||
auto response = hex->EvaluateLinear(Vector{24});
|
||||
Check(response.HasValue(), "Solid reference recovery failed.");
|
||||
@@ -426,15 +421,15 @@ Result<SolidPlan> BuildSolidPlan(const Domain& domain, SolidStateView view,
|
||||
plan.datasets.push_back(std::move(data));
|
||||
};
|
||||
add(Data(root + "/nodal/velocity", {domain.Nodes().size(), 3},
|
||||
Values(frame.velocity), "V1,V2,V3", "length/time", "nodal"));
|
||||
CopyVectorValues(frame.velocity), "V1,V2,V3", "length/time", "nodal"));
|
||||
auto a = Data(root + "/nodal/acceleration", {domain.Nodes().size(), 3},
|
||||
Values(frame.acceleration), "A1,A2,A3", "length/time^2",
|
||||
CopyVectorValues(frame.acceleration), "A1,A2,A3", "length/time^2",
|
||||
"nodal");
|
||||
a.strings["acceleration_kind"] = "physical";
|
||||
add(std::move(a));
|
||||
a = Data(root + "/nodal/algorithmic_acceleration",
|
||||
{domain.Nodes().size(), 3},
|
||||
Values(frame.algorithmic_acceleration), "A1,A2,A3",
|
||||
CopyVectorValues(frame.algorithmic_acceleration), "A1,A2,A3",
|
||||
"length/time^2", "nodal");
|
||||
a.strings["acceleration_kind"] = "algorithmic";
|
||||
add(std::move(a));
|
||||
|
||||
@@ -7,7 +7,9 @@
|
||||
#include <string>
|
||||
#include <vector>
|
||||
|
||||
#include "fesa/analysis/analysis_model.h"
|
||||
#include "fesa/build_info.h"
|
||||
#include "fesa/fem/dof_manager.h"
|
||||
#include "io/hdf5/hdf5_primitives.h"
|
||||
#include "io/hdf5/hdf5_raii.h"
|
||||
|
||||
@@ -424,8 +426,7 @@ void Frames(hid_t f, const ThermalAnalysisState& s) {
|
||||
Number(g.Get(), "is_initial", H5T_STD_U8LE, H5T_NATIVE_UINT8,
|
||||
static_cast<std::uint8_t>(frame.is_initial));
|
||||
ResultValues(f, root + "/nodal/temperature", {s.NodeOrder().size(), 5},
|
||||
{frame.temperature.Data(),
|
||||
frame.temperature.Data() + frame.temperature.Size()},
|
||||
CopyVectorValues(frame.temperature),
|
||||
"NT11,NT12,NT13,NT14,NT15", "temperature", "scalar",
|
||||
"nodal-thickness-stations");
|
||||
if (frame.is_initial) {
|
||||
@@ -433,9 +434,7 @@ void Frames(hid_t f, const ThermalAnalysisState& s) {
|
||||
Absent(f, root + "/element", "heat_flux");
|
||||
} else {
|
||||
const auto& payload = *frame.completed;
|
||||
std::vector<double> reaction(payload.reaction_heat_rate.Size());
|
||||
for (std::size_t i = 0; i < reaction.size(); ++i)
|
||||
reaction[i] = payload.reaction_heat_rate[i];
|
||||
const auto reaction = CopyVectorValues(payload.reaction_heat_rate);
|
||||
ResultValues(f, root + "/nodal/reaction_heat_rate", {reaction.size()},
|
||||
reaction, "RFL", "power", "scalar",
|
||||
"prescribed-temperature-dofs");
|
||||
|
||||
Reference in New Issue
Block a user