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.
|
- If an item becomes obsolete, move it to `PROGRESS.md` with a short reason instead of silently deleting it.
|
||||||
|
|
||||||
## Current Objective
|
## 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
|
## Required Reading For New Agents
|
||||||
1. `AGENTS.md`
|
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 Files
|
||||||
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
- Active phase directory: `phases/1-linear-static-mitc4-rebaseline`
|
||||||
- Execute with: `python scripts/execute.py 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.
|
- 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 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.
|
- 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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 |
|
| 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-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-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 |
|
| 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.
|
- Do not remove history unless the user explicitly asks for archival cleanup.
|
||||||
|
|
||||||
## Current Status
|
## 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
|
## 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
|
### 2026-05-04 - P1R-02 core harness guardrails completed
|
||||||
Author: Codex
|
Author: Codex
|
||||||
|
|
||||||
|
|||||||
@@ -42,6 +42,8 @@ FESA parser rules:
|
|||||||
- Keyword continuation with a trailing comma should be supported for keyword lines.
|
- Keyword continuation with a trailing comma should be supported for keyword lines.
|
||||||
- Data continuation should be supported only where this document explicitly allows it.
|
- 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.
|
- 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.
|
- Include files through `INPUT=` are not supported in Phase 1.
|
||||||
- Part/Assembly/Instance syntax is not supported in Phase 1 unless added by ADR.
|
- 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.
|
- 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()));
|
keyword.name = lower(trim(pieces.front()));
|
||||||
for (std::size_t i = 1; i < pieces.size(); ++i) {
|
for (std::size_t i = 1; i < pieces.size(); ++i) {
|
||||||
const std::string piece = trim(pieces[i]);
|
const std::string piece = trim(pieces[i]);
|
||||||
|
if (piece.empty()) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
const auto eq = piece.find('=');
|
const auto eq = piece.find('=');
|
||||||
if (eq == std::string::npos) {
|
if (eq == std::string::npos) {
|
||||||
keyword.flags.insert(lower(piece));
|
keyword.flags.insert(lower(piece));
|
||||||
@@ -337,9 +340,38 @@ class AbaqusInputParser {
|
|||||||
std::string current_material_key;
|
std::string current_material_key;
|
||||||
KeywordLine current_shell_section;
|
KeywordLine current_shell_section;
|
||||||
LocalIndex line_number = 0;
|
LocalIndex line_number = 0;
|
||||||
|
LocalIndex current_keyword_line = 0;
|
||||||
|
|
||||||
auto add_error = [&](const std::string& code, const std::string& message) {
|
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)) {
|
while (std::getline(stream, line)) {
|
||||||
@@ -349,16 +381,52 @@ class AbaqusInputParser {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (!line.empty() && line.front() == '*') {
|
if (!line.empty() && line.front() == '*') {
|
||||||
current = parseKeywordLine(line);
|
current_keyword_line = line_number;
|
||||||
if (current.name == "node" || current.name == "element" || current.name == "nset" ||
|
std::string keyword_line = line;
|
||||||
current.name == "elset" || current.name == "elastic" || current.name == "shell section" ||
|
while (!keyword_line.empty() && keyword_line.back() == ',') {
|
||||||
current.name == "boundary" || current.name == "cload" || current.name == "static") {
|
std::string continuation;
|
||||||
if (current.name == "shell section") {
|
if (!std::getline(stream, continuation)) {
|
||||||
current_shell_section = current;
|
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;
|
continue;
|
||||||
}
|
}
|
||||||
if (current.name == "material") {
|
if (current.name == "material") {
|
||||||
|
reject_unsupported_controls({"name"}, {});
|
||||||
auto name_it = current.parameters.find("name");
|
auto name_it = current.parameters.find("name");
|
||||||
if (name_it == current.parameters.end() || trim(name_it->second).empty()) {
|
if (name_it == current.parameters.end() || trim(name_it->second).empty()) {
|
||||||
add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME");
|
add_error("FESA-PARSE-MATERIAL-NAME", "*Material requires NAME");
|
||||||
@@ -376,6 +444,7 @@ class AbaqusInputParser {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (current.name == "step") {
|
if (current.name == "step") {
|
||||||
|
reject_unsupported_controls({"name", "nlgeom"}, {});
|
||||||
auto nlgeom = current.parameters.find("nlgeom");
|
auto nlgeom = current.parameters.find("nlgeom");
|
||||||
if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") {
|
if (nlgeom != current.parameters.end() && lower(trim(nlgeom->second)) == "yes") {
|
||||||
add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1");
|
add_error("FESA-PARSE-UNSUPPORTED-NLGEOM", "NLGEOM=YES is not supported in Phase 1");
|
||||||
@@ -389,6 +458,7 @@ class AbaqusInputParser {
|
|||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
if (current.name == "end step") {
|
if (current.name == "end step") {
|
||||||
|
reject_unsupported_controls({}, {});
|
||||||
continue;
|
continue;
|
||||||
}
|
}
|
||||||
add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name);
|
add_error("FESA-PARSE-UNSUPPORTED-KEYWORD", "Unsupported keyword: *" + current.name);
|
||||||
@@ -433,8 +503,16 @@ class AbaqusInputParser {
|
|||||||
}
|
}
|
||||||
|
|
||||||
private:
|
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) {
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-NODE", "*Node data requires id,x,y,z", {file_name, line, "node"}});
|
||||||
return;
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-UNSUPPORTED-ELEMENT", "Unsupported element type: " + type_it->second, {file_name, line, "element"}});
|
||||||
return;
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELEMENT", "S4 element requires id,n1,n2,n3,n4", {file_name, line, "element"}});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -524,7 +602,8 @@ class AbaqusInputParser {
|
|||||||
static void parseSetData(const std::vector<std::string>& fields, bool generate, std::vector<GlobalId>& output,
|
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) {
|
std::vector<Diagnostic>& diagnostics, const std::string& file_name, LocalIndex line, const std::string& keyword) {
|
||||||
if (generate) {
|
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}});
|
diagnostics.push_back({Severity::Error, "FESA-PARSE-GENERATE", "Generated set requires first,last,increment", {file_name, line, keyword}});
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
@@ -540,7 +619,9 @@ class AbaqusInputParser {
|
|||||||
}
|
}
|
||||||
return;
|
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()) {
|
if (trim(field).empty()) {
|
||||||
continue;
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC-MATERIAL", "*Elastic must follow *Material", {file_name, line, "elastic"}});
|
||||||
return;
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-ELASTIC", "*Elastic requires E,nu", {file_name, line, "elastic"}});
|
||||||
return;
|
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 e = parseReal(fields[0]);
|
||||||
auto nu = parseReal(fields[1]);
|
auto nu = parseReal(fields[1]);
|
||||||
if (!e || !nu || *e <= 0.0 || *nu <= -1.0 || *nu >= 0.5) {
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION-PARAM", "*Shell Section requires ELSET and MATERIAL", {file_name, line, "shell section"}});
|
||||||
return;
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-SHELL-SECTION", "*Shell Section requires thickness", {file_name, line, "shell section"}});
|
||||||
return;
|
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]);
|
auto thickness = parseReal(fields[0]);
|
||||||
if (!thickness || *thickness <= 0.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"}});
|
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) {
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY", "*Boundary requires target,first_dof", {file_name, line, "boundary"}});
|
||||||
return;
|
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 first = parseInt64(fields[1]);
|
||||||
auto last = fields.size() >= 3 && !fields[2].empty() ? parseInt64(fields[2]) : first;
|
auto last = field_count >= 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 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) {
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-BOUNDARY-DOF", "Invalid boundary DOF range", {file_name, line, "boundary"}});
|
||||||
return;
|
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) {
|
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"}});
|
result.diagnostics.push_back({Severity::Error, "FESA-PARSE-CLOAD", "*Cload requires target,dof,magnitude", {file_name, line, "cload"}});
|
||||||
return;
|
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 dof = parseInt64(fields[1]);
|
||||||
auto magnitude = parseReal(fields[2]);
|
auto magnitude = parseReal(fields[2]);
|
||||||
if (!dof || !magnitude || !dofFromAbaqus(static_cast<int>(*dof))) {
|
if (!dof || !magnitude || !dofFromAbaqus(static_cast<int>(*dof))) {
|
||||||
|
|||||||
@@ -5,7 +5,7 @@
|
|||||||
{ "step": 0, "name": "rebaseline-audit", "status": "completed" },
|
{ "step": 0, "name": "rebaseline-audit", "status": "completed" },
|
||||||
{ "step": 1, "name": "reference-onboarding", "status": "completed" },
|
{ "step": 1, "name": "reference-onboarding", "status": "completed" },
|
||||||
{ "step": 2, "name": "core-harness-guardrails", "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": 4, "name": "validation-singular-diagnostics", "status": "pending" },
|
||||||
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" },
|
{ "step": 5, "name": "dof-manager-reaction-foundation", "status": "pending" },
|
||||||
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
|
{ "step": 6, "name": "results-comparator-foundation", "status": "pending" },
|
||||||
|
|||||||
@@ -105,6 +105,25 @@ fesa::Domain parsedPhase1Domain() {
|
|||||||
return parsed.domain;
|
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
|
} // namespace
|
||||||
|
|
||||||
FESA_TEST(core_types_and_dof_mapping_are_stable) {
|
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_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) {
|
FESA_TEST(parser_rejects_unsupported_features) {
|
||||||
const std::string text = R"inp(
|
const std::string text = R"inp(
|
||||||
*Part, name=P1
|
*Part, name=P1
|
||||||
|
*Assembly, name=A1
|
||||||
|
*Instance, name=I1, part=P1
|
||||||
|
*Include, input=other.inp
|
||||||
*Node
|
*Node
|
||||||
1, 0, 0, 0
|
1, 0, 0, 0
|
||||||
*Element, type=S4R
|
*Element, type=S4R
|
||||||
1, 1, 2, 3, 4
|
1, 1, 2, 3, 4
|
||||||
|
*Density
|
||||||
|
7850
|
||||||
*Step, nlgeom=YES
|
*Step, nlgeom=YES
|
||||||
*End Step
|
*End Step
|
||||||
)inp";
|
)inp";
|
||||||
fesa::AbaqusInputParser parser;
|
fesa::AbaqusInputParser parser;
|
||||||
auto parsed = parser.parseString(text);
|
auto parsed = parser.parseString(text);
|
||||||
FESA_CHECK(!parsed.ok());
|
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-KEYWORD"));
|
||||||
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-ELEMENT"));
|
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-ELEMENT"));
|
||||||
FESA_CHECK(fesa::containsDiagnostic(parsed.diagnostics, "FESA-PARSE-UNSUPPORTED-NLGEOM"));
|
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_TEST(quad01_reference_input_remains_unsupported) {
|
||||||
fesa::AbaqusInputParser parser;
|
fesa::AbaqusInputParser parser;
|
||||||
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_01.inp");
|
auto parsed = parser.parseFile(sourceRoot() + "/references/quad_01.inp");
|
||||||
|
|||||||
Reference in New Issue
Block a user