feat(solid): add Hex8 input and three-DOF semantics

This commit is contained in:
KOKO\Mimi
2026-10-09 01:12:30 +09:00
parent df0388ae2f
commit 9acf18db4b
16 changed files with 1468 additions and 98 deletions
+474 -51
View File
@@ -50,7 +50,7 @@ struct RawNode {
};
struct RawElement {
enum class Type { kB33, kS4, kS4r, kDs4 };
enum class Type { kB33, kS4, kS4r, kDs4, kC3d8 };
std::int64_t label;
std::string label_text;
@@ -59,7 +59,7 @@ struct RawElement {
SourceLocation location;
};
enum class ElementFamily { kBeam, kShell, kThermalShell };
enum class ElementFamily { kBeam, kShell, kThermalShell, kSolid };
struct RawSet {
std::string name;
@@ -83,6 +83,12 @@ struct RawShellSection {
SourceLocation location;
};
struct RawSolidSection {
std::string element_set_name;
std::string material_name;
SourceLocation location;
};
struct RawPart {
std::string name;
SourceLocation location;
@@ -92,6 +98,7 @@ struct RawPart {
std::vector<RawSet> element_sets;
std::vector<RawSection> sections;
std::vector<RawShellSection> shell_sections;
std::vector<RawSolidSection> solid_sections;
};
struct RawInstance {
@@ -170,6 +177,8 @@ struct RawStep {
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};
@@ -746,6 +755,10 @@ class DomainMappingContext::Impl {
ParseAssemblyStart(block);
return;
}
if (block.canonical_name == "AMPLITUDE") {
ParseAmplitude(block);
return;
}
if (block.canonical_name == "MATERIAL") {
ParseMaterial(block);
return;
@@ -841,7 +854,8 @@ class DomainMappingContext::Impl {
model_element_family_ == ElementFamily::kShell ||
model_element_family_ == ElementFamily::kThermalShell;
if (part.nodes.empty() || part.elements.empty() ||
(!shell_part && part.sections.empty())) {
(!shell_part && model_element_family_ != ElementFamily::kSolid &&
part.sections.empty())) {
InvalidKeywordLocation(block,
"A part closes only after node, element, "
"and matching section blocks.");
@@ -917,7 +931,8 @@ class DomainMappingContext::Impl {
return;
}
if (model_element_family_ == ElementFamily::kShell ||
model_element_family_ == ElementFamily::kThermalShell) {
model_element_family_ == ElementFamily::kThermalShell ||
model_element_family_ == ElementFamily::kSolid) {
InputFailure(
"unsupported-mixed-element-model", block.location,
block.canonical_name, "",
@@ -934,7 +949,8 @@ class DomainMappingContext::Impl {
block, "SHELL SECTION follows the part mesh and optional sets.");
return;
}
if (model_element_family_ == ElementFamily::kBeam) {
if (model_element_family_ == ElementFamily::kBeam ||
model_element_family_ == ElementFamily::kSolid) {
InputFailure(
"unsupported-mixed-element-model", block.location,
block.canonical_name, "",
@@ -943,6 +959,17 @@ class DomainMappingContext::Impl {
}
ParseShellSection(block, part);
part_sections_seen_ = !failure_;
} else if (block.canonical_name == "SOLID SECTION") {
beam_section_context_active_ = false;
if (!part_elements_seen_ ||
model_element_family_ != ElementFamily::kSolid) {
InputFailure("invalid-solid-section", block.location,
block.canonical_name, "",
"SOLID SECTION requires an exclusive C3D8 mesh.");
return;
}
ParseSolidSection(block, part);
part_sections_seen_ = !failure_;
} else if (block.canonical_name == "SECTION POINTS") {
ParseSectionPoints(block, part);
} else if (block.canonical_name == "TRANSVERSE SHEAR STIFFNESS") {
@@ -1011,10 +1038,12 @@ class DomainMappingContext::Impl {
const bool thermal_block =
type_parameter && type_parameter->value &&
AsciiCaseInsensitiveEquals(*type_parameter->value, "DS4");
if (!ValidateParameters(block,
thermal_block
? std::vector<std::string_view>{"TYPE", "ELSET"}
: std::vector<std::string_view>{"TYPE"})) {
if (!ValidateParameters(
block, (thermal_block || (type_parameter && type_parameter->value &&
AsciiCaseInsensitiveEquals(
*type_parameter->value, "C3D8")))
? std::vector<std::string_view>{"TYPE", "ELSET"}
: std::vector<std::string_view>{"TYPE"})) {
return;
}
const auto* type = RequiredParameterValue(block, "TYPE");
@@ -1036,6 +1065,10 @@ class DomainMappingContext::Impl {
element_type = RawElement::Type::kS4r;
element_family = ElementFamily::kShell;
expected_field_count = 5U;
} else if (AsciiCaseInsensitiveEquals(*type, "C3D8")) {
element_type = RawElement::Type::kC3d8;
element_family = ElementFamily::kSolid;
expected_field_count = 9U;
} else if (AsciiCaseInsensitiveEquals(*type, "DS4")) {
element_type = RawElement::Type::kDs4;
element_family = ElementFamily::kThermalShell;
@@ -1043,15 +1076,16 @@ class DomainMappingContext::Impl {
} else {
InputFailure("unsupported-element-formulation", block.location,
block.canonical_name, *type,
"Only TYPE=B33, TYPE=S4, TYPE=S4R, and TYPE=DS4 belong to "
"Only B33, S4, S4R, DS4, and C3D8 belong to "
"the approved "
"element subsets.");
return;
}
if (model_element_family_ && *model_element_family_ != element_family) {
InputFailure("unsupported-mixed-element-model", block.location,
block.canonical_name, *type,
"Beam and shell elements cannot be mixed in one model.");
InputFailure(
"unsupported-mixed-element-model", block.location,
block.canonical_name, *type,
"Beam, Shell, thermal, and Solid families cannot mix in one model.");
return;
}
model_element_family_ = element_family;
@@ -1067,9 +1101,11 @@ class DomainMappingContext::Impl {
inline_set = RawSet{*name, {}, block.location};
}
if (block.data.empty()) {
InputFailure(element_family == ElementFamily::kShell
? "invalid-shell-connectivity"
: "invalid-data-arity",
InputFailure(element_family == ElementFamily::kSolid
? "invalid-solid-connectivity"
: (element_family == ElementFamily::kShell
? "invalid-shell-connectivity"
: "invalid-data-arity"),
block.location, block.canonical_name, "",
"ELEMENT requires at least one row with the approved "
"connectivity arity.");
@@ -1078,9 +1114,11 @@ class DomainMappingContext::Impl {
for (const auto& row : block.data) {
if (row.fields.size() != expected_field_count) {
InputFailure(
element_family == ElementFamily::kShell
? "invalid-shell-connectivity"
: "invalid-data-arity",
element_family == ElementFamily::kSolid
? "invalid-solid-connectivity"
: (element_family == ElementFamily::kShell
? "invalid-shell-connectivity"
: "invalid-data-arity"),
row.location, block.canonical_name, "",
element_family == ElementFamily::kShell
? "S4 and S4R rows require a label and exactly four nodes."
@@ -1108,10 +1146,12 @@ class DomainMappingContext::Impl {
const std::set<std::int64_t> distinct_nodes{element.node_labels.begin(),
element.node_labels.end()};
if (distinct_nodes.size() != element.node_labels.size()) {
InputFailure(
"invalid-shell-connectivity", row.location, block.canonical_name,
element.label_text,
"Shell connectivity requires four distinct source nodes.");
InputFailure(element_family == ElementFamily::kSolid
? "invalid-solid-connectivity"
: "invalid-shell-connectivity",
row.location, block.canonical_name, element.label_text,
"Connectivity requires the specified number of distinct "
"source nodes.");
return;
}
}
@@ -1132,6 +1172,101 @@ class DomainMappingContext::Impl {
if (inline_set) part.element_sets.push_back(std::move(*inline_set));
}
/// @brief Parses material-only Solid assignment without thickness defaults.
void ParseSolidSection(const KeywordBlock& block, RawPart& part) {
if (!ValidateParameters(block, {"ELSET", "MATERIAL"})) return;
const auto* elset = RequiredParameterValue(block, "ELSET");
const auto* material = RequiredParameterValue(block, "MATERIAL");
if (!elset || !material) return;
if (block.data.size() > 1U ||
(!block.data.empty() &&
std::any_of(block.data[0].fields.begin(), block.data[0].fields.end(),
[](const auto& field) { return !field.empty(); }))) {
InputFailure("invalid-solid-section", block.location,
block.canonical_name, *elset,
"SOLID SECTION permits omitted or blank data only.");
return;
}
part.solid_sections.push_back({*elset, *material, block.location});
}
/// @brief Validates and owns one model-level supported load amplitude.
void ParseAmplitude(const KeywordBlock& block) {
const bool dynamic_source = std::any_of(
input_.blocks.begin(), input_.blocks.end(), [](const auto& candidate) {
return candidate.canonical_name == "DYNAMIC";
});
if (model_element_family_ != ElementFamily::kSolid || !dynamic_source) {
InputFailure("unsupported-load-amplitude", block.location,
block.canonical_name, "",
"Amplitude is supported only in Solid dynamic models.");
return;
}
if (!ValidateParameters(block, {"NAME", "DEFINITION", "TIME", "VALUE"}))
return;
const auto* name = RequiredParameterValue(block, "NAME");
if (!name) return;
for (const auto& option :
std::array<std::pair<const char*, const char*>, 3>{
{{"DEFINITION", "TABULAR"},
{"TIME", "STEP TIME"},
{"VALUE", "RELATIVE"}}}) {
const auto* parameter = Parameter(block, option.first);
if (parameter &&
(!parameter->value ||
!AsciiCaseInsensitiveEquals(*parameter->value, option.second))) {
InputFailure("unsupported-amplitude-option", block.location,
block.canonical_name, option.first,
"Only TABULAR STEP TIME RELATIVE amplitude is supported.");
return;
}
}
const std::string canonical = CanonicalizeAscii(*name);
if (std::any_of(definition_.amplitudes.begin(),
definition_.amplitudes.end(), [&canonical](const auto& a) {
return a.name_canonical == canonical;
})) {
InputFailure("duplicate-amplitude", block.location, block.canonical_name,
*name,
"Amplitude names must be unique ignoring ASCII case.");
return;
}
TabularAmplitudeDefinition amplitude{*name, canonical, block.location, {}};
for (const auto& row : block.data) {
if (row.fields.empty() || row.fields.size() % 2U != 0U) {
InputFailure("invalid-amplitude-data", row.location,
block.canonical_name, *name,
"Amplitude rows require complete time/value pairs.");
return;
}
for (std::size_t field = 0U; field < row.fields.size(); field += 2U) {
double time = 0.0;
double value = 0.0;
if (!ParseDouble(row.fields[field], time, row.location,
block.canonical_name) ||
!ParseDouble(row.fields[field + 1U], value, row.location,
block.canonical_name))
return;
if (time < 0.0 || (!amplitude.points.empty() &&
!(time > amplitude.points.back()[0]))) {
InputFailure(
"invalid-amplitude-data", row.location, block.canonical_name,
*name,
"Amplitude times must be nonnegative and strictly increasing.");
return;
}
amplitude.points.push_back({time, value});
}
}
if (amplitude.points.empty()) {
InputFailure("invalid-amplitude-data", block.location,
block.canonical_name, *name,
"Amplitude requires at least one finite time/value pair.");
return;
}
definition_.amplitudes.push_back(std::move(amplitude));
}
void ParseShellSection(const KeywordBlock& block, RawPart& part) {
for (const auto& candidate : block.parameters) {
if (candidate.name != "ELSET" && candidate.name != "MATERIAL") {
@@ -1735,13 +1870,17 @@ class DomainMappingContext::Impl {
}
if (!ParseModelDouble(block.data[0].fields[0], material.youngs_modulus,
block.data[0].location, block.canonical_name,
model_element_family_ == ElementFamily::kShell
model_element_family_ == ElementFamily::kSolid
? "invalid-solid-material"
: model_element_family_ == ElementFamily::kShell
? "invalid-shell-material"
: "invalid-beam-property",
"Elastic material values must be finite.") ||
!ParseModelDouble(block.data[0].fields[1], material.poisson_ratio,
block.data[0].location, block.canonical_name,
model_element_family_ == ElementFamily::kShell
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.")) {
@@ -2006,6 +2145,8 @@ class DomainMappingContext::Impl {
}
double value = 0.0;
if (row.fields.size() == 4U &&
!(model_element_family_ == ElementFamily::kSolid &&
row.fields[3].empty()) &&
!ParseDouble(row.fields[3], value, row.location,
block.canonical_name)) {
return;
@@ -2016,19 +2157,46 @@ class DomainMappingContext::Impl {
}
void ParseCload(const KeywordBlock& block, std::vector<NodalLoad>& loads) {
if (step_.dynamic && !block.parameters.empty()) {
if (step_.dynamic && model_element_family_ != ElementFamily::kSolid &&
!block.parameters.empty()) {
InputFailure("unsupported-dynamic-load", block.location,
block.canonical_name, "",
"Dynamic CLOAD does not support keyword parameters.");
return;
}
if (!ValidateParameters(block, {}) || block.data.empty()) {
const bool solid_dynamic =
step_.dynamic && model_element_family_ == ElementFamily::kSolid;
if (!ValidateParameters(
block, solid_dynamic ? std::vector<std::string_view>{"AMPLITUDE"}
: std::vector<std::string_view>{}) ||
block.data.empty()) {
if (!failure_) {
InputFailure("invalid-data-arity", block.location, block.canonical_name,
"", "CLOAD requires one or more rows.");
}
return;
}
std::optional<EntityIndex> amplitude_index;
std::string amplitude_name;
if (Parameter(block, "AMPLITUDE")) {
const auto* name = RequiredParameterValue(block, "AMPLITUDE");
if (!name) return;
amplitude_name = *name;
for (std::size_t index = 0U; index < definition_.amplitudes.size();
++index) {
if (AsciiCaseInsensitiveEquals(definition_.amplitudes[index].name_raw,
*name)) {
amplitude_index = static_cast<EntityIndex>(index);
break;
}
}
if (!amplitude_index) {
InputFailure("unresolved-amplitude", block.location,
block.canonical_name, *name,
"CLOAD amplitude must resolve to a model-level curve.");
return;
}
}
for (const auto& row : block.data) {
if (row.fields.size() != 3U || row.fields[0].empty()) {
InputFailure("invalid-data-arity", row.location, block.canonical_name,
@@ -2048,15 +2216,17 @@ class DomainMappingContext::Impl {
row.fields[0], "CLOAD DOF must be in 1..6.");
return;
}
if (step_.dynamic && dof > 3) {
if ((step_.dynamic || model_element_family_ == ElementFamily::kSolid) &&
dof > 3) {
InputFailure("unsupported-dynamic-load", row.location,
block.canonical_name, row.fields[1],
"Dynamic CLOAD supports global translational DOF 1 "
"through 3 only.");
return;
}
loads.push_back(
{row.fields[0], static_cast<int>(dof), magnitude, row.location});
loads.push_back({row.fields[0], static_cast<int>(dof), magnitude,
row.location, amplitude_index, amplitude_name,
block.location});
}
}
@@ -2148,10 +2318,11 @@ class DomainMappingContext::Impl {
return;
}
if (AsciiCaseInsensitiveEquals(*nlgeom_parameter->value, "YES")) {
if (model_element_family_ != ElementFamily::kShell) {
if (model_element_family_ != ElementFamily::kShell &&
model_element_family_ != ElementFamily::kSolid) {
InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, *nlgeom_parameter->value,
"NLGEOM=YES requires an S4 or S4R shell model.");
"NLGEOM=YES requires a supported Shell or Solid model.");
return;
}
procedure_kind = StaticProcedureKind::kNonlinearFullNewton;
@@ -2353,7 +2524,8 @@ class DomainMappingContext::Impl {
return;
}
step_no_op_seen_ = true;
if (step_.frequency || step_.has_heat_transfer) {
if (step_.frequency || step_.has_heat_transfer ||
(step_.dynamic && model_element_family_ == ElementFamily::kSolid)) {
const auto* write = Parameter(block, "WRITE");
const auto* frequency = Parameter(block, "FREQUENCY");
if (!ValidateParameters(block, {"WRITE", "FREQUENCY"}) ||
@@ -2553,18 +2725,26 @@ class DomainMappingContext::Impl {
}
void ParseDynamic(const KeywordBlock& block) {
if (step_.perturbation_explicit || !step_.nlgeom ||
model_element_family_ != ElementFamily::kShell ||
if (step_.perturbation_explicit ||
(model_element_family_ != ElementFamily::kSolid &&
(!step_.nlgeom || model_element_family_ != ElementFamily::kShell)) ||
!ValidateParameters(block, {"DIRECT"},
"unsupported-dynamic-parameter")) {
if (!failure_) {
InputFailure(
"unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"DYNAMIC requires the approved nonlinear shell configuration.");
InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, "",
"DYNAMIC requires nonlinear Shell or linear/nonlinear "
"Solid geometry.");
}
return;
}
if (model_element_family_ == ElementFamily::kSolid &&
block.data.size() == 1U && block.data[0].fields.size() > 3U) {
InputFailure("invalid-dynamic-data", block.data[0].location,
block.canonical_name, "",
"Solid DYNAMIC requires h,T with at most one blank field.");
return;
}
const auto* direct = Parameter(block, "DIRECT");
if (direct == nullptr || direct->value || block.data.size() != 1U ||
block.data[0].fields.size() < 2U ||
@@ -2622,6 +2802,7 @@ class DomainMappingContext::Impl {
"DYNAMIC,DIRECT increment count cannot exceed STEP INC.");
return;
}
dynamic.nlgeom = step_.nlgeom;
step_.dynamic = std::move(dynamic);
}
@@ -2716,6 +2897,46 @@ class DomainMappingContext::Impl {
}
void ParseStatic(const KeywordBlock& block) {
if (model_element_family_ == ElementFamily::kSolid &&
Parameter(block, "DIRECT")) {
if (step_.perturbation_explicit || step_.has_static || step_.frequency ||
!ValidateParameters(block, {"DIRECT"}))
return;
const auto* direct = Parameter(block, "DIRECT");
if (direct->value || block.data.size() != 1U ||
(block.data[0].fields.size() < 2U ||
block.data[0].fields.size() > 3U ||
std::any_of(block.data[0].fields.begin() + 2,
block.data[0].fields.end(),
[](const auto& field) { return !field.empty(); }))) {
InputFailure("invalid-static-data", block.location,
block.canonical_name, "DIRECT",
"STATIC DIRECT requires exactly h,T.");
return;
}
double h = 0.0;
double period = 0.0;
if (!ParseDouble(block.data[0].fields[0], h, block.data[0].location,
"STATIC") ||
!ParseDouble(block.data[0].fields[1], period, block.data[0].location,
"STATIC"))
return;
const double count = std::round(period / h);
if (!(h > 0.0) || !(period > 0.0) || !std::isfinite(count) ||
count < 1.0 ||
count > static_cast<double>(step_.maximum_increments) ||
h * count != period) {
InputFailure("invalid-static-schedule", block.data[0].location,
block.canonical_name, "DIRECT",
"STATIC DIRECT requires positive exact N*h=T and N<=INC.");
return;
}
step_.static_values = {h, period, h, h};
step_.static_direct = true;
step_.static_increment_count = static_cast<std::uint32_t>(count);
step_.has_static = true;
return;
}
if (step_.perturbation_explicit || step_.frequency) {
InputFailure("unsupported-analysis-procedure", block.location,
block.canonical_name, "PERTURBATION",
@@ -2811,6 +3032,17 @@ class DomainMappingContext::Impl {
"OUTPUT must select exactly FIELD or HISTORY.");
return;
}
if (model_element_family_ == ElementFamily::kSolid &&
((variable &&
(!variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) ||
!RequireNoData(block))) {
if (!failure_)
InputFailure("unsupported-keyword", block.location,
block.canonical_name, "VARIABLE",
"Solid OUTPUT accepts only PRESELECT requests.");
return;
}
if (step_.nlgeom &&
(variable == nullptr || !variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) {
@@ -2865,6 +3097,50 @@ class DomainMappingContext::Impl {
"Output variable keywords require an active OUTPUT request.");
return;
}
if (model_element_family_ == ElementFamily::kSolid && step_.dynamic) {
InputFailure(
"unsupported-keyword", block.location, block.canonical_name, "",
"Solid dynamic output children are outside the approved subset.");
return;
}
if (model_element_family_ == ElementFamily::kSolid && step_.has_static) {
const auto* variable = Parameter(block, "VARIABLE");
const auto* directions = Parameter(block, "DIRECTIONS");
const bool node = block.canonical_name == "NODE OUTPUT";
const bool element = block.canonical_name == "ELEMENT OUTPUT";
const bool contact = block.canonical_name == "CONTACT OUTPUT";
if (!active_output_is_field_ || (!node && !element && !contact) ||
!ValidateParameters(
block,
element ? std::vector<std::string_view>{"VARIABLE", "DIRECTIONS"}
: std::vector<std::string_view>{"VARIABLE"}) ||
(directions &&
(!directions->value ||
!AsciiCaseInsensitiveEquals(*directions->value, "YES"))) ||
(variable &&
(!variable->value ||
!AsciiCaseInsensitiveEquals(*variable->value, "PRESELECT"))) ||
(contact && (!variable || !block.data.empty()))) {
if (!failure_)
InputFailure("unsupported-keyword", block.location,
block.canonical_name, "",
"Solid static output child is unsupported.");
return;
}
for (const auto& row : block.data) {
for (const auto& field : row.fields) {
const std::string token = CanonicalizeAscii(field);
if (!(node ? (token == "U" || token == "RF")
: (token == "S" || token == "E"))) {
InputFailure("unsupported-keyword", row.location,
block.canonical_name, field,
"Solid output quantities must be physical supported "
"components.");
return;
}
}
}
}
if (step_.frequency) {
const std::string_view required_variable =
block.canonical_name == "NODE OUTPUT" ? "PRESELECT" : "ALL";
@@ -3001,16 +3277,32 @@ class DomainMappingContext::Impl {
"A material must own exactly one ELASTIC row.");
return;
}
if (model_element_family_ == ElementFamily::kShell) {
if (model_element_family_ == ElementFamily::kShell ||
model_element_family_ == ElementFamily::kSolid) {
if (!(material.youngs_modulus > 0.0) ||
!(material.poisson_ratio > -1.0) ||
!(material.poisson_ratio < 0.5)) {
ModelFailure(
"invalid-shell-material", material.elastic_location, "ELASTIC",
material.name,
"Shell isotropic elasticity requires E>0 and -1<nu<0.5.");
ModelFailure(model_element_family_ == ElementFamily::kSolid
? "invalid-solid-material"
: "invalid-shell-material",
material.elastic_location, "ELASTIC", material.name,
"Isotropic elasticity requires E>0 and -1<nu<0.5.");
return;
}
if (model_element_family_ == ElementFamily::kSolid) {
const double mu =
material.youngs_modulus / (2.0 * (1.0 + material.poisson_ratio));
const double lambda = material.youngs_modulus *
material.poisson_ratio /
((1.0 + material.poisson_ratio) *
(1.0 - 2.0 * material.poisson_ratio));
if (!std::isfinite(mu) || !std::isfinite(lambda)) {
ModelFailure("invalid-solid-material", material.elastic_location,
"ELASTIC", material.name,
"Solid Lame constants must be finite.");
return;
}
}
definition_.materials.push_back({material.name, material.youngs_modulus,
material.poisson_ratio,
material.location});
@@ -3063,6 +3355,16 @@ class DomainMappingContext::Impl {
"PHYSICAL CONSTANTS is supported only by thermal input.");
return;
}
if (!step_.frequency && !step_.dynamic &&
model_element_family_ == ElementFamily::kSolid) {
for (std::size_t index = 0U; index < materials_.size(); ++index) {
if (materials_[index].density)
definition_.densities.push_back({static_cast<EntityIndex>(index),
*materials_[index].density,
materials_[index].density_location});
}
return;
}
if (!step_.frequency && !step_.dynamic) {
if (std::any_of(materials_.begin(), materials_.end(),
[](const RawMaterial& material) {
@@ -3090,10 +3392,11 @@ class DomainMappingContext::Impl {
}
}
}
if (model_element_family_ != ElementFamily::kShell) {
if (model_element_family_ != ElementFamily::kShell &&
model_element_family_ != ElementFamily::kSolid) {
InputFailure("unsupported-analysis-procedure", step_.location,
step_.frequency ? "FREQUENCY" : "DYNAMIC", step_.name_raw,
"The procedure requires the approved shell model family.");
"The procedure requires an approved Shell or Solid model.");
return;
}
if (!element_surfaces_.empty()) {
@@ -3112,6 +3415,10 @@ class DomainMappingContext::Impl {
break;
}
}
for (const auto& section : definition_.solid_sections) {
if (section.MaterialIndex() == static_cast<EntityIndex>(index))
active = true;
}
if (active) {
ModelFailure(
"missing-material-density", material.location, "MATERIAL",
@@ -3196,7 +3503,8 @@ class DomainMappingContext::Impl {
++part_index) {
const RawPart& part = parts_[part_index];
if (part.nodes.empty() || part.elements.empty() ||
(!shell_model && part.sections.empty())) {
(!shell_model && model_element_family_ != ElementFamily::kSolid &&
part.sections.empty())) {
InputFailure("invalid-model-cardinality", part.location, "PART",
part.name,
"A part requires nodes, elements, and its approved "
@@ -3245,6 +3553,45 @@ class DomainMappingContext::Impl {
}
definition_.parts.push_back(std::move(part_definition));
if (model_element_family_ == ElementFamily::kSolid) {
auto& assignments = part_shell_section_assignments_[part_index];
for (const auto& section : part.solid_sections) {
const auto* element_set =
FindSet(part.element_sets, section.element_set_name);
const auto material = FindMaterialIndex(section.material_name);
if (!element_set || !material) {
InputFailure("unresolved-solid-section", section.location,
"SOLID SECTION", section.element_set_name,
"Solid ELSET and MATERIAL must resolve.");
return;
}
const auto index =
static_cast<EntityIndex>(definition_.solid_sections.size());
definition_.solid_sections.emplace_back(
SourceEntityId{part.name, static_cast<std::int64_t>(index),
section.element_set_name},
section.element_set_name, *material, section.location);
for (const auto label : element_set->members) {
if (!assignments.emplace(label, std::make_pair(index, *material))
.second) {
InputFailure(
"invalid-solid-section-assignment", section.location,
"SOLID SECTION", std::to_string(label),
"Solid elements require exactly one section assignment.");
return;
}
}
}
for (const auto& element : part.elements) {
if (assignments.find(element.label) == assignments.end()) {
InputFailure("invalid-solid-section-assignment", element.location,
"ELEMENT", element.label_text,
"A Solid section assignment is missing.");
return;
}
}
continue;
}
if (shell_model) {
auto& assignments = part_shell_section_assignments_[part_index];
for (const auto& section : part.shell_sections) {
@@ -3475,6 +3822,36 @@ class DomainMappingContext::Impl {
assignment->second.second,
assignment->second.first,
raw_element.location});
} else if (raw_element.type == RawElement::Type::kC3d8) {
std::array<EntityIndex, 8> connected_nodes{};
for (std::size_t node = 0U; node < connected_nodes.size(); ++node) {
const auto found = node_indices.find(raw_element.node_labels[node]);
if (found == node_indices.end()) {
InputFailure("invalid-solid-connectivity", raw_element.location,
"ELEMENT", raw_element.label_text,
"Solid nodes must resolve.");
return;
}
connected_nodes[node] = found->second;
}
const auto& assignments =
part_shell_section_assignments_[PartIndex(*part)];
const auto assignment = assignments.find(raw_element.label);
if (assignment == assignments.end() ||
definition_.solid_elements.size() >
std::numeric_limits<EntityIndex>::max()) {
InputFailure("invalid-solid-section-assignment",
raw_element.location, "ELEMENT",
raw_element.label_text,
"Solid assignment must resolve.");
return;
}
index = static_cast<EntityIndex>(definition_.solid_elements.size());
definition_.solid_elements.emplace_back(
SourceEntityId{raw_instance.name, raw_element.label,
raw_element.label_text},
connected_nodes, assignment->second.second,
assignment->second.first, raw_element.location);
} else {
std::array<EntityIndex, 4> connected_nodes{};
for (std::size_t node = 0U; node < connected_nodes.size(); ++node) {
@@ -4048,6 +4425,47 @@ class DomainMappingContext::Impl {
const SourceTargetIndex target_index = BuildSourceTargetIndex();
const SourceTargetResolver target_resolver{target_index};
if (model_element_family_ == ElementFamily::kSolid) {
std::vector<PrescribedDisplacementDefinition> physical;
for (auto boundary : boundaries) {
if (!ResolveNodeTarget(target_resolver, boundary.target,
boundary.location, "BOUNDARY"))
return;
if (boundary.last_dof > 3) {
if (boundary.value != 0.0) {
InputFailure("unsupported-solid-rotation-boundary",
boundary.location, "BOUNDARY", boundary.target,
"Solid rotational boundaries must be exact zero to be "
"inactive.");
return;
}
definition_.warnings.push_back(
{Severity::kWarning, "inactive-solid-rotation-boundary",
boundary.location, "BOUNDARY", boundary.target,
"Exact-zero rotational boundary DOFs " +
std::to_string(std::max(4, boundary.first_dof)) + ".." +
std::to_string(boundary.last_dof) +
" are inactive; Solid equations contain translations "
"only."});
boundary.last_dof = 3;
}
if (boundary.first_dof <= boundary.last_dof)
physical.push_back(std::move(boundary));
}
boundaries = std::move(physical);
std::vector<bool> active(definition_.nodes.size(), false);
for (const auto& element : definition_.solid_elements)
for (const auto node : element.node_indices) active[node] = true;
for (std::size_t node = 0U; node < active.size(); ++node) {
if (!active[node]) {
ModelFailure("unused-solid-node", definition_.nodes[node].location,
"NODE",
definition_.nodes[node].source_id.source_label_text,
"Every Solid node must connect to a C3D8 element.");
return;
}
}
}
std::map<std::pair<EntityIndex, int>, double> prescribed_values;
for (const auto& boundary : boundaries) {
auto target = ResolveNodeTarget(target_resolver, boundary.target,
@@ -4115,10 +4533,15 @@ class DomainMappingContext::Impl {
// Static time-control values are provenance only: V0 creates exactly
// the canonical final frame 0 and performs no increment loop here.
definition_.steps.push_back(
{"Step-1", std::move(boundaries), step_.loads,
std::move(surface_pressure_loads), step_.procedure_kind, step_.nlgeom,
step_.static_values[0], step_.static_values[1], step_.static_values[2],
step_.static_values[3], step_.location});
{model_element_family_ == ElementFamily::kSolid ? step_.name_raw
: "Step-1",
std::move(boundaries), step_.loads, std::move(surface_pressure_loads),
step_.procedure_kind, step_.nlgeom, step_.static_values[0],
step_.static_values[1], step_.static_values[2], step_.static_values[3],
step_.location});
definition_.steps.back().direct = step_.static_direct;
definition_.steps.back().increment_count = step_.static_increment_count;
definition_.steps.back().maximum_increments = step_.maximum_increments;
}
const ParsedInput& input_;