fix: enforce phase1 parser subset
This commit is contained in:
@@ -13,7 +13,7 @@ Every new agent session must read this file together with `PROGRESS.md` before p
|
||||
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
||||
|
||||
## Current Objective
|
||||
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-03 parser/domain subset revalidation. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
|
||||
Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebaseline`, starting with P1R-04 validation and singular diagnostic coverage. The old `phases/1-linear-static-mitc4` path is historical and superseded by the paper-based MITC4 formulation reset.
|
||||
|
||||
## Required Reading For New Agents
|
||||
1. `AGENTS.md`
|
||||
@@ -36,7 +36,7 @@ Continue the new Phase 1 rebaseline plan in `phases/1-linear-static-mitc4-rebase
|
||||
## Active Phase Files
|
||||
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
||||
- Execute with: `python scripts/execute.py 1-linear-static-mitc4-rebaseline`
|
||||
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step15.md` is the independent evaluator closeout.
|
||||
- Step numbering is zero-based. `step0.md` is complete and recorded in `phases/1-linear-static-mitc4-rebaseline/step0-audit.md`; `step1.md` is complete and created the `quad_02_phase1.inp` normalized reference path; `step2.md` is complete and revalidated core harness guardrails; `step3.md` is complete and revalidated the Phase 1 parser/domain subset; `step15.md` is the independent evaluator closeout.
|
||||
- Every step file contains a sprint contract with objective, required reading, scope, allowed files, explicit non-goals, tests to write first, reference artifacts, acceptance command, evaluator checklist, and handoff requirements.
|
||||
- Historical phase directory: `phases/1-linear-static-mitc4`
|
||||
- Historical phase status: blocked/superseded. Do not resume the old P1-15/P1-16 path unless the user explicitly requests recovery of that exact phase.
|
||||
@@ -74,7 +74,7 @@ Each gate should be satisfied before moving to the next implementation band unle
|
||||
|---|---|---|---|
|
||||
| G0 - Planning readiness | partial | Readiness task R-011 is resolved by `quad_02_phase1.inp`; R-010 and R-013 remain open. | Updated docs, PLAN.md, PROGRESS.md |
|
||||
| G1 - Build and validation | satisfied | Build system, test framework, and `scripts/validate_workspace.py` run real checks. | Validation command output |
|
||||
| G2 - Parser and domain | pending rebaseline | Must be revalidated through steps 3 and 4 against the current parser subset and stored-reference compatibility policy. | Future parser and validation tests |
|
||||
| G2 - Parser and domain | partial | Parser subset revalidated in step 3; validation and singular diagnostics remain for step 4. | Parser acceptance/rejection tests and validation output |
|
||||
| G3 - DOF/math/results infrastructure | partial | Core aliases, DOF mapping, validation harness, and model diagnostic context were revalidated in step 2; DofManager/results/assembly remain for steps 5, 6, and 12. | P1R-02 validation output |
|
||||
| G4 - MITC4 element readiness | reopened | MITC4 formulation was rewritten from local papers; element implementation must be rebuilt or revalidated through steps 7 through 11. | Revised `docs/MITC4_FORMULATION.md`, future element tests |
|
||||
| G5 - End-to-end solver | reopened | Linear static path must be revalidated through steps 13 and 14 after the MITC4 rebuild and `quad_02` compatibility path. | Future integration/reference regression output |
|
||||
@@ -84,7 +84,7 @@ All milestones are intended to become one or more self-contained sprint contract
|
||||
|
||||
| ID | Status | Owner | Objective | Depends On | Acceptance Focus |
|
||||
|---|---|---|---|---|---|
|
||||
| P1R-03 | pending | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
|
||||
| P1R-03 | completed | parser generator | Revalidate Phase 1 parser and immutable Domain subset. | none | Supported keywords accepted; unsupported features rejected |
|
||||
| P1R-04 | pending | validation generator | Rebuild validation and singular diagnostic coverage. | P1R-03 | Missing-reference and singular-prone negative tests |
|
||||
| P1R-05 | pending | DOF generator | Rebuild six-DOF DofManager, constrained/free mapping, equation numbering, and full-vector reconstruction. | none | DOF mapping and reaction foundation tests |
|
||||
| P1R-06 | pending | results generator | Rebuild minimum results model and displacement CSV comparator. | none | U/RF schema tests and CSV comparator tests |
|
||||
|
||||
+29
-1
@@ -13,10 +13,38 @@ Every new agent session must read this file together with `PLAN.md` before plann
|
||||
- Do not remove history unless the user explicitly asks for archival cleanup.
|
||||
|
||||
## Current Status
|
||||
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 2 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, and model diagnostic context have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||
Phase 1 has a new rebaseline phase definition in `phases/1-linear-static-mitc4-rebaseline`. Steps 0 through 3 are complete. `quad_02_phase1.inp` is now the normalized Phase 1-compatible input path for the stored `quad_02` S4 reference pair, while the original `quad_02.inp` remains preserved unsupported provenance. Core numeric aliases, DOF mapping, validation harness, model diagnostic context, and the Phase 1 parser/domain subset have been revalidated. The old `phases/1-linear-static-mitc4` path is historical and superseded after the MITC4 formulation reset.
|
||||
|
||||
## Completed Work
|
||||
|
||||
### 2026-05-04 - P1R-03 parser/domain subset completed
|
||||
Author: Codex
|
||||
|
||||
Changed files:
|
||||
- `include/fesa/fesa.hpp`
|
||||
- `tests/test_main.cpp`
|
||||
- `docs/ABAQUS_INPUT_SUBSET.md`
|
||||
- `phases/1-linear-static-mitc4-rebaseline/index.json`
|
||||
- `PLAN.md`
|
||||
- `PROGRESS.md`
|
||||
|
||||
Summary:
|
||||
- Added parser acceptance coverage for repeated explicit sets, generated `*Nset` and `*Elset`, `D` exponent numeric input, keyword-line continuation, and every Phase 1 structural input keyword.
|
||||
- Expanded unsupported-feature coverage for `S4R`, `Part`, `Assembly`, `Instance`, `*Include`, `*Density`, and `NLGEOM=YES`.
|
||||
- Added negative coverage for unsupported parameters and flags on otherwise supported keywords, unsupported shell-section data modes, malformed numeric fields, and invalid DOF fields with file, line, and keyword diagnostics.
|
||||
- Tightened the parser so supported keywords reject unknown parameters/flags, fixed-width data rows reject extra non-empty fields, generated sets require exactly `first,last,increment`, and keyword continuation lines are assembled before keyword parsing.
|
||||
- Clarified `docs/ABAQUS_INPUT_SUBSET.md` so the strict parameter and fixed-width data-row rules are explicit.
|
||||
|
||||
Verification:
|
||||
- First ran `python scripts/validate_workspace.py` after adding unsupported-parameter tests; it failed as expected because the parser still accepted supported keywords with unknown controls.
|
||||
- After implementing strict keyword-control rejection, validation passed.
|
||||
- Added keyword-continuation tests and first observed the expected failure; after implementing continuation handling, `python scripts/validate_workspace.py` configured CMake, built `fesa_core` and `fesa_tests`, and ran CTest successfully.
|
||||
- CTest result: 1 test executable passed.
|
||||
|
||||
Follow-up:
|
||||
- Continue with P1R-04 validation and singular diagnostic coverage.
|
||||
- Keep `Part/Assembly/Instance`, `S4R`, `*Density`, and `NLGEOM=YES` outside the Phase 1 parser until an ADR and parser contract intentionally broaden the subset.
|
||||
|
||||
### 2026-05-04 - P1R-02 core harness guardrails completed
|
||||
Author: Codex
|
||||
|
||||
|
||||
@@ -42,6 +42,8 @@ FESA parser rules:
|
||||
- Keyword continuation with a trailing comma should be supported for keyword lines.
|
||||
- Data continuation should be supported only where this document explicitly allows it.
|
||||
- Abbreviated Abaqus keywords are not supported in Phase 1. Require exact keyword names after case normalization.
|
||||
- Supported keywords may use only the parameters and flags listed in this document. Unknown parameters and flags are errors, even when the keyword itself is supported.
|
||||
- Fixed-width data rows for `*Node`, `*Element`, `*Elastic`, `*Shell Section`, `*Boundary`, and `*Cload` must not contain extra non-empty fields beyond the supported form.
|
||||
- Include files through `INPUT=` are not supported in Phase 1.
|
||||
- Part/Assembly/Instance syntax is not supported in Phase 1 unless added by ADR.
|
||||
- Normalized derivative inputs such as `references/quad_02_phase1.inp` may be used for Phase 1 parser and solver tests when the original stored Abaqus file contains unsupported Abaqus/CAE scaffolding.
|
||||
|
||||
+128
-17
@@ -308,6 +308,9 @@ inline KeywordLine parseKeywordLine(const std::string& line) {
|
||||
keyword.name = lower(trim(pieces.front()));
|
||||
for (std::size_t i = 1; i < pieces.size(); ++i) {
|
||||
const std::string piece = trim(pieces[i]);
|
||||
if (piece.empty()) {
|
||||
continue;
|
||||
}
|
||||
const auto eq = piece.find('=');
|
||||
if (eq == std::string::npos) {
|
||||
keyword.flags.insert(lower(piece));
|
||||
@@ -337,9 +340,38 @@ class AbaqusInputParser {
|
||||
std::string current_material_key;
|
||||
KeywordLine current_shell_section;
|
||||
LocalIndex line_number = 0;
|
||||
LocalIndex current_keyword_line = 0;
|
||||
|
||||
auto add_error = [&](const std::string& code, const std::string& message) {
|
||||
result.diagnostics.push_back({Severity::Error, code, message, {file_name, line_number, current.name}});
|
||||
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
||||
result.diagnostics.push_back({Severity::Error, code, message, {file_name, source_line, current.name}});
|
||||
};
|
||||
auto is_allowed = [](const std::string& value, std::initializer_list<const char*> allowed_values) {
|
||||
return std::any_of(allowed_values.begin(), allowed_values.end(), [&](const char* allowed) {
|
||||
return value == allowed;
|
||||
});
|
||||
};
|
||||
auto reject_unsupported_controls = [&](std::initializer_list<const char*> allowed_parameters,
|
||||
std::initializer_list<const char*> allowed_flags) {
|
||||
for (const auto& [parameter, value] : current.parameters) {
|
||||
(void)value;
|
||||
if (!is_allowed(parameter, allowed_parameters)) {
|
||||
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
||||
result.diagnostics.push_back({Severity::Error,
|
||||
"FESA-PARSE-UNSUPPORTED-PARAMETER",
|
||||
"Unsupported *" + current.name + " parameter: " + parameter,
|
||||
{file_name, source_line, current.name}});
|
||||
}
|
||||
}
|
||||
for (const std::string& flag : current.flags) {
|
||||
if (!is_allowed(flag, allowed_flags)) {
|
||||
const LocalIndex source_line = current_keyword_line == 0 ? line_number : current_keyword_line;
|
||||
result.diagnostics.push_back({Severity::Error,
|
||||
"FESA-PARSE-UNSUPPORTED-PARAMETER",
|
||||
"Unsupported *" + current.name + " flag: " + flag,
|
||||
{file_name, source_line, current.name}});
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
while (std::getline(stream, line)) {
|
||||
@@ -349,16 +381,52 @@ class AbaqusInputParser {
|
||||
continue;
|
||||
}
|
||||
if (!line.empty() && line.front() == '*') {
|
||||
current = parseKeywordLine(line);
|
||||
if (current.name == "node" || current.name == "element" || current.name == "nset" ||
|
||||
current.name == "elset" || current.name == "elastic" || current.name == "shell section" ||
|
||||
current.name == "boundary" || current.name == "cload" || current.name == "static") {
|
||||
if (current.name == "shell section") {
|
||||
current_shell_section = current;
|
||||
current_keyword_line = line_number;
|
||||
std::string keyword_line = line;
|
||||
while (!keyword_line.empty() && keyword_line.back() == ',') {
|
||||
std::string continuation;
|
||||
if (!std::getline(stream, continuation)) {
|
||||
break;
|
||||
}
|
||||
++line_number;
|
||||
continuation = trim(continuation);
|
||||
if (continuation.empty() || continuation.rfind("**", 0) == 0) {
|
||||
continue;
|
||||
}
|
||||
keyword_line += continuation;
|
||||
}
|
||||
current = parseKeywordLine(keyword_line);
|
||||
if (current.name == "node") {
|
||||
reject_unsupported_controls({}, {});
|
||||
continue;
|
||||
}
|
||||
if (current.name == "element") {
|
||||
reject_unsupported_controls({"type", "elset"}, {});
|
||||
continue;
|
||||
}
|
||||
if (current.name == "nset") {
|
||||
reject_unsupported_controls({"nset"}, {"generate"});
|
||||
continue;
|
||||
}
|
||||
if (current.name == "elset") {
|
||||
reject_unsupported_controls({"elset"}, {"generate"});
|
||||
continue;
|
||||
}
|
||||
if (current.name == "elastic") {
|
||||
reject_unsupported_controls({}, {});
|
||||
continue;
|
||||
}
|
||||
if (current.name == "shell section") {
|
||||
reject_unsupported_controls({"elset", "material"}, {});
|
||||
current_shell_section = current;
|
||||
continue;
|
||||
}
|
||||
if (current.name == "boundary" || current.name == "cload" || current.name == "static") {
|
||||
reject_unsupported_controls({}, {});
|
||||
continue;
|
||||
}
|
||||
if (current.name == "material") {
|
||||
reject_unsupported_controls({"name"}, {});
|
||||
auto name_it = current.parameters.find("name");
|
||||
if (name_it == current.parameters.end() || trim(name_it->second).empty()) {
|
||||
add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME");
|
||||
@@ -376,6 +444,7 @@ class AbaqusInputParser {
|
||||
continue;
|
||||
}
|
||||
if (current.name == "step") {
|
||||
reject_unsupported_controls({"name", "nlgeom"}, {});
|
||||
auto nlgeom = current.parameters.find("nlgeom");
|
||||
if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") {
|
||||
add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1");
|
||||
@@ -389,6 +458,7 @@ class AbaqusInputParser {
|
||||
continue;
|
||||
}
|
||||
if (current.name == "end step") {
|
||||
reject_unsupported_controls({}, {});
|
||||
continue;
|
||||
}
|
||||
add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name);
|
||||
@@ -433,8 +503,16 @@ class AbaqusInputParser {
|
||||
}
|
||||
|
||||
private:
|
||||
static std::size_t effectiveFieldCount(const std::vector<std::string>& fields) {
|
||||
std::size_t count = fields.size();
|
||||
while (count > 0 && trim(fields[count - 1]).empty()) {
|
||||
--count;
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
static void parseNode(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
||||
if (fields.size() < 4) {
|
||||
if (effectiveFieldCount(fields) != 4) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-NODE", "*Node data requires id,x,y,z", {file_name, line, "node"}});
|
||||
return;
|
||||
}
|
||||
@@ -464,7 +542,7 @@ class AbaqusInputParser {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-ELEMENT", "Unsupported element type: " + type_it->second, {file_name, line, "element"}});
|
||||
return;
|
||||
}
|
||||
if (fields.size() < 5) {
|
||||
if (effectiveFieldCount(fields) != 5) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT", "S4 element requires id,n1,n2,n3,n4", {file_name, line, "element"}});
|
||||
return;
|
||||
}
|
||||
@@ -524,7 +602,8 @@ class AbaqusInputParser {
|
||||
static void parseSetData(const std::vector<std::string>& fields, bool generate, std::vector<GlobalId>& output,
|
||||
std::vector<Diagnostic>& diagnostics, const std::string& file_name, LocalIndex line, const std::string& keyword) {
|
||||
if (generate) {
|
||||
if (fields.size() < 3) {
|
||||
const std::size_t field_count = effectiveFieldCount(fields);
|
||||
if (field_count != 3) {
|
||||
diagnostics.push_back({Severity::Error, "FESA-PARSE-GENERATE", "Generated set requires first,last,increment", {file_name, line, keyword}});
|
||||
return;
|
||||
}
|
||||
@@ -540,7 +619,9 @@ class AbaqusInputParser {
|
||||
}
|
||||
return;
|
||||
}
|
||||
for (const std::string& field : fields) {
|
||||
const std::size_t field_count = effectiveFieldCount(fields);
|
||||
for (std::size_t i = 0; i < field_count; ++i) {
|
||||
const std::string& field = fields[i];
|
||||
if (trim(field).empty()) {
|
||||
continue;
|
||||
}
|
||||
@@ -558,10 +639,16 @@ class AbaqusInputParser {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-MATERIAL", "*Elastic must follow *Material", {file_name, line, "elastic"}});
|
||||
return;
|
||||
}
|
||||
if (fields.size() < 2) {
|
||||
const std::size_t field_count = effectiveFieldCount(fields);
|
||||
if (field_count < 2) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC", "*Elastic requires E,nu", {file_name, line, "elastic"}});
|
||||
return;
|
||||
}
|
||||
if (field_count > 2) {
|
||||
result.diagnostics.push_back(
|
||||
{Severity::Error, "FESA-PARSE-ELASTIC-UNSUPPORTED", "Only isotropic E,nu elastic data is supported", {file_name, line, "elastic"}});
|
||||
return;
|
||||
}
|
||||
auto e = parseReal(fields[0]);
|
||||
auto nu = parseReal(fields[1]);
|
||||
if (!e || !nu || *e <= 0.0 || *nu <= -1.0 || *nu >= 0.5) {
|
||||
@@ -579,10 +666,18 @@ class AbaqusInputParser {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION-PARAM", "*Shell Section requires ELSET and MATERIAL", {file_name, line, "shell section"}});
|
||||
return;
|
||||
}
|
||||
if (fields.empty()) {
|
||||
const std::size_t field_count = effectiveFieldCount(fields);
|
||||
if (field_count == 0) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION", "*Shell Section requires thickness", {file_name, line, "shell section"}});
|
||||
return;
|
||||
}
|
||||
if (field_count > 1) {
|
||||
result.diagnostics.push_back({Severity::Error,
|
||||
"FESA-PARSE-SHELL-SECTION-UNSUPPORTED",
|
||||
"Only homogeneous shell thickness data is supported",
|
||||
{file_name, line, "shell section"}});
|
||||
return;
|
||||
}
|
||||
auto thickness = parseReal(fields[0]);
|
||||
if (!thickness || *thickness <= 0.0) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-THICKNESS", "Shell thickness must be positive", {file_name, line, "shell section"}});
|
||||
@@ -592,13 +687,21 @@ class AbaqusInputParser {
|
||||
}
|
||||
|
||||
static void parseBoundary(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
||||
if (fields.size() < 2) {
|
||||
const std::size_t field_count = effectiveFieldCount(fields);
|
||||
if (field_count < 2) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY", "*Boundary requires target,first_dof", {file_name, line, "boundary"}});
|
||||
return;
|
||||
}
|
||||
if (field_count > 4) {
|
||||
result.diagnostics.push_back({Severity::Error,
|
||||
"FESA-PARSE-BOUNDARY-UNSUPPORTED",
|
||||
"Only direct zero-valued boundary data is supported",
|
||||
{file_name, line, "boundary"}});
|
||||
return;
|
||||
}
|
||||
auto first = parseInt64(fields[1]);
|
||||
auto last = fields.size() >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
|
||||
auto magnitude = fields.size() >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional<Real>(0.0);
|
||||
auto last = field_count >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
|
||||
auto magnitude = field_count >= 4 && !fields[3].empty() ? parseReal(fields[3]) : std::optional<Real>(0.0);
|
||||
if (!first || !last || !magnitude || !dofFromAbaqus(static_cast<int>(*first)) || !dofFromAbaqus(static_cast<int>(*last)) || *first > *last) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-DOF", "Invalid boundary DOF range", {file_name, line, "boundary"}});
|
||||
return;
|
||||
@@ -611,10 +714,18 @@ class AbaqusInputParser {
|
||||
}
|
||||
|
||||
static void parseLoad(const std::vector<std::string>& fields, ParseResult& result, const std::string& file_name, LocalIndex line) {
|
||||
if (fields.size() < 3) {
|
||||
const std::size_t field_count = effectiveFieldCount(fields);
|
||||
if (field_count < 3) {
|
||||
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-CLOAD", "*Cload requires target,dof,magnitude", {file_name, line, "cload"}});
|
||||
return;
|
||||
}
|
||||
if (field_count > 3) {
|
||||
result.diagnostics.push_back({Severity::Error,
|
||||
"FESA-PARSE-CLOAD-UNSUPPORTED",
|
||||
"Only direct concentrated load data is supported",
|
||||
{file_name, line, "cload"}});
|
||||
return;
|
||||
}
|
||||
auto dof = parseInt64(fields[1]);
|
||||
auto magnitude = parseReal(fields[2]);
|
||||
if (!dof || !magnitude || !dofFromAbaqus(static_cast<int>(*dof))) {
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
{ "step": 0, "name": "rebaseline-audit", "status": "completed" },
|
||||
{ "step": 1, "name": "reference-onboarding", "status": "completed" },
|
||||
{ "step": 2, "name": "core-harness-guardrails", "status": "completed" },
|
||||
{ "step": 3, "name": "parser-domain-subset", "status": "pending" },
|
||||
{ "step": 3, "name": "parser-domain-subset", "status": "completed" },
|
||||
{ "step": 4, "name": "validation-singular-diagnostics", "status": "pending" },
|
||||
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" },
|
||||
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
|
||||
|
||||
@@ -105,6 +105,25 @@ fesa::Domain parsedPhase1Domain() {
|
||||
return parsed.domain;
|
||||
}
|
||||
|
||||
const fesa::Diagnostic* findDiagnostic(const std::vector<fesa::Diagnostic>& diagnostics, const std::string& code) {
|
||||
for (const auto& diagnostic : diagnostics) {
|
||||
if (diagnostic.code == code) {
|
||||
return &diagnostic;
|
||||
}
|
||||
}
|
||||
return nullptr;
|
||||
}
|
||||
|
||||
std::size_t diagnosticCount(const std::vector<fesa::Diagnostic>& diagnostics, const std::string& code) {
|
||||
std::size_t count = 0;
|
||||
for (const auto& diagnostic : diagnostics) {
|
||||
if (diagnostic.code == code) {
|
||||
++count;
|
||||
}
|
||||
}
|
||||
return count;
|
||||
}
|
||||
|
||||
} // namespace
|
||||
|
||||
FESA_TEST(core_types_and_dof_mapping_are_stable) {
|
||||
@@ -144,24 +163,170 @@ FESA_TEST(parser_accepts_phase1_subset) {
|
||||
FESA_CHECK(parsed.domain.loads.size() == 2);
|
||||
}
|
||||
|
||||
FESA_TEST(parser_accepts_repeated_and_generated_sets) {
|
||||
const std::string text = R"inp(
|
||||
*Node
|
||||
1, 0, 0, 0
|
||||
2, 1, 0, 0
|
||||
3, 1, 1, 0
|
||||
4, 0, 1, 0
|
||||
*Element, type=S4, elset=EALL
|
||||
1, 1, 2, 3, 4
|
||||
*Nset, nset=FIXED
|
||||
1, 2, 2
|
||||
3
|
||||
*Nset, nset=FIXED, generate
|
||||
3, 4, 1
|
||||
*Nset, nset=LOADS, generate
|
||||
2, 4, 2
|
||||
*Elset, elset=EALL
|
||||
1, 1
|
||||
*Elset, elset=CHECK, generate
|
||||
1, 5, 2
|
||||
*Material, name=MAT
|
||||
*Elastic
|
||||
2.0D5, 0.25
|
||||
*Shell Section, elset=EALL, material=MAT
|
||||
0.2
|
||||
*Boundary
|
||||
FIXED, 1, 6
|
||||
*Cload
|
||||
LOADS, 3, -2.5
|
||||
*Step, name=Step-A, nlgeom=NO
|
||||
*Static
|
||||
*End Step
|
||||
)inp";
|
||||
fesa::AbaqusInputParser parser;
|
||||
auto parsed = parser.parseString(text);
|
||||
FESA_CHECK(parsed.ok());
|
||||
FESA_CHECK(parsed.domain.node_sets.at("fixed").node_ids == std::vector<fesa::GlobalId>({1, 2, 3, 4}));
|
||||
FESA_CHECK(parsed.domain.node_sets.at("loads").node_ids == std::vector<fesa::GlobalId>({2, 4}));
|
||||
FESA_CHECK(parsed.domain.element_sets.at("eall").element_ids == std::vector<fesa::GlobalId>({1}));
|
||||
FESA_CHECK(parsed.domain.element_sets.at("check").element_ids == std::vector<fesa::GlobalId>({1, 3, 5}));
|
||||
FESA_CHECK(parsed.domain.materials.at("mat").elastic_modulus == 2.0e5);
|
||||
FESA_CHECK(parsed.domain.steps.front().name == "Step-A");
|
||||
}
|
||||
|
||||
FESA_TEST(parser_accepts_keyword_line_continuation) {
|
||||
const std::string text = R"inp(
|
||||
*Node
|
||||
1, 0, 0, 0
|
||||
2, 1, 0, 0
|
||||
3, 1, 1, 0
|
||||
4, 0, 1, 0
|
||||
*Element,
|
||||
type=S4, elset=EALL
|
||||
1, 1, 2, 3, 4
|
||||
*Nset,
|
||||
nset=FIXED, generate
|
||||
1, 4, 3
|
||||
*Elset,
|
||||
elset=EALL
|
||||
1
|
||||
*Material,
|
||||
name=MAT
|
||||
*Elastic
|
||||
2.0e5, 0.25
|
||||
*Shell Section,
|
||||
elset=EALL, material=MAT
|
||||
0.2
|
||||
*Boundary
|
||||
FIXED, 1, 6
|
||||
*Cload
|
||||
2, 3, -1.0
|
||||
*Step,
|
||||
name=Step-1
|
||||
*Static
|
||||
*End Step
|
||||
)inp";
|
||||
fesa::AbaqusInputParser parser;
|
||||
auto parsed = parser.parseString(text);
|
||||
FESA_CHECK(parsed.ok());
|
||||
FESA_CHECK(parsed.domain.elements.at(1).source_elset == "EALL");
|
||||
FESA_CHECK(parsed.domain.node_sets.at("fixed").node_ids == std::vector<fesa::GlobalId>({1, 4}));
|
||||
FESA_CHECK(parsed.domain.materials.count("mat") == 1);
|
||||
FESA_CHECK(parsed.domain.shell_sections.front().material == "MAT");
|
||||
}
|
||||
|
||||
FESA_TEST(parser_rejects_unsupported_features) {
|
||||
const std::string text = R"inp(
|
||||
*Part, name=P1
|
||||
*Assembly, name=A1
|
||||
*Instance, name=I1, part=P1
|
||||
*Include, input=other.inp
|
||||
*Node
|
||||
1, 0, 0, 0
|
||||
*Element, type=S4R
|
||||
1, 1, 2, 3, 4
|
||||
*Density
|
||||
7850
|
||||
*Step, nlgeom=YES
|
||||
*End Step
|
||||
)inp";
|
||||
fesa::AbaqusInputParser parser;
|
||||
auto parsed = parser.parseString(text);
|
||||
FESA_CHECK(!parsed.ok());
|
||||
FESA_CHECK(diagnosticCount(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD") >= 4);
|
||||
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-KEYWORD"));
|
||||
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-ELEMENT"));
|
||||
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-NLGEOM"));
|
||||
}
|
||||
|
||||
FESA_TEST(parser_rejects_unsupported_keyword_parameters_and_modes) {
|
||||
const std::string text = R"inp(
|
||||
*Node, input=nodes.csv
|
||||
1, 0, 0, 0
|
||||
2, 1, 0, 0
|
||||
3, 1, 1, 0
|
||||
4, 0, 1, 0
|
||||
*Element, type=S4, elset=EALL, orientation=OR1
|
||||
1, 1, 2, 3, 4
|
||||
*Nset, nset=FIXED, unsorted
|
||||
1, 4
|
||||
*Material, name=MAT, description=bad
|
||||
*Elastic, type=ENGINEERING CONSTANTS
|
||||
2.0e5, 0.25
|
||||
*Shell Section, elset=EALL, material=MAT, offset=SPOS
|
||||
0.2, 5
|
||||
*Boundary, op=NEW
|
||||
FIXED, 1, 6
|
||||
*Cload, amplitude=A1
|
||||
2, 3, -1.0
|
||||
*Step, name=Step-1, inc=100
|
||||
*Static, stabilize
|
||||
*End Step
|
||||
)inp";
|
||||
fesa::AbaqusInputParser parser;
|
||||
auto parsed = parser.parseString(text, "unsupported_modes.inp");
|
||||
FESA_CHECK(!parsed.ok());
|
||||
FESA_CHECK(diagnosticCount(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-PARAMETER") >= 8);
|
||||
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-SHELL-SECTION-UNSUPPORTED"));
|
||||
}
|
||||
|
||||
FESA_TEST(parser_diagnostics_include_file_line_and_keyword) {
|
||||
const std::string text = R"inp(
|
||||
*Node
|
||||
1, bad, 0, 0
|
||||
*Boundary
|
||||
FIXED, 7, 7
|
||||
)inp";
|
||||
fesa::AbaqusInputParser parser;
|
||||
auto parsed = parser.parseString(text, "malformed.inp");
|
||||
FESA_CHECK(!parsed.ok());
|
||||
|
||||
const fesa::Diagnostic* node = findDiagnostic(parsed.diagnostics, "FESA-PARSE-NODE-NUMERIC");
|
||||
FESA_CHECK(node != nullptr);
|
||||
FESA_CHECK(node->source.file == "malformed.inp");
|
||||
FESA_CHECK(node->source.line == 3);
|
||||
FESA_CHECK(node->source.keyword == "node");
|
||||
|
||||
const fesa::Diagnostic* boundary = findDiagnostic(parsed.diagnostics, "FESA-PARSE-BOUNDARY-DOF");
|
||||
FESA_CHECK(boundary != nullptr);
|
||||
FESA_CHECK(boundary->source.file == "malformed.inp");
|
||||
FESA_CHECK(boundary->source.line == 5);
|
||||
FESA_CHECK(boundary->source.keyword == "boundary");
|
||||
}
|
||||
|
||||
FESA_TEST(quad01_reference_input_remains_unsupported) {
|
||||
fesa::AbaqusInputParser parser;
|
||||
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_01.inp");
|
||||
|
||||
Reference in New Issue
Block a user